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>
This commit is contained in:
Ivan Fontosh
2026-07-30 17:11:01 +08:00
parent 101f595bac
commit cc50e64e21
38 changed files with 1803 additions and 59 deletions
+32 -5
View File
@@ -60,6 +60,12 @@ import {
normalizeSceneNpcToken,
type SceneNpcToken,
} from '../../shared/types/appPlayers';
import {
DEFAULT_NPC_DISPOSITION,
normalizeNpcDisposition,
npcDispositionRingColor,
type NpcDisposition,
} from '../../shared/types/npcDisposition';
import { DEFAULT_SCENE_GRID, normalizeSceneGrid } from '../../shared/types/sceneGrid';
import { normalizeSceneTrap } from '../../shared/types/sceneTraps';
import type {
@@ -1263,6 +1269,7 @@ export class ZipProjectStore {
filePath?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
disposition?: NpcDisposition;
imageOffset?: { x: number; y: number };
imageScale?: number;
},
@@ -1344,6 +1351,12 @@ export class ZipProjectStore {
...(input.ringColor !== undefined
? { ringColor: normalizeHexColor(input.ringColor, DEFAULT_PLAYER_RING_COLOR) }
: {}),
...(input.disposition !== undefined
? {
disposition: normalizeNpcDisposition(input.disposition),
ringColor: npcDispositionRingColor(normalizeNpcDisposition(input.disposition)),
}
: {}),
...(input.imageOffset !== undefined
? { imageOffset: clampPlayerImageOffset(input.imageOffset) }
: {}),
@@ -1354,6 +1367,7 @@ export class ZipProjectStore {
} else {
if (!nextAssetId) throw new Error('NPC avatar is required');
const count = npcs.length;
const disposition = normalizeNpcDisposition(input.disposition ?? DEFAULT_NPC_DISPOSITION);
npcs.push({
id: asNpcId(`npc_${this.randomId()}`),
name,
@@ -1362,7 +1376,8 @@ export class ZipProjectStore {
x: 80 + (count % 4) * 220,
y: 80 + Math.floor(count / 4) * 200,
groupId: resolveGroup(input.groupId, null),
ringColor: normalizeHexColor(input.ringColor, DEFAULT_PLAYER_RING_COLOR),
disposition,
ringColor: npcDispositionRingColor(disposition),
imageOffset: clampPlayerImageOffset(input.imageOffset),
imageScale: clampPlayerImageScale(input.imageScale),
});
@@ -1383,6 +1398,7 @@ export class ZipProjectStore {
description?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
disposition?: NpcDisposition;
imageOffset?: { x: number; y: number };
imageScale?: number;
},
@@ -1405,14 +1421,22 @@ export class ZipProjectStore {
if (patch.groupId !== undefined) {
groupId = patch.groupId === null ? null : groupIds.has(patch.groupId) ? patch.groupId : null;
}
const disposition =
patch.disposition !== undefined
? normalizeNpcDisposition(patch.disposition)
: normalizeNpcDisposition(n.disposition);
return {
...n,
...(name !== undefined ? { name } : {}),
...(typeof patch.description === 'string' ? { description: patch.description } : {}),
groupId,
...(patch.ringColor !== undefined
? { ringColor: normalizeHexColor(patch.ringColor, DEFAULT_PLAYER_RING_COLOR) }
: {}),
disposition,
ringColor:
patch.disposition !== undefined
? npcDispositionRingColor(disposition)
: patch.ringColor !== undefined
? normalizeHexColor(patch.ringColor, DEFAULT_PLAYER_RING_COLOR)
: n.ringColor || npcDispositionRingColor(disposition),
...(patch.imageOffset !== undefined
? { imageOffset: clampPlayerImageOffset(patch.imageOffset) }
: {}),
@@ -2495,7 +2519,10 @@ function normalizeProject(p: Project): Project {
x,
y,
groupId: resolveNpcGroupId(obj.groupId, groupIdSet),
ringColor: normalizeHexColor(obj.ringColor, DEFAULT_PLAYER_RING_COLOR),
disposition: normalizeNpcDisposition((obj as { disposition?: unknown }).disposition),
ringColor: npcDispositionRingColor(
normalizeNpcDisposition((obj as { disposition?: unknown }).disposition),
),
imageOffset: clampPlayerImageOffset(obj.imageOffset),
imageScale: clampPlayerImageScale((obj as { imageScale?: unknown }).imageScale),
};