1017 lines
30 KiB
TypeScript
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,
|
|
}));
|
|
});
|
|
}
|