Files
DndGamePlayer/app/main/players/scenePlayerTokensSessionStore.ts
T
Ivan Fontosh cc50e64e21 feat(players): NPC disposition types and launch-with-players session tokens
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>
2026-07-30 17:11:01 +08:00

144 lines
4.7 KiB
TypeScript

import {
clampSceneNpcTokenSizeN,
DEFAULT_SCENE_NPC_TOKEN_SIZE_N,
layoutPlayerTokensBottom,
type ScenePlayerTokensSessionEvent,
type ScenePlayerTokensSessionState,
} from '../../shared/types/appPlayers';
function emptyState(revision = 1): ScenePlayerTokensSessionState {
return {
revision,
selectedPlayerIds: [],
visible: false,
byPlayerId: {},
};
}
function normalizePlayerIds(raw: readonly string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const id of raw) {
const s = String(id ?? '').trim();
if (!s || seen.has(s)) continue;
seen.add(s);
out.push(s);
}
return out;
}
export class ScenePlayerTokensSessionStore {
private state: ScenePlayerTokensSessionState = emptyState();
getState(): ScenePlayerTokensSessionState {
return this.state;
}
reset(): ScenePlayerTokensSessionState {
if (
this.state.selectedPlayerIds.length === 0 &&
!this.state.visible &&
Object.keys(this.state.byPlayerId).length === 0
) {
return this.state;
}
this.state = emptyState(this.state.revision + 1);
return this.state;
}
/** Смена сцены: спрятать токены, сбросить позиции; выбор игроков сохранить. */
clearPlacements(): ScenePlayerTokensSessionState {
if (!this.state.visible && Object.keys(this.state.byPlayerId).length === 0) {
return this.state;
}
this.state = {
...this.state,
revision: this.state.revision + 1,
visible: false,
byPlayerId: {},
};
return this.state;
}
dispatch(event: ScenePlayerTokensSessionEvent): ScenePlayerTokensSessionState {
switch (event.kind) {
case 'clear':
return this.reset();
case 'clearPlacements':
return this.clearPlacements();
case 'setSelection': {
const selectedPlayerIds = normalizePlayerIds(event.playerIds);
const same =
selectedPlayerIds.length === this.state.selectedPlayerIds.length &&
selectedPlayerIds.every((id, i) => id === this.state.selectedPlayerIds[i]);
if (same) return this.state;
this.state = {
revision: this.state.revision + 1,
selectedPlayerIds,
visible: false,
byPlayerId: {},
};
return this.state;
}
case 'setVisible': {
const visible = Boolean(event.visible);
if (this.state.visible === visible) return this.state;
if (visible && this.state.selectedPlayerIds.length === 0) return this.state;
this.state = { ...this.state, revision: this.state.revision + 1, visible };
return this.state;
}
case 'show': {
if (this.state.selectedPlayerIds.length === 0) return this.state;
const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
const byPlayerId =
Object.keys(this.state.byPlayerId).length > 0
? this.state.byPlayerId
: layoutPlayerTokensBottom(this.state.selectedPlayerIds, sizeN);
if (this.state.visible && byPlayerId === this.state.byPlayerId) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
visible: true,
byPlayerId,
};
return this.state;
}
case 'seedBottom': {
if (this.state.selectedPlayerIds.length === 0) return this.state;
if (Object.keys(this.state.byPlayerId).length > 0) return this.state;
const sizeN = clampSceneNpcTokenSizeN(event.sizeN);
this.state = {
...this.state,
revision: this.state.revision + 1,
byPlayerId: layoutPlayerTokensBottom(this.state.selectedPlayerIds, sizeN),
};
return this.state;
}
case 'move': {
const playerId = String(event.playerId ?? '');
if (!playerId || !this.state.selectedPlayerIds.includes(playerId)) 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.byPlayerId[playerId];
if (prev && prev.nx === nx && prev.ny === ny) return this.state;
const sizeN = clampSceneNpcTokenSizeN(prev?.sizeN ?? DEFAULT_SCENE_NPC_TOKEN_SIZE_N);
this.state = {
...this.state,
revision: this.state.revision + 1,
byPlayerId: {
...this.state.byPlayerId,
[playerId]: { nx, ny, sizeN },
},
};
return this.state;
}
default: {
const _exhaustive: never = event;
void _exhaustive;
return this.state;
}
}
}
}