Rework story engine flow and reorganize project docs
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -12,8 +12,8 @@ import {
|
||||
createEmptyEquipmentLoadout,
|
||||
getEquipmentBonuses,
|
||||
} from '../src/data/equipmentEffects.ts';
|
||||
import { createSceneHostileNpcsFromIds } from '../src/data/hostileNpcs.ts';
|
||||
import { isInventoryItemUsable, resolveInventoryItemUseEffect } from '../src/data/inventoryEffects.ts';
|
||||
import { createSceneMonstersFromIds } from '../src/data/monsters.ts';
|
||||
import { buildInitialNpcState, buildInitialPlayerInventory, buildNpcEncounterStoryMoment, checkTradeItem, createNpcBattleMonster } from '../src/data/npcInteractions.ts';
|
||||
import {
|
||||
acceptQuest,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import { createInitialGameRuntimeStats } from '../src/data/runtimeStats.ts';
|
||||
import { createSceneCallOutEncounter, createSceneEncounterPreview, ensureSceneEncounterPreview } from '../src/data/sceneEncounterPreviews.ts';
|
||||
import { buildSceneObserveSignsStoryText } from '../src/data/sceneObservation.ts';
|
||||
import { getScenePresetsByWorld } from '../src/data/scenePresets.ts';
|
||||
import { getSceneHostileNpcPresetIds, getScenePresetsByWorld } from '../src/data/scenePresets.ts';
|
||||
import { buildTreasureEncounterStoryMoment, buildTreasureResultText, resolveTreasureReward } from '../src/data/treasureInteractions.ts';
|
||||
import { AnimationState, GameState, WorldType } from '../src/types.ts';
|
||||
|
||||
@@ -55,7 +55,7 @@ function createBaseState(worldType: WorldType, sceneId?: string): GameState {
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
currentScenePreset,
|
||||
sceneMonsters: [],
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
@@ -92,10 +92,10 @@ function smokeScenePreviews() {
|
||||
|
||||
const preview = createSceneEncounterPreview(createBaseState(worldType, scene.id));
|
||||
assert(preview.currentEncounter?.kind !== 'treasure', `[preview] treasure encounter should be disabled for ${worldType}`);
|
||||
assert(preview.currentEncounter || preview.sceneMonsters.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} produced no preview entity`);
|
||||
assert(preview.currentEncounter || preview.sceneHostileNpcs.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} produced no preview entity`);
|
||||
|
||||
const ensured = ensureSceneEncounterPreview(createBaseState(worldType, scene.id));
|
||||
assert(ensured.currentEncounter || ensured.sceneMonsters.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} failed ensureSceneEncounterPreview`);
|
||||
assert(ensured.currentEncounter || ensured.sceneHostileNpcs.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} failed ensureSceneEncounterPreview`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,20 +163,21 @@ function smokeTreasureStories() {
|
||||
|
||||
function smokeMonsterCreation() {
|
||||
for (const worldType of [WorldType.WUXIA, WorldType.XIANXIA]) {
|
||||
const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => scene.monsterIds.length > 0);
|
||||
const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => getSceneHostileNpcPresetIds(scene).length > 0);
|
||||
assert(sceneWithMonster, `[monster] missing monster scene for ${worldType}`);
|
||||
const monsters = createSceneMonstersFromIds(worldType, sceneWithMonster.monsterIds, 0);
|
||||
const hostileNpcPresetIds = getSceneHostileNpcPresetIds(sceneWithMonster);
|
||||
const monsters = createSceneHostileNpcsFromIds(worldType, hostileNpcPresetIds, 0);
|
||||
assert(monsters.length > 0, `[monster] ${sceneWithMonster.id} failed to create scene monsters`);
|
||||
assert(
|
||||
monsters.length === Math.min(sceneWithMonster.monsterIds.length, 3),
|
||||
monsters.length === Math.min(hostileNpcPresetIds.length, 3),
|
||||
`[monster] ${sceneWithMonster.id} should keep the full configured encounter group`,
|
||||
);
|
||||
|
||||
const resolvedState = createBaseState(worldType, sceneWithMonster.id);
|
||||
resolvedState.sceneMonsters = monsters;
|
||||
resolvedState.sceneHostileNpcs = monsters;
|
||||
resolvedState.inBattle = true;
|
||||
assert(
|
||||
resolvedState.sceneMonsters.length === monsters.length,
|
||||
resolvedState.sceneHostileNpcs.length === monsters.length,
|
||||
`[monster] ${sceneWithMonster.id} multi-enemy battle state lost monsters`,
|
||||
);
|
||||
}
|
||||
@@ -206,7 +207,7 @@ function smokeObserveAndCallOut() {
|
||||
const baseState = createBaseState(worldType, scene.id);
|
||||
const callOutResult = createSceneCallOutEncounter(baseState);
|
||||
assert(callOutResult.currentEncounter?.kind !== 'treasure', `[idle] treasure call_out should be disabled for ${worldType}`);
|
||||
assert(callOutResult.currentEncounter || callOutResult.sceneMonsters.length > 0 || scene.monsterIds.length === 0, `[idle] call_out failed for ${scene.id}`);
|
||||
assert(callOutResult.currentEncounter || callOutResult.sceneHostileNpcs.length > 0 || getSceneHostileNpcPresetIds(scene).length === 0, `[idle] call_out failed for ${scene.id}`);
|
||||
|
||||
const observeText = buildSceneObserveSignsStoryText(worldType, scene.id);
|
||||
assert(observeText.length > 12, `[idle] observe_signs text too short for ${scene.id}`);
|
||||
@@ -281,19 +282,20 @@ function smokeTradeEconomyLoop() {
|
||||
|
||||
function smokeEncounterTransitionLoop() {
|
||||
for (const worldType of [WorldType.WUXIA, WorldType.XIANXIA]) {
|
||||
const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => scene.monsterIds.length >= 2);
|
||||
const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => getSceneHostileNpcPresetIds(scene).length >= 2);
|
||||
assert(sceneWithMonster, `[transition] missing multi-monster scene for ${worldType}`);
|
||||
|
||||
const finalMonsters = createSceneMonstersFromIds(worldType, sceneWithMonster.monsterIds, 0);
|
||||
const hostileNpcPresetIds = getSceneHostileNpcPresetIds(sceneWithMonster);
|
||||
const finalMonsters = createSceneHostileNpcsFromIds(worldType, hostileNpcPresetIds, 0);
|
||||
const finalState = {
|
||||
...createBaseState(worldType, sceneWithMonster.id),
|
||||
inBattle: true,
|
||||
sceneMonsters: finalMonsters,
|
||||
sceneHostileNpcs: finalMonsters,
|
||||
};
|
||||
const previewState = {
|
||||
...finalState,
|
||||
inBattle: false,
|
||||
sceneMonsters: finalMonsters.map((monster, index) => ({
|
||||
sceneHostileNpcs: finalMonsters.map((monster, index) => ({
|
||||
...monster,
|
||||
xMeters: 12 + (index * 1.8),
|
||||
})),
|
||||
@@ -301,15 +303,15 @@ function smokeEncounterTransitionLoop() {
|
||||
|
||||
const transitionState = buildEncounterTransitionState(finalState, previewState);
|
||||
assert(
|
||||
transitionState.sceneMonsters[1]?.xMeters === previewState.sceneMonsters[1]?.xMeters,
|
||||
transitionState.sceneHostileNpcs[1]?.xMeters === previewState.sceneHostileNpcs[1]?.xMeters,
|
||||
`[transition] second monster should keep its preview x during transition for ${worldType}`,
|
||||
);
|
||||
|
||||
const halfwayState = interpolateEncounterTransitionState(transitionState, finalState, 0.5);
|
||||
assert(
|
||||
halfwayState.sceneMonsters.every((monster, index) => {
|
||||
const startX = transitionState.sceneMonsters[index]?.xMeters ?? monster.xMeters;
|
||||
const endX = finalState.sceneMonsters[index]?.xMeters ?? monster.xMeters;
|
||||
halfwayState.sceneHostileNpcs.every((monster, index) => {
|
||||
const startX = transitionState.sceneHostileNpcs[index]?.xMeters ?? monster.xMeters;
|
||||
const endX = finalState.sceneHostileNpcs[index]?.xMeters ?? monster.xMeters;
|
||||
return monster.xMeters !== startX && monster.xMeters !== endX;
|
||||
}),
|
||||
`[transition] all monsters should interpolate instead of only the first one for ${worldType}`,
|
||||
@@ -317,7 +319,7 @@ function smokeEncounterTransitionLoop() {
|
||||
|
||||
const offscreenState = buildEncounterEntryState(finalState, 18);
|
||||
assert(
|
||||
offscreenState.sceneMonsters.every(monster => monster.xMeters >= 18),
|
||||
offscreenState.sceneHostileNpcs.every(monster => monster.xMeters >= 18),
|
||||
`[transition] offscreen entry should place the entire encounter group offscreen for ${worldType}`,
|
||||
);
|
||||
}
|
||||
@@ -361,7 +363,7 @@ function smokeRosterLoop() {
|
||||
function smokeQuestLoop() {
|
||||
for (const worldType of [WorldType.WUXIA, WorldType.XIANXIA]) {
|
||||
const sceneWithNpcAndMonster = getScenePresetsByWorld(worldType).find(
|
||||
scene => scene.npcs.length > 0 && scene.monsterIds.length > 0,
|
||||
scene => scene.npcs.length > 0 && getSceneHostileNpcPresetIds(scene).length > 0,
|
||||
);
|
||||
assert(sceneWithNpcAndMonster, `[quest] missing npc+monster scene for ${worldType}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user