basic state switching from loading to running to editing
This commit is contained in:
parent
623a324625
commit
791ba42ceb
329
bundle.js
329
bundle.js
@ -2,6 +2,7 @@
|
||||
// lib/context.ts
|
||||
var contextStack = [];
|
||||
var defaultContext = {};
|
||||
var debug = JSON.parse(localStorage.getItem("debug") || "false");
|
||||
function setDefaultContext(context) {
|
||||
Object.assign(defaultContext, context);
|
||||
}
|
||||
@ -17,9 +18,58 @@
|
||||
}
|
||||
}
|
||||
);
|
||||
function getContext() {
|
||||
return ctx;
|
||||
}
|
||||
function getContextItem(prop) {
|
||||
return ctx[prop];
|
||||
}
|
||||
function setContextItem(prop, value) {
|
||||
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, 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);
|
||||
}
|
||||
function safeStringify(obj) {
|
||||
const seen = /* @__PURE__ */ new WeakSet();
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return "[Circular]";
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
}, 2);
|
||||
}
|
||||
|
||||
// lib/input.ts
|
||||
var InputManager = class {
|
||||
@ -71,7 +121,9 @@
|
||||
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);
|
||||
@ -1077,6 +1129,164 @@
|
||||
};
|
||||
}
|
||||
|
||||
// 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.");
|
||||
}
|
||||
};
|
||||
|
||||
// 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();
|
||||
update(dt) {
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
const track = getContextItem("track");
|
||||
const firstSegment = track.firstSegment;
|
||||
if (firstSegment) {
|
||||
const firstPoint = firstSegment.points[0].copy();
|
||||
const { x, y } = inputManager2.getMouseLocation();
|
||||
firstSegment.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, firstPoint);
|
||||
p.set(x, y);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
}
|
||||
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);
|
||||
const state2 = getContextItem("state");
|
||||
state2.transitionTo(1 /* RUNNING */);
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
if (this.heldEvents.size > 0) {
|
||||
for (const [key, cb] of this.heldEvents) {
|
||||
if (cb) {
|
||||
getContextItem("inputManager").onKey(key, cb);
|
||||
}
|
||||
this.heldEvents.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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 */);
|
||||
});
|
||||
}
|
||||
|
||||
// math/path.ts
|
||||
var PathSegment = class {
|
||||
points;
|
||||
@ -1224,11 +1434,16 @@
|
||||
doodler;
|
||||
constructor(segments) {
|
||||
this.doodler = getContextItem("doodler");
|
||||
this.segments = segments;
|
||||
for (const segment of segments) {
|
||||
this.segments.set(segment.id, segment);
|
||||
}
|
||||
}
|
||||
draw() {
|
||||
for (const segment of this.segments) {
|
||||
segment.draw();
|
||||
get firstSegment() {
|
||||
return this.segments.values().next().value;
|
||||
}
|
||||
draw(showControls = false) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.draw(showControls);
|
||||
}
|
||||
try {
|
||||
if (getContextItem("showEnds")) {
|
||||
@ -1256,7 +1471,7 @@
|
||||
}
|
||||
findEnds() {
|
||||
const ends = [];
|
||||
for (const segment of this.segments) {
|
||||
for (const segment of this.segments.values()) {
|
||||
const [a, b, c, d] = segment.points;
|
||||
{
|
||||
const tangent = Vector.sub(a, b).normalize();
|
||||
@ -1274,12 +1489,33 @@
|
||||
serialize() {
|
||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||
}
|
||||
static deserialize(data) {
|
||||
const track2 = new _TrackSystem([]);
|
||||
for (const segment of data) {
|
||||
track2.segments.set(segment.id, TrackSegment.deserialize(segment));
|
||||
copy() {
|
||||
const track = new _TrackSystem([]);
|
||||
for (const segment of this.segments.values()) {
|
||||
track.segments.set(segment.id, segment.copy());
|
||||
}
|
||||
return track2;
|
||||
return track;
|
||||
}
|
||||
static deserialize(data) {
|
||||
if (data.length === 0) return void 0;
|
||||
const track = new _TrackSystem([]);
|
||||
const neighborMap = /* @__PURE__ */ new Map();
|
||||
for (const segment of data) {
|
||||
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);
|
||||
segment.frontNeighbours = neighbors[0].map(
|
||||
(id) => track.segments.get(id)
|
||||
).filter((s) => s);
|
||||
}
|
||||
}
|
||||
return track;
|
||||
}
|
||||
};
|
||||
var TrackSegment = class _TrackSegment extends PathSegment {
|
||||
@ -1296,7 +1532,7 @@
|
||||
setTrack(t) {
|
||||
this.track = t;
|
||||
}
|
||||
draw() {
|
||||
draw(showControls = false) {
|
||||
this.doodler.drawBezier(
|
||||
this.points[0],
|
||||
this.points[1],
|
||||
@ -1306,6 +1542,16 @@
|
||||
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() {
|
||||
return {
|
||||
@ -1315,6 +1561,12 @@
|
||||
fNeighbors: this.frontNeighbours.map((n) => n.id)
|
||||
};
|
||||
}
|
||||
copy() {
|
||||
return new _TrackSegment(
|
||||
this.points.map((p) => p.copy()),
|
||||
this.id
|
||||
);
|
||||
}
|
||||
static deserialize(data) {
|
||||
return new _TrackSegment(
|
||||
data.p.map((p) => new Vector(p[0], p[1], p[2])),
|
||||
@ -1328,7 +1580,7 @@
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super([
|
||||
start.copy(),
|
||||
start,
|
||||
start.copy().add(25, 0),
|
||||
start.copy().add(75, 0),
|
||||
start.copy().add(100, 0)
|
||||
@ -1336,6 +1588,52 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
var inputManager = new InputManager();
|
||||
var resources = new ResourceManager();
|
||||
@ -1350,6 +1648,8 @@
|
||||
debug: true,
|
||||
showEnds: true
|
||||
});
|
||||
var state = bootstrapGameStateMachine();
|
||||
setContextItem("state", state);
|
||||
doodler.init();
|
||||
addButton({
|
||||
text: "Hello World!",
|
||||
@ -1365,8 +1665,7 @@
|
||||
color: "white"
|
||||
}
|
||||
});
|
||||
var track = new TrackSystem([new StraightTrack()]);
|
||||
doodler.createLayer(() => {
|
||||
track.draw();
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
})();
|
||||
|
@ -13,7 +13,6 @@
|
||||
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
||||
},
|
||||
"imports": {
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.3",
|
||||
"@lib/": "./lib/"
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.3"
|
||||
}
|
||||
}
|
9
deno.lock
generated
9
deno.lock
generated
@ -3,6 +3,7 @@
|
||||
"specifiers": {
|
||||
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
|
||||
"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": "1.0.10",
|
||||
"jsr:@std/bytes@^1.0.2": "1.0.2",
|
||||
@ -32,6 +33,14 @@
|
||||
"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": {
|
||||
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
|
||||
"dependencies": [
|
||||
|
2
dev.ts
2
dev.ts
@ -1,7 +1,7 @@
|
||||
/// <reference lib="deno.ns" />
|
||||
|
||||
import * as esbuild from "npm:esbuild";
|
||||
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
|
||||
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11.1";
|
||||
import { serveDir } from "jsr:@std/http";
|
||||
|
||||
async function* crawl(dir: string): AsyncIterable<string> {
|
||||
|
12
index.html
12
index.html
@ -14,6 +14,18 @@
|
||||
height: 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>
|
||||
</head>
|
||||
<body>
|
||||
|
12
inputs.ts
Normal file
12
inputs.ts
Normal file
@ -0,0 +1,12 @@
|
||||
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,6 +3,8 @@ type ContextStore = Record<string, any>;
|
||||
const contextStack: ContextStore[] = [];
|
||||
const defaultContext: ContextStore = {};
|
||||
|
||||
const debug = JSON.parse(localStorage.getItem("debug") || "false");
|
||||
|
||||
export function setDefaultContext(context: ContextStore) {
|
||||
Object.assign(defaultContext, context);
|
||||
}
|
||||
@ -23,7 +25,7 @@ export const ctx = new Proxy(
|
||||
for (let i = contextStack.length - 1; i >= 0; i--) {
|
||||
if (prop in contextStack[i]) return contextStack[i][prop];
|
||||
}
|
||||
if (prop in defaultContext) return defaultContext[prop]; // ✅ Fallback to default
|
||||
if (prop in defaultContext) return defaultContext[prop];
|
||||
throw new Error(`Context variable '${prop}' is not defined.`);
|
||||
},
|
||||
},
|
||||
@ -35,3 +37,51 @@ export function getContext() {
|
||||
export function getContextItem<T>(prop: string): 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);
|
||||
}
|
||||
|
@ -53,7 +53,9 @@ export class InputManager {
|
||||
}
|
||||
|
||||
offKey(key: string | number) {
|
||||
const events = this.keyEvents.get(key);
|
||||
this.keyEvents.delete(key);
|
||||
return events;
|
||||
}
|
||||
offMouse(key: string | number) {
|
||||
this.mouseEvents.delete(key);
|
||||
|
17
main.ts
17
main.ts
@ -1,4 +1,8 @@
|
||||
import { setDefaultContext } from "./lib/context.ts";
|
||||
import {
|
||||
getContext,
|
||||
setContextItem,
|
||||
setDefaultContext,
|
||||
} from "./lib/context.ts";
|
||||
import { InputManager } from "./lib/input.ts";
|
||||
|
||||
import { Doodler, Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
||||
@ -6,6 +10,8 @@ import { ResourceManager } from "./lib/resources.ts";
|
||||
import { addButton } from "./ui/button.ts";
|
||||
import { TrackSystem } from "./track/system.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 resources = new ResourceManager();
|
||||
@ -22,6 +28,9 @@ setDefaultContext({
|
||||
showEnds: true,
|
||||
});
|
||||
|
||||
const state = bootstrapGameStateMachine();
|
||||
setContextItem("state", state);
|
||||
|
||||
doodler.init();
|
||||
addButton({
|
||||
text: "Hello World!",
|
||||
@ -38,8 +47,6 @@ addButton({
|
||||
},
|
||||
});
|
||||
|
||||
const track = new TrackSystem([new StraightTrack()]);
|
||||
|
||||
doodler.createLayer(() => {
|
||||
track.draw();
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
|
@ -1,13 +1,9 @@
|
||||
export class StateMachine {
|
||||
private _states: Map<string, State> = new Map();
|
||||
private currentState: State;
|
||||
export class StateMachine<T> {
|
||||
private _states: Map<T, State<T>> = new Map();
|
||||
private currentState?: State<T>;
|
||||
|
||||
constructor(states: State[]) {
|
||||
this.currentState = states[0];
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
this.currentState.update(dt);
|
||||
update(dt: number, ctx?: CanvasRenderingContext2D) {
|
||||
this.currentState?.update(dt, ctx);
|
||||
}
|
||||
|
||||
get current() {
|
||||
@ -18,32 +14,37 @@ export class StateMachine {
|
||||
return this._states;
|
||||
}
|
||||
|
||||
addState(state: State) {
|
||||
addState(state: State<T>) {
|
||||
this.states.set(state.name, state);
|
||||
}
|
||||
|
||||
transitionTo(state: State) {
|
||||
if (this.current.canTransitionTo(state)) {
|
||||
transitionTo(state: T) {
|
||||
if (!this.current) {
|
||||
this.currentState = this._states.get(state)!;
|
||||
this.currentState.start();
|
||||
return;
|
||||
}
|
||||
if (this.current?.canTransitionTo(state) && this._states.has(state)) {
|
||||
this.current.stop();
|
||||
this.currentState = state;
|
||||
this.currentState = this._states.get(state)!;
|
||||
this.current.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class State<T> {
|
||||
private stateMachine: StateMachine;
|
||||
protected stateMachine: StateMachine<T>;
|
||||
protected abstract validTransitions: Set<T>;
|
||||
|
||||
abstract readonly name: T;
|
||||
|
||||
constructor(
|
||||
stateMachine: StateMachine,
|
||||
stateMachine: StateMachine<T>,
|
||||
) {
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
abstract update(dt: number): void;
|
||||
abstract update(dt: number, ctx?: CanvasRenderingContext2D): void;
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
|
||||
@ -51,3 +52,25 @@ export abstract class State<T> {
|
||||
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
139
state/states.ts
@ -1,139 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
}
|
70
state/states/EditTrackState.ts
Normal file
70
state/states/EditTrackState.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { 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";
|
||||
|
||||
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();
|
||||
|
||||
override update(dt: number): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
|
||||
const firstSegment = track.firstSegment;
|
||||
if (firstSegment) {
|
||||
const firstPoint = firstSegment.points[0].copy();
|
||||
const { x, y } = inputManager.getMouseLocation();
|
||||
firstSegment.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, firstPoint);
|
||||
p.set(x, y);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
const state = getContextItem<StateMachine<States>>("state");
|
||||
state.transitionTo(States.RUNNING);
|
||||
});
|
||||
// TODO
|
||||
// Cache trains and save
|
||||
// Stash track in context
|
||||
}
|
||||
override stop(): void {
|
||||
if (this.heldEvents.size > 0) {
|
||||
for (const [key, cb] of this.heldEvents) {
|
||||
if (cb) {
|
||||
getContextItem<InputManager>("inputManager").onKey(key, cb);
|
||||
}
|
||||
this.heldEvents.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
state/states/EditTrainState.ts
Normal file
25
state/states/EditTrainState.ts
Normal file
@ -0,0 +1,25 @@
|
||||
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.");
|
||||
}
|
||||
}
|
51
state/states/LoadState.ts
Normal file
51
state/states/LoadState.ts
Normal file
@ -0,0 +1,51 @@
|
||||
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;
|
||||
}
|
||||
}
|
30
state/states/PausedState.ts
Normal file
30
state/states/PausedState.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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
|
||||
}
|
||||
}
|
32
state/states/RunningState.ts
Normal file
32
state/states/RunningState.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
}
|
26
state/states/index.ts
Normal file
26
state/states/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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,10 +1,16 @@
|
||||
import {
|
||||
getContextItem,
|
||||
setContextItem,
|
||||
setDefaultContext,
|
||||
withContext,
|
||||
} from "@lib/context.ts"; // adjust path as needed
|
||||
} from "../lib/context.ts"; // adjust path as needed
|
||||
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", () => {
|
||||
console.log("Context Benchmark - run within frame time");
|
||||
testPerformance(
|
||||
@ -32,4 +38,12 @@ Deno.test("Context Benchmark", () => {
|
||||
100000,
|
||||
240,
|
||||
);
|
||||
|
||||
testPerformance(
|
||||
() => {
|
||||
setContextItem("a", 1);
|
||||
},
|
||||
10000,
|
||||
60,
|
||||
);
|
||||
});
|
||||
|
@ -13,9 +13,13 @@ export class TrackSystem {
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
get firstSegment() {
|
||||
return this.segments.values().next().value;
|
||||
}
|
||||
|
||||
draw(showControls = false) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.draw();
|
||||
segment.draw(showControls);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -65,15 +69,39 @@ export class TrackSystem {
|
||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||
}
|
||||
|
||||
static deserialize(data: any) {
|
||||
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) {
|
||||
if (data.length === 0) return undefined;
|
||||
const track = new TrackSystem([]);
|
||||
const neighborMap = new Map<string, [string[], string[]]>();
|
||||
for (const segment of data) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
type VectorSet = [Vector, Vector, Vector, Vector];
|
||||
|
||||
export class TrackSegment extends PathSegment {
|
||||
frontNeighbours: TrackSegment[] = [];
|
||||
backNeighbours: TrackSegment[] = [];
|
||||
@ -84,7 +112,7 @@ export class TrackSegment extends PathSegment {
|
||||
|
||||
id: string;
|
||||
|
||||
constructor(p: [Vector, Vector, Vector, Vector], id?: string) {
|
||||
constructor(p: VectorSet, id?: string) {
|
||||
super(p);
|
||||
this.doodler = getContextItem<Doodler>("doodler");
|
||||
this.id = id ?? crypto.randomUUID();
|
||||
@ -94,7 +122,7 @@ export class TrackSegment extends PathSegment {
|
||||
this.track = t;
|
||||
}
|
||||
|
||||
draw() {
|
||||
draw(showControls = false) {
|
||||
this.doodler.drawBezier(
|
||||
this.points[0],
|
||||
this.points[1],
|
||||
@ -104,6 +132,24 @@ export class TrackSegment extends PathSegment {
|
||||
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() {
|
||||
@ -115,6 +161,13 @@ export class TrackSegment extends PathSegment {
|
||||
};
|
||||
}
|
||||
|
||||
copy() {
|
||||
return new TrackSegment(
|
||||
this.points.map((p) => p.copy()) as VectorSet,
|
||||
this.id,
|
||||
);
|
||||
}
|
||||
|
||||
static deserialize(data: any) {
|
||||
return new TrackSegment(
|
||||
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 { Vector } from "doodler";
|
||||
import { Follower } from "../physics/follower.ts";
|
||||
import { Mover } from "../physics/mover.ts";
|
||||
import { Spline, Track } from "../track.ts";
|
||||
import { getContextItem } from "../lib/context.ts";
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
|
||||
export class Train {
|
||||
nodes: Vector[] = [];
|
||||
@ -83,6 +83,12 @@ export class Train {
|
||||
// }
|
||||
// }
|
||||
|
||||
draw() {
|
||||
for (const car of this.cars) {
|
||||
car.draw();
|
||||
}
|
||||
}
|
||||
|
||||
real2Track(length: number) {
|
||||
return length / this.path.pointSpacing;
|
||||
}
|
||||
@ -113,6 +119,7 @@ export class TrainCar {
|
||||
|
||||
draw() {
|
||||
if (!this.points) return;
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
const [a, b] = this.points;
|
||||
const origin = Vector.add(Vector.sub(a, b).div(2), b);
|
||||
const angle = Vector.sub(b, a).heading();
|
||||
|
Loading…
x
Reference in New Issue
Block a user