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:
@@ -448,6 +448,7 @@ export async function buildProjectFromFoundryDocuments(
|
||||
y: 80 + Math.floor(npcIndex / 4) * 200,
|
||||
groupId,
|
||||
ringColor: '#c9a227',
|
||||
disposition: 'neutral',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
});
|
||||
|
||||
+37
-3
@@ -31,6 +31,7 @@ import { SceneViewStore } from './sceneView/sceneViewStore';
|
||||
import { registerDndAssetProtocol } from './protocol/dndAssetProtocol';
|
||||
import { PlayersStore } from './players/playersStore';
|
||||
import { SceneNpcTokensSessionStore } from './players/sceneNpcTokensSessionStore';
|
||||
import { ScenePlayerTokensSessionStore } from './players/scenePlayerTokensSessionStore';
|
||||
import { SceneTokensSessionStore } from './tokens/sceneTokensSessionStore';
|
||||
import { TokensStore } from './tokens/tokensStore';
|
||||
import { installAutoUpdater } from './update/installAutoUpdater';
|
||||
@@ -174,6 +175,7 @@ const materialsOverlayStore = new MaterialsOverlayStore();
|
||||
const npcsOverlayStore = new NpcsOverlayStore();
|
||||
const sceneTokensSessionStore = new SceneTokensSessionStore();
|
||||
const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore();
|
||||
const scenePlayerTokensSessionStore = new ScenePlayerTokensSessionStore();
|
||||
let tokensStore: TokensStore | null = null;
|
||||
let playersStore: PlayersStore | null = null;
|
||||
|
||||
@@ -265,6 +267,13 @@ function emitSceneNpcTokensSessionState(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function emitScenePlayerTokensSessionState(): void {
|
||||
const state = scenePlayerTokensSessionStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.scenePlayerTokensSession.stateChanged, { state });
|
||||
}
|
||||
}
|
||||
|
||||
function syncSceneDarknessForProject(project: Project): void {
|
||||
const cacheKey = project.currentGraphNodeId ?? project.currentSceneId ?? null;
|
||||
const scene = project.currentSceneId ? project.scenes[project.currentSceneId] : undefined;
|
||||
@@ -435,11 +444,16 @@ async function main() {
|
||||
registerHandler(ipcChannels.license.setToken, async ({ token }) => licenseService.setToken(token));
|
||||
registerHandler(ipcChannels.license.clearToken, () => licenseService.clearToken());
|
||||
registerHandler(ipcChannels.license.acceptEula, ({ version }) => licenseService.acceptEula(version));
|
||||
registerHandler(ipcChannels.windows.openMultiWindow, () => {
|
||||
registerHandler(ipcChannels.windows.openMultiWindow, (req) => {
|
||||
sceneDarknessStore.resetSession();
|
||||
sceneTrapsStore.resetSession();
|
||||
sceneTokensSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
scenePlayerTokensSessionStore.reset();
|
||||
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
|
||||
if (playerIds.length > 0) {
|
||||
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
|
||||
}
|
||||
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
|
||||
openMultiWindow();
|
||||
const project = projectStore.getOpenProject();
|
||||
@@ -451,6 +465,7 @@ async function main() {
|
||||
emitSceneTrapsState();
|
||||
emitSceneTokensSessionState();
|
||||
emitSceneNpcTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
emitEffectsState();
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -548,9 +563,11 @@ async function main() {
|
||||
sceneViewStore.reset();
|
||||
sceneTokensSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
scenePlayerTokensSessionStore.reset();
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitSceneNpcTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
emitSessionState();
|
||||
warmNpcsEditorWindow();
|
||||
return { project };
|
||||
@@ -566,6 +583,7 @@ async function main() {
|
||||
sceneViewStore.reset();
|
||||
sceneTokensSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
scenePlayerTokensSessionStore.reset();
|
||||
emitEffectsState();
|
||||
emitMaterialsOverlayState();
|
||||
emitNpcsOverlayState();
|
||||
@@ -574,6 +592,7 @@ async function main() {
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitSceneNpcTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
emitSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -590,6 +609,7 @@ async function main() {
|
||||
materialsOverlayStore.clear();
|
||||
npcsOverlayStore.clear();
|
||||
sceneViewStore.reset();
|
||||
scenePlayerTokensSessionStore.clearPlacements();
|
||||
// Token moves persist for the whole play session (reset only on project open/close).
|
||||
const project = projectStore.getOpenProject();
|
||||
if (project) {
|
||||
@@ -603,6 +623,7 @@ async function main() {
|
||||
emitSceneTrapsState();
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
emitSessionState();
|
||||
return { currentSceneId: projectStore.getOpenProject()?.currentSceneId ?? null };
|
||||
});
|
||||
@@ -619,6 +640,7 @@ async function main() {
|
||||
materialsOverlayStore.clear();
|
||||
npcsOverlayStore.clear();
|
||||
sceneViewStore.reset();
|
||||
scenePlayerTokensSessionStore.clearPlacements();
|
||||
// Token moves persist for the whole play session (reset only on project open/close).
|
||||
const project = projectStore.getOpenProject();
|
||||
if (project) {
|
||||
@@ -632,6 +654,7 @@ async function main() {
|
||||
emitSceneTrapsState();
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitScenePlayerTokensSessionState();
|
||||
emitSessionState();
|
||||
const p = projectStore.getOpenProject();
|
||||
return {
|
||||
@@ -784,7 +807,7 @@ async function main() {
|
||||
});
|
||||
registerHandler(
|
||||
ipcChannels.project.upsertNpc,
|
||||
async ({ npcId, name, description, filePath: pathFromDrop, groupId, ringColor, imageOffset }) => {
|
||||
async ({ npcId, name, description, filePath: pathFromDrop, groupId, ringColor, disposition, imageOffset }) => {
|
||||
let filePath = pathFromDrop;
|
||||
if (!filePath && !npcId) {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
@@ -809,6 +832,7 @@ async function main() {
|
||||
...(filePath ? { filePath } : {}),
|
||||
...(groupId !== undefined ? { groupId } : {}),
|
||||
...(ringColor !== undefined ? { ringColor } : {}),
|
||||
...(disposition !== undefined ? { disposition } : {}),
|
||||
...(imageOffset !== undefined ? { imageOffset } : {}),
|
||||
},
|
||||
(p) => emitNpcUpsertProgress(p),
|
||||
@@ -821,13 +845,15 @@ async function main() {
|
||||
);
|
||||
registerHandler(
|
||||
ipcChannels.project.updateNpcFields,
|
||||
async ({ npcId, name, description, groupId, ringColor, imageOffset }) => {
|
||||
async ({ npcId, name, description, groupId, ringColor, disposition, imageOffset, imageScale }) => {
|
||||
const project = await projectStore.updateNpcFields(npcId, {
|
||||
...(typeof name === 'string' ? { name } : {}),
|
||||
...(typeof description === 'string' ? { description } : {}),
|
||||
...(groupId !== undefined ? { groupId } : {}),
|
||||
...(ringColor !== undefined ? { ringColor } : {}),
|
||||
...(disposition !== undefined ? { disposition } : {}),
|
||||
...(imageOffset !== undefined ? { imageOffset } : {}),
|
||||
...(imageScale !== undefined ? { imageScale } : {}),
|
||||
});
|
||||
emitSessionState();
|
||||
return { project };
|
||||
@@ -1428,6 +1454,14 @@ async function main() {
|
||||
emitSceneNpcTokensSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.scenePlayerTokensSession.getState, () => {
|
||||
return { state: scenePlayerTokensSessionStore.getState() };
|
||||
});
|
||||
registerHandler(ipcChannels.scenePlayerTokensSession.dispatch, ({ event }) => {
|
||||
scenePlayerTokensSessionStore.dispatch(event);
|
||||
emitScenePlayerTokensSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.video.getState, () => {
|
||||
return { state: videoStore.getState() };
|
||||
|
||||
@@ -30,3 +30,31 @@ void test('SceneNpcTokensSessionStore setScale', () => {
|
||||
assert.equal(s3.scale, DEFAULT_NPC_TOKEN_SESSION_SCALE);
|
||||
assert.deepEqual(s3.byPlacementId, {});
|
||||
});
|
||||
|
||||
void test('SceneNpcTokensSessionStore setDisposition without inventing position', () => {
|
||||
const store = new SceneNpcTokensSessionStore();
|
||||
const s1 = store.dispatch({
|
||||
kind: 'setDisposition',
|
||||
placementId: 'a',
|
||||
disposition: 'hostile',
|
||||
});
|
||||
assert.equal(s1.byPlacementId.a?.disposition, 'hostile');
|
||||
assert.equal(s1.byPlacementId.a?.nx, undefined);
|
||||
assert.equal(s1.byPlacementId.a?.ny, undefined);
|
||||
const s2 = store.dispatch({ kind: 'move', placementId: 'a', nx: 0.4, ny: 0.6 });
|
||||
assert.equal(s2.byPlacementId.a?.nx, 0.4);
|
||||
assert.equal(s2.byPlacementId.a?.disposition, 'hostile');
|
||||
});
|
||||
|
||||
void test('SceneNpcTokensSessionStore setInactive is session flag', () => {
|
||||
const store = new SceneNpcTokensSessionStore();
|
||||
const s1 = store.dispatch({ kind: 'setInactive', placementId: 'a', inactive: true });
|
||||
assert.equal(s1.byPlacementId.a?.inactive, true);
|
||||
const s2 = store.dispatch({ kind: 'setInactive', placementId: 'a', inactive: false });
|
||||
assert.deepEqual(s2.byPlacementId, {});
|
||||
store.dispatch({ kind: 'move', placementId: 'a', nx: 0.2, ny: 0.3 });
|
||||
store.dispatch({ kind: 'setInactive', placementId: 'a', inactive: true });
|
||||
const s3 = store.dispatch({ kind: 'setInactive', placementId: 'a', inactive: false });
|
||||
assert.equal(s3.byPlacementId.a?.nx, 0.2);
|
||||
assert.equal(s3.byPlacementId.a?.inactive, undefined);
|
||||
});
|
||||
|
||||
@@ -2,8 +2,10 @@ 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 {
|
||||
@@ -13,6 +15,36 @@ function emptyState(revision = 1): SceneNpcTokensSessionState {
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -57,7 +89,55 @@ export class SceneNpcTokensSessionStore {
|
||||
revision: this.state.revision + 1,
|
||||
byPlacementId: {
|
||||
...this.state.byPlacementId,
|
||||
[placementId]: { nx, ny },
|
||||
[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,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { ScenePlayerTokensSessionStore } from './scenePlayerTokensSessionStore';
|
||||
|
||||
void test('ScenePlayerTokensSessionStore selection and show/hide', () => {
|
||||
const store = new ScenePlayerTokensSessionStore();
|
||||
store.dispatch({ kind: 'setSelection', playerIds: ['a', 'b', 'a'] });
|
||||
assert.deepEqual(store.getState().selectedPlayerIds, ['a', 'b']);
|
||||
assert.equal(store.getState().visible, false);
|
||||
|
||||
store.dispatch({ kind: 'show', sizeN: 0.08 });
|
||||
assert.equal(store.getState().visible, true);
|
||||
assert.equal(Object.keys(store.getState().byPlayerId).length, 2);
|
||||
assert.ok(store.getState().byPlayerId.a);
|
||||
assert.equal(store.getState().byPlayerId.a?.ny, 0.88);
|
||||
|
||||
store.dispatch({ kind: 'setVisible', visible: false });
|
||||
assert.equal(store.getState().visible, false);
|
||||
assert.ok(store.getState().byPlayerId.a);
|
||||
|
||||
store.dispatch({ kind: 'clearPlacements' });
|
||||
assert.equal(store.getState().visible, false);
|
||||
assert.deepEqual(store.getState().byPlayerId, {});
|
||||
assert.deepEqual(store.getState().selectedPlayerIds, ['a', 'b']);
|
||||
});
|
||||
|
||||
void test('ScenePlayerTokensSessionStore move and reset', () => {
|
||||
const store = new ScenePlayerTokensSessionStore();
|
||||
store.dispatch({ kind: 'setSelection', playerIds: ['p1'] });
|
||||
store.dispatch({ kind: 'show', sizeN: 0.1 });
|
||||
const s1 = store.dispatch({ kind: 'move', playerId: 'p1', nx: 0.3, ny: 0.4 });
|
||||
assert.equal(s1.byPlayerId.p1?.nx, 0.3);
|
||||
assert.equal(s1.byPlayerId.p1?.ny, 0.4);
|
||||
const s2 = store.reset();
|
||||
assert.deepEqual(s2.selectedPlayerIds, []);
|
||||
assert.equal(s2.visible, false);
|
||||
assert.deepEqual(s2.byPlayerId, {});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user