feat(play): hide NPC tokens from the player screen

A token can be hidden per session from the control menu. The GM still sees it, and its path keeps moving. Scene editor grid labels no longer call a removed helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-10-02 10:15:32 +08:00
parent 060bbd471a
commit f6c301a12b
10 changed files with 165 additions and 23 deletions
@@ -34,7 +34,8 @@ function isEmptyPlacement(p: SceneNpcTokensSessionPlacement): boolean {
p.ny === undefined &&
p.sizeN === undefined &&
!p.disposition &&
!p.inactive
!p.inactive &&
!p.hiddenFromPlayers
);
}
@@ -45,6 +46,7 @@ function restOf(prev: SceneNpcTokensSessionPlacement | undefined): SceneNpcToken
...(prev?.sizeN !== undefined ? { sizeN: prev.sizeN } : {}),
...(prev?.disposition ? { disposition: prev.disposition } : {}),
...(prev?.inactive ? { inactive: true } : {}),
...(prev?.hiddenFromPlayers ? { hiddenFromPlayers: true } : {}),
};
}
@@ -117,6 +119,7 @@ function applyEvent(
...(cur?.sizeN !== undefined ? { sizeN: cur.sizeN } : {}),
disposition,
...(cur?.inactive ? { inactive: true } : {}),
...(cur?.hiddenFromPlayers ? { hiddenFromPlayers: true } : {}),
},
},
scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
@@ -135,6 +138,7 @@ function applyEvent(
...(cur?.sizeN !== undefined ? { sizeN: cur.sizeN } : {}),
...(cur?.disposition ? { disposition: cur.disposition } : {}),
...(inactive ? { inactive: true } : {}),
...(cur?.hiddenFromPlayers ? { hiddenFromPlayers: true } : {}),
};
if (isEmptyPlacement(next)) {
const { [placementId]: _removed, ...rest } = prev.byPlacementId;
@@ -155,6 +159,36 @@ function applyEvent(
spawned,
};
}
if (event.kind === 'setHiddenFromPlayers') {
const placementId = String(event.placementId ?? '');
if (!placementId) return prev;
const hidden = Boolean(event.hidden);
const cur = prev.byPlacementId[placementId];
if (Boolean(cur?.hiddenFromPlayers) === hidden) return prev;
const next: SceneNpcTokensSessionPlacement = {
...restOf(cur),
...(hidden ? { hiddenFromPlayers: true } : {}),
};
if (!hidden) delete next.hiddenFromPlayers;
if (isEmptyPlacement(next)) {
const { [placementId]: _removed, ...rest } = prev.byPlacementId;
return {
revision: prev.revision + 1,
byPlacementId: rest,
scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
spawned,
};
}
return {
revision: prev.revision + 1,
byPlacementId: {
...prev.byPlacementId,
[placementId]: next,
},
scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,
spawned,
};
}
if (event.kind === 'setSize') {
const placementId = String(event.placementId ?? '');
if (!placementId) return prev;
@@ -187,6 +221,7 @@ function applyEvent(
...(cur?.sizeN !== undefined ? { sizeN: cur.sizeN } : {}),
...(cur?.disposition ? { disposition: cur.disposition } : {}),
...(cur?.inactive ? { inactive: true } : {}),
...(cur?.hiddenFromPlayers ? { hiddenFromPlayers: true } : {}),
},
},
scale: prev.scale ?? DEFAULT_NPC_TOKEN_SESSION_SCALE,