Compare commits
No commits in common. "8dc0af650fa751b8d197c80cb58f57274a5d76fc" and "952b5dd57f5e620accade65e98199418fe4945ac" have entirely different histories.
8dc0af650f
...
952b5dd57f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
bundle.js
|
|
||||||
dist/
|
|
574
bundle.js
574
bundle.js
@ -2,7 +2,6 @@
|
|||||||
// lib/context.ts
|
// lib/context.ts
|
||||||
var contextStack = [];
|
var contextStack = [];
|
||||||
var defaultContext = {};
|
var defaultContext = {};
|
||||||
var debug = JSON.parse(localStorage.getItem("debug") || "false");
|
|
||||||
function setDefaultContext(context) {
|
function setDefaultContext(context) {
|
||||||
Object.assign(defaultContext, context);
|
Object.assign(defaultContext, context);
|
||||||
}
|
}
|
||||||
@ -18,58 +17,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
function getContext() {
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
function getContextItem(prop) {
|
function getContextItem(prop) {
|
||||||
return ctx[prop];
|
return ctx[prop];
|
||||||
}
|
}
|
||||||
function setContextItem(prop, value) {
|
|
||||||
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
// lib/input.ts
|
||||||
[prop]: value
|
var InputManager = class {
|
||||||
|
keyStates = /* @__PURE__ */ new Map();
|
||||||
|
mouseStates = /* @__PURE__ */ new Map();
|
||||||
|
mouseLocation = { x: 0, y: 0 };
|
||||||
|
mouseDelta = { x: 0, y: 0 };
|
||||||
|
keyEvents = /* @__PURE__ */ new Map();
|
||||||
|
mouseEvents = /* @__PURE__ */ new Map();
|
||||||
|
constructor() {
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
this.keyStates.set(e.key, true);
|
||||||
|
this.keyEvents.get(e.key)?.call(e);
|
||||||
|
});
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
|
this.keyStates.set(e.key, false);
|
||||||
|
});
|
||||||
|
document.addEventListener("mousedown", (e) => {
|
||||||
|
this.mouseStates.set(e.button, true);
|
||||||
|
this.mouseEvents.get(e.button)?.call(e);
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", (e) => {
|
||||||
|
this.mouseStates.set(e.button, false);
|
||||||
|
});
|
||||||
|
self.addEventListener("mousemove", (e) => {
|
||||||
|
this.mouseLocation = { x: e.clientX, y: e.clientY };
|
||||||
|
this.mouseDelta = {
|
||||||
|
x: e.movementX,
|
||||||
|
y: e.movementY
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (debug) {
|
getKeyState(key) {
|
||||||
setInterval(() => {
|
return this.keyStates.get(key);
|
||||||
let ctxEl = document.getElementById("context");
|
|
||||||
if (!ctxEl) {
|
|
||||||
ctxEl = document.createElement("div");
|
|
||||||
ctxEl.id = "context";
|
|
||||||
document.body.append(ctxEl);
|
|
||||||
}
|
}
|
||||||
ctxEl.innerHTML = "";
|
getMouseState(key) {
|
||||||
const div = document.createElement("div");
|
return this.mouseStates.get(key);
|
||||||
const pre = document.createElement("pre");
|
|
||||||
const h3 = document.createElement("h3");
|
|
||||||
h3.textContent = "Default";
|
|
||||||
div.append(h3);
|
|
||||||
pre.textContent = safeStringify(defaultContext);
|
|
||||||
div.append(pre);
|
|
||||||
ctxEl.append(div);
|
|
||||||
for (const [idx, ctx2] of contextStack.entries()) {
|
|
||||||
const div2 = document.createElement("div");
|
|
||||||
const pre2 = document.createElement("pre");
|
|
||||||
const h32 = document.createElement("h3");
|
|
||||||
h32.textContent = "CTX " + idx;
|
|
||||||
div2.append(h32);
|
|
||||||
pre2.textContent = safeStringify(ctx2);
|
|
||||||
div2.append(pre2);
|
|
||||||
ctxEl.append(div2);
|
|
||||||
}
|
}
|
||||||
}, 1e3);
|
getMouseLocation() {
|
||||||
|
return this.mouseLocation;
|
||||||
}
|
}
|
||||||
function safeStringify(obj) {
|
getMouseDelta() {
|
||||||
const seen = /* @__PURE__ */ new WeakSet();
|
return this.mouseDelta;
|
||||||
return JSON.stringify(obj, (key, value) => {
|
|
||||||
if (typeof value === "object" && value !== null) {
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return "[Circular]";
|
|
||||||
}
|
}
|
||||||
seen.add(value);
|
onKey(key, cb) {
|
||||||
|
this.keyEvents.set(key, cb);
|
||||||
}
|
}
|
||||||
return value;
|
onMouse(key, cb) {
|
||||||
}, 2);
|
this.mouseEvents.set(key, cb);
|
||||||
}
|
}
|
||||||
|
offKey(key) {
|
||||||
|
this.keyEvents.delete(key);
|
||||||
|
}
|
||||||
|
offMouse(key) {
|
||||||
|
this.mouseEvents.delete(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// https://jsr.io/@bearmetal/doodler/0.0.3/geometry/constants.ts
|
// https://jsr.io/@bearmetal/doodler/0.0.3/geometry/constants.ts
|
||||||
var Constants = {
|
var Constants = {
|
||||||
@ -1013,82 +1020,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// lib/input.ts
|
|
||||||
var InputManager = class {
|
|
||||||
keyStates = /* @__PURE__ */ new Map();
|
|
||||||
mouseStates = /* @__PURE__ */ new Map();
|
|
||||||
mouseLocation = { x: 0, y: 0 };
|
|
||||||
mouseDelta = { x: 0, y: 0 };
|
|
||||||
keyEvents = /* @__PURE__ */ new Map();
|
|
||||||
mouseEvents = /* @__PURE__ */ new Map();
|
|
||||||
constructor() {
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
this.keyStates.set(e.key, true);
|
|
||||||
this.keyEvents.get(e.key)?.call(e);
|
|
||||||
});
|
|
||||||
document.addEventListener("keyup", (e) => {
|
|
||||||
this.keyStates.set(e.key, false);
|
|
||||||
});
|
|
||||||
document.addEventListener("mousedown", (e) => {
|
|
||||||
this.mouseStates.set(e.button, true);
|
|
||||||
this.mouseEvents.get(e.button)?.call(e);
|
|
||||||
});
|
|
||||||
document.addEventListener("mouseup", (e) => {
|
|
||||||
this.mouseStates.set(e.button, false);
|
|
||||||
});
|
|
||||||
self.addEventListener("mousemove", (e) => {
|
|
||||||
this.mouseLocation = { x: e.clientX, y: e.clientY };
|
|
||||||
this.mouseDelta = {
|
|
||||||
x: e.movementX,
|
|
||||||
y: e.movementY
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
getKeyState(key) {
|
|
||||||
return this.keyStates.get(key);
|
|
||||||
}
|
|
||||||
getMouseState(key) {
|
|
||||||
return this.mouseStates.get(key);
|
|
||||||
}
|
|
||||||
getMouseLocation() {
|
|
||||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
|
||||||
return getContextItem("doodler").screenToWorld(
|
|
||||||
this.mouseLocation.x,
|
|
||||||
this.mouseLocation.y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.mouseLocation;
|
|
||||||
}
|
|
||||||
getMouseLocationV() {
|
|
||||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
|
||||||
return new Vector(
|
|
||||||
getContextItem("doodler").screenToWorld(
|
|
||||||
this.mouseLocation.x,
|
|
||||||
this.mouseLocation.y
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new Vector(this.mouseLocation);
|
|
||||||
}
|
|
||||||
getMouseDelta() {
|
|
||||||
return this.mouseDelta;
|
|
||||||
}
|
|
||||||
onKey(key, cb) {
|
|
||||||
this.keyEvents.set(key, cb);
|
|
||||||
}
|
|
||||||
onMouse(key, cb) {
|
|
||||||
this.mouseEvents.set(key, cb);
|
|
||||||
}
|
|
||||||
offKey(key) {
|
|
||||||
const events = this.keyEvents.get(key);
|
|
||||||
this.keyEvents.delete(key);
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
offMouse(key) {
|
|
||||||
this.mouseEvents.delete(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// lib/resources.ts
|
// lib/resources.ts
|
||||||
var ResourceManager = class {
|
var ResourceManager = class {
|
||||||
resources = /* @__PURE__ */ new Map();
|
resources = /* @__PURE__ */ new Map();
|
||||||
@ -1146,72 +1077,13 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// state/machine.ts
|
|
||||||
var StateMachine = class {
|
|
||||||
_states = /* @__PURE__ */ new Map();
|
|
||||||
currentState;
|
|
||||||
update(dt, ctx2) {
|
|
||||||
this.currentState?.update(dt, ctx2);
|
|
||||||
}
|
|
||||||
get current() {
|
|
||||||
return this.currentState;
|
|
||||||
}
|
|
||||||
get states() {
|
|
||||||
return this._states;
|
|
||||||
}
|
|
||||||
addState(state2) {
|
|
||||||
this.states.set(state2.name, state2);
|
|
||||||
}
|
|
||||||
transitionTo(state2) {
|
|
||||||
if (!this.current) {
|
|
||||||
this.currentState = this._states.get(state2);
|
|
||||||
this.currentState.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.current?.canTransitionTo(state2) && this._states.has(state2)) {
|
|
||||||
this.current.stop();
|
|
||||||
this.currentState = this._states.get(state2);
|
|
||||||
this.current.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var State = class {
|
|
||||||
stateMachine;
|
|
||||||
constructor(stateMachine) {
|
|
||||||
this.stateMachine = stateMachine;
|
|
||||||
}
|
|
||||||
canTransitionTo(state2) {
|
|
||||||
return this.validTransitions.has(state2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// state/states/EditTrainState.ts
|
|
||||||
var EditTrainState = class extends State {
|
|
||||||
name = 4 /* EDIT_TRAIN */;
|
|
||||||
validTransitions = /* @__PURE__ */ new Set([
|
|
||||||
1 /* RUNNING */,
|
|
||||||
2 /* PAUSED */
|
|
||||||
]);
|
|
||||||
update(dt) {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// math/path.ts
|
// math/path.ts
|
||||||
var PathSegment = class {
|
var PathSegment = class {
|
||||||
points;
|
points;
|
||||||
length;
|
length;
|
||||||
startingLength;
|
|
||||||
constructor(points) {
|
constructor(points) {
|
||||||
this.points = points;
|
this.points = points;
|
||||||
this.length = this.calculateApproxLength(100);
|
this.length = this.calculateApproxLength(100);
|
||||||
this.startingLength = Math.round(this.length);
|
|
||||||
}
|
}
|
||||||
getPointAtT(t) {
|
getPointAtT(t) {
|
||||||
const [a, b, c, d] = this.points;
|
const [a, b, c, d] = this.points;
|
||||||
@ -1319,7 +1191,7 @@
|
|||||||
}, { prev: void 0, length: 0 }).length;
|
}, { prev: void 0, length: 0 }).length;
|
||||||
return this.length;
|
return this.length;
|
||||||
}
|
}
|
||||||
calculateEvenlySpacedPoints(spacing, resolution = 1, targetLength) {
|
calculateEvenlySpacedPoints(spacing, resolution = 1) {
|
||||||
const points = [];
|
const points = [];
|
||||||
points.push(this.points[0]);
|
points.push(this.points[0]);
|
||||||
let prev = points[0];
|
let prev = points[0];
|
||||||
@ -1342,41 +1214,8 @@
|
|||||||
}
|
}
|
||||||
prev = point;
|
prev = point;
|
||||||
}
|
}
|
||||||
if (targetLength && points.length < targetLength) {
|
|
||||||
while (points.length < targetLength) {
|
|
||||||
t += 1 / div;
|
|
||||||
const point = this.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
calculateSubdividedPoints(numberOfPoints) {
|
|
||||||
const points = [];
|
|
||||||
for (let i = 0; i < numberOfPoints; i++) {
|
|
||||||
const point = this.getPointAtT(i / numberOfPoints);
|
|
||||||
points.push(point);
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
clampLength() {
|
|
||||||
const curveLength = this.startingLength;
|
|
||||||
const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1);
|
|
||||||
if (points.length >= curveLength) {
|
|
||||||
this.points[3].set(points[curveLength]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// track/system.ts
|
// track/system.ts
|
||||||
@ -1385,23 +1224,11 @@
|
|||||||
doodler;
|
doodler;
|
||||||
constructor(segments) {
|
constructor(segments) {
|
||||||
this.doodler = getContextItem("doodler");
|
this.doodler = getContextItem("doodler");
|
||||||
for (const segment of segments) {
|
this.segments = segments;
|
||||||
this.segments.set(segment.id, segment);
|
|
||||||
}
|
}
|
||||||
}
|
draw() {
|
||||||
get firstSegment() {
|
for (const segment of this.segments) {
|
||||||
return this.segments.values().next().value;
|
segment.draw();
|
||||||
}
|
|
||||||
get lastSegment() {
|
|
||||||
return this.segments.values().toArray().pop();
|
|
||||||
}
|
|
||||||
registerSegment(segment) {
|
|
||||||
segment.setTrack(this);
|
|
||||||
this.segments.set(segment.id, segment);
|
|
||||||
}
|
|
||||||
draw(showControls = false) {
|
|
||||||
for (const segment of this.segments.values()) {
|
|
||||||
segment.draw(showControls);
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (getContextItem("showEnds")) {
|
if (getContextItem("showEnds")) {
|
||||||
@ -1429,7 +1256,7 @@
|
|||||||
}
|
}
|
||||||
findEnds() {
|
findEnds() {
|
||||||
const ends = [];
|
const ends = [];
|
||||||
for (const segment of this.segments.values()) {
|
for (const segment of this.segments) {
|
||||||
const [a, b, c, d] = segment.points;
|
const [a, b, c, d] = segment.points;
|
||||||
{
|
{
|
||||||
const tangent = Vector.sub(a, b).normalize();
|
const tangent = Vector.sub(a, b).normalize();
|
||||||
@ -1447,33 +1274,12 @@
|
|||||||
serialize() {
|
serialize() {
|
||||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||||
}
|
}
|
||||||
copy() {
|
|
||||||
const track = new _TrackSystem([]);
|
|
||||||
for (const segment of this.segments.values()) {
|
|
||||||
track.segments.set(segment.id, segment.copy());
|
|
||||||
}
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
static deserialize(data) {
|
static deserialize(data) {
|
||||||
if (data.length === 0) return void 0;
|
const track2 = new _TrackSystem([]);
|
||||||
const track = new _TrackSystem([]);
|
|
||||||
const neighborMap = /* @__PURE__ */ new Map();
|
|
||||||
for (const segment of data) {
|
for (const segment of data) {
|
||||||
track.segments.set(segment.id, TrackSegment.deserialize(segment));
|
track2.segments.set(segment.id, TrackSegment.deserialize(segment));
|
||||||
}
|
}
|
||||||
for (const segment of track.segments.values()) {
|
return track2;
|
||||||
segment.setTrack(track);
|
|
||||||
const neighbors = neighborMap.get(segment.id);
|
|
||||||
if (neighbors) {
|
|
||||||
segment.backNeighbours = neighbors[1].map(
|
|
||||||
(id) => track.segments.get(id)
|
|
||||||
).filter((s) => s);
|
|
||||||
segment.frontNeighbours = neighbors[0].map(
|
|
||||||
(id) => track.segments.get(id)
|
|
||||||
).filter((s) => s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return track;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var TrackSegment = class _TrackSegment extends PathSegment {
|
var TrackSegment = class _TrackSegment extends PathSegment {
|
||||||
@ -1490,7 +1296,7 @@
|
|||||||
setTrack(t) {
|
setTrack(t) {
|
||||||
this.track = t;
|
this.track = t;
|
||||||
}
|
}
|
||||||
draw(showControls = false) {
|
draw() {
|
||||||
this.doodler.drawBezier(
|
this.doodler.drawBezier(
|
||||||
this.points[0],
|
this.points[0],
|
||||||
this.points[1],
|
this.points[1],
|
||||||
@ -1500,16 +1306,6 @@
|
|||||||
strokeColor: "#ffffff50"
|
strokeColor: "#ffffff50"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (showControls) {
|
|
||||||
this.doodler.drawCircle(this.points[1], 4, {
|
|
||||||
color: "red",
|
|
||||||
weight: 3
|
|
||||||
});
|
|
||||||
this.doodler.drawCircle(this.points[2], 4, {
|
|
||||||
color: "red",
|
|
||||||
weight: 3
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
serialize() {
|
serialize() {
|
||||||
return {
|
return {
|
||||||
@ -1519,47 +1315,6 @@
|
|||||||
fNeighbors: this.frontNeighbours.map((n) => n.id)
|
fNeighbors: this.frontNeighbours.map((n) => n.id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
copy() {
|
|
||||||
return new _TrackSegment(
|
|
||||||
this.points.map((p) => p.copy()),
|
|
||||||
this.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
propagate() {
|
|
||||||
const [_, __, p3, p4] = this.points;
|
|
||||||
const tangent = Vector.sub(p4, p3);
|
|
||||||
for (const fNeighbour of this.frontNeighbours) {
|
|
||||||
fNeighbour.receivePropagation(tangent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastHeading;
|
|
||||||
receivePropagation(tangent) {
|
|
||||||
const [p1, p2, p3, p4] = this.points;
|
|
||||||
this.rotate(tangent);
|
|
||||||
this.propagate();
|
|
||||||
}
|
|
||||||
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
|
|
||||||
rotate(angle) {
|
|
||||||
const [p1, p2, p3, p4] = this.points;
|
|
||||||
let newP2;
|
|
||||||
if (angle instanceof Vector) {
|
|
||||||
const tan = angle;
|
|
||||||
angle = tan.heading() - (this.lastHeading ?? 0);
|
|
||||||
this.lastHeading = tan.heading();
|
|
||||||
newP2 = Vector.add(p1, tan);
|
|
||||||
} else {
|
|
||||||
const p1ToP2 = Vector.sub(p2, p1);
|
|
||||||
p1ToP2.rotate(angle);
|
|
||||||
newP2 = Vector.add(p1, p1ToP2);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
static deserialize(data) {
|
static deserialize(data) {
|
||||||
return new _TrackSegment(
|
return new _TrackSegment(
|
||||||
data.p.map((p) => new Vector(p[0], p[1], p[2])),
|
data.p.map((p) => new Vector(p[0], p[1], p[2])),
|
||||||
@ -1573,7 +1328,7 @@
|
|||||||
constructor(start) {
|
constructor(start) {
|
||||||
start = start || new Vector(100, 100);
|
start = start || new Vector(100, 100);
|
||||||
super([
|
super([
|
||||||
start,
|
start.copy(),
|
||||||
start.copy().add(25, 0),
|
start.copy().add(25, 0),
|
||||||
start.copy().add(75, 0),
|
start.copy().add(75, 0),
|
||||||
start.copy().add(100, 0)
|
start.copy().add(100, 0)
|
||||||
@ -1581,207 +1336,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// state/states/EditTrackState.ts
|
|
||||||
var EditTrackState = class extends State {
|
|
||||||
name = 3 /* EDIT_TRACK */;
|
|
||||||
validTransitions = /* @__PURE__ */ new Set([
|
|
||||||
1 /* RUNNING */,
|
|
||||||
2 /* PAUSED */
|
|
||||||
]);
|
|
||||||
heldEvents = /* @__PURE__ */ new Map();
|
|
||||||
currentSegment;
|
|
||||||
update(dt) {
|
|
||||||
const inputManager2 = getContextItem("inputManager");
|
|
||||||
const track = getContextItem("track");
|
|
||||||
const doodler2 = getContextItem("doodler");
|
|
||||||
const segment = this.currentSegment;
|
|
||||||
if (segment) {
|
|
||||||
segment.propagate();
|
|
||||||
const mousePos = inputManager2.getMouseLocationV();
|
|
||||||
const p1 = segment.points[0];
|
|
||||||
const p2 = segment.points[1];
|
|
||||||
const p3 = segment.points[2];
|
|
||||||
const p4 = segment.points[3];
|
|
||||||
const prevp3 = p3.copy();
|
|
||||||
const dirToMouse = Vector.sub(mousePos, p2).normalize();
|
|
||||||
const angleToMouse = dirToMouse.heading();
|
|
||||||
const angleToP1 = Vector.sub(p2, p1).heading();
|
|
||||||
const p2DistToMouse = Vector.dist(p2, mousePos);
|
|
||||||
const p3DistToMouse = Vector.dist(p3, mousePos);
|
|
||||||
const distToP3 = Vector.dist(p2, p3);
|
|
||||||
const distToP4 = Vector.dist(prevp3, p4);
|
|
||||||
if (Math.abs(angleToMouse - angleToP1) < 0.6 && p2DistToMouse > distToP3 && p3DistToMouse > distToP4) {
|
|
||||||
{
|
|
||||||
const dirToNewP3 = dirToMouse.copy().rotate(
|
|
||||||
-(angleToMouse - angleToP1) / 2
|
|
||||||
);
|
|
||||||
dirToNewP3.setMag(distToP3);
|
|
||||||
p3.set(Vector.add(p2, dirToNewP3));
|
|
||||||
doodler2.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
|
|
||||||
doodler2.line(p2, Vector.add(p2, dirToMouse.mult(100)), {
|
|
||||||
color: "red"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const dirToMouse2 = Vector.sub(mousePos, p3).normalize();
|
|
||||||
dirToMouse2.setMag(distToP4);
|
|
||||||
p4.set(Vector.add(p3, dirToMouse2));
|
|
||||||
doodler2.line(p3, Vector.add(p3, dirToMouse2), { color: "green" });
|
|
||||||
}
|
|
||||||
segment.clampLength();
|
|
||||||
}
|
|
||||||
doodler2.fillText(
|
|
||||||
segment.calculateApproxLength().toFixed(2),
|
|
||||||
p2.copy().add(10, 0),
|
|
||||||
100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
track.draw(true);
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
const inputManager2 = getContextItem("inputManager");
|
|
||||||
this.heldEvents.set("e", inputManager2.offKey("e"));
|
|
||||||
this.heldEvents.set("Escape", inputManager2.offKey("Escape"));
|
|
||||||
inputManager2.onKey("e", () => {
|
|
||||||
const state2 = getContextItem("state");
|
|
||||||
state2.transitionTo(1 /* RUNNING */);
|
|
||||||
});
|
|
||||||
const track = getContextItem("track");
|
|
||||||
setContextItem("trackCopy", track.copy());
|
|
||||||
inputManager2.onKey("Escape", () => {
|
|
||||||
const trackCopy = getContextItem("trackCopy");
|
|
||||||
setContextItem("track", trackCopy);
|
|
||||||
setContextItem("trackCopy", void 0);
|
|
||||||
const state2 = getContextItem("state");
|
|
||||||
state2.transitionTo(1 /* RUNNING */);
|
|
||||||
});
|
|
||||||
inputManager2.onKey("w", () => {
|
|
||||||
const track2 = getContextItem("track");
|
|
||||||
const segment = track2.lastSegment;
|
|
||||||
if (!segment) return;
|
|
||||||
const n = new StraightTrack(segment.points[3]);
|
|
||||||
const t = segment.tangent(1).heading();
|
|
||||||
n.rotate(t);
|
|
||||||
segment.frontNeighbours.push(n);
|
|
||||||
track2.registerSegment(n);
|
|
||||||
this.currentSegment = n;
|
|
||||||
});
|
|
||||||
inputManager2.onKey("1", () => {
|
|
||||||
this.currentSegment = track.firstSegment;
|
|
||||||
});
|
|
||||||
this.currentSegment = track.lastSegment;
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
const inputManager2 = getContextItem("inputManager");
|
|
||||||
inputManager2.offKey("e");
|
|
||||||
inputManager2.offKey("Escape");
|
|
||||||
if (this.heldEvents.size > 0) {
|
|
||||||
for (const [key, cb] of this.heldEvents) {
|
|
||||||
if (cb) {
|
|
||||||
getContextItem("inputManager").onKey(key, cb);
|
|
||||||
}
|
|
||||||
this.heldEvents.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setContextItem("trackCopy", void 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// state/states/PausedState.ts
|
|
||||||
var PausedState = class extends State {
|
|
||||||
name = 2 /* PAUSED */;
|
|
||||||
validTransitions = /* @__PURE__ */ new Set([
|
|
||||||
0 /* LOAD */,
|
|
||||||
1 /* RUNNING */,
|
|
||||||
3 /* EDIT_TRACK */,
|
|
||||||
4 /* EDIT_TRAIN */
|
|
||||||
]);
|
|
||||||
update(dt) {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// state/states/RunningState.ts
|
|
||||||
var RunningState = class extends State {
|
|
||||||
name = 1 /* RUNNING */;
|
|
||||||
validTransitions = /* @__PURE__ */ new Set([
|
|
||||||
2 /* PAUSED */,
|
|
||||||
3 /* EDIT_TRACK */
|
|
||||||
]);
|
|
||||||
update(dt) {
|
|
||||||
const ctx2 = getContext();
|
|
||||||
ctx2.track.draw();
|
|
||||||
for (const train of ctx2.trains) {
|
|
||||||
train.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// inputs.ts
|
|
||||||
function bootstrapInputs() {
|
|
||||||
const inputManager2 = getContextItem("inputManager");
|
|
||||||
inputManager2.onKey("e", () => {
|
|
||||||
const state2 = getContextItem("state");
|
|
||||||
state2.transitionTo(3 /* EDIT_TRACK */);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// state/states/LoadState.ts
|
|
||||||
var LoadState = class extends State {
|
|
||||||
name = 0 /* LOAD */;
|
|
||||||
validTransitions = /* @__PURE__ */ new Set([
|
|
||||||
1 /* RUNNING */
|
|
||||||
]);
|
|
||||||
update() {
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
const track = this.loadTrack() ?? new TrackSystem([new StraightTrack()]);
|
|
||||||
setContextItem("track", track);
|
|
||||||
const trains = this.loadTrains() ?? [];
|
|
||||||
setContextItem("trains", trains);
|
|
||||||
const resources2 = new ResourceManager();
|
|
||||||
setContextItem("resources", resources2);
|
|
||||||
const inputManager2 = new InputManager();
|
|
||||||
setContextItem("inputManager", inputManager2);
|
|
||||||
bootstrapInputs();
|
|
||||||
this.stateMachine.transitionTo(1 /* RUNNING */);
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
}
|
|
||||||
loadTrack() {
|
|
||||||
const track = TrackSystem.deserialize(
|
|
||||||
JSON.parse(localStorage.getItem("track") || "[]")
|
|
||||||
);
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
loadTrains() {
|
|
||||||
const trains = JSON.parse(localStorage.getItem("trains") || "[]");
|
|
||||||
return trains;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// state/states/index.ts
|
|
||||||
function bootstrapGameStateMachine() {
|
|
||||||
const stateMachine = new StateMachine();
|
|
||||||
stateMachine.addState(new LoadState(stateMachine));
|
|
||||||
stateMachine.addState(new RunningState(stateMachine));
|
|
||||||
stateMachine.addState(new PausedState(stateMachine));
|
|
||||||
stateMachine.addState(new EditTrackState(stateMachine));
|
|
||||||
stateMachine.addState(new EditTrainState(stateMachine));
|
|
||||||
stateMachine.transitionTo(0 /* LOAD */);
|
|
||||||
return stateMachine;
|
|
||||||
}
|
|
||||||
|
|
||||||
// main.ts
|
// main.ts
|
||||||
var inputManager = new InputManager();
|
var inputManager = new InputManager();
|
||||||
var resources = new ResourceManager();
|
var resources = new ResourceManager();
|
||||||
@ -1789,7 +1343,6 @@
|
|||||||
fillScreen: true,
|
fillScreen: true,
|
||||||
bg: "#302040"
|
bg: "#302040"
|
||||||
});
|
});
|
||||||
doodler.scale = doodler.maxScale;
|
|
||||||
setDefaultContext({
|
setDefaultContext({
|
||||||
inputManager,
|
inputManager,
|
||||||
doodler,
|
doodler,
|
||||||
@ -1797,8 +1350,6 @@
|
|||||||
debug: true,
|
debug: true,
|
||||||
showEnds: true
|
showEnds: true
|
||||||
});
|
});
|
||||||
var state = bootstrapGameStateMachine();
|
|
||||||
setContextItem("state", state);
|
|
||||||
doodler.init();
|
doodler.init();
|
||||||
addButton({
|
addButton({
|
||||||
text: "Hello World!",
|
text: "Hello World!",
|
||||||
@ -1814,7 +1365,8 @@
|
|||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
doodler.createLayer((_, __, dTime) => {
|
var track = new TrackSystem([new StraightTrack()]);
|
||||||
state.update(dTime);
|
doodler.createLayer(() => {
|
||||||
|
track.draw();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"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.3",
|
||||||
|
"@lib/": "./lib/"
|
||||||
}
|
}
|
||||||
}
|
}
|
9
deno.lock
generated
9
deno.lock
generated
@ -3,7 +3,6 @@
|
|||||||
"specifiers": {
|
"specifiers": {
|
||||||
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
|
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
|
||||||
"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:@std/assert@*": "1.0.10",
|
"jsr:@std/assert@*": "1.0.10",
|
||||||
"jsr:@std/assert@^1.0.10": "1.0.10",
|
"jsr:@std/assert@^1.0.10": "1.0.10",
|
||||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
||||||
@ -33,14 +32,6 @@
|
|||||||
"jsr:@std/path@^1.0.6"
|
"jsr:@std/path@^1.0.6"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@luca/esbuild-deno-loader@0.11.1": {
|
|
||||||
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/bytes",
|
|
||||||
"jsr:@std/encoding",
|
|
||||||
"jsr:@std/path@^1.0.6"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/assert@1.0.10": {
|
"@std/assert@1.0.10": {
|
||||||
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
|
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
2
dev.ts
2
dev.ts
@ -1,7 +1,7 @@
|
|||||||
/// <reference lib="deno.ns" />
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
import * as esbuild from "npm:esbuild";
|
import * as esbuild from "npm:esbuild";
|
||||||
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11.1";
|
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
|
||||||
import { serveDir } from "jsr:@std/http";
|
import { serveDir } from "jsr:@std/http";
|
||||||
|
|
||||||
async function* crawl(dir: string): AsyncIterable<string> {
|
async function* crawl(dir: string): AsyncIterable<string> {
|
||||||
|
12
index.html
12
index.html
@ -14,18 +14,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
#context {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 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>
|
||||||
|
12
inputs.ts
12
inputs.ts
@ -1,12 +0,0 @@
|
|||||||
import { getContextItem } from "./lib/context.ts";
|
|
||||||
import { InputManager } from "./lib/input.ts";
|
|
||||||
import { StateMachine } from "./state/machine.ts";
|
|
||||||
import { States } from "./state/states/index.ts";
|
|
||||||
|
|
||||||
export function bootstrapInputs() {
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
|
||||||
inputManager.onKey("e", () => {
|
|
||||||
const state = getContextItem<StateMachine<States>>("state");
|
|
||||||
state.transitionTo(States.EDIT_TRACK);
|
|
||||||
});
|
|
||||||
}
|
|
@ -3,8 +3,6 @@ type ContextStore = Record<string, any>;
|
|||||||
const contextStack: ContextStore[] = [];
|
const contextStack: ContextStore[] = [];
|
||||||
const defaultContext: ContextStore = {};
|
const defaultContext: ContextStore = {};
|
||||||
|
|
||||||
const debug = JSON.parse(localStorage.getItem("debug") || "false");
|
|
||||||
|
|
||||||
export function setDefaultContext(context: ContextStore) {
|
export function setDefaultContext(context: ContextStore) {
|
||||||
Object.assign(defaultContext, context);
|
Object.assign(defaultContext, context);
|
||||||
}
|
}
|
||||||
@ -25,7 +23,7 @@ export const ctx = new Proxy(
|
|||||||
for (let i = contextStack.length - 1; i >= 0; i--) {
|
for (let i = contextStack.length - 1; i >= 0; i--) {
|
||||||
if (prop in contextStack[i]) return contextStack[i][prop];
|
if (prop in contextStack[i]) return contextStack[i][prop];
|
||||||
}
|
}
|
||||||
if (prop in defaultContext) return defaultContext[prop];
|
if (prop in defaultContext) return defaultContext[prop]; // ✅ Fallback to default
|
||||||
throw new Error(`Context variable '${prop}' is not defined.`);
|
throw new Error(`Context variable '${prop}' is not defined.`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -37,51 +35,3 @@ 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 setContextItem<T>(prop: string, value: T) {
|
|
||||||
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
|
||||||
[prop]: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
setInterval(() => {
|
|
||||||
let ctxEl = document.getElementById("context");
|
|
||||||
if (!ctxEl) {
|
|
||||||
ctxEl = document.createElement("div");
|
|
||||||
ctxEl.id = "context";
|
|
||||||
document.body.append(ctxEl);
|
|
||||||
}
|
|
||||||
ctxEl.innerHTML = "";
|
|
||||||
const div = document.createElement("div");
|
|
||||||
const pre = document.createElement("pre");
|
|
||||||
const h3 = document.createElement("h3");
|
|
||||||
h3.textContent = "Default";
|
|
||||||
div.append(h3);
|
|
||||||
pre.textContent = safeStringify(defaultContext);
|
|
||||||
div.append(pre);
|
|
||||||
ctxEl.append(div);
|
|
||||||
for (const [idx, ctx] of contextStack.entries()) {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
const pre = document.createElement("pre");
|
|
||||||
const h3 = document.createElement("h3");
|
|
||||||
h3.textContent = "CTX " + idx;
|
|
||||||
div.append(h3);
|
|
||||||
pre.textContent = safeStringify(ctx);
|
|
||||||
div.append(pre);
|
|
||||||
ctxEl.append(div);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeStringify(obj: any) {
|
|
||||||
const seen = new WeakSet();
|
|
||||||
return JSON.stringify(obj, (key, value) => {
|
|
||||||
if (typeof value === "object" && value !== null) {
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return "[Circular]"; // Replace circular references
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}, 2);
|
|
||||||
}
|
|
||||||
|
22
lib/input.ts
22
lib/input.ts
@ -1,6 +1,3 @@
|
|||||||
import { Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
|
||||||
import { getContextItem } from "./context.ts";
|
|
||||||
|
|
||||||
export class InputManager {
|
export class InputManager {
|
||||||
private keyStates: Map<string | number, boolean> = new Map();
|
private keyStates: Map<string | number, boolean> = new Map();
|
||||||
private mouseStates: Map<string | number, boolean> = new Map();
|
private mouseStates: Map<string | number, boolean> = new Map();
|
||||||
@ -42,25 +39,8 @@ export class InputManager {
|
|||||||
return this.mouseStates.get(key);
|
return this.mouseStates.get(key);
|
||||||
}
|
}
|
||||||
getMouseLocation() {
|
getMouseLocation() {
|
||||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
|
||||||
return getContextItem<ZoomableDoodler>("doodler").screenToWorld(
|
|
||||||
this.mouseLocation.x,
|
|
||||||
this.mouseLocation.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.mouseLocation;
|
return this.mouseLocation;
|
||||||
}
|
}
|
||||||
getMouseLocationV() {
|
|
||||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
|
||||||
return new Vector(
|
|
||||||
getContextItem<ZoomableDoodler>("doodler").screenToWorld(
|
|
||||||
this.mouseLocation.x,
|
|
||||||
this.mouseLocation.y,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new Vector(this.mouseLocation);
|
|
||||||
}
|
|
||||||
getMouseDelta() {
|
getMouseDelta() {
|
||||||
return this.mouseDelta;
|
return this.mouseDelta;
|
||||||
}
|
}
|
||||||
@ -73,9 +53,7 @@ export class InputManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
offKey(key: string | number) {
|
offKey(key: string | number) {
|
||||||
const events = this.keyEvents.get(key);
|
|
||||||
this.keyEvents.delete(key);
|
this.keyEvents.delete(key);
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
offMouse(key: string | number) {
|
offMouse(key: string | number) {
|
||||||
this.mouseEvents.delete(key);
|
this.mouseEvents.delete(key);
|
||||||
|
19
main.ts
19
main.ts
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { setDefaultContext } from "./lib/context.ts";
|
||||||
getContext,
|
|
||||||
setContextItem,
|
|
||||||
setDefaultContext,
|
|
||||||
} from "./lib/context.ts";
|
|
||||||
import { InputManager } from "./lib/input.ts";
|
import { InputManager } from "./lib/input.ts";
|
||||||
|
|
||||||
import { Doodler, Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
import { Doodler, Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
||||||
@ -10,8 +6,6 @@ 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 { bootstrapGameStateMachine } from "./state/states/index.ts";
|
|
||||||
|
|
||||||
const inputManager = new InputManager();
|
const inputManager = new InputManager();
|
||||||
const resources = new ResourceManager();
|
const resources = new ResourceManager();
|
||||||
@ -19,8 +13,6 @@ const doodler = new ZoomableDoodler({
|
|||||||
fillScreen: true,
|
fillScreen: true,
|
||||||
bg: "#302040",
|
bg: "#302040",
|
||||||
});
|
});
|
||||||
// doodler.minScale = 0.1;
|
|
||||||
(doodler as any).scale = doodler.maxScale;
|
|
||||||
|
|
||||||
setDefaultContext({
|
setDefaultContext({
|
||||||
inputManager,
|
inputManager,
|
||||||
@ -30,9 +22,6 @@ setDefaultContext({
|
|||||||
showEnds: true,
|
showEnds: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = bootstrapGameStateMachine();
|
|
||||||
setContextItem("state", state);
|
|
||||||
|
|
||||||
doodler.init();
|
doodler.init();
|
||||||
addButton({
|
addButton({
|
||||||
text: "Hello World!",
|
text: "Hello World!",
|
||||||
@ -49,6 +38,8 @@ addButton({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
doodler.createLayer((_, __, dTime) => {
|
const track = new TrackSystem([new StraightTrack()]);
|
||||||
state.update(dTime);
|
|
||||||
|
doodler.createLayer(() => {
|
||||||
|
track.draw();
|
||||||
});
|
});
|
||||||
|
48
math/path.ts
48
math/path.ts
@ -41,12 +41,10 @@ export class PathSegment {
|
|||||||
points: [Vector, Vector, Vector, Vector];
|
points: [Vector, Vector, Vector, Vector];
|
||||||
|
|
||||||
length: number;
|
length: number;
|
||||||
startingLength: number;
|
|
||||||
|
|
||||||
constructor(points: [Vector, Vector, Vector, Vector]) {
|
constructor(points: [Vector, Vector, Vector, Vector]) {
|
||||||
this.points = points;
|
this.points = points;
|
||||||
this.length = this.calculateApproxLength(100);
|
this.length = this.calculateApproxLength(100);
|
||||||
this.startingLength = Math.round(this.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPointAtT(t: number) {
|
getPointAtT(t: number) {
|
||||||
@ -179,11 +177,7 @@ export class PathSegment {
|
|||||||
return this.length;
|
return this.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateEvenlySpacedPoints(
|
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||||
spacing: number,
|
|
||||||
resolution = 1,
|
|
||||||
targetLength?: number,
|
|
||||||
) {
|
|
||||||
const points: Vector[] = [];
|
const points: Vector[] = [];
|
||||||
|
|
||||||
points.push(this.points[0]);
|
points.push(this.points[0]);
|
||||||
@ -212,46 +206,6 @@ export class PathSegment {
|
|||||||
prev = point;
|
prev = point;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetLength && points.length < targetLength) {
|
|
||||||
while (points.length < targetLength) {
|
|
||||||
t += 1 / div;
|
|
||||||
const point = this.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateSubdividedPoints(numberOfPoints: number) {
|
|
||||||
const points: Vector[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfPoints; i++) {
|
|
||||||
const point = this.getPointAtT(i / numberOfPoints);
|
|
||||||
points.push(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
clampLength() {
|
|
||||||
const curveLength = this.startingLength;
|
|
||||||
const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1);
|
|
||||||
if (points.length >= curveLength) {
|
|
||||||
this.points[3].set(points[curveLength]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
export class StateMachine<T> {
|
export class StateMachine {
|
||||||
private _states: Map<T, State<T>> = new Map();
|
private _states: Map<string, State> = new Map();
|
||||||
private currentState?: State<T>;
|
private currentState: State;
|
||||||
|
|
||||||
update(dt: number, ctx?: CanvasRenderingContext2D) {
|
constructor(states: State[]) {
|
||||||
this.currentState?.update(dt, ctx);
|
this.currentState = states[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt: number) {
|
||||||
|
this.currentState.update(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
get current() {
|
get current() {
|
||||||
@ -14,37 +18,32 @@ export class StateMachine<T> {
|
|||||||
return this._states;
|
return this._states;
|
||||||
}
|
}
|
||||||
|
|
||||||
addState(state: State<T>) {
|
addState(state: State) {
|
||||||
this.states.set(state.name, state);
|
this.states.set(state.name, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
transitionTo(state: T) {
|
transitionTo(state: State) {
|
||||||
if (!this.current) {
|
if (this.current.canTransitionTo(state)) {
|
||||||
this.currentState = this._states.get(state)!;
|
|
||||||
this.currentState.start();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.current?.canTransitionTo(state) && this._states.has(state)) {
|
|
||||||
this.current.stop();
|
this.current.stop();
|
||||||
this.currentState = this._states.get(state)!;
|
this.currentState = state;
|
||||||
this.current.start();
|
this.current.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class State<T> {
|
export abstract class State<T> {
|
||||||
protected stateMachine: StateMachine<T>;
|
private stateMachine: StateMachine;
|
||||||
protected abstract validTransitions: Set<T>;
|
protected abstract validTransitions: Set<T>;
|
||||||
|
|
||||||
abstract readonly name: T;
|
abstract readonly name: T;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stateMachine: StateMachine<T>,
|
stateMachine: StateMachine,
|
||||||
) {
|
) {
|
||||||
this.stateMachine = stateMachine;
|
this.stateMachine = stateMachine;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract update(dt: number, ctx?: CanvasRenderingContext2D): void;
|
abstract update(dt: number): void;
|
||||||
abstract start(): void;
|
abstract start(): void;
|
||||||
abstract stop(): void;
|
abstract stop(): void;
|
||||||
|
|
||||||
@ -52,25 +51,3 @@ export abstract class State<T> {
|
|||||||
return this.validTransitions.has(state);
|
return this.validTransitions.has(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ExtensibleState<T> extends State<T> {
|
|
||||||
extensions: Map<string, (...args: unknown[]) => void> = new Map();
|
|
||||||
registerExtension(name: string, cb: (...args: unknown[]) => void) {
|
|
||||||
this.extensions.set(name, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(stateMachine: StateMachine<T>) {
|
|
||||||
super(stateMachine);
|
|
||||||
const oldUpdate = this.update;
|
|
||||||
this.update = function (dt: number, ctx?: CanvasRenderingContext2D) {
|
|
||||||
oldUpdate.apply(this, [dt, ctx]);
|
|
||||||
this.runExtensions(dt, ctx);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
runExtensions(...args: unknown[]) {
|
|
||||||
for (const [name, cb] of this.extensions) {
|
|
||||||
cb(...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
139
state/states.ts
Normal file
139
state/states.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { State } from "./machine.ts";
|
||||||
|
|
||||||
|
enum States {
|
||||||
|
LOAD,
|
||||||
|
RUNNING,
|
||||||
|
PAUSED,
|
||||||
|
EDIT_TRACK,
|
||||||
|
EDIT_TRAIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoadState extends State<States> {
|
||||||
|
override name: States = States.LOAD;
|
||||||
|
override validTransitions: Set<States> = new Set([
|
||||||
|
States.RUNNING,
|
||||||
|
]);
|
||||||
|
|
||||||
|
override update(dt: number): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
override start(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// load track into context
|
||||||
|
// Load trains into context
|
||||||
|
// Load resources into context
|
||||||
|
// Switch to running state
|
||||||
|
}
|
||||||
|
override stop(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RunningState extends State<States> {
|
||||||
|
override name: States = States.RUNNING;
|
||||||
|
override validTransitions: Set<States> = new Set([
|
||||||
|
States.PAUSED,
|
||||||
|
States.EDIT_TRACK,
|
||||||
|
]);
|
||||||
|
override update(dt: number): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Update trains
|
||||||
|
// Update world
|
||||||
|
// Handle input
|
||||||
|
// Draw (maybe via a layer system that syncs with doodler)
|
||||||
|
// Monitor world events
|
||||||
|
}
|
||||||
|
override start(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
override stop(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PausedState extends State<States> {
|
||||||
|
override name: States = States.PAUSED;
|
||||||
|
override validTransitions: Set<States> = new Set([
|
||||||
|
States.LOAD,
|
||||||
|
States.RUNNING,
|
||||||
|
States.EDIT_TRACK,
|
||||||
|
States.EDIT_TRAIN,
|
||||||
|
]);
|
||||||
|
override update(dt: number): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Handle input
|
||||||
|
// Draw ui
|
||||||
|
}
|
||||||
|
override start(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Save tracks to cache
|
||||||
|
// Save trains to cache
|
||||||
|
// Save resources to cache
|
||||||
|
}
|
||||||
|
override stop(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditTrackState extends State<States> {
|
||||||
|
override name: States = States.EDIT_TRACK;
|
||||||
|
override validTransitions: Set<States> = new Set([
|
||||||
|
States.RUNNING,
|
||||||
|
States.PAUSED,
|
||||||
|
]);
|
||||||
|
override update(dt: number): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Handle input
|
||||||
|
// Draw ui
|
||||||
|
// Draw track
|
||||||
|
// Draw track points
|
||||||
|
// Draw track tangents
|
||||||
|
}
|
||||||
|
override start(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Cache trains and save
|
||||||
|
// Stash track in context
|
||||||
|
}
|
||||||
|
override stop(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditTrainState extends State<States> {
|
||||||
|
override name: States = States.EDIT_TRAIN;
|
||||||
|
override validTransitions: Set<States> = new Set([
|
||||||
|
States.RUNNING,
|
||||||
|
States.PAUSED,
|
||||||
|
]);
|
||||||
|
|
||||||
|
override update(dt: number): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
override start(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
// TODO
|
||||||
|
// Cache trains
|
||||||
|
// Stash train in context
|
||||||
|
// Draw track
|
||||||
|
// Draw train (filtered by train ID)
|
||||||
|
}
|
||||||
|
override stop(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
@ -1,165 +0,0 @@
|
|||||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
|
||||||
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
|
||||||
import { InputManager } from "../../lib/input.ts";
|
|
||||||
import { TrackSystem } from "../../track/system.ts";
|
|
||||||
import { State, StateMachine } from "../machine.ts";
|
|
||||||
import { States } from "./index.ts";
|
|
||||||
import { StraightTrack } from "../../track/shapes.ts";
|
|
||||||
import { TrackSegment } from "../../track/system.ts";
|
|
||||||
|
|
||||||
export class EditTrackState extends State<States> {
|
|
||||||
override name: States = States.EDIT_TRACK;
|
|
||||||
override validTransitions: Set<States> = new Set([
|
|
||||||
States.RUNNING,
|
|
||||||
States.PAUSED,
|
|
||||||
]);
|
|
||||||
|
|
||||||
private heldEvents: Map<string | number, (() => void) | undefined> =
|
|
||||||
new Map();
|
|
||||||
|
|
||||||
private currentSegment?: TrackSegment;
|
|
||||||
|
|
||||||
override update(dt: number): void {
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
|
||||||
const track = getContextItem<TrackSystem>("track");
|
|
||||||
|
|
||||||
// For moving a segment, i.e. the currently active one
|
|
||||||
// const segment = track.lastSegment;
|
|
||||||
// if (segment) {
|
|
||||||
// const firstPoint = segment.points[0].copy();
|
|
||||||
// const { x, y } = inputManager.getMouseLocation();
|
|
||||||
// segment.points.forEach((p, i) => {
|
|
||||||
// const relativePoint = Vector.sub(p, firstPoint);
|
|
||||||
// p.set(x, y);
|
|
||||||
// p.add(relativePoint);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// manipulate only end of segment while maintaining length
|
|
||||||
// const segment = track.lastSegment;
|
|
||||||
// if (segment) {
|
|
||||||
// const p3 = segment.points[2];
|
|
||||||
// const p4 = segment.points[3];
|
|
||||||
// let curveLength = Math.round(segment.calculateApproxLength());
|
|
||||||
// this.startingLength = this.startingLength ?? curveLength;
|
|
||||||
// curveLength = this.startingLength;
|
|
||||||
// const { x, y } = inputManager.getMouseLocation();
|
|
||||||
// p4.set(x, y);
|
|
||||||
// const points = segment.calculateEvenlySpacedPoints(1);
|
|
||||||
// if (points.length > curveLength) p4.set(points[curveLength - 1]);
|
|
||||||
|
|
||||||
// // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100);
|
|
||||||
// }
|
|
||||||
const doodler = getContextItem<Doodler>("doodler");
|
|
||||||
|
|
||||||
// Adjust angles until tangent points to mouse
|
|
||||||
const segment = this.currentSegment;
|
|
||||||
if (segment) {
|
|
||||||
segment.propagate();
|
|
||||||
|
|
||||||
const mousePos = inputManager.getMouseLocationV();
|
|
||||||
const p1 = segment.points[0];
|
|
||||||
const p2 = segment.points[1];
|
|
||||||
const p3 = segment.points[2];
|
|
||||||
const p4 = segment.points[3];
|
|
||||||
|
|
||||||
const prevp3 = p3.copy();
|
|
||||||
const dirToMouse = Vector.sub(mousePos, p2).normalize();
|
|
||||||
const angleToMouse = dirToMouse.heading();
|
|
||||||
const angleToP1 = Vector.sub(p2, p1).heading();
|
|
||||||
const p2DistToMouse = Vector.dist(p2, mousePos);
|
|
||||||
const p3DistToMouse = Vector.dist(p3, mousePos);
|
|
||||||
const distToP3 = Vector.dist(p2, p3);
|
|
||||||
const distToP4 = Vector.dist(prevp3, p4);
|
|
||||||
if (
|
|
||||||
Math.abs(angleToMouse - angleToP1) < .6 &&
|
|
||||||
p2DistToMouse > distToP3 &&
|
|
||||||
p3DistToMouse > distToP4
|
|
||||||
) {
|
|
||||||
{
|
|
||||||
const dirToNewP3 = dirToMouse.copy().rotate(
|
|
||||||
-(angleToMouse - angleToP1) / 2,
|
|
||||||
);
|
|
||||||
dirToNewP3.setMag(distToP3);
|
|
||||||
p3.set(Vector.add(p2, dirToNewP3));
|
|
||||||
doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
|
|
||||||
doodler.line(p2, Vector.add(p2, dirToMouse.mult(100)), {
|
|
||||||
color: "red",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const dirToMouse = Vector.sub(mousePos, p3).normalize();
|
|
||||||
dirToMouse.setMag(distToP4);
|
|
||||||
p4.set(Vector.add(p3, dirToMouse));
|
|
||||||
doodler.line(p3, Vector.add(p3, dirToMouse), { color: "green" });
|
|
||||||
}
|
|
||||||
segment.clampLength();
|
|
||||||
}
|
|
||||||
doodler.fillText(
|
|
||||||
segment.calculateApproxLength().toFixed(2),
|
|
||||||
p2.copy().add(10, 0),
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
track.draw(true);
|
|
||||||
// TODO
|
|
||||||
// Draw ui
|
|
||||||
// Draw track points
|
|
||||||
// Draw track tangents
|
|
||||||
}
|
|
||||||
override start(): void {
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
|
||||||
this.heldEvents.set("e", inputManager.offKey("e"));
|
|
||||||
this.heldEvents.set("Escape", inputManager.offKey("Escape"));
|
|
||||||
inputManager.onKey("e", () => {
|
|
||||||
const state = getContextItem<StateMachine<States>>("state");
|
|
||||||
state.transitionTo(States.RUNNING);
|
|
||||||
});
|
|
||||||
|
|
||||||
const track = getContextItem<TrackSystem>("track");
|
|
||||||
setContextItem("trackCopy", track.copy());
|
|
||||||
inputManager.onKey("Escape", () => {
|
|
||||||
const trackCopy = getContextItem<TrackSystem>("trackCopy");
|
|
||||||
setContextItem("track", trackCopy);
|
|
||||||
setContextItem("trackCopy", undefined);
|
|
||||||
const state = getContextItem<StateMachine<States>>("state");
|
|
||||||
state.transitionTo(States.RUNNING);
|
|
||||||
});
|
|
||||||
|
|
||||||
inputManager.onKey("w", () => {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentSegment = track.lastSegment;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Cache trains and save
|
|
||||||
}
|
|
||||||
override stop(): void {
|
|
||||||
const inputManager = getContextItem<InputManager>("inputManager");
|
|
||||||
inputManager.offKey("e");
|
|
||||||
inputManager.offKey("Escape");
|
|
||||||
if (this.heldEvents.size > 0) {
|
|
||||||
for (const [key, cb] of this.heldEvents) {
|
|
||||||
if (cb) {
|
|
||||||
getContextItem<InputManager>("inputManager").onKey(key, cb);
|
|
||||||
}
|
|
||||||
this.heldEvents.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setContextItem("trackCopy", undefined);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { State } from "../machine.ts";
|
|
||||||
import { States } from "./index.ts";
|
|
||||||
|
|
||||||
export class EditTrainState extends State<States> {
|
|
||||||
override name: States = States.EDIT_TRAIN;
|
|
||||||
override validTransitions: Set<States> = new Set([
|
|
||||||
States.RUNNING,
|
|
||||||
States.PAUSED,
|
|
||||||
]);
|
|
||||||
|
|
||||||
override update(dt: number): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
override start(): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
// TODO
|
|
||||||
// Cache trains
|
|
||||||
// Stash train in context
|
|
||||||
// Draw track
|
|
||||||
// Draw train (filtered by train ID)
|
|
||||||
}
|
|
||||||
override stop(): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import { bootstrapInputs } from "../../inputs.ts";
|
|
||||||
import { setContextItem } from "../../lib/context.ts";
|
|
||||||
import { InputManager } from "../../lib/input.ts";
|
|
||||||
import { ResourceManager } from "../../lib/resources.ts";
|
|
||||||
import { StraightTrack } from "../../track/shapes.ts";
|
|
||||||
import { TrackSystem } from "../../track/system.ts";
|
|
||||||
import { State } from "../machine.ts";
|
|
||||||
import { States } from "./index.ts";
|
|
||||||
|
|
||||||
export class LoadState extends State<States> {
|
|
||||||
override name: States = States.LOAD;
|
|
||||||
override validTransitions: Set<States> = new Set([
|
|
||||||
States.RUNNING,
|
|
||||||
]);
|
|
||||||
|
|
||||||
override update(): void {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
override start(): void {
|
|
||||||
const track = this.loadTrack() ?? new TrackSystem([new StraightTrack()]);
|
|
||||||
setContextItem("track", track);
|
|
||||||
|
|
||||||
const trains = this.loadTrains() ?? [];
|
|
||||||
setContextItem("trains", trains);
|
|
||||||
|
|
||||||
const resources = new ResourceManager();
|
|
||||||
setContextItem("resources", resources);
|
|
||||||
|
|
||||||
const inputManager = new InputManager();
|
|
||||||
setContextItem("inputManager", inputManager);
|
|
||||||
|
|
||||||
bootstrapInputs();
|
|
||||||
|
|
||||||
this.stateMachine.transitionTo(States.RUNNING);
|
|
||||||
}
|
|
||||||
override stop(): void {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadTrack() {
|
|
||||||
const track = TrackSystem.deserialize(
|
|
||||||
JSON.parse(localStorage.getItem("track") || "[]"),
|
|
||||||
);
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadTrains() {
|
|
||||||
const trains = JSON.parse(localStorage.getItem("trains") || "[]");
|
|
||||||
return trains;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { State } from "../machine.ts";
|
|
||||||
import { States } from "./index.ts";
|
|
||||||
|
|
||||||
export class PausedState extends State<States> {
|
|
||||||
override name: States = States.PAUSED;
|
|
||||||
override validTransitions: Set<States> = new Set([
|
|
||||||
States.LOAD,
|
|
||||||
States.RUNNING,
|
|
||||||
States.EDIT_TRACK,
|
|
||||||
States.EDIT_TRAIN,
|
|
||||||
]);
|
|
||||||
override update(dt: number): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
// TODO
|
|
||||||
// Handle input
|
|
||||||
// Draw ui
|
|
||||||
}
|
|
||||||
override start(): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
// TODO
|
|
||||||
// Save tracks to cache
|
|
||||||
// Save trains to cache
|
|
||||||
// Save resources to cache
|
|
||||||
}
|
|
||||||
override stop(): void {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
// TODO
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import { getContext } from "../../lib/context.ts";
|
|
||||||
import { TrackSystem } from "../../track/system.ts";
|
|
||||||
import { Train } from "../../train/train.ts";
|
|
||||||
import { State } from "../machine.ts";
|
|
||||||
import { States } from "./index.ts";
|
|
||||||
|
|
||||||
export class RunningState extends State<States> {
|
|
||||||
override name: States = States.RUNNING;
|
|
||||||
override validTransitions: Set<States> = new Set([
|
|
||||||
States.PAUSED,
|
|
||||||
States.EDIT_TRACK,
|
|
||||||
]);
|
|
||||||
override update(dt: number): void {
|
|
||||||
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
|
|
||||||
// TODO
|
|
||||||
// Update trains
|
|
||||||
// Update world
|
|
||||||
// Handle input
|
|
||||||
// Draw (maybe via a layer system that syncs with doodler)
|
|
||||||
ctx.track.draw();
|
|
||||||
for (const train of ctx.trains) {
|
|
||||||
train.draw();
|
|
||||||
}
|
|
||||||
// Monitor world events
|
|
||||||
}
|
|
||||||
override start(): void {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
override stop(): void {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { StateMachine } from "../machine.ts";
|
|
||||||
import { Track } from "../../track.ts";
|
|
||||||
import { EditTrainState } from "./EditTrainState.ts";
|
|
||||||
import { EditTrackState } from "./EditTrackState.ts";
|
|
||||||
import { PausedState } from "./PausedState.ts";
|
|
||||||
import { RunningState } from "./RunningState.ts";
|
|
||||||
import { LoadState } from "./LoadState.ts";
|
|
||||||
|
|
||||||
export enum States {
|
|
||||||
LOAD,
|
|
||||||
RUNNING,
|
|
||||||
PAUSED,
|
|
||||||
EDIT_TRACK,
|
|
||||||
EDIT_TRAIN,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bootstrapGameStateMachine() {
|
|
||||||
const stateMachine = new StateMachine<States>();
|
|
||||||
stateMachine.addState(new LoadState(stateMachine));
|
|
||||||
stateMachine.addState(new RunningState(stateMachine));
|
|
||||||
stateMachine.addState(new PausedState(stateMachine));
|
|
||||||
stateMachine.addState(new EditTrackState(stateMachine));
|
|
||||||
stateMachine.addState(new EditTrainState(stateMachine));
|
|
||||||
stateMachine.transitionTo(States.LOAD);
|
|
||||||
return stateMachine;
|
|
||||||
}
|
|
@ -1,16 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
getContextItem,
|
getContextItem,
|
||||||
setContextItem,
|
|
||||||
setDefaultContext,
|
setDefaultContext,
|
||||||
withContext,
|
withContext,
|
||||||
} from "../lib/context.ts"; // adjust path as needed
|
} from "@lib/context.ts"; // adjust path as needed
|
||||||
import { testPerformance } from "./bench.ts";
|
import { testPerformance } from "./bench.ts";
|
||||||
|
|
||||||
/**
|
|
||||||
* Benchmarks the performance of setting and getting context items.
|
|
||||||
* All context transactions should run 10000 times within the 60 FPS frame time.
|
|
||||||
* getContextItem should run 100000 times within the 240 FPS frame time to ensure adequate performance.
|
|
||||||
*/
|
|
||||||
Deno.test("Context Benchmark", () => {
|
Deno.test("Context Benchmark", () => {
|
||||||
console.log("Context Benchmark - run within frame time");
|
console.log("Context Benchmark - run within frame time");
|
||||||
testPerformance(
|
testPerformance(
|
||||||
@ -38,12 +32,4 @@ Deno.test("Context Benchmark", () => {
|
|||||||
100000,
|
100000,
|
||||||
240,
|
240,
|
||||||
);
|
);
|
||||||
|
|
||||||
testPerformance(
|
|
||||||
() => {
|
|
||||||
setContextItem("a", 1);
|
|
||||||
},
|
|
||||||
10000,
|
|
||||||
60,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ export class StraightTrack extends TrackSegment {
|
|||||||
constructor(start?: Vector) {
|
constructor(start?: Vector) {
|
||||||
start = start || new Vector(100, 100);
|
start = start || new Vector(100, 100);
|
||||||
super([
|
super([
|
||||||
start,
|
start.copy(),
|
||||||
start.copy().add(25, 0),
|
start.copy().add(25, 0),
|
||||||
start.copy().add(75, 0),
|
start.copy().add(75, 0),
|
||||||
start.copy().add(100, 0),
|
start.copy().add(100, 0),
|
||||||
|
118
track/system.ts
118
track/system.ts
@ -13,22 +13,9 @@ export class TrackSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstSegment() {
|
draw() {
|
||||||
return this.segments.values().next().value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get lastSegment() {
|
|
||||||
return this.segments.values().toArray().pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
registerSegment(segment: TrackSegment) {
|
|
||||||
segment.setTrack(this);
|
|
||||||
this.segments.set(segment.id, segment);
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(showControls = false) {
|
|
||||||
for (const segment of this.segments.values()) {
|
for (const segment of this.segments.values()) {
|
||||||
segment.draw(showControls);
|
segment.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -78,39 +65,15 @@ export class TrackSystem {
|
|||||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
copy() {
|
|
||||||
const track = new TrackSystem([]);
|
|
||||||
for (const segment of this.segments.values()) {
|
|
||||||
track.segments.set(segment.id, segment.copy());
|
|
||||||
}
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
|
|
||||||
static deserialize(data: any) {
|
static deserialize(data: any) {
|
||||||
if (data.length === 0) return undefined;
|
|
||||||
const track = new TrackSystem([]);
|
const track = new TrackSystem([]);
|
||||||
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));
|
||||||
}
|
}
|
||||||
for (const segment of track.segments.values()) {
|
|
||||||
segment.setTrack(track);
|
|
||||||
const neighbors = neighborMap.get(segment.id);
|
|
||||||
if (neighbors) {
|
|
||||||
segment.backNeighbours = neighbors[1].map((id) =>
|
|
||||||
track.segments.get(id)
|
|
||||||
).filter((s) => s) as TrackSegment[];
|
|
||||||
segment.frontNeighbours = neighbors[0].map((id) =>
|
|
||||||
track.segments.get(id)
|
|
||||||
).filter((s) => s) as TrackSegment[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type VectorSet = [Vector, Vector, Vector, Vector];
|
|
||||||
|
|
||||||
export class TrackSegment extends PathSegment {
|
export class TrackSegment extends PathSegment {
|
||||||
frontNeighbours: TrackSegment[] = [];
|
frontNeighbours: TrackSegment[] = [];
|
||||||
backNeighbours: TrackSegment[] = [];
|
backNeighbours: TrackSegment[] = [];
|
||||||
@ -121,7 +84,7 @@ export class TrackSegment extends PathSegment {
|
|||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
constructor(p: VectorSet, id?: string) {
|
constructor(p: [Vector, Vector, Vector, Vector], 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();
|
||||||
@ -131,7 +94,7 @@ export class TrackSegment extends PathSegment {
|
|||||||
this.track = t;
|
this.track = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(showControls = false) {
|
draw() {
|
||||||
this.doodler.drawBezier(
|
this.doodler.drawBezier(
|
||||||
this.points[0],
|
this.points[0],
|
||||||
this.points[1],
|
this.points[1],
|
||||||
@ -141,24 +104,6 @@ export class TrackSegment extends PathSegment {
|
|||||||
strokeColor: "#ffffff50",
|
strokeColor: "#ffffff50",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (showControls) {
|
|
||||||
// this.doodler.drawCircle(this.points[0], 4, {
|
|
||||||
// color: "red",
|
|
||||||
// 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() {
|
||||||
@ -170,61 +115,6 @@ export class TrackSegment extends PathSegment {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
copy() {
|
|
||||||
return new TrackSegment(
|
|
||||||
this.points.map((p) => p.copy()) as VectorSet,
|
|
||||||
this.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
propagate() {
|
|
||||||
const [_, __, p3, p4] = this.points;
|
|
||||||
const tangent = Vector.sub(p4, p3);
|
|
||||||
for (const fNeighbour of this.frontNeighbours) {
|
|
||||||
fNeighbour.receivePropagation(tangent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastHeading?: number;
|
|
||||||
|
|
||||||
receivePropagation(tangent: Vector) {
|
|
||||||
const [p1, p2, p3, p4] = this.points;
|
|
||||||
// const angle = tangent.heading() - (this.lastHeading ?? 0);
|
|
||||||
// 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
|
|
||||||
rotate(angle: number | Vector) {
|
|
||||||
const [p1, p2, p3, p4] = this.points;
|
|
||||||
let newP2;
|
|
||||||
if (angle instanceof Vector) {
|
|
||||||
const tan = angle;
|
|
||||||
angle = tan.heading() - (this.lastHeading ?? 0);
|
|
||||||
this.lastHeading = tan.heading();
|
|
||||||
newP2 = Vector.add(p1, tan);
|
|
||||||
} else {
|
|
||||||
const p1ToP2 = Vector.sub(p2, p1);
|
|
||||||
p1ToP2.rotate(angle);
|
|
||||||
newP2 = Vector.add(p1, p1ToP2);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
static deserialize(data: any) {
|
static deserialize(data: any) {
|
||||||
return new TrackSegment(
|
return new TrackSegment(
|
||||||
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),
|
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import { drawLine } from "../drawing/line.ts";
|
||||||
import { ComplexPath, PathSegment } from "../math/path.ts";
|
import { ComplexPath, PathSegment } from "../math/path.ts";
|
||||||
|
import { Vector } from "doodler";
|
||||||
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 { Spline, Track } from "../track.ts";
|
||||||
import { getContextItem } from "../lib/context.ts";
|
|
||||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
|
||||||
|
|
||||||
export class Train {
|
export class Train {
|
||||||
nodes: Vector[] = [];
|
nodes: Vector[] = [];
|
||||||
@ -83,12 +83,6 @@ export class Train {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
draw() {
|
|
||||||
for (const car of this.cars) {
|
|
||||||
car.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
real2Track(length: number) {
|
real2Track(length: number) {
|
||||||
return length / this.path.pointSpacing;
|
return length / this.path.pointSpacing;
|
||||||
}
|
}
|
||||||
@ -119,7 +113,6 @@ export class TrainCar {
|
|||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
if (!this.points) return;
|
if (!this.points) return;
|
||||||
const doodler = getContextItem<Doodler>("doodler");
|
|
||||||
const [a, b] = this.points;
|
const [a, b] = this.points;
|
||||||
const origin = Vector.add(Vector.sub(a, b).div(2), b);
|
const origin = Vector.add(Vector.sub(a, b).div(2), b);
|
||||||
const angle = Vector.sub(b, a).heading();
|
const angle = Vector.sub(b, a).heading();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user