feat: unify life-sim play controls
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Renderer-neutral ownership for the one local player and the one local camera.
|
||||
*
|
||||
* A mode is deliberately more specific than "play": it names the subsystem
|
||||
* allowed to consume held input and write the follow camera. Keeping this as a
|
||||
* small pure state machine lets the DOM, Three.js scene and Journey reducer
|
||||
* agree on a transition without any of them becoming the source of truth for
|
||||
* the others.
|
||||
*/
|
||||
|
||||
export type CityControlMode = "overview" | "drive" | "actor" | "aircraft";
|
||||
export type OfficeControlMode = "office-overview" | "office-walk";
|
||||
export type ControlMode = CityControlMode | OfficeControlMode;
|
||||
|
||||
export interface ControlModeAvailability {
|
||||
insideOffice: boolean;
|
||||
drive: boolean;
|
||||
actor: boolean;
|
||||
aircraft: boolean;
|
||||
officeWalk: boolean;
|
||||
}
|
||||
|
||||
export interface ControlModeState {
|
||||
mode: ControlMode;
|
||||
/** Increases only for an accepted change, useful to invalidate stale input. */
|
||||
revision: number;
|
||||
}
|
||||
|
||||
export interface ControlModeTransition {
|
||||
previous: ControlMode;
|
||||
state: ControlModeState;
|
||||
changed: boolean;
|
||||
accepted: boolean;
|
||||
reason: "unchanged" | "unavailable" | null;
|
||||
}
|
||||
|
||||
export interface CityControlOwnership {
|
||||
orbit: boolean;
|
||||
drive: boolean;
|
||||
actor: boolean;
|
||||
aircraft: boolean;
|
||||
}
|
||||
|
||||
/** The renderer consumes this table; exactly one camera writer is always true. */
|
||||
export function cityControlOwnership(mode: CityControlMode): CityControlOwnership {
|
||||
return {
|
||||
orbit: mode === "overview",
|
||||
drive: mode === "drive",
|
||||
actor: mode === "actor",
|
||||
aircraft: mode === "aircraft",
|
||||
};
|
||||
}
|
||||
|
||||
export function createControlModeState(
|
||||
mode: ControlMode = "overview",
|
||||
): ControlModeState {
|
||||
return { mode, revision: 0 };
|
||||
}
|
||||
|
||||
export function cityControlMode(mode: ControlMode): CityControlMode {
|
||||
if (mode === "drive" || mode === "actor" || mode === "aircraft") return mode;
|
||||
return "overview";
|
||||
}
|
||||
|
||||
export function controlModeAvailable(
|
||||
mode: ControlMode,
|
||||
availability: Readonly<ControlModeAvailability>,
|
||||
): boolean {
|
||||
if (availability.insideOffice) {
|
||||
return mode === "office-overview" || (mode === "office-walk" && availability.officeWalk);
|
||||
}
|
||||
if (mode === "overview") return true;
|
||||
if (mode === "drive") return availability.drive;
|
||||
if (mode === "actor") return availability.actor;
|
||||
if (mode === "aircraft") return availability.aircraft;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Resolve one requested transition without performing any side effects. */
|
||||
export function transitionControlMode(
|
||||
current: Readonly<ControlModeState>,
|
||||
requested: ControlMode,
|
||||
availability: Readonly<ControlModeAvailability>,
|
||||
): ControlModeTransition {
|
||||
const fallback: ControlMode = availability.insideOffice ? "office-overview" : "overview";
|
||||
const next = controlModeAvailable(requested, availability) ? requested : fallback;
|
||||
const accepted = next === requested;
|
||||
if (next === current.mode) {
|
||||
return {
|
||||
previous: current.mode,
|
||||
state: { ...current },
|
||||
changed: false,
|
||||
accepted,
|
||||
reason: accepted ? "unchanged" : "unavailable",
|
||||
};
|
||||
}
|
||||
return {
|
||||
previous: current.mode,
|
||||
state: { mode: next, revision: current.revision + 1 },
|
||||
changed: true,
|
||||
accepted,
|
||||
reason: accepted ? null : "unavailable",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user