import { HostileNpcAnimationConfig, HostileNpcSpriteConfig } from '../components/HostileNpcAnimator'; import { GameState, InventoryItem, MonsterLootEntry, RoleActionDefinition, SceneHostileNpc, WorldType } from '../types'; import { buildMonsterAttributeProfile } from './attributeProfileGenerator'; import { buildDefaultAxisVector } from './attributeResolver'; import { buildRuntimeCustomWorldInventoryItems, resolveRuleWorldType } from './customWorldRuntime'; import hostileNpcOverridesJson from './hostileNpcOverrides.json'; import { buildRuntimeItemGenerationContext } from './runtimeItemContext'; import { buildDirectedRuntimeReward } from './runtimeItemDirector'; import { flattenDirectedRuntimeRewardItems } from './runtimeItemNarrative'; import { getPresetWorldAttributeSchema } 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, '鏁h惤鐨勪功椤靛拰鏄撶鐨勭鐗囷紝浠嶆畫鐣欑潃鎶ょ澧ㄦ按鐨勭棔杩广€?', ), ), buildHostileNpcLootEntry( 'monster-02-archive-sigil', 0.24, buildHostileNpcLootItem( 'archive-sigil', '閬楃墿', '妗f鍗拌', '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', '鏉愭枡', '鐭冲3纰庣墖', '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, '浠庢紓娴溂 stalker 韬笂鎵笅鐨勭粏涓濄€?', ), ), 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', '鎶ょ敳', '鐢插3鏉?', '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, '浠嶅湪鏁h惤鏆栫伆鐨勭儳鐒︾繀缈肩鐗囥€?', ), ), 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', 'Consumable', 'Thorn Nectar', 'uncommon', ['healing', 'material'], 1, 'Sticky sap that can be refined into emergency recovery tonic.', ), ), buildHostileNpcLootEntry( 'monster-15-devour-bloom', 0.12, buildHostileNpcLootItem( 'devour-bloom', 'Relic', 'Devour Bloom', 'epic', ['relic'], 1, 'A predatory blossom that stores concentrated life force.', ), ), ], 'monster-16': [ buildHostileNpcLootEntry( 'monster-16-mist-wing', 0.72, buildHostileNpcLootItem( 'mist-wing', 'Material', 'Mist Wing', 'common', ['material'], 2, 'Thin membrane steeped in drifting fog and static charge.', ), ), buildHostileNpcLootEntry( 'monster-16-chase-rune', 0.2, buildHostileNpcLootItem( 'chase-rune', 'Relic', 'Chase Rune', 'rare', ['relic'], 1, 'A pursuit mark that resonates with speed and pressure.', ), ), ], 'monster-18': [ buildHostileNpcLootEntry( 'monster-18-ancient-hide', 0.62, buildHostileNpcLootItem( 'ancient-hide', 'Armor', 'Ancient Hide', 'uncommon', ['armor', 'material'], 1, 'Thick hide layered with old dust, moss, and impact scars.', ), ), buildHostileNpcLootEntry( 'monster-18-ruin-heart', 0.14, buildHostileNpcLootItem( 'ruin-heart', 'Relic', 'Ruin Heart', 'epic', ['relic'], 1, 'A heavy core pulsing with the stubborn will of abandoned ruins.', ), ), ], 'monster-20': [ buildHostileNpcLootEntry( 'monster-20-spring-essence', 0.44, buildHostileNpcLootItem( 'spring-essence', 'Consumable', 'Spring Essence', 'uncommon', ['mana'], 1, 'A cool droplet that restores focus and spiritual rhythm.', ), ), buildHostileNpcLootEntry( 'monster-20-tide-mother-core', 0.11, buildHostileNpcLootItem( 'tide-mother-core', 'Relic', 'Tide Mother Core', 'epic', ['relic', 'mana'], 1, 'A refined water-heart formed only in the deepest luminous springs.', ), ), ], }; 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 = getPresetWorldAttributeSchema( 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.filter(monster => monster.worldType === WorldType.WUXIA), }; export const MONSTER_PRESETS_BY_WORLD = HOSTILE_NPC_PRESETS_BY_WORLD; export function getHostileNpcPresetById(worldType: WorldType, monsterId: string) { const resolvedWorldType = resolveRuleWorldType(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) { const resolvedWorldType = resolveRuleWorldType(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; } export 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: ['鏉愭枡', '娑堣€楀搧', '绋€鏈夊搧', '涓撳睘鐗?'], }, )); } return defeatedHostileNpcs.flatMap(monster => { const context = buildRuntimeItemGenerationContext({ state, generationChannel: 'monster_drop', encounter: { id: monster.id, kind: 'npc', npcName: monster.name, npcDescription: `${monster.name}倒下后留下的战利痕迹。`, npcAvatar: '', context: state.currentScenePreset?.name ?? '战场余烬', hostileNpcPresetId: 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 runtimeItems = flattenDirectedRuntimeRewardItems(directedReward); if (runtimeItems.length > 0) { return runtimeItems; } const preset = getHostileNpcPresetById(state.worldType!, monster.id); if (!preset) return []; return preset.lootTable .filter(entry => Math.random() <= entry.dropRate) .map(entry => ({ ...entry.item, })); }); }