Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
import { HostileNpcAnimationConfig, HostileNpcSpriteConfig } from '../components/HostileNpcAnimator';
|
||||
import { GameState, InventoryItem, MonsterLootEntry, RoleActionDefinition, SceneHostileNpc, WorldType } from '../types';
|
||||
import {generateRuntimeItemAiIntents} from '../services/runtimeItemAiDirector';
|
||||
import { GameState, InventoryItem, MonsterLootEntry, RoleActionDefinition, RuntimeItemPlan, SceneHostileNpc, WorldType } from '../types';
|
||||
import { buildMonsterAttributeProfile } from './attributeProfileGenerator';
|
||||
import { buildDefaultAxisVector } from './attributeResolver';
|
||||
import {normalizeBuildTags} from './buildTags';
|
||||
import { buildRuntimeCustomWorldInventoryItems, resolveRuleWorldType } from './customWorldRuntime';
|
||||
import hostileNpcOverridesJson from './hostileNpcOverrides.json';
|
||||
import { buildRuntimeItemGenerationContext } from './runtimeItemContext';
|
||||
import { buildDirectedRuntimeReward } from './runtimeItemDirector';
|
||||
import { flattenDirectedRuntimeRewardItems } from './runtimeItemNarrative';
|
||||
import { generateDirectedRuntimeReward } from './runtimeItemDirector';
|
||||
import {
|
||||
applyRuntimeItemNarrativeToExistingItem,
|
||||
buildRuntimeItemAiIntent,
|
||||
flattenDirectedRuntimeRewardItems,
|
||||
} from './runtimeItemNarrative';
|
||||
import { getPresetWorldAttributeSchema } from './worldAttributeSchemas';
|
||||
|
||||
export interface HostileNpcPreset extends HostileNpcSpriteConfig {
|
||||
@@ -940,12 +946,15 @@ const ALL_HOSTILE_NPC_PRESETS = BASE_HOSTILE_NPC_PRESETS.map(basePreset => merge
|
||||
export const HOSTILE_NPC_PRESETS_BY_WORLD: Record<WorldType, HostileNpcPreset[]> = {
|
||||
[WorldType.WUXIA]: ALL_HOSTILE_NPC_PRESETS.filter(monster => monster.worldType === WorldType.WUXIA),
|
||||
[WorldType.XIANXIA]: ALL_HOSTILE_NPC_PRESETS.filter(monster => monster.worldType === WorldType.XIANXIA),
|
||||
[WorldType.CUSTOM]: ALL_HOSTILE_NPC_PRESETS.filter(monster => monster.worldType === WorldType.WUXIA),
|
||||
[WorldType.CUSTOM]: [...ALL_HOSTILE_NPC_PRESETS],
|
||||
};
|
||||
|
||||
export const MONSTER_PRESETS_BY_WORLD = HOSTILE_NPC_PRESETS_BY_WORLD;
|
||||
|
||||
export function getHostileNpcPresetById(worldType: WorldType, monsterId: string) {
|
||||
if (worldType === WorldType.CUSTOM) {
|
||||
return HOSTILE_NPC_PRESETS_BY_WORLD[WorldType.CUSTOM].find(monster => monster.id === monsterId) ?? null;
|
||||
}
|
||||
const resolvedWorldType = resolveRuleWorldType(worldType) ?? WorldType.WUXIA;
|
||||
return HOSTILE_NPC_PRESETS_BY_WORLD[resolvedWorldType].find(monster => monster.id === monsterId) ?? null;
|
||||
}
|
||||
@@ -953,6 +962,9 @@ export function getHostileNpcPresetById(worldType: WorldType, monsterId: string)
|
||||
export const getMonsterPresetById = getHostileNpcPresetById;
|
||||
|
||||
export function getHostileNpcPresetsByWorld(worldType: WorldType) {
|
||||
if (worldType === WorldType.CUSTOM) {
|
||||
return HOSTILE_NPC_PRESETS_BY_WORLD[WorldType.CUSTOM];
|
||||
}
|
||||
const resolvedWorldType = resolveRuleWorldType(worldType) ?? WorldType.WUXIA;
|
||||
return HOSTILE_NPC_PRESETS_BY_WORLD[resolvedWorldType];
|
||||
}
|
||||
@@ -963,7 +975,100 @@ export function getHostileNpcPresetOverrideById(monsterId: string) {
|
||||
return HOSTILE_NPC_OVERRIDES[monsterId] ?? null;
|
||||
}
|
||||
|
||||
export function rollHostileNpcLoot(
|
||||
function inferRuntimePlanFromLootItem(
|
||||
item: InventoryItem,
|
||||
context: ReturnType<typeof buildRuntimeItemGenerationContext>,
|
||||
index: number,
|
||||
): RuntimeItemPlan {
|
||||
const normalizedBuildTags = normalizeBuildTags(item.tags, 3);
|
||||
const targetBuildDirection = normalizedBuildTags.length > 0
|
||||
? normalizedBuildTags
|
||||
: normalizeBuildTags(context.playerBuildTags, 3);
|
||||
|
||||
return {
|
||||
slot: item.rarity === 'epic' || item.rarity === 'legendary'
|
||||
? 'primary'
|
||||
: index === 0
|
||||
? 'secondary'
|
||||
: 'support',
|
||||
itemKind: item.category === '武器' || item.category === '护甲'
|
||||
? 'equipment'
|
||||
: item.category === '消耗品' || item.tags.includes('consumable') || item.tags.includes('healing') || item.tags.includes('mana')
|
||||
? 'consumable'
|
||||
: item.category === '材料' || item.tags.includes('material')
|
||||
? 'material'
|
||||
: item.category === '专属品' || item.category === '专属物' || item.category === '专属物品'
|
||||
? 'quest'
|
||||
: 'relic',
|
||||
permanence: item.category === '材料'
|
||||
? 'resource'
|
||||
: item.category === '消耗品'
|
||||
? 'timed'
|
||||
: 'permanent',
|
||||
narrativeWeight: item.rarity === 'epic' || item.rarity === 'legendary' ? 'heavy' : 'medium',
|
||||
targetBuildDirection: targetBuildDirection.length > 0 ? targetBuildDirection : ['均衡'],
|
||||
relationAnchor: context.encounter?.monsterPresetId
|
||||
? {
|
||||
type: 'monster' as const,
|
||||
monsterId: context.encounter.monsterPresetId,
|
||||
monsterName: context.encounter.npcName,
|
||||
}
|
||||
: context.encounterNpcName
|
||||
? {
|
||||
type: 'npc' as const,
|
||||
npcId: context.encounterNpcId ?? undefined,
|
||||
npcName: context.encounterNpcName,
|
||||
roleText: context.encounterContextText ?? undefined,
|
||||
}
|
||||
: {
|
||||
type: 'scene' as const,
|
||||
sceneId: context.sceneId ?? undefined,
|
||||
sceneName: context.sceneName ?? '战场余烬',
|
||||
},
|
||||
} satisfies RuntimeItemPlan;
|
||||
}
|
||||
|
||||
async function decoratePresetLootWithNarrative(
|
||||
items: InventoryItem[],
|
||||
context: ReturnType<typeof buildRuntimeItemGenerationContext>,
|
||||
seedKeyPrefix: string,
|
||||
) {
|
||||
if (items.length <= 0) return items;
|
||||
|
||||
const plans = items.map((item, index) => inferRuntimePlanFromLootItem(item, context, index));
|
||||
const fallbackIntents = plans.map(plan => buildRuntimeItemAiIntent(context, plan));
|
||||
let intents = fallbackIntents;
|
||||
|
||||
try {
|
||||
intents = await generateRuntimeItemAiIntents({
|
||||
context,
|
||||
plans,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[HostileNpcPresets] preset loot narrative fallback', error);
|
||||
}
|
||||
|
||||
return items.map((item, index) =>
|
||||
applyRuntimeItemNarrativeToExistingItem({
|
||||
item: {
|
||||
...item,
|
||||
runtimeMetadata: {
|
||||
origin: 'procedural',
|
||||
generationChannel: 'monster_drop',
|
||||
relationAnchor: plans[index]!.relationAnchor,
|
||||
seedKey: `${seedKeyPrefix}:preset:${index}`,
|
||||
sourceReason: intents[index]!.reasonToAppear,
|
||||
},
|
||||
},
|
||||
context,
|
||||
plan: plans[index]!,
|
||||
intent: intents[index]!,
|
||||
preserveName: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function rollHostileNpcLoot(
|
||||
state: GameState,
|
||||
defeatedHostileNpcs: Array<Pick<SceneHostileNpc, 'id' | 'name'>>,
|
||||
) {
|
||||
@@ -979,7 +1084,7 @@ export function rollHostileNpcLoot(
|
||||
));
|
||||
}
|
||||
|
||||
return defeatedHostileNpcs.flatMap(monster => {
|
||||
const rewardBatches = await Promise.all(defeatedHostileNpcs.map(async monster => {
|
||||
const preset = getHostileNpcPresetById(state.worldType!, monster.id);
|
||||
const presetLoot = preset
|
||||
? preset.lootTable
|
||||
@@ -1001,13 +1106,19 @@ export function rollHostileNpcLoot(
|
||||
monsterPresetId: monster.id,
|
||||
},
|
||||
});
|
||||
const directedReward = buildDirectedRuntimeReward(context, {
|
||||
seedKey: `monster-loot:${monster.id}:${monster.name}:${state.currentScenePreset?.id ?? 'scene'}`,
|
||||
itemCount: 2,
|
||||
fixedKinds: ['material', 'consumable'],
|
||||
fixedPermanence: ['resource', 'timed'],
|
||||
});
|
||||
const seedKey = `monster-loot:${monster.id}:${monster.name}:${state.currentScenePreset?.id ?? 'scene'}`;
|
||||
const [decoratedPresetLoot, directedReward] = await Promise.all([
|
||||
decoratePresetLootWithNarrative(presetLoot, context, seedKey),
|
||||
generateDirectedRuntimeReward(context, {
|
||||
seedKey,
|
||||
itemCount: 2,
|
||||
fixedKinds: ['material', 'consumable'],
|
||||
fixedPermanence: ['resource', 'timed'],
|
||||
}),
|
||||
]);
|
||||
const runtimeItems = flattenDirectedRuntimeRewardItems(directedReward);
|
||||
return [...presetLoot, ...runtimeItems];
|
||||
});
|
||||
return [...decoratedPresetLoot, ...runtimeItems];
|
||||
}));
|
||||
|
||||
return rewardBatches.flat();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user