Files
Genarrative/src/data/hostileNpcPresets.ts
高物 c49c64896a
Some checks failed
CI / verify (push) Has been cancelled
初始仓库迁移
2026-04-04 23:57:06 +08:00

1017 lines
30 KiB
TypeScript

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<SceneHostileNpc, 'attackRange' | 'speed' | 'hp' | 'maxHp'>;
combatTags: string[];
habitatTags: string[];
lootTable: MonsterLootEntry[];
attributeProfile: NonNullable<SceneHostileNpc['attributeProfile']>;
behaviorVectors: RoleActionDefinition[];
}
export type HostileNpcPresetOverride = Partial<Omit<HostileNpcPreset, 'animations' | 'baseStats'>> & {
animations?: Partial<HostileNpcPreset['animations']>;
baseStats?: Partial<HostileNpcPreset['baseStats']>;
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<HostileNpcPreset, 'lootTable' | 'combatTags' | 'attributeProfile' | 'behaviorVectors'> & {
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<string, MonsterLootEntry[]> = {
'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<string, HostileNpcPresetOverride>;
const BASE_HOSTILE_NPC_COMBAT_TAGS: Record<string, string[]> = {
'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<HostileNpcPreset, 'combatTags' | 'lootTable' | 'attributeProfile' | 'behaviorVectors'> & { 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<SceneHostileNpc, 'attackRange' | 'speed' | 'maxHp'>;
}) {
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, 'attributeProfile' | 'behaviorVectors'>,
): 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, 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),
};
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<Pick<SceneHostileNpc, 'id' | 'name'>>,
) {
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,
}));
});
}