feat(players): app Players library and circular NPC tokens on scenes

Add userData players/teams, scene npcTokens with hex-inscribed sizing, session scale synced to presentation, and Playwright e2e coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-30 13:38:35 +08:00
parent a3a03eb9e3
commit 101f595bac
57 changed files with 4176 additions and 317 deletions
@@ -0,0 +1,73 @@
import {
clampNpcTokenSessionScale,
DEFAULT_NPC_TOKEN_SESSION_SCALE,
type SceneNpcTokensSessionEvent,
type SceneNpcTokensSessionState,
} from '../../shared/types/appPlayers';
function emptyState(revision = 1): SceneNpcTokensSessionState {
return {
revision,
byPlacementId: {},
scale: DEFAULT_NPC_TOKEN_SESSION_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]: { nx, ny },
},
scale: this.state.scale,
};
return this.state;
}
default: {
const _exhaustive: never = event;
void _exhaustive;
return this.state;
}
}
}
}