cc50e64e21
Add Hostile/Neutral/Friendly ring types with session-only inactive overrides on control, plus launch-with-players flow and live player tokens on the map. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
4.8 KiB
TypeScript
154 lines
4.8 KiB
TypeScript
import {
|
|
clampNpcTokenSessionScale,
|
|
DEFAULT_NPC_TOKEN_SESSION_SCALE,
|
|
type SceneNpcTokensSessionEvent,
|
|
type SceneNpcTokensSessionPlacement,
|
|
type SceneNpcTokensSessionState,
|
|
} from '../../shared/types/appPlayers';
|
|
import { normalizeNpcDisposition } from '../../shared/types/npcDisposition';
|
|
|
|
function emptyState(revision = 1): SceneNpcTokensSessionState {
|
|
return {
|
|
revision,
|
|
byPlacementId: {},
|
|
scale: DEFAULT_NPC_TOKEN_SESSION_SCALE,
|
|
};
|
|
}
|
|
|
|
function isEmptyPlacement(p: SceneNpcTokensSessionPlacement): boolean {
|
|
return p.nx === undefined && p.ny === undefined && !p.disposition && !p.inactive;
|
|
}
|
|
|
|
function withMove(
|
|
prev: SceneNpcTokensSessionPlacement | undefined,
|
|
nx: number,
|
|
ny: number,
|
|
): SceneNpcTokensSessionPlacement {
|
|
return {
|
|
nx,
|
|
ny,
|
|
...(prev?.disposition ? { disposition: prev.disposition } : {}),
|
|
...(prev?.inactive ? { inactive: true } : {}),
|
|
};
|
|
}
|
|
|
|
function withoutPlacement(
|
|
state: SceneNpcTokensSessionState,
|
|
placementId: string,
|
|
): SceneNpcTokensSessionState {
|
|
if (!(placementId in state.byPlacementId)) return state;
|
|
const { [placementId]: _removed, ...rest } = state.byPlacementId;
|
|
return {
|
|
revision: state.revision + 1,
|
|
byPlacementId: rest,
|
|
scale: state.scale,
|
|
};
|
|
}
|
|
|
|
export class SceneNpcTokensSessionStore {
|
|
private state: SceneNpcTokensSessionState = emptyState();
|
|
|
|
getState(): SceneNpcTokensSessionState {
|
|
return this.state;
|
|
}
|
|
|
|
reset(): SceneNpcTokensSessionState {
|
|
if (
|
|
Object.keys(this.state.byPlacementId).length === 0 &&
|
|
this.state.scale === DEFAULT_NPC_TOKEN_SESSION_SCALE
|
|
) {
|
|
return this.state;
|
|
}
|
|
this.state = emptyState(this.state.revision + 1);
|
|
return this.state;
|
|
}
|
|
|
|
dispatch(event: SceneNpcTokensSessionEvent): SceneNpcTokensSessionState {
|
|
switch (event.kind) {
|
|
case 'clear':
|
|
return this.reset();
|
|
case 'setScale': {
|
|
const scale = clampNpcTokenSessionScale(event.scale);
|
|
if (this.state.scale === scale) return this.state;
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
scale,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'move': {
|
|
const placementId = String(event.placementId ?? '');
|
|
if (!placementId) return this.state;
|
|
const nx = Math.max(0, Math.min(1, event.nx));
|
|
const ny = Math.max(0, Math.min(1, event.ny));
|
|
if (!Number.isFinite(nx) || !Number.isFinite(ny)) return this.state;
|
|
const prev = this.state.byPlacementId[placementId];
|
|
if (prev && prev.nx === nx && prev.ny === ny) return this.state;
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
byPlacementId: {
|
|
...this.state.byPlacementId,
|
|
[placementId]: withMove(prev, nx, ny),
|
|
},
|
|
scale: this.state.scale,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'setDisposition': {
|
|
const placementId = String(event.placementId ?? '');
|
|
if (!placementId) return this.state;
|
|
const disposition = normalizeNpcDisposition(event.disposition);
|
|
const prev = this.state.byPlacementId[placementId];
|
|
if (prev?.disposition === disposition) return this.state;
|
|
const next: SceneNpcTokensSessionPlacement = {
|
|
...(prev?.nx !== undefined ? { nx: prev.nx } : {}),
|
|
...(prev?.ny !== undefined ? { ny: prev.ny } : {}),
|
|
disposition,
|
|
...(prev?.inactive ? { inactive: true } : {}),
|
|
};
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
byPlacementId: {
|
|
...this.state.byPlacementId,
|
|
[placementId]: next,
|
|
},
|
|
scale: this.state.scale,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'setInactive': {
|
|
const placementId = String(event.placementId ?? '');
|
|
if (!placementId) return this.state;
|
|
const inactive = Boolean(event.inactive);
|
|
const prev = this.state.byPlacementId[placementId];
|
|
if (Boolean(prev?.inactive) === inactive) return this.state;
|
|
const next: SceneNpcTokensSessionPlacement = {
|
|
...(prev?.nx !== undefined ? { nx: prev.nx } : {}),
|
|
...(prev?.ny !== undefined ? { ny: prev.ny } : {}),
|
|
...(prev?.disposition ? { disposition: prev.disposition } : {}),
|
|
...(inactive ? { inactive: true } : {}),
|
|
};
|
|
if (isEmptyPlacement(next)) {
|
|
this.state = withoutPlacement(this.state, placementId);
|
|
return this.state;
|
|
}
|
|
this.state = {
|
|
revision: this.state.revision + 1,
|
|
byPlacementId: {
|
|
...this.state.byPlacementId,
|
|
[placementId]: next,
|
|
},
|
|
scale: this.state.scale,
|
|
};
|
|
return this.state;
|
|
}
|
|
default: {
|
|
const _exhaustive: never = event;
|
|
void _exhaustive;
|
|
return this.state;
|
|
}
|
|
}
|
|
}
|
|
}
|