diff --git a/deno.json b/deno.json index bd4e0a1..c537b4b 100644 --- a/deno.json +++ b/deno.json @@ -14,7 +14,7 @@ ] }, "imports": { - "@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-b", + "@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-c", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4", "vite": "npm:vite@^6.0.1" } diff --git a/deno.lock b/deno.lock index d526b23..847748c 100644 --- a/deno.lock +++ b/deno.lock @@ -1,15 +1,16 @@ { "version": "4", "specifiers": { - "jsr:@bearmetal/doodler@0.0.5-b": "0.0.5-b", + "jsr:@bearmetal/doodler@0.0.5-c": "0.0.5-c", "npm:@deno/vite-plugin@^1.0.4": "1.0.4_vite@6.1.0", + "npm:@types/node@*": "22.5.4", "npm:vite@*": "6.1.0", "npm:vite@^6.0.1": "6.1.0", "npm:web-ext@*": "8.4.0" }, "jsr": { - "@bearmetal/doodler@0.0.5-b": { - "integrity": "94f265ea21162f943291526800de7f3f6560634a4fe762a38cd73892685b6742" + "@bearmetal/doodler@0.0.5-c": { + "integrity": "34b0db85af1393b1b01622915963a8b33ee923c14b381afe9c771efd3d631cf1" } }, "npm": { @@ -2082,7 +2083,7 @@ }, "workspace": { "dependencies": [ - "jsr:@bearmetal/doodler@0.0.5-b", + "jsr:@bearmetal/doodler@0.0.5-c", "npm:@deno/vite-plugin@^1.0.4", "npm:vite@^6.0.1" ] diff --git a/public/blobs/snr/sprite/LargeLady.png b/public/blobs/snr/sprite/LargeLady.png index 54166bd..5d594c8 100644 Binary files a/public/blobs/snr/sprite/LargeLady.png and b/public/blobs/snr/sprite/LargeLady.png differ diff --git a/src/lib/resources.ts b/src/lib/resources.ts index 5e18280..2a8c2cb 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -1,5 +1,16 @@ // namespace:type/location -type NamespacedId = `${string}:${"img" | "audio" | "sprite"}/${string}`; +type sprite = "sprite"; +type img = "img"; +type audio = "audio"; +type ResourceType = keyof ResourceMap; +type SpriteId = `${string}:${sprite}/${string}`; + +interface ResourceMap { + sprite: HTMLImageElement; + img: HTMLImageElement; + audio: HTMLAudioElement; +} +type NamespacedId = `${string}:${T}/${string}`; /** * Resources are stored in namespaces, and can be accessed by their namespaced id. @@ -13,15 +24,19 @@ export class ResourceManager { private resources: Map = new Map(); private statuses: Map> = new Map(); - get(name: NamespacedId): T { + get(name: NamespacedId): ResourceMap[K]; + get(name: string): T; + get( + name: string, + ): T { if (!this.resources.has(name)) { throw new Error(`Resource ${name} not found`); } return this.resources.get(name) as T; } - set( - name: NamespacedId, + set( + name: NamespacedId, value: unknown, ) { const identifier = parseNamespacedId(name); @@ -60,8 +75,6 @@ export class ResourceManager { } } -type ResourceType = "img" | "audio" | "sprite"; - function extensionByType(type: ResourceType) { switch (type) { case "img": @@ -73,14 +86,16 @@ function extensionByType(type: ResourceType) { } } -type NamespaceIdentifier = { +type NamespaceIdentifier = { namespace: string; - type: ResourceType; + type: T; name: string; }; -function parseNamespacedId(id: NamespacedId): NamespaceIdentifier { +function parseNamespacedId( + id: NamespacedId, +): NamespaceIdentifier { const [namespace, location] = id.split(":"); const [type, ...name] = location.split("/"); - return { namespace, type: type as ResourceType, name: name.join("/") }; + return { namespace, type: type as T, name: name.join("/") }; } diff --git a/src/main.ts b/src/main.ts index f9c68e9..0f456fd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,9 +20,12 @@ const resources = new ResourceManager(); const doodler = new ZoomableDoodler({ fillScreen: true, bg: "#302040", -}); -(doodler as any as { ctx: CanvasRenderingContext2D }).ctx - .imageSmoothingEnabled = false; + noSmooth: true, +}, () => {}); +setTimeout(() => { + (doodler as any as { ctx: CanvasRenderingContext2D }).ctx + .imageSmoothingEnabled = false; +}, 0); // doodler.minScale = 0.1; (doodler as any).scale = 3.14; @@ -37,13 +40,18 @@ const colors = [ "violet", ]; -const _debug: Debug = JSON.parse(localStorage.getItem("debug") || "0") || { +const _fullDebug: Debug = { track: false, train: false, path: false, car: false, + bogies: false, + angles: false, }; +const storedDebug = JSON.parse(localStorage.getItem("debug") || "0"); +const _debug: Debug = Object.assign({}, _fullDebug, storedDebug); + const debug = new Proxy(_debug, { get: (_, prop: string) => { // if (prop !in _debug) { @@ -107,3 +115,8 @@ gameLoop.start(state); if (import.meta.env.DEV) { console.log("Running in development mode"); } + +globalThis.TWO_PI = Math.PI * 2; +declare global { + var TWO_PI: number; +} diff --git a/src/math/lerp.ts b/src/math/lerp.ts index a3e4386..d26316e 100644 --- a/src/math/lerp.ts +++ b/src/math/lerp.ts @@ -9,3 +9,19 @@ export const map = ( x2: number, y2: number, ) => (value - x1) * (y2 - x2) / (y1 - x1) + x2; + +export function lerpAngle(a: number, b: number, t: number) { + let diff = b - a; + // Wrap difference to [-PI, PI] + while (diff < -Math.PI) diff += 2 * Math.PI; + while (diff > Math.PI) diff -= 2 * Math.PI; + return a + diff * t; +} + +export function averageAngles(angle1: number, angle2: number) { + // Convert angles to unit vectors + const x = Math.cos(angle1) + Math.cos(angle2); + const y = Math.sin(angle1) + Math.sin(angle2); + // Compute the angle of the resulting vector + return Math.atan2(y, x); +} diff --git a/src/state/states/RunningState.ts b/src/state/states/RunningState.ts index 49b843a..b6c7def 100644 --- a/src/state/states/RunningState.ts +++ b/src/state/states/RunningState.ts @@ -8,7 +8,7 @@ import { DotFollower } from "../../train/newTrain.ts"; import { Train } from "../../train/train.ts"; import { State } from "../machine.ts"; import { States } from "./index.ts"; -import { LargeLady } from "../../train/LargeLady.ts"; +import { LargeLady, LargeLadyTender } from "../../train/LargeLady.ts"; export class RunningState extends State { override name: States = States.RUNNING; @@ -62,7 +62,10 @@ export class RunningState extends State { // const train = new Train(track.path, [new LargeLady(), new Tender()]); // ctx.trains.push(train); }); - const train = new Train(track.path, [new LargeLady()]); + const train = new Train(track.path, [ + new LargeLady(), + new LargeLadyTender(), + ]); ctx.trains.push(train); // const trainCount = 1000; // for (let i = 0; i < trainCount; i++) { diff --git a/src/track/system.ts b/src/track/system.ts index f20fc75..12f0d7c 100644 --- a/src/track/system.ts +++ b/src/track/system.ts @@ -499,7 +499,14 @@ export class Spline { ctx?: CanvasRenderingContext2D; evenPoints: PathPoint[]; - pointSpacing: number; + _pointSpacing: number; + get pointSpacing() { + return this._pointSpacing; + } + set pointSpacing(value: number) { + this._pointSpacing = value; + this.evenPoints = this.calculateEvenlySpacedPoints(value); + } get points() { return Array.from(new Set(this.segments.flatMap((s) => s.points))); @@ -514,8 +521,8 @@ export class Spline { if (this.segments.at(-1)?.next === this.segments[0]) { this.looped = true; } - this.pointSpacing = 1; - this.evenPoints = this.calculateEvenlySpacedPoints(1); + this._pointSpacing = 1; + this.evenPoints = this.calculateEvenlySpacedPoints(this._pointSpacing); this.nodes = []; // for (let i = 0; i < this.points.length; i += 3) { // const node: IControlNode = { @@ -549,7 +556,7 @@ export class Spline { } calculateEvenlySpacedPoints(spacing: number, resolution = 1): PathPoint[] { - this.pointSpacing = 1; + // this._pointSpacing = 1; // return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution)); const points: PathPoint[] = []; @@ -589,7 +596,7 @@ export class Spline { } } - this.evenPoints = points; + // this.evenPoints = points; return points; } diff --git a/src/train/LargeLady.ts b/src/train/LargeLady.ts index 7966651..b04f697 100644 --- a/src/train/LargeLady.ts +++ b/src/train/LargeLady.ts @@ -1,16 +1,18 @@ import { Doodler, Vector } from "@bearmetal/doodler"; -import { TrainCar } from "./train.ts"; +import { Train, TrainCar } from "./train.ts"; import { getContextItem } from "../lib/context.ts"; import { ResourceManager } from "../lib/resources.ts"; +import { debug } from "node:console"; +import { averageAngles } from "../math/lerp.ts"; export class LargeLady extends TrainCar { scale = 1; constructor() { const resources = getContextItem("resources"); const img = resources.get("snr:sprite/LargeLady")!; - super(50, 10, img, 160, 23, { + super(50, 10, img, 132, 23, { at: new Vector(0, 0), - width: 160, + width: 132, height: 23, }); @@ -20,10 +22,10 @@ export class LargeLady extends TrainCar { angle: 0, length: 35 * this.scale, sprite: { - at: new Vector(0, 23), - width: 33, - height: 19, - offset: new Vector(-19, -9), + at: new Vector(0, 24), + width: 35, + height: 23, + offset: new Vector(-23, -11.5), }, }, { @@ -37,10 +39,10 @@ export class LargeLady extends TrainCar { // offset: new Vector(-19, -9.5), // }, sprite: { - at: new Vector(34, 23), - width: 51, - height: 19, - offset: new Vector(-25.5, -9.5), + at: new Vector(36, 24), + width: 60, + height: 23, + offset: new Vector(-35, -11.5), }, }, { @@ -48,22 +50,21 @@ export class LargeLady extends TrainCar { angle: 0, length: 35 * this.scale, sprite: { - at: new Vector(34, 23), + at: new Vector(36, 24), width: 60, - height: 19, - offset: new Vector(-25.5, -9.5), + height: 23, + offset: new Vector(-35, -11.5), }, - rotate: true, }, { pos: new Vector(0, 0), angle: 0, - length: 0, + length: 28, sprite: { - at: new Vector(95, 23), - width: 16, - height: 19, - offset: new Vector(-8, -9.5), + at: new Vector(97, 24), + width: 22, + height: 23, + offset: new Vector(-11, -11.5), }, }, ]; @@ -88,17 +89,17 @@ export class LargeLady extends TrainCar { const b = this.bogies[2]; const a = this.bogies[1]; - const origin = b.pos.copy().add(new Vector(18, 0).rotate(b.angle)); // const origin = Vector.add(Vector.sub(a.pos, b.pos).div(2), b.pos); // const angle = Vector.sub(b.pos, a.pos).heading(); + const debug = getContextItem("debug"); + if (debug.bogies) return; - // const difAngle = Vector.sub(b.pos, c.pos).heading(); - const angle = b.angle + Math.PI; - // const avgAngle = (difAngle + angle) / 2; + const difAngle = Vector.sub(a.pos, b.pos).heading(); + const origin = b.pos.copy().add(new Vector(33, 0).rotate(difAngle)); + const angle = b.angle; + const avgAngle = averageAngles(difAngle, angle) + Math.PI; - doodler.drawCircle(origin, 4, { color: "blue" }); - - doodler.drawRotated(origin, angle, () => { + doodler.drawRotated(origin, avgAngle, () => { this.sprite ? doodler.drawSprite( this.img, @@ -117,5 +118,71 @@ export class LargeLady extends TrainCar { origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), ); }); + // doodler.drawCircle(origin, 4, { color: "blue" }); + + doodler.deferDrawing(() => { + doodler.drawRotated(origin, avgAngle + Math.PI, () => { + doodler.drawSprite( + this.img, + new Vector(133, 0), + 28, + 23, + origin.copy().sub(93, this.imgHeight / 2), + 28, + 23, + ); + }); + }); + } +} + +export class LargeLadyTender extends TrainCar { + constructor() { + const resources = getContextItem("resources"); + const sprite = resources.get("snr:sprite/LargeLady"); + super(40, 39, sprite, 98, 23, { + at: new Vector(0, 48), + width: 98, + height: 23, + }); + + this.leading = 10; + } + + override draw(): void { + const doodler = getContextItem("doodler"); + const b = this.bogies[0]; + doodler.drawRotated(b.pos, b.angle, () => { + doodler.drawSprite( + this.img, + new Vector(97, 24), + 22, + 23, + b.pos.copy().sub(11, 11.5), + 22, + 23, + ); + }); + + const angle = Vector.sub(this.bogies[1].pos, this.bogies[0].pos).heading(); + const origin = this.bogies[1].pos.copy().add( + new Vector(-11, 0).rotate(angle), + ); + doodler.drawRotated(origin, angle, () => { + this.sprite + ? doodler.drawSprite( + this.img, + this.sprite.at, + this.sprite.width, + this.sprite.height, + origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), + this.imgWidth, + this.imgHeight, + ) + : doodler.drawImage( + this.img, + origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2), + ); + }); } } diff --git a/src/train/train.ts b/src/train/train.ts index b0fd9fd..d32fa56 100644 --- a/src/train/train.ts +++ b/src/train/train.ts @@ -2,8 +2,7 @@ import { getContextItem } from "../lib/context.ts"; import { Doodler, Vector } from "@bearmetal/doodler"; import { Spline, TrackSegment, TrackSystem } from "../track/system.ts"; import { Debuggable } from "../lib/debuggable.ts"; -import { map } from "../math/lerp.ts"; -import { off } from "node:process"; +import { lerp, lerpAngle, map } from "../math/lerp.ts"; export class Train extends Debuggable { nodes: Vector[] = []; @@ -13,7 +12,7 @@ export class Train extends Debuggable { path: Spline; t: number; - spacing = 20; + spacing = 5; speed = 0; @@ -26,35 +25,37 @@ export class Train extends Debuggable { constructor(track: Spline, cars: TrainCar[], t = 0) { super("train", "path"); this.path = track; - this.t = t; + this.path.pointSpacing = 5; this.cars = cars; + this.t = this.cars.reduce((acc, c) => acc + c.length, 0) + + (this.cars.length - 1) * this.spacing; + this.t = this.t / this.path.pointSpacing; let currentOffset = 0; - try { - for (const car of this.cars) { - car.train = this; - currentOffset += car.moveAlongPath(this.t - currentOffset) + - this.spacing; - } - console.log("forward"); - } catch { - currentOffset = 0; - console.log("Reversed"); - for (const car of this.cars.toReversed()) { - for (const [i, bogie] of car.bogies.entries().toArray().reverse()) { - currentOffset += bogie.length; - const a = this.path.followEvenPoints(this.t - currentOffset); - car.setBogiePosition(a.p, i); - this.nodes.push(a.p); - car.segments.add(a.segmentId); - } - } + // try { + for (const car of this.cars) { + car.train = this; + currentOffset += car.moveAlongPath(this.t - currentOffset, true) + + this.spacing / this.path.pointSpacing; } + // } catch { + // currentOffset = 0; + // console.log("Reversed"); + // for (const car of this.cars.toReversed()) { + // for (const [i, bogie] of car.bogies.entries().toArray().reverse()) { + // currentOffset += bogie.length; + // const a = this.path.followEvenPoints(this.t - currentOffset); + // car.setBogiePosition(a.p, i); + // this.nodes.push(a.p); + // car.segments.add(a.segmentId); + // } + // } + // } } move(dTime: number) { if (!this.speed) return; - this.t = this.t + this.speed * dTime * 10; + this.t = this.t + (this.speed / this.path.pointSpacing) * dTime * 10; // % this.path.evenPoints.length; // This should probably be on the track system let currentOffset = 0; for (const car of this.cars) { @@ -70,7 +71,9 @@ export class Train extends Debuggable { // car.segments = [nA.segmentId, nB.segmentId]; // car.draw(); - currentOffset += car.moveAlongPath(this.t - currentOffset) + this.spacing; + currentOffset += car.moveAlongPath(this.t - currentOffset) + + this.spacing / this.path.pointSpacing + + car.leading / this.path.pointSpacing; } // this.draw(); } @@ -152,7 +155,8 @@ export class TrainCar extends Debuggable { sprite?: ISprite; points?: [Vector, Vector, ...Vector[]]; - length: number; + _length: number; + leading: number = 0; bogies: Bogie[] = []; @@ -168,12 +172,12 @@ export class TrainCar extends Debuggable { h: number, sprite?: ISprite, ) { - super(true, "car"); + super(true, "car", "bogies", "angles"); this.img = img; this.sprite = sprite; this.imgWidth = w; this.imgHeight = h; - this.length = length; + this._length = length; this.bogies = [ { @@ -189,6 +193,10 @@ export class TrainCar extends Debuggable { ]; } + get length() { + return this.bogies.reduce((acc, b) => acc + b.length, 0) + this.leading; + } + setBogiePosition(pos: Vector, idx: number) { this.bogies[idx].pos.set(pos); } @@ -196,20 +204,24 @@ export class TrainCar extends Debuggable { update(dTime: number, t: number) { if (this.train) { for (const [i, bogie] of this.bogies.entries()) { - const a = this.train.path.followEvenPoints(t - this.length * i); + const a = this.train.path.followEvenPoints(t - this._length * i); } } } - moveAlongPath(t: number): number { + moveAlongPath(t: number, initial = false): number { if (!this.train) return 0; - let offset = 0; + let offset = this.leading / this.train.path.pointSpacing; this.segments.clear(); for (const [i, bogie] of this.bogies.entries()) { const a = this.train.path.followEvenPoints(t - offset); - offset += bogie.length; + a.tangent.rotate(TWO_PI); + offset += bogie.length / this.train.path.pointSpacing; this.setBogiePosition(a.p, i); - bogie.angle = a.tangent.heading(); + if (initial) bogie.angle = a.tangent.heading(); + else { + bogie.angle = lerpAngle(a.tangent.heading(), bogie.angle, .1); + } this.segments.add(a.segmentId); } return offset; @@ -242,19 +254,72 @@ export class TrainCar extends Debuggable { } override debugDraw(...args: unknown[]): void { const doodler = getContextItem("doodler"); - doodler.drawLine(this.bogies.map((b) => b.pos), { - color: "blue", - weight: 2, - }); - doodler.deferDrawing(() => { - const colors = getContextItem("colors"); - for (const [i, b] of this.bogies.entries()) { - doodler.drawCircle(b.pos, 5, { color: colors[i % colors.length] }); - doodler.fillText(b.length.toString(), b.pos.copy().add(10, 0), 100, { - color: "white", + const debug = getContextItem("debug"); + if (debug.bogies) { + doodler.deferDrawing(() => { + for (const [i, b] of this.bogies.entries()) { + const next = this.bogies[i + 1]; + if (!next) continue; + const dist = Vector.dist(b.pos, next.pos); + doodler.drawCircle(b.pos, 5, { color: "red" }); + doodler.fillText( + dist.toFixed(1).toString(), + b.pos.copy().add(10, 10), + 100, + { + color: "white", + }, + ); + } + }); + } + if (debug.car) { + doodler.deferDrawing(() => { + doodler.drawLine(this.bogies.map((b) => b.pos), { + color: "blue", + weight: 2, }); - } - }); + doodler.deferDrawing(() => { + const colors = getContextItem("colors"); + for (const [i, b] of this.bogies.entries()) { + doodler.drawCircle(b.pos, 5, { color: colors[i % colors.length] }); + doodler.fillText( + b.length.toString(), + b.pos.copy().add(10, 0), + 100, + { + color: "white", + }, + ); + } + }); + }); + } + + if (debug.angles) { + doodler.deferDrawing(() => { + const ps: { pos: Vector; angle: number }[] = []; + for (const [i, b] of this.bogies.entries()) { + ps.push({ pos: b.pos, angle: b.angle }); + const next = this.bogies[i + 1]; + if (!next) continue; + const heading = Vector.sub(next.pos, b.pos); + const p = b.pos.copy().add(heading.mult(.5)); + ps.push({ pos: p, angle: heading.heading() }); + } + for (const p of ps) { + doodler.dot(p.pos, { color: "green" }); + doodler.fillText( + p.angle.toFixed(1).toString(), + p.pos.copy().add(0, 20), + 100, + { + color: "white", + }, + ); + } + }); + } } } diff --git a/src/types.ts b/src/types.ts index c088a1b..65d0cc1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,5 +21,7 @@ declare global { train: boolean; car: boolean; path: boolean; + bogies: boolean; + angles: boolean; }; }