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:
@@ -41,6 +41,8 @@ const EXCLUDED_PREFIXES = [
|
||||
'.codex-logs/',
|
||||
'.git/',
|
||||
'dist/',
|
||||
'dist_check/',
|
||||
'dist_check_monster_position/',
|
||||
'media/',
|
||||
'node_modules/',
|
||||
'public/Icons/',
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getCharacterHomeSceneId, getCharacterNpcSceneIds, PRESET_CHARACTERS } from '../src/data/characterPresets.ts';
|
||||
import { MONSTER_PRESETS_BY_WORLD } from '../src/data/hostileNpcPresets.ts';
|
||||
import { getScenePresetsByWorld } from '../src/data/scenePresets.ts';
|
||||
import { getSceneHostileNpcPresetIds, getScenePresetsByWorld } from '../src/data/scenePresets.ts';
|
||||
import { buildStateFunctionDefinitions } from '../src/data/stateFunctions.ts';
|
||||
import { WorldType } from '../src/types.ts';
|
||||
|
||||
@@ -32,7 +32,7 @@ function validateScenes(errors: string[]) {
|
||||
}
|
||||
});
|
||||
|
||||
scene.monsterIds.forEach(monsterId => {
|
||||
getSceneHostileNpcPresetIds(scene).forEach(monsterId => {
|
||||
if (!monsterIdSet.has(monsterId)) {
|
||||
addError(errors, `[scene] ${scene.id} references unknown monster "${monsterId}" in ${worldType}`);
|
||||
}
|
||||
|
||||
@@ -87,12 +87,12 @@ function validateMonsterOverrides(errors: string[]) {
|
||||
const overrides = readJsonFile<Record<string, unknown>>('src/data/monsterOverrides.json');
|
||||
if (!expectPlainObject(errors, 'monsterOverrides', overrides)) return;
|
||||
|
||||
const monsterIds = new Set(
|
||||
const hostilePresetIds = new Set(
|
||||
[...MONSTER_PRESETS_BY_WORLD[WorldType.WUXIA], ...MONSTER_PRESETS_BY_WORLD[WorldType.XIANXIA]].map(monster => monster.id),
|
||||
);
|
||||
|
||||
Object.entries(overrides).forEach(([monsterId, override]) => {
|
||||
if (!monsterIds.has(monsterId)) {
|
||||
if (!hostilePresetIds.has(monsterId)) {
|
||||
errors.push(`[override] monsterOverrides contains unknown monster id "${monsterId}"`);
|
||||
return;
|
||||
}
|
||||
@@ -109,10 +109,6 @@ function validateSceneOverrides(errors: string[]) {
|
||||
const sceneIds = new Set(
|
||||
[WorldType.WUXIA, WorldType.XIANXIA].flatMap(worldType => getScenePresetsByWorld(worldType).map(scene => scene.id)),
|
||||
);
|
||||
const monsterIds = new Set(
|
||||
[...MONSTER_PRESETS_BY_WORLD[WorldType.WUXIA], ...MONSTER_PRESETS_BY_WORLD[WorldType.XIANXIA]].map(monster => monster.id),
|
||||
);
|
||||
|
||||
Object.entries(overrides).forEach(([sceneId, override]) => {
|
||||
if (!sceneIds.has(sceneId)) {
|
||||
errors.push(`[override] sceneOverrides contains unknown scene id "${sceneId}"`);
|
||||
@@ -134,13 +130,6 @@ function validateSceneOverrides(errors: string[]) {
|
||||
errors.push(`[override] sceneOverrides["${sceneId}"] has invalid connectedSceneIds`);
|
||||
}
|
||||
}
|
||||
|
||||
const overrideMonsterIds = override.monsterIds;
|
||||
if (overrideMonsterIds !== undefined) {
|
||||
if (!Array.isArray(overrideMonsterIds) || overrideMonsterIds.some(id => typeof id !== 'string' || !monsterIds.has(id))) {
|
||||
errors.push(`[override] sceneOverrides["${sceneId}"] has invalid monsterIds`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user