basic state switching from loading to running to editing

This commit is contained in:
2025-02-08 01:16:09 -07:00
parent 623a324625
commit 791ba42ceb
21 changed files with 769 additions and 187 deletions

View File

@@ -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);
}
}
}

View File

@@ -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.");
}
}

View 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);
}
}
}
}

View 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
View 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;
}
}

View 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
}
}

View 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
View 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;
}