Compare commits
6 Commits
8dc0af650f
...
3befb69f51
Author | SHA1 | Date | |
---|---|---|---|
3befb69f51 | |||
43a5268ed5 | |||
e3194e45ff | |||
69475b1bd8 | |||
68eec35ea2 | |||
3d4596f8fb |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
bundle.js
|
bundle.js
|
||||||
dist/
|
dist/
|
||||||
|
temp.ts
|
63
GameLoop.ts
Normal file
63
GameLoop.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Doodler } from "@bearmetal/doodler";
|
||||||
|
import { StateMachine } from "./state/machine.ts";
|
||||||
|
import { getContextItem } from "./lib/context.ts";
|
||||||
|
|
||||||
|
export class GameLoop<T> {
|
||||||
|
lastTime: number;
|
||||||
|
running: boolean;
|
||||||
|
targetFps: number;
|
||||||
|
|
||||||
|
constructor(targetFps: number = 60) {
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
this.running = false;
|
||||||
|
this.targetFps = targetFps;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(state: StateMachine<T>) {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
this.lastTime = performance.now();
|
||||||
|
|
||||||
|
while (this.running) {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
|
||||||
|
this.lastTime = currentTime;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for state update to complete before continuing
|
||||||
|
await state.update(deltaTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in game loop:", error);
|
||||||
|
this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use setTimeout to prevent immediate loop continuation
|
||||||
|
// and allow other tasks to run
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Usage example:
|
||||||
|
// const gameState = {
|
||||||
|
// update: async (deltaTime) => {
|
||||||
|
// console.log(`Updating with delta time: ${deltaTime.toFixed(3)}s`);
|
||||||
|
// // Simulate some async work
|
||||||
|
// await new Promise(resolve => setTimeout(resolve, 16)); // ~60fps
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Create and start the loop
|
||||||
|
// const loop = new GameLoop();
|
||||||
|
// loop.start(gameState);
|
||||||
|
|
||||||
|
// // Stop the loop after 5 seconds (example)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// loop.stop();
|
||||||
|
// console.log('Loop stopped');
|
||||||
|
// }, 5000);
|
@ -13,6 +13,6 @@
|
|||||||
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.3"
|
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-b"
|
||||||
}
|
}
|
||||||
}
|
}
|
11
deno.lock
generated
11
deno.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "4",
|
"version": "4",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
|
"jsr:@bearmetal/doodler@0.0.5-b": "0.0.5-b",
|
||||||
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
|
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
|
||||||
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
|
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
|
||||||
"jsr:@std/assert@*": "1.0.10",
|
"jsr:@std/assert@*": "1.0.10",
|
||||||
@ -22,8 +22,11 @@
|
|||||||
"npm:esbuild@*": "0.24.2"
|
"npm:esbuild@*": "0.24.2"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@bearmetal/doodler@0.0.3": {
|
"@bearmetal/doodler@0.0.4": {
|
||||||
"integrity": "42c04b672f4a6bc7ebd45ad936197a2e32856364b66a9a9fe2b81a4aa45c7a08"
|
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
|
||||||
|
},
|
||||||
|
"@bearmetal/doodler@0.0.5-b": {
|
||||||
|
"integrity": "94f265ea21162f943291526800de7f3f6560634a4fe762a38cd73892685b6742"
|
||||||
},
|
},
|
||||||
"@luca/esbuild-deno-loader@0.11.0": {
|
"@luca/esbuild-deno-loader@0.11.0": {
|
||||||
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
||||||
@ -229,7 +232,7 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@bearmetal/doodler@^0.0.3"
|
"jsr:@bearmetal/doodler@0.0.5-b"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
index.html
12
index.html
@ -26,6 +26,18 @@
|
|||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
#fps {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -9,4 +9,9 @@ export function bootstrapInputs() {
|
|||||||
const state = getContextItem<StateMachine<States>>("state");
|
const state = getContextItem<StateMachine<States>>("state");
|
||||||
state.transitionTo(States.EDIT_TRACK);
|
state.transitionTo(States.EDIT_TRACK);
|
||||||
});
|
});
|
||||||
|
inputManager.onKey("Delete", () => {
|
||||||
|
if (inputManager.getKeyState("Control")) {
|
||||||
|
localStorage.removeItem("track");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,13 @@ export function getContext() {
|
|||||||
export function getContextItem<T>(prop: string): T {
|
export function getContextItem<T>(prop: string): T {
|
||||||
return ctx[prop] as T;
|
return ctx[prop] as T;
|
||||||
}
|
}
|
||||||
|
export function getContextItemOrDefault<T>(prop: string, defaultValue: T): T {
|
||||||
|
try {
|
||||||
|
return ctx[prop] as T;
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
export function setContextItem<T>(prop: string, value: T) {
|
export function setContextItem<T>(prop: string, value: T) {
|
||||||
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
||||||
[prop]: value,
|
[prop]: value,
|
||||||
|
58
lib/input.ts
58
lib/input.ts
@ -1,14 +1,36 @@
|
|||||||
import { Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
import { Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
||||||
import { getContextItem } from "./context.ts";
|
import { getContextItem } from "./context.ts";
|
||||||
|
|
||||||
|
function mouseButtonToString(button: number) {
|
||||||
|
switch (button) {
|
||||||
|
case 0:
|
||||||
|
return "left";
|
||||||
|
case 1:
|
||||||
|
return "middle";
|
||||||
|
case 2:
|
||||||
|
return "right";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseButtonToNumber(button: string) {
|
||||||
|
switch (button) {
|
||||||
|
case "left":
|
||||||
|
return 0;
|
||||||
|
case "middle":
|
||||||
|
return 1;
|
||||||
|
case "right":
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class InputManager {
|
export class InputManager {
|
||||||
private keyStates: Map<string | number, boolean> = new Map();
|
private keyStates: Map<string, boolean> = new Map();
|
||||||
private mouseStates: Map<string | number, boolean> = new Map();
|
private mouseStates: Map<string, boolean> = new Map();
|
||||||
private mouseLocation: { x: number; y: number } = { x: 0, y: 0 };
|
private mouseLocation: { x: number; y: number } = { x: 0, y: 0 };
|
||||||
private mouseDelta: { x: number; y: number } = { x: 0, y: 0 };
|
private mouseDelta: { x: number; y: number } = { x: 0, y: 0 };
|
||||||
|
|
||||||
private keyEvents: Map<string | number, () => void> = new Map();
|
private keyEvents: Map<string, () => void> = new Map();
|
||||||
private mouseEvents: Map<string | number, () => void> = new Map();
|
private mouseEvents: Map<string, () => void> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
@ -19,11 +41,15 @@ export class InputManager {
|
|||||||
this.keyStates.set(e.key, false);
|
this.keyStates.set(e.key, false);
|
||||||
});
|
});
|
||||||
document.addEventListener("mousedown", (e) => {
|
document.addEventListener("mousedown", (e) => {
|
||||||
this.mouseStates.set(e.button, true);
|
const button = mouseButtonToString(e.button);
|
||||||
this.mouseEvents.get(e.button)?.call(e);
|
if (!button) throw "Mouse button not found: " + e.button;
|
||||||
|
this.mouseStates.set(button, true);
|
||||||
|
this.mouseEvents.get(button)?.call(e);
|
||||||
});
|
});
|
||||||
document.addEventListener("mouseup", (e) => {
|
document.addEventListener("mouseup", (e) => {
|
||||||
this.mouseStates.set(e.button, false);
|
const button = mouseButtonToString(e.button);
|
||||||
|
if (!button) throw "Mouse button not found: " + e.button;
|
||||||
|
this.mouseStates.set(button, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("mousemove", (e) => {
|
self.addEventListener("mousemove", (e) => {
|
||||||
@ -35,10 +61,10 @@ export class InputManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeyState(key: string | number) {
|
getKeyState(key: string) {
|
||||||
return this.keyStates.get(key);
|
return this.keyStates.get(key);
|
||||||
}
|
}
|
||||||
getMouseState(key: string | number) {
|
getMouseState(key: string) {
|
||||||
return this.mouseStates.get(key);
|
return this.mouseStates.get(key);
|
||||||
}
|
}
|
||||||
getMouseLocation() {
|
getMouseLocation() {
|
||||||
@ -65,19 +91,25 @@ export class InputManager {
|
|||||||
return this.mouseDelta;
|
return this.mouseDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
onKey(key: string | number, cb: () => void) {
|
onKey(key: string, cb: () => void) {
|
||||||
this.keyEvents.set(key, cb);
|
this.keyEvents.set(key, cb);
|
||||||
}
|
}
|
||||||
onMouse(key: string | number, cb: () => void) {
|
onMouse(key: string, cb: () => void) {
|
||||||
this.mouseEvents.set(key, cb);
|
this.mouseEvents.set(key, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
offKey(key: string | number) {
|
offKey(key: string) {
|
||||||
const events = this.keyEvents.get(key);
|
const events = this.keyEvents.get(key);
|
||||||
this.keyEvents.delete(key);
|
this.keyEvents.delete(key);
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
offMouse(key: string | number) {
|
offMouse(key: string) {
|
||||||
this.mouseEvents.delete(key);
|
this.mouseEvents.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNumberKey(arg0: (arg: number) => void) {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
this.onKey(i.toString(), () => arg0(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
67
main.ts
67
main.ts
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
getContext,
|
getContext,
|
||||||
|
getContextItem,
|
||||||
setContextItem,
|
setContextItem,
|
||||||
setDefaultContext,
|
setDefaultContext,
|
||||||
} from "./lib/context.ts";
|
} from "./lib/context.ts";
|
||||||
@ -10,8 +11,9 @@ import { ResourceManager } from "./lib/resources.ts";
|
|||||||
import { addButton } from "./ui/button.ts";
|
import { addButton } from "./ui/button.ts";
|
||||||
import { TrackSystem } from "./track/system.ts";
|
import { TrackSystem } from "./track/system.ts";
|
||||||
import { StraightTrack } from "./track/shapes.ts";
|
import { StraightTrack } from "./track/shapes.ts";
|
||||||
import { StateMachine } from "./state/machine.ts";
|
import { State, StateMachine } from "./state/machine.ts";
|
||||||
import { bootstrapGameStateMachine } from "./state/states/index.ts";
|
import { bootstrapGameStateMachine } from "./state/states/index.ts";
|
||||||
|
import { GameLoop } from "./GameLoop.ts";
|
||||||
|
|
||||||
const inputManager = new InputManager();
|
const inputManager = new InputManager();
|
||||||
const resources = new ResourceManager();
|
const resources = new ResourceManager();
|
||||||
@ -19,8 +21,21 @@ const doodler = new ZoomableDoodler({
|
|||||||
fillScreen: true,
|
fillScreen: true,
|
||||||
bg: "#302040",
|
bg: "#302040",
|
||||||
});
|
});
|
||||||
|
(doodler as any as { ctx: CanvasRenderingContext2D }).ctx
|
||||||
|
.imageSmoothingEnabled = false;
|
||||||
// doodler.minScale = 0.1;
|
// doodler.minScale = 0.1;
|
||||||
(doodler as any).scale = doodler.maxScale;
|
// (doodler as any).scale = doodler.maxScale;
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
"red",
|
||||||
|
"orange",
|
||||||
|
"yellow",
|
||||||
|
"green",
|
||||||
|
"blue",
|
||||||
|
"indigo",
|
||||||
|
"purple",
|
||||||
|
"violet",
|
||||||
|
];
|
||||||
|
|
||||||
setDefaultContext({
|
setDefaultContext({
|
||||||
inputManager,
|
inputManager,
|
||||||
@ -28,27 +43,43 @@ setDefaultContext({
|
|||||||
resources,
|
resources,
|
||||||
debug: true,
|
debug: true,
|
||||||
showEnds: true,
|
showEnds: true,
|
||||||
|
colors,
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = bootstrapGameStateMachine();
|
const state = bootstrapGameStateMachine();
|
||||||
setContextItem("state", state);
|
setContextItem("state", state);
|
||||||
|
|
||||||
doodler.init();
|
doodler.init();
|
||||||
addButton({
|
|
||||||
text: "Hello World!",
|
// doodler.createLayer((_, __, dTime) => {
|
||||||
onClick: () => {
|
// state.update(dTime);
|
||||||
console.log("Hello World!");
|
// });
|
||||||
},
|
|
||||||
at: [
|
document.addEventListener("keydown", (e) => {
|
||||||
new Vector(10, doodler.height - 50),
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
new Vector(110, doodler.height - 10),
|
e.preventDefault();
|
||||||
],
|
const track = getContextItem<TrackSystem>("track");
|
||||||
style: {
|
localStorage.setItem("track", track.serialize());
|
||||||
fillColor: "blue",
|
console.log("Saved track to local storage");
|
||||||
color: "white",
|
}
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
doodler.createLayer((_, __, dTime) => {
|
setInterval(() => {
|
||||||
state.update(dTime);
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
});
|
const frameRate = doodler.fps;
|
||||||
|
if (frameRate < 0.5) return;
|
||||||
|
let fpsEl = document.getElementById("fps");
|
||||||
|
if (!fpsEl) {
|
||||||
|
fpsEl = document.createElement("div");
|
||||||
|
fpsEl.id = "fps";
|
||||||
|
document.body.appendChild(fpsEl);
|
||||||
|
}
|
||||||
|
const fPerc = frameRate / 60;
|
||||||
|
if (fPerc < 0.6) {
|
||||||
|
state.optimizePerformance(fPerc);
|
||||||
|
}
|
||||||
|
fpsEl.textContent = frameRate.toFixed(1) + " fps";
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const gameLoop = new GameLoop();
|
||||||
|
gameLoop.start(state);
|
||||||
|
3
math/clamp.ts
Normal file
3
math/clamp.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
55
math/path.ts
55
math/path.ts
@ -2,10 +2,12 @@ import { Vector } from "@bearmetal/doodler";
|
|||||||
|
|
||||||
export class ComplexPath {
|
export class ComplexPath {
|
||||||
points: Vector[] = [];
|
points: Vector[] = [];
|
||||||
|
segments: PathSegment[] = [];
|
||||||
|
|
||||||
radius = 50;
|
radius = 50;
|
||||||
|
|
||||||
ctx?: CanvasRenderingContext2D;
|
ctx?: CanvasRenderingContext2D;
|
||||||
|
evenPoints: Vector[] = [];
|
||||||
|
|
||||||
constructor(points?: Vector[]) {
|
constructor(points?: Vector[]) {
|
||||||
points && (this.points = points);
|
points && (this.points = points);
|
||||||
@ -35,15 +37,66 @@ export class ComplexPath {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
followEvenPoints(t: number) {
|
||||||
|
if (t < 0) t += this.evenPoints.length;
|
||||||
|
const i = Math.floor(t);
|
||||||
|
const a = this.evenPoints[i];
|
||||||
|
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
|
||||||
|
|
||||||
|
return Vector.lerp(a, b, t % 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||||
|
// this.pointSpacing = 1;
|
||||||
|
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
|
||||||
|
const points: Vector[] = [];
|
||||||
|
|
||||||
|
points.push(this.segments[0].points[0]);
|
||||||
|
let prev = points[0];
|
||||||
|
let distSinceLastEvenPoint = 0;
|
||||||
|
for (const seg of this.segments) {
|
||||||
|
let t = 0;
|
||||||
|
|
||||||
|
const div = Math.ceil(seg.length * resolution * 10);
|
||||||
|
while (t < 1) {
|
||||||
|
t += 1 / div;
|
||||||
|
const point = seg.getPointAtT(t);
|
||||||
|
distSinceLastEvenPoint += prev.dist(point);
|
||||||
|
|
||||||
|
if (distSinceLastEvenPoint >= spacing) {
|
||||||
|
const overshoot = distSinceLastEvenPoint - spacing;
|
||||||
|
const evenPoint = Vector.add(
|
||||||
|
point,
|
||||||
|
Vector.sub(point, prev).normalize().mult(overshoot),
|
||||||
|
);
|
||||||
|
distSinceLastEvenPoint = overshoot;
|
||||||
|
points.push(evenPoint);
|
||||||
|
prev = evenPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.evenPoints = points;
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PathSegment {
|
export class PathSegment {
|
||||||
|
id: string;
|
||||||
points: [Vector, Vector, Vector, Vector];
|
points: [Vector, Vector, Vector, Vector];
|
||||||
|
|
||||||
length: number;
|
length: number;
|
||||||
startingLength: number;
|
startingLength: number;
|
||||||
|
|
||||||
|
next?: PathSegment;
|
||||||
|
prev?: PathSegment;
|
||||||
|
|
||||||
constructor(points: [Vector, Vector, Vector, Vector]) {
|
constructor(points: [Vector, Vector, Vector, Vector]) {
|
||||||
|
this.id = crypto.randomUUID();
|
||||||
this.points = points;
|
this.points = points;
|
||||||
this.length = this.calculateApproxLength(100);
|
this.length = this.calculateApproxLength(100);
|
||||||
this.startingLength = Math.round(this.length);
|
this.startingLength = Math.round(this.length);
|
||||||
@ -254,4 +307,6 @@ export class PathSegment {
|
|||||||
this.points[3].set(points[curveLength]);
|
this.points[3].set(points[curveLength]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draw(): void {}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { lerp } from "./math/lerp.ts";
|
import { lerp } from "./math/lerp.ts";
|
||||||
import { ComplexPath, PathSegment } from "./math/path.ts";
|
import { ComplexPath, PathSegment } from "./math/path.ts";
|
||||||
import { Mover } from "./physics/mover.ts";
|
import { Mover } from "./physics/mover.ts";
|
||||||
import { Train, TrainCar } from "./train/train.ts";
|
import { Train, TrainCar } from "./train/train.old.ts";
|
||||||
import { generateSquareTrack, IControlNode, loadFromJson } from "./track.ts";
|
import { generateSquareTrack, IControlNode, loadFromJson } from "./track.ts";
|
||||||
import { drawLine } from "./drawing/line.ts";
|
import { drawLine } from "./drawing/line.ts";
|
||||||
import { initializeDoodler, Vector } from "doodler";
|
import { initializeDoodler, Vector } from "doodler";
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { getContext } from "../lib/context.ts";
|
||||||
|
import { TrackSystem } from "../track/system.ts";
|
||||||
|
import { Train } from "../train.old.ts";
|
||||||
|
|
||||||
export class StateMachine<T> {
|
export class StateMachine<T> {
|
||||||
private _states: Map<T, State<T>> = new Map();
|
private _states: Map<T, State<T>> = new Map();
|
||||||
private currentState?: State<T>;
|
private currentState?: State<T>;
|
||||||
@ -6,6 +10,13 @@ export class StateMachine<T> {
|
|||||||
this.currentState?.update(dt, ctx);
|
this.currentState?.update(dt, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optimizePerformance(percent: number) {
|
||||||
|
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||||
|
if (percent < 0.5) {
|
||||||
|
ctx.track.optimize(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get current() {
|
get current() {
|
||||||
return this.currentState;
|
return this.currentState;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||||
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
import {
|
||||||
|
getContextItem,
|
||||||
|
getContextItemOrDefault,
|
||||||
|
setContextItem,
|
||||||
|
} from "../../lib/context.ts";
|
||||||
import { InputManager } from "../../lib/input.ts";
|
import { InputManager } from "../../lib/input.ts";
|
||||||
import { TrackSystem } from "../../track/system.ts";
|
import { TrackSystem } from "../../track/system.ts";
|
||||||
import { State, StateMachine } from "../machine.ts";
|
import { State, StateMachine } from "../machine.ts";
|
||||||
import { States } from "./index.ts";
|
import { States } from "./index.ts";
|
||||||
import { StraightTrack } from "../../track/shapes.ts";
|
import {
|
||||||
|
BankLeft,
|
||||||
|
BankRight,
|
||||||
|
SBendLeft,
|
||||||
|
SBendRight,
|
||||||
|
StraightTrack,
|
||||||
|
} from "../../track/shapes.ts";
|
||||||
import { TrackSegment } from "../../track/system.ts";
|
import { TrackSegment } from "../../track/system.ts";
|
||||||
|
import { clamp } from "../../math/clamp.ts";
|
||||||
|
|
||||||
export class EditTrackState extends State<States> {
|
export class EditTrackState extends State<States> {
|
||||||
override name: States = States.EDIT_TRACK;
|
override name: States = States.EDIT_TRACK;
|
||||||
@ -14,14 +25,20 @@ export class EditTrackState extends State<States> {
|
|||||||
States.PAUSED,
|
States.PAUSED,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private heldEvents: Map<string | number, (() => void) | undefined> =
|
private heldEvents: Map<string, (() => void) | undefined> = new Map();
|
||||||
new Map();
|
|
||||||
|
|
||||||
private currentSegment?: TrackSegment;
|
private currentSegment?: TrackSegment;
|
||||||
|
private selectedSegment?: TrackSegment;
|
||||||
|
private ghostSegment?: TrackSegment;
|
||||||
|
private ghostRotated = false;
|
||||||
|
private closestEnd?: End;
|
||||||
|
|
||||||
|
layers: number[] = [];
|
||||||
|
|
||||||
override update(dt: number): void {
|
override update(dt: number): void {
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
const track = getContextItem<TrackSystem>("track");
|
const track = getContextItem<TrackSystem>("track");
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
|
||||||
// For moving a segment, i.e. the currently active one
|
// For moving a segment, i.e. the currently active one
|
||||||
// const segment = track.lastSegment;
|
// const segment = track.lastSegment;
|
||||||
@ -35,6 +52,96 @@ export class EditTrackState extends State<States> {
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (this.selectedSegment) {
|
||||||
|
const segment = this.selectedSegment;
|
||||||
|
const firstPoint = segment.points[0].copy();
|
||||||
|
const mousePos = inputManager.getMouseLocationV();
|
||||||
|
segment.points.forEach((p, i) => {
|
||||||
|
const relativePoint = Vector.sub(p, firstPoint);
|
||||||
|
p.set(mousePos);
|
||||||
|
p.add(relativePoint);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ends = track.findEnds();
|
||||||
|
setContextItem("showEnds", true);
|
||||||
|
const nearbyEnds = ends.filter((end) => {
|
||||||
|
const dist = Vector.dist(end.pos, mousePos);
|
||||||
|
return dist < 20 && end.segment !== segment;
|
||||||
|
});
|
||||||
|
let closestEnd = nearbyEnds[0];
|
||||||
|
for (const end of nearbyEnds) {
|
||||||
|
if (end === closestEnd) continue;
|
||||||
|
const closestEndTangent = Vector.add(
|
||||||
|
closestEnd.tangent.copy().mult(20),
|
||||||
|
closestEnd.pos,
|
||||||
|
);
|
||||||
|
const endTangent = Vector.add(
|
||||||
|
end.tangent.copy().rotate(Math.PI).mult(20),
|
||||||
|
end.pos,
|
||||||
|
);
|
||||||
|
doodler.drawCircle(closestEndTangent, 4, { color: "red", weight: 1 });
|
||||||
|
doodler.drawCircle(endTangent, 4, { color: "blue", weight: 1 });
|
||||||
|
if (
|
||||||
|
endTangent.dist(mousePos) < closestEndTangent.dist(mousePos) ||
|
||||||
|
end.pos.dist(mousePos) < closestEnd.pos.dist(mousePos)
|
||||||
|
) {
|
||||||
|
closestEnd = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closestEnd !== this.closestEnd) {
|
||||||
|
this.closestEnd = closestEnd;
|
||||||
|
this.ghostSegment = undefined;
|
||||||
|
this.ghostRotated = false;
|
||||||
|
}
|
||||||
|
if (closestEnd) {
|
||||||
|
// doodler.drawCircle(closestEnd.pos, 4, { color: "green", weight: 1 });
|
||||||
|
doodler.line(
|
||||||
|
closestEnd.pos,
|
||||||
|
Vector.add(closestEnd.pos, closestEnd.tangent.copy().mult(20)),
|
||||||
|
{ color: "green" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.closestEnd
|
||||||
|
) {
|
||||||
|
if (!this.ghostSegment) {
|
||||||
|
this.ghostSegment = segment.copy();
|
||||||
|
this.ghostRotated = false;
|
||||||
|
}
|
||||||
|
switch (this.closestEnd.frontOrBack) {
|
||||||
|
case "front":
|
||||||
|
this.ghostSegment.setPositionByPoint(
|
||||||
|
this.closestEnd.pos,
|
||||||
|
this.ghostSegment.points[0],
|
||||||
|
);
|
||||||
|
// this.ghostSegment.points[0] = this.closestEnd.pos;
|
||||||
|
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
||||||
|
this.closestEnd.tangent.heading(),
|
||||||
|
this.ghostSegment.points[0],
|
||||||
|
);
|
||||||
|
this.ghostRotated = true;
|
||||||
|
break;
|
||||||
|
case "back":
|
||||||
|
this.ghostSegment.setPositionByPoint(
|
||||||
|
this.closestEnd.pos,
|
||||||
|
this.ghostSegment.points[3],
|
||||||
|
);
|
||||||
|
// this.ghostSegment.points[3] = this.closestEnd.pos;
|
||||||
|
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
||||||
|
this.closestEnd.tangent.heading(),
|
||||||
|
this.ghostSegment.points[3],
|
||||||
|
);
|
||||||
|
this.ghostRotated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// } else if (closestEnd) {
|
||||||
|
// this.closestEnd = closestEnd;
|
||||||
|
} else if (!this.closestEnd || !closestEnd) {
|
||||||
|
this.ghostSegment = undefined;
|
||||||
|
this.ghostRotated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// manipulate only end of segment while maintaining length
|
// manipulate only end of segment while maintaining length
|
||||||
// const segment = track.lastSegment;
|
// const segment = track.lastSegment;
|
||||||
// if (segment) {
|
// if (segment) {
|
||||||
@ -50,65 +157,135 @@ export class EditTrackState extends State<States> {
|
|||||||
|
|
||||||
// // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100);
|
// // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100);
|
||||||
// }
|
// }
|
||||||
const doodler = getContextItem<Doodler>("doodler");
|
|
||||||
|
|
||||||
// Adjust angles until tangent points to mouse
|
// Adjust angles until tangent points to mouse
|
||||||
const segment = this.currentSegment;
|
// const segment = this.currentSegment;
|
||||||
if (segment) {
|
// if (segment) {
|
||||||
segment.propagate();
|
// segment.propagate();
|
||||||
|
|
||||||
const mousePos = inputManager.getMouseLocationV();
|
// const mousePos = inputManager.getMouseLocationV();
|
||||||
const p1 = segment.points[0];
|
// const p1 = segment.points[0];
|
||||||
const p2 = segment.points[1];
|
// const p2 = segment.points[1];
|
||||||
const p3 = segment.points[2];
|
// const p3 = segment.points[2];
|
||||||
const p4 = segment.points[3];
|
// const p4 = segment.points[3];
|
||||||
|
|
||||||
const prevp3 = p3.copy();
|
// const prevp3 = p3.copy();
|
||||||
const dirToMouse = Vector.sub(mousePos, p2).normalize();
|
// const dirToMouse = Vector.sub(mousePos, p2).normalize();
|
||||||
const angleToMouse = dirToMouse.heading();
|
// const angleToMouse = dirToMouse.heading();
|
||||||
const angleToP1 = Vector.sub(p2, p1).heading();
|
// const dirToP1 = Vector.sub(p2, p1).normalize();
|
||||||
const p2DistToMouse = Vector.dist(p2, mousePos);
|
// const angleToP1 = dirToP1.heading();
|
||||||
const p3DistToMouse = Vector.dist(p3, mousePos);
|
// const p2DistToMouse = Vector.dist(p2, mousePos);
|
||||||
const distToP3 = Vector.dist(p2, p3);
|
// const p3DistToMouse = Vector.dist(p3, mousePos);
|
||||||
const distToP4 = Vector.dist(prevp3, p4);
|
// const distToP3 = Vector.dist(p2, p3);
|
||||||
if (
|
// const distToP4 = Vector.dist(prevp3, p4);
|
||||||
Math.abs(angleToMouse - angleToP1) < .6 &&
|
|
||||||
p2DistToMouse > distToP3 &&
|
// const goodangle = clamp(
|
||||||
p3DistToMouse > distToP4
|
// angleToMouse - angleToP1,
|
||||||
) {
|
// angleToP1 - .6,
|
||||||
{
|
// angleToP1 + .6,
|
||||||
const dirToNewP3 = dirToMouse.copy().rotate(
|
// );
|
||||||
-(angleToMouse - angleToP1) / 2,
|
// if (
|
||||||
);
|
// // Math.abs(goodangle) < .6 &&
|
||||||
dirToNewP3.setMag(distToP3);
|
// p2DistToMouse > distToP3 &&
|
||||||
p3.set(Vector.add(p2, dirToNewP3));
|
// p3DistToMouse > distToP4
|
||||||
doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
|
// ) {
|
||||||
doodler.line(p2, Vector.add(p2, dirToMouse.mult(100)), {
|
// {
|
||||||
color: "red",
|
// const dirToNewP3 = dirToP1.copy().rotate(
|
||||||
});
|
// goodangle / 2,
|
||||||
}
|
// );
|
||||||
{
|
// dirToNewP3.setMag(distToP3);
|
||||||
const dirToMouse = Vector.sub(mousePos, p3).normalize();
|
// p3.set(Vector.add(p2, dirToNewP3));
|
||||||
dirToMouse.setMag(distToP4);
|
// doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
|
||||||
p4.set(Vector.add(p3, dirToMouse));
|
// doodler.line(
|
||||||
doodler.line(p3, Vector.add(p3, dirToMouse), { color: "green" });
|
// p2,
|
||||||
}
|
// Vector.add(p2, dirToNewP3),
|
||||||
segment.clampLength();
|
// {
|
||||||
}
|
// color: "red",
|
||||||
doodler.fillText(
|
// },
|
||||||
segment.calculateApproxLength().toFixed(2),
|
// );
|
||||||
p2.copy().add(10, 0),
|
// }
|
||||||
100,
|
// {
|
||||||
);
|
// const dirToMouse = Vector.sub(mousePos, p3).normalize();
|
||||||
|
// const dirToP3 = Vector.sub(p3, p2).normalize();
|
||||||
|
// const angleToP3 = dirToP3.heading();
|
||||||
|
// const goodangle = clamp(
|
||||||
|
// dirToMouse.heading() - angleToP3,
|
||||||
|
// angleToP3 - .6,
|
||||||
|
// angleToP3 + .6,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const dirToNewP4 = dirToP3.copy().rotate(
|
||||||
|
// goodangle / 2,
|
||||||
|
// );
|
||||||
|
// dirToNewP4.setMag(distToP4);
|
||||||
|
// p4.set(Vector.add(p3, dirToNewP4));
|
||||||
|
// doodler.line(p3, Vector.add(p3, dirToNewP4), { color: "green" });
|
||||||
|
// }
|
||||||
|
// segment.clampLength();
|
||||||
|
// }
|
||||||
|
// // doodler.fillText(
|
||||||
|
// // segment.calculateApproxLength().toFixed(2),
|
||||||
|
// // p2.copy().add(10, 0),
|
||||||
|
// // 100,
|
||||||
|
// // );
|
||||||
|
// }
|
||||||
|
|
||||||
|
const translation = new Vector(0, 0);
|
||||||
|
|
||||||
|
if (inputManager.getKeyState("ArrowUp")) {
|
||||||
|
translation.y -= 1;
|
||||||
|
}
|
||||||
|
if (inputManager.getKeyState("ArrowDown")) {
|
||||||
|
translation.y += 1;
|
||||||
|
}
|
||||||
|
if (inputManager.getKeyState("ArrowLeft")) {
|
||||||
|
translation.x -= 1;
|
||||||
|
}
|
||||||
|
if (inputManager.getKeyState("ArrowRight")) {
|
||||||
|
translation.x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translation.x !== 0 || translation.y !== 0) {
|
||||||
|
track.translate(translation);
|
||||||
}
|
}
|
||||||
|
|
||||||
track.draw(true);
|
|
||||||
// TODO
|
// TODO
|
||||||
// Draw ui
|
// Draw ui
|
||||||
// Draw track points
|
// Draw track points
|
||||||
// Draw track tangents
|
// Draw track tangents
|
||||||
}
|
}
|
||||||
override start(): void {
|
override start(): void {
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
this.layers.push(
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
this.selectedSegment?.draw();
|
||||||
|
if (this.ghostSegment) {
|
||||||
|
doodler.drawWithAlpha(0.5, () => {
|
||||||
|
if (!this.ghostSegment) return;
|
||||||
|
this.ghostSegment.draw();
|
||||||
|
if (getContextItemOrDefault("debug", false)) {
|
||||||
|
const colors = getContextItem<string[]>("colors");
|
||||||
|
for (
|
||||||
|
const [i, point] of this.ghostSegment.points.entries() ?? []
|
||||||
|
) {
|
||||||
|
doodler.fillCircle(point, 4, { color: colors[i + 3] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
track.draw(true);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setContextItem("trackSegments", [
|
||||||
|
undefined,
|
||||||
|
new StraightTrack(),
|
||||||
|
new SBendLeft(),
|
||||||
|
new SBendRight(),
|
||||||
|
new BankLeft(),
|
||||||
|
new BankRight(),
|
||||||
|
]);
|
||||||
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
this.heldEvents.set("e", inputManager.offKey("e"));
|
this.heldEvents.set("e", inputManager.offKey("e"));
|
||||||
this.heldEvents.set("Escape", inputManager.offKey("Escape"));
|
this.heldEvents.set("Escape", inputManager.offKey("Escape"));
|
||||||
@ -127,31 +304,103 @@ export class EditTrackState extends State<States> {
|
|||||||
state.transitionTo(States.RUNNING);
|
state.transitionTo(States.RUNNING);
|
||||||
});
|
});
|
||||||
|
|
||||||
inputManager.onKey("w", () => {
|
inputManager.onKey(" ", () => {
|
||||||
|
if (this.selectedSegment) {
|
||||||
|
this.selectedSegment = undefined;
|
||||||
|
} else {
|
||||||
|
this.selectedSegment = new StraightTrack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
inputManager.onMouse("left", () => {
|
||||||
const track = getContextItem<TrackSystem>("track");
|
const track = getContextItem<TrackSystem>("track");
|
||||||
const segment = track.lastSegment;
|
if (this.ghostSegment && this.closestEnd) {
|
||||||
if (!segment) return;
|
const segment = this.ghostSegment.cleanCopy();
|
||||||
const n = new StraightTrack(segment.points[3]);
|
|
||||||
const t = segment.tangent(1).heading();
|
switch (this.closestEnd.frontOrBack) {
|
||||||
n.rotate(t);
|
case "front":
|
||||||
segment.frontNeighbours.push(n);
|
this.closestEnd.segment.frontNeighbours.push(segment);
|
||||||
track.registerSegment(n);
|
segment.backNeighbours.push(this.closestEnd.segment);
|
||||||
this.currentSegment = n;
|
break;
|
||||||
|
case "back":
|
||||||
|
this.closestEnd.segment.backNeighbours.push(segment);
|
||||||
|
segment.frontNeighbours.push(this.closestEnd.segment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
track.registerSegment(segment);
|
||||||
|
this.ghostSegment = undefined;
|
||||||
|
this.closestEnd = undefined;
|
||||||
|
} else if (this.selectedSegment) {
|
||||||
|
track.registerSegment(this.selectedSegment.cleanCopy());
|
||||||
|
// this.selectedSegment = new StraightTrack();
|
||||||
|
} else {
|
||||||
|
this.selectedSegment = undefined;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
inputManager.onKey("1", () => {
|
// inputManager.onKey("w", () => {
|
||||||
this.currentSegment = track.firstSegment;
|
// const track = getContextItem<TrackSystem>("track");
|
||||||
|
// const segment = track.lastSegment;
|
||||||
|
// if (!segment) return;
|
||||||
|
// const n = new StraightTrack(segment.points[3]);
|
||||||
|
// const t = segment.tangent(1).heading();
|
||||||
|
// n.rotate(t);
|
||||||
|
// segment.frontNeighbours.push(n);
|
||||||
|
// track.registerSegment(n);
|
||||||
|
// this.currentSegment = n;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// inputManager.onKey("1", () => {
|
||||||
|
// this.currentSegment = track.firstSegment;
|
||||||
|
// });
|
||||||
|
|
||||||
|
inputManager.onNumberKey((i) => {
|
||||||
|
const segments = getContextItem<TrackSegment[]>("trackSegments");
|
||||||
|
this.selectedSegment = segments[i];
|
||||||
|
this.ghostRotated = false;
|
||||||
|
this.ghostSegment = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentSegment = track.lastSegment;
|
// this.currentSegment = track.lastSegment;
|
||||||
|
inputManager.onKey("z", () => {
|
||||||
|
if (inputManager.getKeyState("Control")) {
|
||||||
|
const segment = track.lastSegment;
|
||||||
|
if (!segment) return;
|
||||||
|
this.redoBuffer.push(segment);
|
||||||
|
if (this.redoBuffer.length > 100) {
|
||||||
|
this.redoBuffer.shift();
|
||||||
|
}
|
||||||
|
track.unregisterSegment(segment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
inputManager.onKey("y", () => {
|
||||||
|
if (inputManager.getKeyState("Control")) {
|
||||||
|
const segment = this.redoBuffer.pop();
|
||||||
|
if (!segment) return;
|
||||||
|
track.registerSegment(segment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// Cache trains and save
|
// Cache trains and save
|
||||||
|
|
||||||
|
// const trackCount = 2000;
|
||||||
|
// for (let i = 0; i < trackCount; i++) {
|
||||||
|
// const seg = new StraightTrack();
|
||||||
|
// track.registerSegment(seg);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
redoBuffer: TrackSegment[] = [];
|
||||||
override stop(): void {
|
override stop(): void {
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
||||||
|
}
|
||||||
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
inputManager.offKey("e");
|
inputManager.offKey("e");
|
||||||
|
inputManager.offKey("w");
|
||||||
inputManager.offKey("Escape");
|
inputManager.offKey("Escape");
|
||||||
|
inputManager.offMouse("left");
|
||||||
if (this.heldEvents.size > 0) {
|
if (this.heldEvents.size > 0) {
|
||||||
for (const [key, cb] of this.heldEvents) {
|
for (const [key, cb] of this.heldEvents) {
|
||||||
if (cb) {
|
if (cb) {
|
||||||
@ -161,5 +410,6 @@ export class EditTrackState extends State<States> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setContextItem("trackCopy", undefined);
|
setContextItem("trackCopy", undefined);
|
||||||
|
setContextItem("trackSegments", undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||||
import { bootstrapInputs } from "../../inputs.ts";
|
import { bootstrapInputs } from "../../inputs.ts";
|
||||||
import { setContextItem } from "../../lib/context.ts";
|
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
||||||
import { InputManager } from "../../lib/input.ts";
|
import { InputManager } from "../../lib/input.ts";
|
||||||
import { ResourceManager } from "../../lib/resources.ts";
|
import { ResourceManager } from "../../lib/resources.ts";
|
||||||
import { StraightTrack } from "../../track/shapes.ts";
|
import { StraightTrack } from "../../track/shapes.ts";
|
||||||
@ -13,6 +14,8 @@ export class LoadState extends State<States> {
|
|||||||
States.RUNNING,
|
States.RUNNING,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
layers: number[] = [];
|
||||||
|
|
||||||
override update(): void {
|
override update(): void {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
@ -31,7 +34,20 @@ export class LoadState extends State<States> {
|
|||||||
|
|
||||||
bootstrapInputs();
|
bootstrapInputs();
|
||||||
|
|
||||||
this.stateMachine.transitionTo(States.RUNNING);
|
resources.set("engine-sprites", new Image());
|
||||||
|
resources.get<HTMLImageElement>("engine-sprites")!.src =
|
||||||
|
"/sprites/EngineSprites.png";
|
||||||
|
resources.ready().then(() => {
|
||||||
|
this.stateMachine.transitionTo(States.RUNNING);
|
||||||
|
});
|
||||||
|
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
this.layers.push(doodler.createLayer((_, __, dTime) => {
|
||||||
|
doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
|
||||||
|
doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
|
||||||
|
color: "#302040",
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
override stop(): void {
|
override stop(): void {
|
||||||
// noop
|
// noop
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { getContext } from "../../lib/context.ts";
|
import { Doodler } from "@bearmetal/doodler";
|
||||||
|
import { getContext, getContextItem } from "../../lib/context.ts";
|
||||||
|
import { InputManager } from "../../lib/input.ts";
|
||||||
import { TrackSystem } from "../../track/system.ts";
|
import { TrackSystem } from "../../track/system.ts";
|
||||||
|
import { Tender } from "../../train/cars.ts";
|
||||||
|
import { RedEngine } from "../../train/engines.ts";
|
||||||
|
import { DotFollower } from "../../train/newTrain.ts";
|
||||||
import { Train } from "../../train/train.ts";
|
import { Train } from "../../train/train.ts";
|
||||||
import { State } from "../machine.ts";
|
import { State } from "../machine.ts";
|
||||||
import { States } from "./index.ts";
|
import { States } from "./index.ts";
|
||||||
@ -10,23 +15,74 @@ export class RunningState extends State<States> {
|
|||||||
States.PAUSED,
|
States.PAUSED,
|
||||||
States.EDIT_TRACK,
|
States.EDIT_TRACK,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
layers: number[] = [];
|
||||||
|
|
||||||
override update(dt: number): void {
|
override update(dt: number): void {
|
||||||
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
||||||
|
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
|
||||||
// TODO
|
// TODO
|
||||||
// Update trains
|
// Update trains
|
||||||
// Update world
|
// Update world
|
||||||
// Handle input
|
// Handle input
|
||||||
// Draw (maybe via a layer system that syncs with doodler)
|
// Draw (maybe via a layer system that syncs with doodler)
|
||||||
ctx.track.draw();
|
|
||||||
for (const train of ctx.trains) {
|
|
||||||
train.draw();
|
|
||||||
}
|
|
||||||
// Monitor world events
|
// Monitor world events
|
||||||
|
for (const train of ctx.trains) {
|
||||||
|
train.move(dt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override start(): void {
|
override start(): void {
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
this.layers.push(
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
const track = getContextItem<TrackSystem>("track");
|
||||||
|
track.draw();
|
||||||
|
}),
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
const trains = getContextItem<Train[]>("trains");
|
||||||
|
for (const train of trains) {
|
||||||
|
// if (input.getKeyState("ArrowUp")) {
|
||||||
|
// train.acceleration.x += 10;
|
||||||
|
// }
|
||||||
|
train.draw();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// noop
|
// noop
|
||||||
|
const inputManager = getContextItem<InputManager>("inputManager");
|
||||||
|
const track = getContextItem<TrackSystem>("track");
|
||||||
|
const ctx = getContext() as { trains: Train[] };
|
||||||
|
// const ctx = getContext() as { trains: DotFollower[] };
|
||||||
|
inputManager.onKey(" ", () => {
|
||||||
|
// const path = track.path;
|
||||||
|
// const follower = new DotFollower(path, path.points[0].copy());
|
||||||
|
// ctx.trains.push(follower);
|
||||||
|
const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||||
|
ctx.trains.push(train);
|
||||||
|
});
|
||||||
|
// const trainCount = 2000;
|
||||||
|
// for (let i = 0; i < trainCount; i++) {
|
||||||
|
// const train = new Train(track.path, [new RedEngine(), new Tender()]);
|
||||||
|
// ctx.trains.push(train);
|
||||||
|
// }
|
||||||
|
|
||||||
|
inputManager.onKey("ArrowUp", () => {
|
||||||
|
const trains = getContextItem<Train[]>("trains");
|
||||||
|
for (const train of trains) {
|
||||||
|
train.speed += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
inputManager.onKey("ArrowDown", () => {
|
||||||
|
const trains = getContextItem<Train[]>("trains");
|
||||||
|
for (const train of trains) {
|
||||||
|
train.speed -= 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
override stop(): void {
|
override stop(): void {
|
||||||
// noop
|
for (const layer of this.layers) {
|
||||||
|
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
test/trackSystemBench.test.ts
Normal file
35
test/trackSystemBench.test.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { assert } from "jsr:@std/assert";
|
||||||
|
import { describe, it } from "jsr:@std/testing/bdd";
|
||||||
|
import { TrackSystem } from "../track/system.ts";
|
||||||
|
import { StraightTrack } from "../track/shapes.ts";
|
||||||
|
import { testPerformance } from "./bench.ts";
|
||||||
|
import { setDefaultContext } from "../lib/context.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests if a function can run a given number of iterations within a target frame time.
|
||||||
|
* @param fn The function to test.
|
||||||
|
* @param iterations Number of times to run the function per frame.
|
||||||
|
* @param fps Target frames per second.
|
||||||
|
*/
|
||||||
|
Deno.test("Track System Benchmark", () => {
|
||||||
|
console.log("Track System Benchmark - run within frame time");
|
||||||
|
const mockDoodler = {
|
||||||
|
fillCircle: () => {},
|
||||||
|
line: () => {},
|
||||||
|
};
|
||||||
|
setDefaultContext({
|
||||||
|
doodler: mockDoodler,
|
||||||
|
});
|
||||||
|
const mockTrack = new TrackSystem([]);
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
mockTrack.registerSegment(new StraightTrack());
|
||||||
|
}
|
||||||
|
|
||||||
|
testPerformance(
|
||||||
|
() => {
|
||||||
|
mockTrack.findEnds();
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
60,
|
||||||
|
);
|
||||||
|
});
|
@ -12,3 +12,79 @@ export class StraightTrack extends TrackSegment {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SBendLeft extends TrackSegment {
|
||||||
|
constructor(start?: Vector) {
|
||||||
|
start = start || new Vector(100, 100);
|
||||||
|
super([
|
||||||
|
start,
|
||||||
|
start.copy().add(60, 0),
|
||||||
|
start.copy().add(90, -25),
|
||||||
|
start.copy().add(150, -25),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class SBendRight extends TrackSegment {
|
||||||
|
constructor(start?: Vector) {
|
||||||
|
start = start || new Vector(100, 100);
|
||||||
|
super([
|
||||||
|
start,
|
||||||
|
start.copy().add(60, 0),
|
||||||
|
start.copy().add(90, 25),
|
||||||
|
start.copy().add(150, 25),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BankLeft extends TrackSegment {
|
||||||
|
constructor(start?: Vector) {
|
||||||
|
start = start || new Vector(100, 100);
|
||||||
|
|
||||||
|
const p1 = start.copy();
|
||||||
|
const p2 = start.copy();
|
||||||
|
const p3 = start.copy();
|
||||||
|
const p4 = start.copy();
|
||||||
|
const scale = 33;
|
||||||
|
|
||||||
|
p2.add(new Vector(1, 0).mult(scale));
|
||||||
|
p3.set(p2);
|
||||||
|
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
|
||||||
|
p3.add(dirToP3);
|
||||||
|
p4.set(p3);
|
||||||
|
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
|
||||||
|
p4.add(dirToP4);
|
||||||
|
|
||||||
|
super([
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
p3,
|
||||||
|
p4,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class BankRight extends TrackSegment {
|
||||||
|
constructor(start?: Vector) {
|
||||||
|
start = start || new Vector(100, 100);
|
||||||
|
|
||||||
|
const p1 = start.copy();
|
||||||
|
const p2 = start.copy();
|
||||||
|
const p3 = start.copy();
|
||||||
|
const p4 = start.copy();
|
||||||
|
const scale = 33;
|
||||||
|
|
||||||
|
p2.add(new Vector(1, 0).mult(scale));
|
||||||
|
p3.set(p2);
|
||||||
|
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
|
||||||
|
p3.add(dirToP3);
|
||||||
|
p4.set(p3);
|
||||||
|
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
|
||||||
|
p4.add(dirToP4);
|
||||||
|
|
||||||
|
super([
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
p3,
|
||||||
|
p4,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
561
track/system.ts
561
track/system.ts
@ -1,6 +1,7 @@
|
|||||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
import { Doodler, Point, Vector } from "@bearmetal/doodler";
|
||||||
import { PathSegment } from "../math/path.ts";
|
import { ComplexPath, PathSegment } from "../math/path.ts";
|
||||||
import { getContextItem, setDefaultContext } from "../lib/context.ts";
|
import { getContextItem, setDefaultContext } from "../lib/context.ts";
|
||||||
|
import { clamp } from "../math/clamp.ts";
|
||||||
|
|
||||||
export class TrackSystem {
|
export class TrackSystem {
|
||||||
private segments: Map<string, TrackSegment> = new Map();
|
private segments: Map<string, TrackSegment> = new Map();
|
||||||
@ -21,61 +22,85 @@ export class TrackSystem {
|
|||||||
return this.segments.values().toArray().pop();
|
return this.segments.values().toArray().pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optimize(percent: number) {
|
||||||
|
console.log("Optimizing track", percent * 100 / 4);
|
||||||
|
for (const segment of this.segments.values()) {
|
||||||
|
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerSegment(segment: TrackSegment) {
|
registerSegment(segment: TrackSegment) {
|
||||||
segment.setTrack(this);
|
segment.setTrack(this);
|
||||||
this.segments.set(segment.id, segment);
|
this.segments.set(segment.id, segment);
|
||||||
}
|
}
|
||||||
|
unregisterSegment(segment: TrackSegment) {
|
||||||
|
this.segments.delete(segment.id);
|
||||||
|
for (const s of this.segments.values()) {
|
||||||
|
s.backNeighbours = s.backNeighbours.filter((n) => n !== segment);
|
||||||
|
s.frontNeighbours = s.frontNeighbours.filter((n) => n !== segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
draw(showControls = false) {
|
draw(showControls = false) {
|
||||||
for (const segment of this.segments.values()) {
|
for (const [i, segment] of this.segments.entries()) {
|
||||||
segment.draw(showControls);
|
segment.draw(showControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
if (getContextItem<boolean>("showEnds")) {
|
// if (getContextItem<boolean>("showEnds")) {
|
||||||
const ends = this.findEnds();
|
// const ends = this.findEnds();
|
||||||
for (const end of ends) {
|
// for (const end of ends) {
|
||||||
this.doodler.fillCircle(end.pos, 2, {
|
// this.doodler.fillCircle(end.pos, 2, {
|
||||||
color: "red",
|
// color: "red",
|
||||||
// weight: 3,
|
// // weight: 3,
|
||||||
});
|
// });
|
||||||
if (getContextItem<boolean>("debug")) {
|
// if (getContextItem<boolean>("debug")) {
|
||||||
this.doodler.line(
|
// this.doodler.line(
|
||||||
end.pos,
|
// end.pos,
|
||||||
end.pos.copy().add(end.tangent.copy().mult(20)),
|
// end.pos.copy().add(end.tangent.copy().mult(20)),
|
||||||
{
|
// {
|
||||||
color: "blue",
|
// color: "blue",
|
||||||
// weight: 3,
|
// // weight: 3,
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch {
|
// } catch {
|
||||||
setDefaultContext({ showEnds: false });
|
// setDefaultContext({ showEnds: false });
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ends: Map<TrackSegment, [End, End]> = new Map();
|
||||||
|
endArray: End[] = [];
|
||||||
|
|
||||||
findEnds() {
|
findEnds() {
|
||||||
const ends: { pos: Vector; segment: TrackSegment; tangent: Vector }[] = [];
|
|
||||||
for (const segment of this.segments.values()) {
|
for (const segment of this.segments.values()) {
|
||||||
const [a, b, c, d] = segment.points;
|
if (this.ends.has(segment)) continue;
|
||||||
{
|
const ends: [End, End] = [
|
||||||
const tangent = Vector.sub(a, b).normalize();
|
{
|
||||||
const pos = a.copy();
|
pos: segment.points[0],
|
||||||
ends.push({ pos, segment, tangent });
|
segment,
|
||||||
}
|
tangent: Vector.sub(segment.points[1], segment.points[0]).normalize(),
|
||||||
{
|
frontOrBack: "back",
|
||||||
const tangent = Vector.sub(d, c).normalize();
|
},
|
||||||
const pos = d.copy();
|
{
|
||||||
ends.push({ pos, segment, tangent });
|
pos: segment.points[3],
|
||||||
}
|
segment,
|
||||||
|
tangent: Vector.sub(segment.points[3], segment.points[2]).normalize(),
|
||||||
|
frontOrBack: "front",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this.ends.set(segment, ends);
|
||||||
|
this.endArray.push(...ends);
|
||||||
}
|
}
|
||||||
return ends;
|
return this.endArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize() {
|
serialize() {
|
||||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
return JSON.stringify(
|
||||||
|
this.segments.values().map((s) => s.serialize()).toArray(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
copy() {
|
copy() {
|
||||||
@ -86,13 +111,16 @@ export class TrackSystem {
|
|||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
static deserialize(data: any) {
|
static deserialize(data: SerializedTrackSegment[]) {
|
||||||
if (data.length === 0) return undefined;
|
if (data.length === 0) return undefined;
|
||||||
const track = new TrackSystem([]);
|
const track = new TrackSystem([]);
|
||||||
const neighborMap = new Map<string, [string[], string[]]>();
|
const neighborMap = new Map<string, [string[], string[]]>();
|
||||||
|
|
||||||
for (const segment of data) {
|
for (const segment of data) {
|
||||||
track.segments.set(segment.id, TrackSegment.deserialize(segment));
|
track.segments.set(segment.id, TrackSegment.deserialize(segment));
|
||||||
|
neighborMap.set(segment.id, [segment.fNeighbors, segment.bNeighbors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const segment of track.segments.values()) {
|
for (const segment of track.segments.values()) {
|
||||||
segment.setTrack(track);
|
segment.setTrack(track);
|
||||||
const neighbors = neighborMap.get(segment.id);
|
const neighbors = neighborMap.get(segment.id);
|
||||||
@ -105,8 +133,126 @@ export class TrackSystem {
|
|||||||
).filter((s) => s) as TrackSegment[];
|
).filter((s) => s) as TrackSegment[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
translate(v: Vector) {
|
||||||
|
for (const segment of this.segments.values()) {
|
||||||
|
segment.translate(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _path?: Spline<TrackSegment>;
|
||||||
|
|
||||||
|
get path() {
|
||||||
|
if (!this._path) {
|
||||||
|
this._path = this.generatePath();
|
||||||
|
}
|
||||||
|
return this._path;
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePath() {
|
||||||
|
if (!this.firstSegment) throw new Error("No first segment");
|
||||||
|
const flags = { looping: true };
|
||||||
|
const rightOnlyPath = [
|
||||||
|
this.firstSegment.copy(),
|
||||||
|
...this.findRightPath(
|
||||||
|
this.firstSegment,
|
||||||
|
new Set([this.firstSegment.id]),
|
||||||
|
flags,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
rightOnlyPath.forEach((s, i, arr) => {
|
||||||
|
if (i === 0) return;
|
||||||
|
const prev = arr[i - 1];
|
||||||
|
s.points[0] = prev.points[3];
|
||||||
|
s.prev = prev;
|
||||||
|
prev.next = s;
|
||||||
|
});
|
||||||
|
if (flags.looping) {
|
||||||
|
const first = rightOnlyPath[0];
|
||||||
|
const last = rightOnlyPath[rightOnlyPath.length - 1];
|
||||||
|
first.points[0] = last.points[3];
|
||||||
|
last.points[3] = first.points[0];
|
||||||
|
first.prev = last;
|
||||||
|
last.next = first;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Spline<TrackSegment>(rightOnlyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
*findRightPath(
|
||||||
|
start: TrackSegment,
|
||||||
|
seen: Set<string>,
|
||||||
|
flags: { looping: boolean },
|
||||||
|
): Generator<TrackSegment> {
|
||||||
|
if (start.frontNeighbours.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rightMost = start.frontNeighbours[0];
|
||||||
|
for (const segment of start.frontNeighbours) {
|
||||||
|
if (segment.id === rightMost.id) continue;
|
||||||
|
const rotatedSegment = segment.copy();
|
||||||
|
rotatedSegment.rotateAboutPoint(
|
||||||
|
rotatedSegment.tangent(0).heading(),
|
||||||
|
rotatedSegment.points[0],
|
||||||
|
);
|
||||||
|
const rotatedRightMost = rightMost.copy();
|
||||||
|
rotatedRightMost.rotateAboutPoint(
|
||||||
|
rotatedRightMost.tangent(0).heading(),
|
||||||
|
rotatedRightMost.points[0],
|
||||||
|
);
|
||||||
|
if (rotatedSegment.points[3].y > rotatedRightMost.points[3].y) {
|
||||||
|
rightMost = segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seen.has(rightMost.id)) {
|
||||||
|
if (seen.values().next().value === rightMost.id) {
|
||||||
|
flags.looping = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(rightMost.id);
|
||||||
|
yield rightMost.copy();
|
||||||
|
yield* this.findRightPath(rightMost, seen, flags);
|
||||||
|
}
|
||||||
|
*findLeftPath(
|
||||||
|
start: TrackSegment,
|
||||||
|
seen: Set<string>,
|
||||||
|
flags: { looping: boolean },
|
||||||
|
): Generator<TrackSegment> {
|
||||||
|
if (start.frontNeighbours.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let leftMost = start.frontNeighbours[0];
|
||||||
|
for (const segment of start.frontNeighbours) {
|
||||||
|
if (segment.id === leftMost.id) continue;
|
||||||
|
const rotatedSegment = segment.copy();
|
||||||
|
rotatedSegment.rotateAboutPoint(
|
||||||
|
rotatedSegment.tangent(0).heading(),
|
||||||
|
rotatedSegment.points[0],
|
||||||
|
);
|
||||||
|
const rotatedLeftMost = leftMost.copy();
|
||||||
|
rotatedLeftMost.rotateAboutPoint(
|
||||||
|
rotatedLeftMost.tangent(0).heading(),
|
||||||
|
rotatedLeftMost.points[0],
|
||||||
|
);
|
||||||
|
if (rotatedSegment.points[3].y < rotatedLeftMost.points[3].y) {
|
||||||
|
leftMost = segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seen.has(leftMost.id)) {
|
||||||
|
if (seen.values().next().value === leftMost.id) {
|
||||||
|
flags.looping = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(leftMost.id);
|
||||||
|
yield leftMost.copy();
|
||||||
|
yield* this.findLeftPath(leftMost, seen, flags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type VectorSet = [Vector, Vector, Vector, Vector];
|
type VectorSet = [Vector, Vector, Vector, Vector];
|
||||||
@ -118,50 +264,109 @@ export class TrackSegment extends PathSegment {
|
|||||||
track?: TrackSystem;
|
track?: TrackSystem;
|
||||||
|
|
||||||
doodler: Doodler;
|
doodler: Doodler;
|
||||||
|
normalPoints: Vector[] = [];
|
||||||
id: string;
|
antiNormalPoints: Vector[] = [];
|
||||||
|
|
||||||
constructor(p: VectorSet, id?: string) {
|
constructor(p: VectorSet, id?: string) {
|
||||||
super(p);
|
super(p);
|
||||||
this.doodler = getContextItem<Doodler>("doodler");
|
this.doodler = getContextItem<Doodler>("doodler");
|
||||||
this.id = id ?? crypto.randomUUID();
|
this.id = id ?? crypto.randomUUID();
|
||||||
|
this.recalculateRailPoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
recalculateRailPoints(resolution = 100) {
|
||||||
|
this.normalPoints = [];
|
||||||
|
this.antiNormalPoints = [];
|
||||||
|
for (let i = 0; i <= resolution; i++) {
|
||||||
|
const t = i / resolution;
|
||||||
|
const normal = this.tangent(t).rotate(Math.PI / 2);
|
||||||
|
normal.setMag(6);
|
||||||
|
const p = this.getPointAtT(t);
|
||||||
|
this.normalPoints.push(p.copy().add(normal));
|
||||||
|
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrack(t: TrackSystem) {
|
setTrack(t: TrackSystem) {
|
||||||
this.track = t;
|
this.track = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(showControls = false) {
|
override draw(showControls = false) {
|
||||||
this.doodler.drawBezier(
|
// if (showControls) {
|
||||||
this.points[0],
|
// this.doodler.drawBezier(
|
||||||
this.points[1],
|
// this.points[0],
|
||||||
this.points[2],
|
// this.points[1],
|
||||||
this.points[3],
|
// this.points[2],
|
||||||
{
|
// this.points[3],
|
||||||
strokeColor: "#ffffff50",
|
// {
|
||||||
|
// strokeColor: "#ffffff50",
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
if (showControls) {
|
||||||
|
this.doodler.deferDrawing(() => {
|
||||||
|
this.doodler.fillCircle(this.points[0], 1, {
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
this.doodler.fillCircle(this.points[1], 1, {
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
this.doodler.fillCircle(this.points[2], 1, {
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
this.doodler.fillCircle(this.points[3], 1, {
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ties = Math.ceil(this.length / 10);
|
||||||
|
for (let i = 0; i < ties; i++) {
|
||||||
|
const t = i / ties;
|
||||||
|
const p = this.getPointAtT(t);
|
||||||
|
// this.doodler.drawCircle(p, 2, {
|
||||||
|
// color: "red",
|
||||||
|
// weight: 3,
|
||||||
|
// });
|
||||||
|
this.doodler.drawRotated(p, this.tangent(t).heading(), () => {
|
||||||
|
this.doodler.line(p, p.copy().add(0, 10), {
|
||||||
|
color: "#291b17",
|
||||||
|
weight: 4,
|
||||||
|
});
|
||||||
|
this.doodler.line(p, p.copy().add(0, -10), {
|
||||||
|
color: "#291b17",
|
||||||
|
weight: 4,
|
||||||
|
});
|
||||||
|
// this.doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
|
||||||
|
// color: "grey",
|
||||||
|
// weight: 1,
|
||||||
|
// });
|
||||||
|
// this.doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
|
||||||
|
// color: "grey",
|
||||||
|
// weight: 1,
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doodler.deferDrawing(
|
||||||
|
() => {
|
||||||
|
this.doodler.drawLine(this.normalPoints, {
|
||||||
|
color: "grey",
|
||||||
|
weight: 1.5,
|
||||||
|
});
|
||||||
|
this.doodler.drawLine(this.antiNormalPoints, {
|
||||||
|
color: "grey",
|
||||||
|
weight: 1.5,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (showControls) {
|
// this.doodler.drawCircle(p, 2, {
|
||||||
// this.doodler.drawCircle(this.points[0], 4, {
|
// color: "red",
|
||||||
// color: "red",
|
// weight: 3,
|
||||||
// weight: 3,
|
// });
|
||||||
// });
|
|
||||||
this.doodler.drawCircle(this.points[1], 4, {
|
|
||||||
color: "red",
|
|
||||||
weight: 3,
|
|
||||||
});
|
|
||||||
this.doodler.drawCircle(this.points[2], 4, {
|
|
||||||
color: "red",
|
|
||||||
weight: 3,
|
|
||||||
});
|
|
||||||
// this.doodler.drawCircle(this.points[3], 4, {
|
|
||||||
// color: "red",
|
|
||||||
// weight: 3,
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize() {
|
serialize(): SerializedTrackSegment {
|
||||||
return {
|
return {
|
||||||
p: this.points.map((p) => p.array()),
|
p: this.points.map((p) => p.array()),
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@ -177,30 +382,26 @@ export class TrackSegment extends PathSegment {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
propagate() {
|
cleanCopy() {
|
||||||
const [_, __, p3, p4] = this.points;
|
return new TrackSegment(
|
||||||
const tangent = Vector.sub(p4, p3);
|
this.points.map((p) => p.copy()) as VectorSet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateTranslation(v: Vector) {
|
||||||
for (const fNeighbour of this.frontNeighbours) {
|
for (const fNeighbour of this.frontNeighbours) {
|
||||||
fNeighbour.receivePropagation(tangent);
|
fNeighbour.receivePropagation(v);
|
||||||
|
}
|
||||||
|
for (const bNeighbour of this.backNeighbours) {
|
||||||
|
bNeighbour.receivePropagation(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastHeading?: number;
|
lastHeading?: number;
|
||||||
|
|
||||||
receivePropagation(tangent: Vector) {
|
receivePropagation(v: Vector) {
|
||||||
const [p1, p2, p3, p4] = this.points;
|
this.translate(v);
|
||||||
// const angle = tangent.heading() - (this.lastHeading ?? 0);
|
this.propagateTranslation(v);
|
||||||
// this.lastHeading = tangent.heading();
|
|
||||||
// const newP2 = Vector.add(p1, tangent);
|
|
||||||
// const p2ToP3 = Vector.sub(p3, p2);
|
|
||||||
// p2ToP3.rotate(angle);
|
|
||||||
// p3.set(Vector.add(newP2, p2ToP3));
|
|
||||||
// const p2Top4 = Vector.sub(p4, p2);
|
|
||||||
// p2Top4.rotate(angle);
|
|
||||||
// p4.set(Vector.add(newP2, p2Top4));
|
|
||||||
// p2.set(newP2);
|
|
||||||
this.rotate(tangent);
|
|
||||||
this.propagate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
|
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
|
||||||
@ -231,4 +432,188 @@ export class TrackSegment extends PathSegment {
|
|||||||
data.id,
|
data.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPositionByPoint(pos: Vector, point: Vector) {
|
||||||
|
if (!this.points.includes(point)) return;
|
||||||
|
point = point.copy();
|
||||||
|
this.points.forEach((p, i) => {
|
||||||
|
const relativePoint = Vector.sub(p, point);
|
||||||
|
p.set(pos);
|
||||||
|
p.add(relativePoint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateAboutPoint(angle: number, point: Vector) {
|
||||||
|
if (!this.points.includes(point)) return;
|
||||||
|
point = point.copy();
|
||||||
|
this.points.forEach((p, i) => {
|
||||||
|
const relativePoint = Vector.sub(p, point);
|
||||||
|
relativePoint.rotate(angle);
|
||||||
|
p.set(Vector.add(point, relativePoint));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetRotation() {
|
||||||
|
// const angle = this.tangent(0).heading();
|
||||||
|
// this.rotateAboutPoint(-angle, this.points[0]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
translate(v: Point) {
|
||||||
|
this.points.forEach((p) => {
|
||||||
|
p.add(v.x, v.y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Spline<T extends PathSegment = PathSegment> {
|
||||||
|
segments: T[] = [];
|
||||||
|
ctx?: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
evenPoints: Vector[];
|
||||||
|
pointSpacing: number;
|
||||||
|
|
||||||
|
get points() {
|
||||||
|
return Array.from(new Set(this.segments.flatMap((s) => s.points)));
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes: IControlNode[];
|
||||||
|
|
||||||
|
looped = false;
|
||||||
|
|
||||||
|
constructor(segs: T[]) {
|
||||||
|
this.segments = segs;
|
||||||
|
if (this.segments.at(-1)?.next === this.segments[0]) {
|
||||||
|
this.looped = true;
|
||||||
|
}
|
||||||
|
this.pointSpacing = 1;
|
||||||
|
this.evenPoints = this.calculateEvenlySpacedPoints(1);
|
||||||
|
this.nodes = [];
|
||||||
|
// for (let i = 0; i < this.points.length; i += 3) {
|
||||||
|
// const node: IControlNode = {
|
||||||
|
// anchor: this.points[i],
|
||||||
|
// controls: [
|
||||||
|
// this.points.at(i - 1)!,
|
||||||
|
// this.points[(i + 1) % this.points.length],
|
||||||
|
// ],
|
||||||
|
// mirrored: false,
|
||||||
|
// tangent: true,
|
||||||
|
// };
|
||||||
|
// this.nodes.push(node);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// setContext(ctx: CanvasRenderingContext2D) {
|
||||||
|
// this.ctx = ctx;
|
||||||
|
// for (const segment of this.segments) {
|
||||||
|
// segment.setContext(ctx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
for (const segment of this.segments) {
|
||||||
|
// segment.draw();
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
doodler.drawWithAlpha(0.5, () => {
|
||||||
|
doodler.drawBezier(...segment.points, { color: "red" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||||
|
this.pointSpacing = 1;
|
||||||
|
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
|
||||||
|
const points: Vector[] = [];
|
||||||
|
|
||||||
|
points.push(this.segments[0].points[0]);
|
||||||
|
let prev = points[0];
|
||||||
|
let distSinceLastEvenPoint = 0;
|
||||||
|
for (const seg of this.segments) {
|
||||||
|
let t = 0;
|
||||||
|
|
||||||
|
const div = Math.ceil(seg.length * resolution * 10);
|
||||||
|
while (t < 1) {
|
||||||
|
t += 1 / div;
|
||||||
|
const point = seg.getPointAtT(t);
|
||||||
|
distSinceLastEvenPoint += prev.dist(point);
|
||||||
|
|
||||||
|
if (distSinceLastEvenPoint >= spacing) {
|
||||||
|
const overshoot = distSinceLastEvenPoint - spacing;
|
||||||
|
const evenPoint = Vector.add(
|
||||||
|
point,
|
||||||
|
Vector.sub(point, prev).normalize().mult(overshoot),
|
||||||
|
);
|
||||||
|
distSinceLastEvenPoint = overshoot;
|
||||||
|
points.push(evenPoint);
|
||||||
|
prev = evenPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.evenPoints = points;
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
followEvenPoints(t: number) {
|
||||||
|
if (this.looped) {
|
||||||
|
if (t < 0) t += this.evenPoints.length;
|
||||||
|
const i = Math.floor(t) % this.evenPoints.length;
|
||||||
|
const a = this.evenPoints[i];
|
||||||
|
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
|
||||||
|
return Vector.lerp(a, b, t % 1);
|
||||||
|
}
|
||||||
|
t = clamp(t, 0, this.evenPoints.length - 1);
|
||||||
|
const i = clamp(Math.floor(t), 0, this.evenPoints.length - 1);
|
||||||
|
const a = this.evenPoints[clamp(i, 0, this.evenPoints.length - 1)];
|
||||||
|
const b = this
|
||||||
|
.evenPoints[
|
||||||
|
clamp((i + 1) % this.evenPoints.length, 0, this.evenPoints.length - 1)
|
||||||
|
];
|
||||||
|
return Vector.lerp(a, b, t % 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateApproxLength() {
|
||||||
|
for (const s of this.segments) {
|
||||||
|
s.calculateApproxLength();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleNodeTangent(p: Vector) {
|
||||||
|
const node = this.nodes.find((n) => n.anchor === p);
|
||||||
|
|
||||||
|
node && (node.tangent = !node.tangent);
|
||||||
|
}
|
||||||
|
toggleNodeMirrored(p: Vector) {
|
||||||
|
const node = this.nodes.find((n) => n.anchor === p);
|
||||||
|
|
||||||
|
node && (node.mirrored = !node.mirrored);
|
||||||
|
}
|
||||||
|
handleNodeEdit(p: Vector, movement: { x: number; y: number }) {
|
||||||
|
const node = this.nodes.find((n) =>
|
||||||
|
n.anchor === p || n.controls.includes(p)
|
||||||
|
);
|
||||||
|
if (!node || !(node.mirrored || node.tangent)) return;
|
||||||
|
|
||||||
|
if (node.anchor !== p) {
|
||||||
|
if (node.mirrored || node.tangent) {
|
||||||
|
const mover = node.controls.find((e) => e !== p)!;
|
||||||
|
const v = Vector.sub(node.anchor, p);
|
||||||
|
if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
|
||||||
|
mover.set(Vector.add(v, node.anchor));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const control of node.controls) {
|
||||||
|
control.add(movement.x, movement.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IControlNode {
|
||||||
|
anchor: Vector;
|
||||||
|
controls: [Vector, Vector];
|
||||||
|
tangent: boolean;
|
||||||
|
mirrored: boolean;
|
||||||
}
|
}
|
||||||
|
55
train/cars.ts
Normal file
55
train/cars.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Vector } from "https://jsr.io/@bearmetal/doodler/0.0.4/geometry/vector.ts";
|
||||||
|
import { TrainCar } from "./train.ts";
|
||||||
|
import { ResourceManager } from "../lib/resources.ts";
|
||||||
|
import { getContextItem } from "../lib/context.ts";
|
||||||
|
|
||||||
|
export class Tender extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(25, resources.get<HTMLImageElement>("engine-sprites")!, 40, 20, {
|
||||||
|
at: new Vector(80, 0),
|
||||||
|
width: 40,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Tank extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||||
|
at: new Vector(80, 20),
|
||||||
|
width: 70,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class YellowDumpCar extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||||
|
at: new Vector(80, 40),
|
||||||
|
width: 70,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class GrayDumpCar extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||||
|
at: new Vector(80, 60),
|
||||||
|
width: 70,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class NullCar extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
|
||||||
|
at: new Vector(80, 80),
|
||||||
|
width: 70,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
55
train/engines.ts
Normal file
55
train/engines.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Vector } from "@bearmetal/doodler";
|
||||||
|
import { TrainCar } from "./train.ts";
|
||||||
|
import { getContextItem } from "../lib/context.ts";
|
||||||
|
import { ResourceManager } from "../lib/resources.ts";
|
||||||
|
|
||||||
|
export class RedEngine extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||||
|
at: new Vector(0, 60),
|
||||||
|
width: 80,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class PurpleEngine extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||||
|
at: new Vector(0, 60),
|
||||||
|
width: 80,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class GreenEngine extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||||
|
at: new Vector(0, 40),
|
||||||
|
width: 80,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class GrayEngine extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||||
|
at: new Vector(0, 20),
|
||||||
|
width: 80,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class BlueEngine extends TrainCar {
|
||||||
|
constructor() {
|
||||||
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
|
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
|
||||||
|
at: new Vector(0, 0),
|
||||||
|
width: 80,
|
||||||
|
height: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
147
train/newTrain.ts
Normal file
147
train/newTrain.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||||
|
import { getContextItem } from "../lib/context.ts";
|
||||||
|
import { Spline, TrackSegment } from "../track/system.ts";
|
||||||
|
|
||||||
|
export class DotFollower {
|
||||||
|
position: Vector;
|
||||||
|
velocity: Vector;
|
||||||
|
acceleration: Vector;
|
||||||
|
maxSpeed: number;
|
||||||
|
maxForce: number;
|
||||||
|
_trailingPoint: number;
|
||||||
|
protected _leadingPoint: number;
|
||||||
|
|
||||||
|
path: Spline<TrackSegment>;
|
||||||
|
|
||||||
|
get trailingPoint() {
|
||||||
|
const desired = this.velocity.copy();
|
||||||
|
desired.normalize();
|
||||||
|
desired.mult(-this._trailingPoint);
|
||||||
|
|
||||||
|
return Vector.add(this.position, desired);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(path: Spline<TrackSegment>, pos: Vector) {
|
||||||
|
this.path = path;
|
||||||
|
this.position = pos;
|
||||||
|
this.velocity = new Vector();
|
||||||
|
this.acceleration = new Vector();
|
||||||
|
this.maxSpeed = 3;
|
||||||
|
this.maxForce = 0.3;
|
||||||
|
|
||||||
|
this._trailingPoint = 0;
|
||||||
|
this._leadingPoint = 0;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
move(dt: number) {
|
||||||
|
dt *= 10;
|
||||||
|
const force = calculatePathForce(this, this.path.points);
|
||||||
|
this.applyForce(force.mult(dt));
|
||||||
|
this.velocity.limit(this.maxSpeed);
|
||||||
|
this.acceleration.limit(this.maxForce);
|
||||||
|
this.velocity.add(this.acceleration.copy().mult(dt));
|
||||||
|
this.position.add(this.velocity.copy().mult(dt));
|
||||||
|
this.edges();
|
||||||
|
}
|
||||||
|
|
||||||
|
edges() {
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
|
||||||
|
if (this.position.x > doodler.width) this.position.x = 0;
|
||||||
|
if (this.position.y > doodler.height) this.position.y = 0;
|
||||||
|
if (this.position.x < 0) this.position.x = doodler.width;
|
||||||
|
if (this.position.y < 0) this.position.y = doodler.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
doodler.drawRotated(this.position, this.velocity.heading() || 0, () => {
|
||||||
|
doodler.fillCenteredRect(this.position, 20, 20, { fillColor: "white" });
|
||||||
|
});
|
||||||
|
for (const point of this.path.points) {
|
||||||
|
doodler.drawCircle(point, 4, { color: "red", weight: 3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyForce(force: Vector) {
|
||||||
|
this.velocity.add(force);
|
||||||
|
}
|
||||||
|
|
||||||
|
static edges(point: Vector, width: number, height: number) {
|
||||||
|
if (point.x > width) point.x = 0;
|
||||||
|
if (point.y > height) point.y = 0;
|
||||||
|
if (point.x < 0) point.x = width;
|
||||||
|
if (point.y < 0) point.y = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closestPointOnLineSegment(p: Vector, a: Vector, b: Vector): Vector {
|
||||||
|
// Vector AB
|
||||||
|
// const AB = { x: b.x - a.x, y: b.y - a.y };
|
||||||
|
const AB = Vector.sub(b, a);
|
||||||
|
// Vector AP
|
||||||
|
// const AP = { x: p.x - a.x, y: p.y - a.y };
|
||||||
|
const AP = Vector.sub(p, a);
|
||||||
|
// Dot product of AP and AB
|
||||||
|
// const AB_AB = AB.x * AB.x + AB.y * AB.y;
|
||||||
|
const AB_AB = Vector.dot(AB, AB);
|
||||||
|
// const AP_AB = AP.x * AB.x + AP.y * AB.y;
|
||||||
|
const AP_AB = Vector.dot(AP, AB);
|
||||||
|
// Project AP onto AB
|
||||||
|
const t = AP_AB / AB_AB;
|
||||||
|
|
||||||
|
// Clamp t to the range [0, 1] to restrict to the segment
|
||||||
|
const tClamped = Math.max(0, Math.min(1, t));
|
||||||
|
|
||||||
|
// Closest point on the segment
|
||||||
|
return new Vector({ x: a.x + AB.x * tClamped, y: a.y + AB.y * tClamped });
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePathForce(f: DotFollower, path: Vector[]) {
|
||||||
|
let closestPoint: Vector = path[0];
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
// Loop through each segment to find the closest point on the path
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
const segmentStart = path[i];
|
||||||
|
const segmentEnd = path[i + 1];
|
||||||
|
|
||||||
|
// Find the closest point on the segment
|
||||||
|
const closest = closestPointOnLineSegment(
|
||||||
|
f.position,
|
||||||
|
segmentStart,
|
||||||
|
segmentEnd,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the distance from the follower to the closest point
|
||||||
|
// const distance = Math.sqrt(
|
||||||
|
// Math.pow(follower.position.x - closest.x, 2) +
|
||||||
|
// Math.pow(follower.position.y - closest.y, 2),
|
||||||
|
// );
|
||||||
|
|
||||||
|
const distance = Vector.dist(f.position, closest);
|
||||||
|
|
||||||
|
// Track the closest point
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestPoint = closest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the force to apply toward the closest point
|
||||||
|
// const force = {
|
||||||
|
// x: closestPoint.x - f.position.x,
|
||||||
|
// y: closestPoint.y - f.position.y,
|
||||||
|
// };
|
||||||
|
const force = Vector.sub(closestPoint, f.position);
|
||||||
|
|
||||||
|
// Normalize the force and apply a magnitude (this will depend on your desired strength)
|
||||||
|
const magnitude = 100; // Adjust this based on your needs
|
||||||
|
force.setMag(magnitude);
|
||||||
|
return force;
|
||||||
|
}
|
@ -1,62 +1,71 @@
|
|||||||
import { ComplexPath, PathSegment } from "../math/path.ts";
|
import { ComplexPath, PathSegment } from "../math/path.ts";
|
||||||
import { Follower } from "../physics/follower.ts";
|
import { Follower } from "../physics/follower.ts";
|
||||||
import { Mover } from "../physics/mover.ts";
|
import { Mover } from "../physics/mover.ts";
|
||||||
import { Spline, Track } from "../track.ts";
|
|
||||||
import { getContextItem } from "../lib/context.ts";
|
import { getContextItem } from "../lib/context.ts";
|
||||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||||
|
import { Spline, TrackSegment } from "../track/system.ts";
|
||||||
|
import { ResourceManager } from "../lib/resources.ts";
|
||||||
|
|
||||||
export class Train {
|
export class Train {
|
||||||
nodes: Vector[] = [];
|
nodes: Vector[] = [];
|
||||||
|
|
||||||
cars: TrainCar[] = [];
|
cars: TrainCar[] = [];
|
||||||
|
|
||||||
path: Spline<Track>;
|
path: Spline<TrackSegment>;
|
||||||
t: number;
|
t: number;
|
||||||
|
|
||||||
engineLength = 40;
|
engineLength = 40;
|
||||||
spacing = 30;
|
spacing = 30;
|
||||||
|
|
||||||
speed = 0;
|
speed = 10;
|
||||||
|
|
||||||
constructor(track: Spline<Track>, cars: TrainCar[] = []) {
|
constructor(track: Spline<TrackSegment>, cars: TrainCar[]) {
|
||||||
this.path = track;
|
this.path = track;
|
||||||
this.t = 0;
|
this.t = 0;
|
||||||
this.nodes.push(this.path.followEvenPoints(this.t));
|
const resources = getContextItem<ResourceManager>("resources");
|
||||||
this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40)));
|
this.cars = cars;
|
||||||
const engineSprites = document.getElementById(
|
// this.cars.push(
|
||||||
"engine-sprites",
|
// new TrainCar(
|
||||||
)! as HTMLImageElement;
|
// 55,
|
||||||
this.cars.push(
|
// engineSprites,
|
||||||
new TrainCar(
|
// 80,
|
||||||
55,
|
// 20,
|
||||||
engineSprites,
|
// { at: new Vector(0, 60), width: 80, height: 20 },
|
||||||
80,
|
// ),
|
||||||
20,
|
// new TrainCar(
|
||||||
{ at: new Vector(0, 60), width: 80, height: 20 },
|
// 25,
|
||||||
),
|
// engineSprites,
|
||||||
new TrainCar(
|
// 40,
|
||||||
25,
|
// 20,
|
||||||
engineSprites,
|
// { at: new Vector(80, 0), width: 40, height: 20 },
|
||||||
40,
|
// ),
|
||||||
20,
|
// );
|
||||||
{ at: new Vector(80, 0), width: 40, height: 20 },
|
let currentOffset = 0;
|
||||||
),
|
try {
|
||||||
);
|
for (const car of this.cars) {
|
||||||
this.cars[0].points = this.nodes.map((n) => n) as [Vector, Vector];
|
currentOffset += this.spacing;
|
||||||
this.cars[1].points = this.nodes.map((n) => n) as [Vector, Vector];
|
const a = this.path.followEvenPoints(this.t - currentOffset);
|
||||||
let currentOffset = 40;
|
currentOffset += car.length;
|
||||||
for (const car of cars) {
|
const b = this.path.followEvenPoints(this.t - currentOffset);
|
||||||
currentOffset += this.spacing;
|
car.points = [a, b];
|
||||||
const a = this.path.followEvenPoints(this.t - currentOffset);
|
this.nodes.push(a, b);
|
||||||
currentOffset += car.length;
|
}
|
||||||
const b = this.path.followEvenPoints(this.t - currentOffset);
|
} catch {
|
||||||
car.points = [a, b];
|
currentOffset = 0;
|
||||||
this.cars.push(car);
|
for (const car of this.cars.toReversed()) {
|
||||||
|
currentOffset += this.spacing;
|
||||||
|
const a = this.path.followEvenPoints(this.t - currentOffset);
|
||||||
|
currentOffset += car.length;
|
||||||
|
const b = this.path.followEvenPoints(this.t - currentOffset);
|
||||||
|
car.points = [a, b];
|
||||||
|
this.nodes.push(a, b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
move(dTime: number) {
|
move(dTime: number) {
|
||||||
this.t = (this.t + this.speed * dTime * 10) % this.path.evenPoints.length;
|
this.t = this.t + this.speed * dTime * 10;
|
||||||
|
// % this.path.evenPoints.length; // This should probably be on the track system
|
||||||
// console.log(this.t);
|
// console.log(this.t);
|
||||||
let currentOffset = 0;
|
let currentOffset = 0;
|
||||||
for (const car of this.cars) {
|
for (const car of this.cars) {
|
||||||
@ -66,14 +75,17 @@ export class Train {
|
|||||||
currentOffset += car.length;
|
currentOffset += car.length;
|
||||||
b.set(this.path.followEvenPoints(this.t - currentOffset));
|
b.set(this.path.followEvenPoints(this.t - currentOffset));
|
||||||
currentOffset += this.spacing;
|
currentOffset += this.spacing;
|
||||||
car.draw();
|
// car.draw();
|
||||||
}
|
}
|
||||||
// this.draw();
|
// this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw() {
|
// draw() {
|
||||||
|
// const doodler = getContextItem<Doodler>("doodler");
|
||||||
|
// this.path.draw();
|
||||||
// for (const [i, node] of this.nodes.entries()) {
|
// for (const [i, node] of this.nodes.entries()) {
|
||||||
// doodler.drawCircle(node.point, 10, { color: 'purple', weight: 3 })
|
// // doodler.drawCircle(node, 10, { color: "purple", weight: 3 });
|
||||||
|
// doodler.fillCircle(node, 2, { color: "purple" });
|
||||||
// // const next = this.nodes[i + 1];
|
// // const next = this.nodes[i + 1];
|
||||||
// // if (next) {
|
// // if (next) {
|
||||||
// // const to = Vector.sub(node.point, next.point);
|
// // const to = Vector.sub(node.point, next.point);
|
||||||
|
18
types.ts
Normal file
18
types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Vector } from "@bearmetal/doodler";
|
||||||
|
import { TrackSegment } from "./track/system.ts";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
type End = {
|
||||||
|
pos: Vector;
|
||||||
|
segment: TrackSegment;
|
||||||
|
tangent: Vector;
|
||||||
|
frontOrBack: "front" | "back";
|
||||||
|
};
|
||||||
|
|
||||||
|
type SerializedTrackSegment = {
|
||||||
|
p: [number, number, number][];
|
||||||
|
id: string;
|
||||||
|
bNeighbors: string[];
|
||||||
|
fNeighbors: string[];
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user