import { HostileNpcAnimationConfig, HostileNpcSpriteConfig } from '../components/HostileNpcAnimator'; 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, resolveCompatibilityTemplateWorldType, } from './customWorldRuntime'; import hostileNpcOverridesJson from './hostileNpcOverrides.json'; import { buildRuntimeItemGenerationContext } from './runtimeItemContext'; import { generateDirectedRuntimeReward } from './runtimeItemDirector'; import { applyRuntimeItemNarrativeToExistingItem, buildRuntimeItemAiIntent, flattenDirectedRuntimeRewardItems, } from './runtimeItemNarrative'; import { getTemplateWorldAttributeSchema } from './worldAttributeSchemas'; export interface HostileNpcPreset extends HostileNpcSpriteConfig { worldType: WorldType; description: string; introAction: string; baseStats: Pick; combatTags: string[]; habitatTags: string[]; lootTable: MonsterLootEntry[]; attributeProfile: NonNullable; behaviorVectors: RoleActionDefinition[]; } export type HostileNpcPresetOverride = Partial> & { animations?: Partial; baseStats?: Partial; lootTable?: HostileNpcPreset['lootTable']; }; export type MonsterPreset = HostileNpcPreset; export type MonsterPresetOverride = HostileNpcPresetOverride; function rowStart(sheetWidth: number, frameWidth: number, rowIndex: number) { return Math.floor(sheetWidth / frameWidth) * rowIndex; } function rowAnimation( sheetWidth: number, frameWidth: number, rowIndex: number, frames: number, fps = 24, ): HostileNpcAnimationConfig { return { start: rowStart(sheetWidth, frameWidth, rowIndex), frames, fps, }; } function buildHostileNpcLootItem( id: string, category: string, name: string, rarity: InventoryItem['rarity'], tags: string[], quantity = 1, description?: string, ): InventoryItem { return { id: `monster-loot:${id}`, category, name, quantity, rarity, tags, description, }; } function buildHostileNpcLootEntry( id: string, dropRate: number, item: InventoryItem, ): MonsterLootEntry { return { id, dropRate, item, }; } const BASE_HOSTILE_NPC_PRESETS: Array< Omit & { combatTags?: string[]; } > = [ { id: 'monster-03', worldType: WorldType.WUXIA, name: '断骨祟灵', description: '一团缠着碎骨与阴气的游灵,喜欢在地宫、荒村和古旧殿宇中徘徊。', introAction: '摇晃着碎骨在前方游荡,阴气正一点点漫开', src: '/Pixel Monsters Vol1/Sprites/monster_03.png', frameWidth: 61, frameHeight: 37, sheetWidth: 976, animations: { idle: rowAnimation(976, 61, 0, 4), move: rowAnimation(976, 61, 1, 8), attack: rowAnimation(976, 61, 2, 16), die: rowAnimation(976, 61, 4, 11), }, baseStats: { attackRange: 1.3, speed: 6.8, hp: 102, maxHp: 102 }, habitatTags: ['地宫', '荒村', '废寺', '遗迹'], }, { id: 'monster-04', worldType: WorldType.WUXIA, name: '石背蜗怪', description: '驮着厚重石壳缓慢爬行,常盘踞在石阶、桥边与潮湿山路上。', introAction: '拖着沉重石壳缓慢拱上前来', src: '/Pixel Monsters Vol1/Sprites/monster_04.png', frameWidth: 58, frameHeight: 37, sheetWidth: 870, animations: { idle: rowAnimation(870, 58, 0, 12), move: rowAnimation(870, 58, 1, 4), attack: rowAnimation(870, 58, 2, 9), die: rowAnimation(870, 58, 3, 15), }, baseStats: { attackRange: 1.1, speed: 4.8, hp: 152, maxHp: 152 }, habitatTags: ['石阶', '渡口', '古桥', '山门'], }, { id: 'monster-06', worldType: WorldType.WUXIA, name: '岩甲蛛兽', description: '甲壳坚硬、步足沉重,常潜伏在残垣、矿洞和石阶附近拦截来者。', introAction: '守在前方,盯着来者蓄势待发', src: '/Pixel Monsters Vol1/Sprites/monster_06.png', frameWidth: 73, frameHeight: 34, sheetWidth: 1606, animations: { idle: rowAnimation(1606, 73, 0, 16), move: rowAnimation(1606, 73, 1, 4), attack: rowAnimation(1606, 73, 2, 22), die: rowAnimation(1606, 73, 3, 11), }, baseStats: { attackRange: 1.2, speed: 5.8, hp: 140, maxHp: 140 }, habitatTags: ['矿道', '石阶', '废城', '地宫'], }, { id: 'monster-07', worldType: WorldType.WUXIA, name: '孢爆菇灵', description: '伪装成无害的小蘑菇,靠近后会突然爆出孢雾和酸毒。', introAction: '看似无害地立在前方,伞盖正轻轻颤动', src: '/Pixel Monsters Vol1/Sprites/monster_07.png', frameWidth: 76, frameHeight: 59, sheetWidth: 1444, animations: { idle: rowAnimation(1444, 76, 0, 4), move: rowAnimation(1444, 76, 1, 8), attack: rowAnimation(1444, 76, 2, 19), die: rowAnimation(1444, 76, 3, 11), }, baseStats: { attackRange: 1.1, speed: 6.1, hp: 108, maxHp: 108 }, habitatTags: ['雾林', '荒村', '废寺', '山路'], }, { id: 'monster-08', worldType: WorldType.WUXIA, name: '枯藤伏虫', description: '像一截会蠕动的枯藤,贴地潜行,适合在林地和湿地里伏击。', introAction: '贴着地面缓缓爬来,像一截活过来的枯藤', src: '/Pixel Monsters Vol1/Sprites/monster_08.png', frameWidth: 50, frameHeight: 23, sheetWidth: 950, animations: { idle: rowAnimation(950, 50, 0, 19), move: rowAnimation(950, 50, 1, 12), attack: rowAnimation(950, 50, 2, 11), die: rowAnimation(950, 50, 3, 17), }, baseStats: { attackRange: 1.0, speed: 6.7, hp: 118, maxHp: 118 }, habitatTags: ['竹林', '雾林', '沼泽', '荒野'], }, { id: 'monster-11', worldType: WorldType.WUXIA, name: '夜牙潜兽', description: '夜色中贴地匍匐的潜伏兽,惯于在街巷、残垣与营地周边游猎。', introAction: '收拢身形伏在前方,尖牙在阴影里一闪而过', src: '/Pixel Monsters Vol1/Sprites/monster_11.png', frameWidth: 70, frameHeight: 36, sheetWidth: 770, animations: { idle: rowAnimation(770, 70, 0, 6), move: rowAnimation(770, 70, 1, 8), attack: rowAnimation(770, 70, 2, 11), die: rowAnimation(770, 70, 3, 10), }, baseStats: { attackRange: 1.4, speed: 7.4, hp: 124, maxHp: 124 }, habitatTags: ['长街', '营地', '断垣', '宫苑'], }, { id: 'monster-13', worldType: WorldType.WUXIA, name: '青鳞毒蛇', description: '身形细长,吐信极快,最喜欢守在草木掩映和石缝交错之地。', introAction: '盘在前路中央缓缓昂头,蛇信正来回试探', src: '/Pixel Monsters Vol1/Sprites/monster_13.png', frameWidth: 66, frameHeight: 36, sheetWidth: 1056, animations: { idle: rowAnimation(1056, 66, 0, 16), attack: rowAnimation(1056, 66, 1, 15), }, baseStats: { attackRange: 1.7, speed: 8.2, hp: 96, maxHp: 96 }, habitatTags: ['竹林', '雾林', '山路', '花圃'], }, { id: 'monster-18', worldType: WorldType.WUXIA, name: '瘴背古象', description: '体格厚重,背脊总有瘴气或孢苔翻涌,适合盘踞在矿道和废弃重地。', introAction: '拖着厚重身躯踩碎碎石,正迎面压来', src: '/Pixel Monsters Vol1/Sprites/monster_18.png', frameWidth: 77, frameHeight: 43, sheetWidth: 1771, animations: { idle: rowAnimation(1771, 77, 0, 8), move: rowAnimation(1771, 77, 0, 8), attack: rowAnimation(1771, 77, 2, 23), die: rowAnimation(1771, 77, 1, 17), }, baseStats: { attackRange: 1.8, speed: 4.4, hp: 186, maxHp: 186 }, habitatTags: ['矿道', '铸坊', '废城', '边关'], }, { id: 'monster-02', worldType: WorldType.XIANXIA, name: '秘匣书妖', description: '像会自行翻页的秘典与宝匣,常在仙门、遗迹与禁制附近浮游。', introAction: '在前方半空翻页浮动,纸页间渗出幽光', src: '/Pixel Monsters Vol1/Sprites/monster_02.png', frameWidth: 43, frameHeight: 46, sheetWidth: 688, animations: { idle: rowAnimation(688, 43, 0, 16), move: rowAnimation(688, 43, 1, 5), attack: rowAnimation(688, 43, 2, 12), die: rowAnimation(688, 43, 4, 7), }, baseStats: { attackRange: 1.4, speed: 7.3, hp: 112, maxHp: 112 }, habitatTags: ['仙门', '长廊', '遗迹', '祭坛'], }, { id: 'monster-05', worldType: WorldType.XIANXIA, name: '血瞳妖眼', description: '硕大眼球外生细足,擅长隔着雾障与灵光窥伺猎物。', introAction: '转动血色眼瞳在前方缓慢逼近', src: '/Pixel Monsters Vol1/Sprites/monster_05.png', frameWidth: 60, frameHeight: 48, sheetWidth: 840, animations: { idle: rowAnimation(840, 60, 0, 12), move: rowAnimation(840, 60, 1, 8), attack: rowAnimation(840, 60, 2, 9), die: rowAnimation(840, 60, 4, 14), }, baseStats: { attackRange: 1.6, speed: 7.0, hp: 126, maxHp: 126 }, habitatTags: ['妖雾', '洞天', '谷地', '秘境'], }, { id: 'monster-10', worldType: WorldType.XIANXIA, name: '青腐泥灵', description: '浑身流着青绿色黏液,移动时像在地面拖出灵腐痕迹。', introAction: '一边渗出黏液一边缓慢朝前爬来', src: '/Pixel Monsters Vol1/Sprites/monster_10.png', frameWidth: 48, frameHeight: 25, sheetWidth: 720, animations: { idle: rowAnimation(720, 48, 0, 8), move: rowAnimation(720, 48, 1, 8), attack: rowAnimation(720, 48, 2, 13), die: rowAnimation(720, 48, 3, 15), }, baseStats: { attackRange: 1.2, speed: 5.0, hp: 136, maxHp: 136 }, habitatTags: ['洞天', '谷地', '月湖', '石坑'], }, { id: 'monster-12', worldType: WorldType.XIANXIA, name: '幽烬灵蝠', description: '翅翼缭绕灰烬般的灵火,常成群出没于洞天、崖壁与灵脉附近。', introAction: '盘旋在前方半空,幽火忽明忽暗', src: '/Pixel Monsters Vol1/Sprites/monster_12.png', frameWidth: 46, frameHeight: 43, sheetWidth: 506, animations: { idle: rowAnimation(506, 46, 0, 8), move: rowAnimation(506, 46, 1, 4), attack: rowAnimation(506, 46, 2, 7), die: rowAnimation(506, 46, 3, 11), }, baseStats: { attackRange: 1.6, speed: 7.8, hp: 120, maxHp: 120 }, habitatTags: ['洞天', '崖壁', '仙岛', '古迹'], }, { id: 'monster-14', worldType: WorldType.XIANXIA, name: '赤潮墨章', description: '像灵海中游出的赤色异章,触腕灵活,最爱在潮湿灵境与月湖边出没。', introAction: '舒展触腕在前方缓缓游弋', src: '/Pixel Monsters Vol1/Sprites/monster_14.png', frameWidth: 61, frameHeight: 46, sheetWidth: 671, animations: { idle: rowAnimation(671, 61, 0, 8), move: rowAnimation(671, 61, 1, 8), attack: rowAnimation(671, 61, 2, 9), die: rowAnimation(671, 61, 4, 11), }, baseStats: { attackRange: 1.7, speed: 7.1, hp: 128, maxHp: 128 }, habitatTags: ['月湖', '天河', '仙洲', '灵泉'], }, { id: 'monster-15', worldType: WorldType.XIANXIA, name: '噬灵妖花', description: '花口如獠牙,根须会吞食靠近的灵气与血肉,常藏在灵植密地。', introAction: '缓缓舒展花瓣,在前方守着必经之路', src: '/Pixel Monsters Vol1/Sprites/monster_15.png', frameWidth: 75, frameHeight: 62, sheetWidth: 2400, animations: { idle: rowAnimation(2400, 75, 0, 6), attack: rowAnimation(2400, 75, 1, 32), die: rowAnimation(2400, 75, 2, 17), }, baseStats: { attackRange: 1.5, speed: 4.6, hp: 168, maxHp: 168 }, habitatTags: ['花圃', '神木', '谷地', '秘境'], }, { id: 'monster-16', worldType: WorldType.XIANXIA, name: '噬雾飞蛾', description: '借雾气遮身,飞行轨迹诡谲,喜欢围着灵光和人影打转。', introAction: '绕着雾气轻轻盘飞,正朝前方逼近', src: '/Pixel Monsters Vol1/Sprites/monster_16.png', frameWidth: 52, frameHeight: 50, sheetWidth: 728, animations: { idle: rowAnimation(728, 52, 0, 8), move: rowAnimation(728, 52, 1, 4), attack: rowAnimation(728, 52, 2, 14), die: rowAnimation(728, 52, 3, 9), }, baseStats: { attackRange: 1.8, speed: 6.9, hp: 130, maxHp: 130 }, habitatTags: ['妖雾', '仙门', '星舟', '悬空'], }, { id: 'monster-20', worldType: WorldType.XIANXIA, name: '澄潮灵母', description: '像由灵泉和潮汐凝出的母体水灵,甩动触须时会带出大片灵浪。', introAction: '在前方拖着水光般的触须缓缓游来', src: '/Pixel Monsters Vol1/Sprites/monster_20.png', frameWidth: 67, frameHeight: 42, sheetWidth: 804, animations: { idle: rowAnimation(804, 67, 0, 6), move: rowAnimation(804, 67, 1, 6), attack: rowAnimation(804, 67, 2, 12), }, baseStats: { attackRange: 1.4, speed: 6.4, hp: 138, maxHp: 138 }, habitatTags: ['月湖', '灵泉', '天河', '寒玉'], }, ]; const BASE_HOSTILE_NPC_LOOT_TABLES: Record = { 'monster-02': [ buildHostileNpcLootEntry( 'monster-02-script-fragment', 0.74, buildHostileNpcLootItem( 'script-fragment', '材料', '残页碎片', 'common', ['material'], 2, '散落的书页和易碎纸片上,还残留着护符墨水的痕迹。', ), ), buildHostileNpcLootEntry( 'monster-02-archive-sigil', 0.24, buildHostileNpcLootItem( 'archive-sigil', '稀有品', '档案印记', 'rare', ['relic', 'mana'], 1, '一枚悬浮的印记,内部储着周围灵文的回响。', ), ), ], 'monster-03': [ buildHostileNpcLootEntry( 'monster-03-bone-dust', 0.74, buildHostileNpcLootItem( 'bone-dust', '材料', '骨尘', 'common', ['material'], 2, '破碎骨灵散落下来的粉末状残渣。', ), ), buildHostileNpcLootEntry( 'monster-03-whisper-ember', 0.22, buildHostileNpcLootItem( 'whisper-ember', '稀有品', '低语余烬', 'rare', ['relic', 'mana'], 1, '微弱的火星里还带着不安魂力的回响。', ), ), ], 'monster-04': [ buildHostileNpcLootEntry( 'monster-04-stone-shell', 0.7, buildHostileNpcLootItem( 'stone-shell-shard', '材料', '石壳碎片', 'uncommon', ['material'], 2, '从石背蜗怪身上剥落下来的坚硬护壳碎片。', ), ), buildHostileNpcLootEntry( 'monster-04-venom-gland', 0.32, buildHostileNpcLootItem( 'venom-gland', '材料', '毒腺', 'uncommon', ['material'], 1, '仍带着余温的密封毒囊。', ), ), ], 'monster-05': [ buildHostileNpcLootEntry( 'monster-05-watcher-tendon', 0.68, buildHostileNpcLootItem( 'watcher-tendon', '材料', '守望筋丝', 'common', ['material'], 2, '从漂浮妖眼身上扯下的细丝筋络。', ), ), buildHostileNpcLootEntry( 'monster-05-blood-lens', 0.3, buildHostileNpcLootItem( 'blood-lens', '稀有品', '血瞳透镜', 'rare', ['relic', 'mana'], 1, '经由打磨的眼晶,可放大敌意与凝视类术式。', ), ), ], 'monster-06': [ buildHostileNpcLootEntry( 'monster-06-carapace-plate', 0.38, buildHostileNpcLootItem( 'carapace-plate', '护甲', '甲壳板', 'rare', ['armor', 'material'], 1, '可重铸成重型防具的致密甲板。', ), ), buildHostileNpcLootEntry( 'monster-06-guard-core', 0.18, buildHostileNpcLootItem( 'guard-core', '稀有品', '守御核心', 'rare', ['relic'], 1, '蕴着本能防御意志的核心,坚实得近乎难摧。', ), ), ], 'monster-07': [ buildHostileNpcLootEntry( 'monster-07-spore-pouch', 0.72, buildHostileNpcLootItem( 'spore-pouch', '材料', '孢囊', 'uncommon', ['material'], 2, '装满不稳定菌孢的鼓胀囊袋。', ), ), buildHostileNpcLootEntry( 'monster-07-burst-cap', 0.28, buildHostileNpcLootItem( 'burst-cap', '消耗品', '爆菇帽', 'uncommon', ['healing'], 1, '可加工成战地药剂的挥发菌帽。', ), ), ], 'monster-08': [ buildHostileNpcLootEntry( 'monster-08-vine-tendril', 0.76, buildHostileNpcLootItem( 'vine-tendril', '材料', '藤须', 'common', ['material'], 2, '从枯藤伏虫身上取得的韧性藤丝。', ), ), buildHostileNpcLootEntry( 'monster-08-ambush-fang', 0.2, buildHostileNpcLootItem( 'ambush-fang', '武器', '伏袭牙刃', 'rare', ['weapon', 'material'], 1, '近战猎手格外珍视的天然尖牙刃。', ), ), ], 'monster-10': [ buildHostileNpcLootEntry( 'monster-10-spirit-slime', 0.72, buildHostileNpcLootItem( 'spirit-slime', '材料', '灵腐黏液', 'common', ['material'], 2, '在黑暗中微微发光的腐灵残液。', ), ), buildHostileNpcLootEntry( 'monster-10-marsh-core', 0.2, buildHostileNpcLootItem( 'marsh-core', '稀有品', '沼泽核心', 'rare', ['relic', 'mana'], 1, '沼气与阴湿灵力凝成的核心结节。', ), ), ], 'monster-11': [ buildHostileNpcLootEntry( 'monster-11-night-fang', 0.58, buildHostileNpcLootItem( 'night-fang', '材料', '夜牙', 'uncommon', ['material'], 1, '适合炼毒与设陷的刃状兽牙。', ), ), buildHostileNpcLootEntry( 'monster-11-shadow-pelt', 0.24, buildHostileNpcLootItem( 'shadow-pelt', '护甲', '暗影兽皮', 'rare', ['armor', 'material'], 1, '披戴后能稍稍融入夜色的深色皮膜。', ), ), ], 'monster-12': [ buildHostileNpcLootEntry( 'monster-12-ember-wing', 0.64, buildHostileNpcLootItem( 'ember-wing', '材料', '烬翼', 'uncommon', ['material'], 2, '仍散着余热灰烬的焦脆翼片。', ), ), buildHostileNpcLootEntry( 'monster-12-ashfire-feather', 0.22, buildHostileNpcLootItem( 'ashfire-feather', '稀有品', '灰焰翎', 'rare', ['relic', 'mana'], 1, '羽尖里封着微弱却持久的灼灵余火。', ), ), ], 'monster-13': [ buildHostileNpcLootEntry( 'monster-13-venom-sac', 0.66, buildHostileNpcLootItem( 'serpent-venom-sac', '材料', '蛇毒囊', 'uncommon', ['material'], 1, '炼药师与刺客都很看重的高浓毒囊。', ), ), buildHostileNpcLootEntry( 'monster-13-serpent-eye', 0.16, buildHostileNpcLootItem( 'serpent-eye', '稀有品', '蛇瞳', 'rare', ['relic', 'mana'], 1, '即使静止不动,也像仍在追索附近的活物。', ), ), ], 'monster-14': [ buildHostileNpcLootEntry( 'monster-14-tide-ink', 0.7, buildHostileNpcLootItem( 'tide-ink', '材料', '潮墨', 'uncommon', ['material'], 2, '适合封印、符阵与水属术式的浓黑灵墨。', ), ), buildHostileNpcLootEntry( 'monster-14-lake-pearl', 0.18, buildHostileNpcLootItem( 'lake-pearl', '稀有品', '湖珠', 'rare', ['relic', 'mana'], 1, '裹着月华水气的光润珍珠。', ), ), ], 'monster-15': [ buildHostileNpcLootEntry( 'monster-15-thorn-nectar', 0.46, buildHostileNpcLootItem( 'thorn-nectar', '消耗品', '棘露蜜浆', 'uncommon', ['healing', 'material'], 1, '黏稠树露可炼成紧急疗伤的回生药浆。', ), ), buildHostileNpcLootEntry( 'monster-15-devour-bloom', 0.12, buildHostileNpcLootItem( 'devour-bloom', '稀有品', '噬灵花核', 'epic', ['relic'], 1, '会掠食灵机的妖花花核,积着浓缩生机。', ), ), ], 'monster-16': [ buildHostileNpcLootEntry( 'monster-16-mist-wing', 0.72, buildHostileNpcLootItem( 'mist-wing', '材料', '雾翼膜', 'common', ['material'], 2, '浸着游雾与静电的轻薄翼膜。', ), ), buildHostileNpcLootEntry( 'monster-16-chase-rune', 0.2, buildHostileNpcLootItem( 'chase-rune', '稀有品', '追猎符印', 'rare', ['relic'], 1, '能与速度和压迫感共鸣的追索印记。', ), ), ], 'monster-18': [ buildHostileNpcLootEntry( 'monster-18-ancient-hide', 0.62, buildHostileNpcLootItem( 'ancient-hide', '护甲', '古苔兽皮', 'uncommon', ['armor', 'material'], 1, '厚重皮层间压着旧尘、苔痕与碰撞裂纹。', ), ), buildHostileNpcLootEntry( 'monster-18-ruin-heart', 0.14, buildHostileNpcLootItem( 'ruin-heart', '稀有品', '遗墟之心', 'epic', ['relic'], 1, '沉重的核心里跳着废墟不肯坍塌的执念。', ), ), ], 'monster-20': [ buildHostileNpcLootEntry( 'monster-20-spring-essence', 0.44, buildHostileNpcLootItem( 'spring-essence', '消耗品', '灵泉精露', 'uncommon', ['mana'], 1, '清凉的泉露能稳住心神与灵息运转。', ), ), buildHostileNpcLootEntry( 'monster-20-tide-mother-core', 0.11, buildHostileNpcLootItem( 'tide-mother-core', '稀有品', '潮母灵核', 'epic', ['relic', 'mana'], 1, '只有在最深的明泉里才会凝出的水华灵核。', ), ), ], }; const HOSTILE_NPC_OVERRIDES = hostileNpcOverridesJson as Record; const BASE_HOSTILE_NPC_COMBAT_TAGS: Record = { 'monster-03': ['镇邪', '控场', '机动'], 'monster-04': ['重甲', '守御', '反击'], 'monster-06': ['重甲', '守御', '压制'], 'monster-07': ['控场', '回复', '炼药'], 'monster-08': ['快袭', '追击', '机动'], 'monster-11': ['快袭', '突进', '压制'], 'monster-13': ['快袭', '追击', '风行'], 'monster-18': ['重击', '守御', '堡垒'], 'monster-02': ['法修', '符阵', '控场'], 'monster-05': ['控场', '法修', '镇邪'], 'monster-10': ['法力', '回复', '护体'], 'monster-12': ['机动', '远射', '风行'], 'monster-14': ['法力', '符阵', '回复'], 'monster-15': ['回复', '护体', '重甲'], 'monster-16': ['雷法', '机动', '过载'], 'monster-20': ['法力', '回复', '镇邪'], }; function mergeHostileNpcPreset( basePreset: Omit & { combatTags?: string[] }, ): HostileNpcPreset { const override = HOSTILE_NPC_OVERRIDES[basePreset.id]; if (!override) { return hydrateHostileNpcPresetRoleData({ ...basePreset, combatTags: basePreset.combatTags ?? BASE_HOSTILE_NPC_COMBAT_TAGS[basePreset.id] ?? [], lootTable: BASE_HOSTILE_NPC_LOOT_TABLES[basePreset.id] ?? [], }); } return hydrateHostileNpcPresetRoleData({ ...basePreset, ...override, animations: { ...basePreset.animations, ...(override.animations ?? {}), }, baseStats: { ...basePreset.baseStats, ...(override.baseStats ?? {}), }, combatTags: override.combatTags ?? basePreset.combatTags ?? BASE_HOSTILE_NPC_COMBAT_TAGS[basePreset.id] ?? [], habitatTags: override.habitatTags ?? basePreset.habitatTags, lootTable: override.lootTable ?? BASE_HOSTILE_NPC_LOOT_TABLES[basePreset.id] ?? [], }); } function buildHostileNpcBehaviorVectors(preset: { id: string; name: string; combatTags: string[]; baseStats: Pick; }) { const controlBias = preset.combatTags.some(tag => ['控场', '符阵', '法力'].includes(tag)) ? 0.28 : 0.12; const mobilityBias = preset.baseStats.speed >= 7 ? 0.34 : 0.16; const pressureBias = preset.baseStats.attackRange >= 1.5 ? 0.32 : 0.18; const enduranceBias = preset.baseStats.maxHp >= 150 ? 0.3 : 0.16; return [ { id: `${preset.id}:predatory-strike`, name: `${preset.name}的本能压迫`, category: 'combat' as const, baseScore: 0.52, intentVector: buildDefaultAxisVector({ axis_a: enduranceBias, axis_b: mobilityBias, axis_c: controlBias, axis_d: pressureBias, axis_f: 0.2, }), resistVector: buildDefaultAxisVector({ axis_b: 0.24, axis_c: 0.22, axis_f: 0.18, }), }, ]; } function hydrateHostileNpcPresetRoleData( preset: Omit, ): HostileNpcPreset { const schema = getTemplateWorldAttributeSchema( preset.worldType === WorldType.XIANXIA ? WorldType.XIANXIA : WorldType.WUXIA, ); return { ...preset, attributeProfile: buildMonsterAttributeProfile(preset, schema), behaviorVectors: buildHostileNpcBehaviorVectors(preset), }; } const ALL_HOSTILE_NPC_PRESETS = BASE_HOSTILE_NPC_PRESETS.map(basePreset => mergeHostileNpcPreset(basePreset)); export const HOSTILE_NPC_PRESETS_BY_WORLD: Record = { [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], }; 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 = resolveCompatibilityTemplateWorldType(worldType) ?? WorldType.WUXIA; return HOSTILE_NPC_PRESETS_BY_WORLD[resolvedWorldType].find(monster => monster.id === monsterId) ?? null; } export const getMonsterPresetById = getHostileNpcPresetById; export function getHostileNpcPresetsByWorld(worldType: WorldType) { if (worldType === WorldType.CUSTOM) { return HOSTILE_NPC_PRESETS_BY_WORLD[WorldType.CUSTOM]; } const resolvedWorldType = resolveCompatibilityTemplateWorldType(worldType) ?? WorldType.WUXIA; return HOSTILE_NPC_PRESETS_BY_WORLD[resolvedWorldType]; } export const getMonsterPresetsByWorld = getHostileNpcPresetsByWorld; export function getHostileNpcPresetOverrideById(monsterId: string) { return HOSTILE_NPC_OVERRIDES[monsterId] ?? null; } function inferRuntimePlanFromLootItem( item: InventoryItem, context: ReturnType, 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, 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>, ) { if (!state.worldType) return []; if (state.worldType === WorldType.CUSTOM) { return defeatedHostileNpcs.flatMap(monster => buildRuntimeCustomWorldInventoryItems( `monster-loot:${monster.id}:${monster.name}`, { count: 2, categories: ['材料', '消耗品', '稀有品', '专属品'], }, )); } const rewardBatches = await Promise.all(defeatedHostileNpcs.map(async monster => { const preset = getHostileNpcPresetById(state.worldType!, monster.id); const presetLoot = preset ? preset.lootTable .filter(entry => Math.random() <= entry.dropRate) .map(entry => ({ ...entry.item, })) : []; const context = buildRuntimeItemGenerationContext({ state, generationChannel: 'monster_drop', encounter: { id: monster.id, kind: 'npc', npcName: monster.name, npcDescription: `${monster.name}倒下后留下的战利痕迹。`, npcAvatar: '', context: state.currentScenePreset?.name ?? '战场余烬', monsterPresetId: monster.id, }, }); 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 [...decoratedPresetLoot, ...runtimeItems]; })); return rewardBatches.flat(); }