diff --git a/.gitignore b/.gitignore index c930686..6449360 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bundle.js -dist/ \ No newline at end of file +dist/ +temp.ts \ No newline at end of file diff --git a/bundle.js b/bundle.js index f61f891..af6a32d 100644 --- a/bundle.js +++ b/bundle.js @@ -1476,7 +1476,6 @@ ).filter((s) => s); } } - console.log(track.segments); return track; } translate(v) { @@ -1495,7 +1494,7 @@ if (!this.firstSegment) throw new Error("No first segment"); const rightOnlyPath = [ this.firstSegment.copy(), - ...this.findRightPath(this.firstSegment) + ...this.findRightPath(this.firstSegment, /* @__PURE__ */ new Set([this.firstSegment.id])) ]; rightOnlyPath.forEach((s, i, arr) => { if (i === 0) return; @@ -1504,15 +1503,13 @@ s.prev = prev; prev.next = s; }); - console.log(rightOnlyPath); return new Spline(rightOnlyPath); } - *findRightPath(start) { + *findRightPath(start, seen) { if (start.frontNeighbours.length === 0) { - yield start; return; } - let rightMost = start.frontNeighbours[0].copy(); + let rightMost = start.frontNeighbours[0]; for (const segment of start.frontNeighbours) { if (segment.id === rightMost.id) continue; const rotatedSegment = segment.copy(); @@ -1529,8 +1526,36 @@ rightMost = segment; } } + if (seen.has(rightMost.id)) return; + seen.add(rightMost.id); yield rightMost.copy(); - yield* this.findRightPath(rightMost); + yield* this.findRightPath(rightMost, seen); + } + *findLeftPath(start, seen) { + 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)) return; + seen.add(leftMost.id); + yield leftMost.copy(); + yield* this.findLeftPath(leftMost, seen); } }; var TrackSegment = class _TrackSegment extends PathSegment { @@ -2083,22 +2108,17 @@ constructor(track, cars) { this.path = track; this.t = 0; - this.nodes.push(this.path.followEvenPoints(this.t)); - this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40))); const resources2 = getContextItem("resources"); - const engineSprites = resources2.get("engine-sprites"); - console.log(engineSprites); this.cars = cars; - this.cars[0].points = this.nodes.map((n) => n); - this.cars[1].points = this.nodes.map((n) => n); - let currentOffset = 40; - for (const car of cars) { + console.log(track); + let currentOffset = 0; + for (const car of this.cars) { 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.cars.push(car); + this.nodes.push(a, b); } } move(dTime) { @@ -2113,25 +2133,18 @@ currentOffset += this.spacing; } } - // draw() { - // const doodler = getContextItem("doodler"); - // this.path.draw(); - // for (const [i, node] of this.nodes.entries()) { - // // doodler.drawCircle(node, 10, { color: "purple", weight: 3 }); - // doodler.fillCircle(node, 2, { color: "purple" }); - // // const next = this.nodes[i + 1]; - // // if (next) { - // // const to = Vector.sub(node.point, next.point); - // // to.setMag(40); - // // doodler.line(next.point, Vector.add(to, next.point)) - // // } - // } - // } draw() { - for (const car of this.cars) { - car.draw(); + const doodler2 = getContextItem("doodler"); + this.path.draw(); + for (const [i, node] of this.nodes.entries()) { + doodler2.fillCircle(node, 2, { color: "purple" }); } } + // draw() { + // for (const car of this.cars) { + // car.draw(); + // } + // } real2Track(length) { return length / this.path.pointSpacing; } @@ -2207,6 +2220,7 @@ ]); update(dt) { const ctx2 = getContext(); + const input = getContextItem("inputManager"); ctx2.track.draw(); for (const train of ctx2.trains) { train.move(dt); @@ -2292,7 +2306,6 @@ fillScreen: true, bg: "#302040" }); - doodler.scale = doodler.maxScale; var colors = [ "red", "orange", diff --git a/main.ts b/main.ts index 1035d27..9f34628 100644 --- a/main.ts +++ b/main.ts @@ -21,7 +21,7 @@ const doodler = new ZoomableDoodler({ bg: "#302040", }); // doodler.minScale = 0.1; -(doodler as any).scale = doodler.maxScale; +// (doodler as any).scale = doodler.maxScale; const colors = [ "red", diff --git a/state/states/RunningState.ts b/state/states/RunningState.ts index ab91af2..83c86ba 100644 --- a/state/states/RunningState.ts +++ b/state/states/RunningState.ts @@ -3,6 +3,7 @@ import { InputManager } from "../../lib/input.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 { State } from "../machine.ts"; import { States } from "./index.ts"; @@ -15,6 +16,8 @@ export class RunningState extends State { ]); override update(dt: number): void { const ctx = getContext() as { trains: Train[]; track: TrackSystem }; + // const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem }; + const input = getContextItem("inputManager"); // TODO // Update trains // Update world @@ -22,6 +25,9 @@ export class RunningState extends State { // Draw (maybe via a layer system that syncs with doodler) ctx.track.draw(); for (const train of ctx.trains) { + // if (input.getKeyState("ArrowUp")) { + // train.acceleration.x += 10; + // } train.move(dt); train.draw(); } @@ -32,7 +38,11 @@ export class RunningState extends State { const inputManager = getContextItem("inputManager"); const track = getContextItem("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); }); diff --git a/track/system.ts b/track/system.ts index b1a2950..70c2f86 100644 --- a/track/system.ts +++ b/track/system.ts @@ -107,10 +107,12 @@ export class TrackSystem { if (data.length === 0) return undefined; const track = new TrackSystem([]); const neighborMap = new Map(); + for (const segment of data) { track.segments.set(segment.id, TrackSegment.deserialize(segment)); neighborMap.set(segment.id, [segment.fNeighbors, segment.bNeighbors]); } + for (const segment of track.segments.values()) { segment.setTrack(track); const neighbors = neighborMap.get(segment.id); @@ -123,7 +125,7 @@ export class TrackSystem { ).filter((s) => s) as TrackSegment[]; } } - console.log(track.segments); + return track; } @@ -146,7 +148,7 @@ export class TrackSystem { if (!this.firstSegment) throw new Error("No first segment"); const rightOnlyPath = [ this.firstSegment.copy(), - ...this.findRightPath(this.firstSegment), + ...this.findRightPath(this.firstSegment, new Set([this.firstSegment.id])), ]; rightOnlyPath.forEach((s, i, arr) => { @@ -157,17 +159,17 @@ export class TrackSystem { prev.next = s; }); - console.log(rightOnlyPath); - return new Spline(rightOnlyPath); } - *findRightPath(start: TrackSegment): Generator { + *findRightPath( + start: TrackSegment, + seen: Set, + ): Generator { if (start.frontNeighbours.length === 0) { - yield start; return; } - let rightMost = start.frontNeighbours[0].copy(); + let rightMost = start.frontNeighbours[0]; for (const segment of start.frontNeighbours) { if (segment.id === rightMost.id) continue; const rotatedSegment = segment.copy(); @@ -184,8 +186,39 @@ export class TrackSystem { rightMost = segment; } } + if (seen.has(rightMost.id)) return; + seen.add(rightMost.id); yield rightMost.copy(); - yield* this.findRightPath(rightMost); + yield* this.findRightPath(rightMost, seen); + } + *findLeftPath( + start: TrackSegment, + seen: Set, + ): Generator { + 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)) return; + seen.add(leftMost.id); + yield leftMost.copy(); + yield* this.findLeftPath(leftMost, seen); } } @@ -423,7 +456,6 @@ export class Spline { 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); } diff --git a/train/newTrain.ts b/train/newTrain.ts new file mode 100644 index 0000000..6c3cd3d --- /dev/null +++ b/train/newTrain.ts @@ -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; + + get trailingPoint() { + const desired = this.velocity.copy(); + desired.normalize(); + desired.mult(-this._trailingPoint); + + return Vector.add(this.position, desired); + } + + constructor(path: Spline, 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"); + + 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.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; +} diff --git a/train/train.ts b/train/train.ts index dc8936e..8074d63 100644 --- a/train/train.ts +++ b/train/train.ts @@ -22,12 +22,9 @@ export class Train { constructor(track: Spline, cars: TrainCar[]) { this.path = track; this.t = 0; - this.nodes.push(this.path.followEvenPoints(this.t)); - this.nodes.push(this.path.followEvenPoints(this.t - this.real2Track(40))); const resources = getContextItem("resources"); - const engineSprites = resources.get("engine-sprites")!; - console.log(engineSprites); this.cars = cars; + console.log(track); // this.cars.push( // new TrainCar( // 55, @@ -44,16 +41,15 @@ export class Train { // { at: new Vector(80, 0), width: 40, height: 20 }, // ), // ); - this.cars[0].points = this.nodes.map((n) => n) as [Vector, Vector]; - this.cars[1].points = this.nodes.map((n) => n) as [Vector, Vector]; - let currentOffset = 40; - for (const car of cars) { + let currentOffset = 0; + for (const car of this.cars) { 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.cars.push(car); + this.nodes.push(a, b); + // this.cars.push(car); } } @@ -73,27 +69,27 @@ export class Train { // this.draw(); } - // draw() { - // const doodler = getContextItem("doodler"); - // this.path.draw(); - // for (const [i, node] of this.nodes.entries()) { - // // doodler.drawCircle(node, 10, { color: "purple", weight: 3 }); - // doodler.fillCircle(node, 2, { color: "purple" }); - // // const next = this.nodes[i + 1]; - // // if (next) { - // // const to = Vector.sub(node.point, next.point); - // // to.setMag(40); - // // doodler.line(next.point, Vector.add(to, next.point)) - // // } - // } - // } - draw() { - for (const car of this.cars) { - car.draw(); + const doodler = getContextItem("doodler"); + this.path.draw(); + for (const [i, node] of this.nodes.entries()) { + // doodler.drawCircle(node, 10, { color: "purple", weight: 3 }); + doodler.fillCircle(node, 2, { color: "purple" }); + // const next = this.nodes[i + 1]; + // if (next) { + // const to = Vector.sub(node.point, next.point); + // to.setMag(40); + // doodler.line(next.point, Vector.add(to, next.point)) + // } } } + // draw() { + // for (const car of this.cars) { + // car.draw(); + // } + // } + real2Track(length: number) { return length / this.path.pointSpacing; }