init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
import {
buildNpcGiftModalState,
buildNpcRecruitModalState,
buildNpcTradeModalState,
NPC_GIFT_FUNCTION,
NPC_RECRUIT_FUNCTION,
NPC_TRADE_FUNCTION,
shouldNpcRecruitOpenModal,
} from '../../data/functionCatalog';
import {
applyQuestProgressFromSceneReached,
} from '../../data/questFlow';
import {
buildInitialNpcState,
getPreferredGiftItemId,
MAX_COMPANIONS,
} from '../../data/npcInteractions';
import { incrementGameRuntimeStats } from '../../data/runtimeStats';
import { ensureSceneEncounterPreview } from '../../data/sceneEncounterPreviews';
import { getScenePresetById } from '../../data/scenePresets';
import {
AnimationState,
type Encounter,
type GameState,
type StoryOption,
} from '../../types';
import type {
GiftModalState,
RecruitModalState,
TradeModalState,
} from './uiTypes';
export type NpcInteractionDecision =
| { kind: 'none' }
| { kind: 'trade_modal'; modal: TradeModalState }
| { kind: 'gift_modal'; modal: GiftModalState }
| { kind: 'recruit_modal'; modal: RecruitModalState }
| { kind: 'recruit_immediate' };
export type MapTravelResolution = {
nextState: GameState;
actionText: string;
travelResultText: string;
};
function isNpcEncounter(
encounter: GameState['currentEncounter'],
): encounter is Encounter {
return Boolean(encounter?.kind === 'npc');
}
export function getNpcEncounterKey(encounter: Encounter) {
return encounter.id ?? encounter.npcName;
}
function getResolvedNpcState(state: GameState, encounter: Encounter) {
return (
state.npcStates[getNpcEncounterKey(encounter)] ??
buildInitialNpcState(encounter, state.worldType)
);
}
export function resolveNpcInteractionDecision(
state: GameState,
option: StoryOption,
): NpcInteractionDecision {
if (
!state.playerCharacter ||
!option.interaction ||
!isNpcEncounter(state.currentEncounter)
) {
return { kind: 'none' };
}
const encounter = state.currentEncounter;
const npcState = getResolvedNpcState(state, encounter);
switch (option.functionId) {
case NPC_TRADE_FUNCTION.id:
return {
kind: 'trade_modal',
modal: buildNpcTradeModalState(
state,
encounter,
option.actionText,
npcState.inventory,
),
};
case NPC_GIFT_FUNCTION.id:
{
const selectedGiftItemId = getPreferredGiftItemId(
state.playerInventory,
encounter,
{
worldType: state.worldType,
customWorldProfile: state.customWorldProfile,
},
);
if (!selectedGiftItemId) {
return { kind: 'none' };
}
return {
kind: 'gift_modal',
modal: buildNpcGiftModalState(
state,
encounter,
option.actionText,
selectedGiftItemId,
),
};
}
case NPC_RECRUIT_FUNCTION.id:
if (shouldNpcRecruitOpenModal(state.companions.length, MAX_COMPANIONS)) {
return {
kind: 'recruit_modal',
modal: buildNpcRecruitModalState(state, encounter, option.actionText),
};
}
return { kind: 'recruit_immediate' };
default:
return { kind: 'none' };
}
}
export function buildMapTravelResolution(
state: GameState,
sceneId: string,
): MapTravelResolution | null {
if (!state.worldType || !state.playerCharacter) {
return null;
}
const targetScene = getScenePresetById(state.worldType, sceneId);
if (!targetScene || targetScene.id === state.currentScenePreset?.id) {
return null;
}
const nextState = ensureSceneEncounterPreview({
...state,
runtimeStats: incrementGameRuntimeStats(state.runtimeStats, {
scenesTraveled: 1,
}),
quests: applyQuestProgressFromSceneReached(state.quests, targetScene.id),
currentScenePreset: targetScene,
currentEncounter: null,
npcInteractionActive: false,
sceneHostileNpcs: [],
playerX: 0,
playerFacing: 'right' as const,
animationState: AnimationState.IDLE,
playerActionMode: 'idle' as const,
activeCombatEffects: [],
scrollWorld: false,
inBattle: false,
lastObserveSignsSceneId: null,
lastObserveSignsReport: null,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
});
const travelResultText = `你离开${state.currentScenePreset?.name ?? '当前位置'},前往${targetScene.name}`;
return {
nextState,
actionText: `前往${targetScene.name}`,
travelResultText,
};
}