Files
Genarrative/src/data/encounterTransition.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

126 lines
3.7 KiB
TypeScript

import { GameState } from '../types';
import { getFacingTowardPlayer, getMonsterGroupAnchorX, PLAYER_BASE_X_METERS } from './hostileNpcs';
function roundMeters(value: number) {
return Number(value.toFixed(2));
}
function lerp(start: number, end: number, progress: number) {
return roundMeters(start + ((end - start) * progress));
}
export function hasEncounterEntity(state: Pick<GameState, 'sceneHostileNpcs' | 'currentEncounter'>) {
return state.sceneHostileNpcs.length > 0 || Boolean(state.currentEncounter);
}
export function buildEncounterEntryState(
finalState: GameState,
entryX: number,
): GameState {
if (finalState.sceneHostileNpcs.length > 0) {
const anchorX = getMonsterGroupAnchorX(finalState.sceneHostileNpcs);
return {
...finalState,
sceneHostileNpcs: finalState.sceneHostileNpcs.map(monster => {
const offset = monster.xMeters - anchorX;
const xMeters = roundMeters(entryX + offset);
return {
...monster,
xMeters,
animation: 'move' as const,
facing: getFacingTowardPlayer(xMeters, PLAYER_BASE_X_METERS),
};
}),
currentEncounter: null,
};
}
if (finalState.currentEncounter) {
return {
...finalState,
currentEncounter: {
...finalState.currentEncounter,
xMeters: entryX,
},
sceneHostileNpcs: [],
};
}
return finalState;
}
export function buildEncounterTransitionState(
finalState: GameState,
sourceState: Pick<GameState, 'sceneHostileNpcs' | 'currentEncounter'>,
): GameState {
if (finalState.sceneHostileNpcs.length > 0) {
const sourceById = new Map(sourceState.sceneHostileNpcs.map(monster => [monster.id, monster]));
return {
...finalState,
sceneHostileNpcs: finalState.sceneHostileNpcs.map(monster => {
const sourceMonster = sourceById.get(monster.id);
const xMeters = sourceMonster?.xMeters ?? monster.xMeters;
return {
...monster,
xMeters,
animation: 'move' as const,
facing: getFacingTowardPlayer(xMeters, PLAYER_BASE_X_METERS),
};
}),
currentEncounter: null,
};
}
if (finalState.currentEncounter) {
return {
...finalState,
currentEncounter: {
...finalState.currentEncounter,
xMeters: sourceState.currentEncounter?.xMeters ?? finalState.currentEncounter.xMeters,
},
sceneHostileNpcs: [],
};
}
return finalState;
}
export function interpolateEncounterTransitionState(
startState: Pick<GameState, 'sceneHostileNpcs' | 'currentEncounter'>,
finalState: GameState,
progress: number,
): GameState {
if (finalState.sceneHostileNpcs.length > 0) {
const startById = new Map(startState.sceneHostileNpcs.map(monster => [monster.id, monster]));
return {
...finalState,
sceneHostileNpcs: finalState.sceneHostileNpcs.map(monster => {
const startMonster = startById.get(monster.id);
const xMeters = lerp(startMonster?.xMeters ?? monster.xMeters, monster.xMeters, progress);
return {
...monster,
xMeters,
animation: progress < 1 ? ('move' as const) : monster.animation,
facing: getFacingTowardPlayer(xMeters, PLAYER_BASE_X_METERS),
};
}),
currentEncounter: null,
};
}
if (finalState.currentEncounter) {
const startX = startState.currentEncounter?.xMeters ?? finalState.currentEncounter.xMeters ?? 0;
const endX = finalState.currentEncounter.xMeters ?? startX;
return {
...finalState,
currentEncounter: {
...finalState.currentEncounter,
xMeters: lerp(startX, endX, progress),
},
sceneHostileNpcs: [],
};
}
return finalState;
}