From 93961c10bb766750378285b98af02841f6462421 Mon Sep 17 00:00:00 2001 From: Evilom <33251918+Evilom@users.noreply.github.com> Date: Tue, 12 May 2026 00:11:56 +0800 Subject: [PATCH] chore:ui --- CLAUDE.md | 32 +- src/data/DataRegistry.ts | 111 ++--- src/data/crafting/smithing.json | 291 +++++++------ src/data/enemies/enemies.json | 161 +++++++ src/data/items/armor.json | 120 ++++++ src/data/items/weapons.json | 600 +++++++++++++++++++++++++-- src/data/quests/quests.json | 93 +++++ src/data/spells/spells.json | 52 +++ src/data/world/zones.json | 4 +- src/mods/ModE2E.test.ts | 377 +++++++++++++++++ src/scenes/GameScene.ts | 88 +++- src/systems/AISystem.ts | 167 +++++++- src/systems/CombatSystem.ts | 70 +++- src/systems/CorpseSystem.ts | 45 +- src/systems/GroundItemSystem.ts | 40 +- src/systems/LegendarySystem.ts | 4 +- src/systems/LevelingSystem.ts | 3 +- src/systems/MagicSystem.ts | 38 +- src/systems/RegenSystem.ts | 3 +- src/ui/UIManager.ts | 176 ++------ src/ui/components/CompassHUD.ts | 182 ++++++++ src/ui/components/CraftingUI.ts | 89 +++- src/ui/components/MagicUI.ts | 281 +++++++++++++ src/ui/components/QuestJournalUI.ts | 216 ++++++++++ src/ui/components/RadialMenuUI.ts | 204 +++++++++ src/ui/components/SkillTreeUI.ts | 8 + src/ui/components/StatusEffectHUD.ts | 70 ++++ src/ui/components/TimeDisplayHUD.ts | 59 +++ src/ui/components/WorldMapUI.ts | 4 + 29 files changed, 3134 insertions(+), 454 deletions(-) create mode 100644 src/mods/ModE2E.test.ts create mode 100644 src/ui/components/CompassHUD.ts create mode 100644 src/ui/components/MagicUI.ts create mode 100644 src/ui/components/QuestJournalUI.ts create mode 100644 src/ui/components/RadialMenuUI.ts create mode 100644 src/ui/components/StatusEffectHUD.ts create mode 100644 src/ui/components/TimeDisplayHUD.ts diff --git a/CLAUDE.md b/CLAUDE.md index 88e1715..fa89ca9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,8 +47,25 @@ src/ ## Data Architecture (全部可 Mod 覆盖) 所有游戏数据通过 `DataRegistry` 统一加载和管理。Mod 可覆盖任何数据类型: -- **DataRegistry**: 中央数据注册表,加载 JSON 文件,监听 `mod:dataResolved` 事件 -- **数据类型**: items, weapons, armor, enemies, quests, races, skills, perks, standingStones, spells, recipes +- **DataRegistry**: 中央数据注册表,加载 24 个 JSON 文件,监听 `mod:dataResolved` 事件 +- **19 个数据域**: items, armor, enemies, races, skills, perkTrees, spells, shouts, standingStones, quests, recipes, enchantments, soulGems, smithing, cooking, dialogue, transforms, vampireStages, gameConfig +- **JSON 数据文件** (24 个): + - `src/data/items/` — items.json, weapons.json, armor.json, enchantments.json, soul-gems.json + - `src/data/spells/` — spells.json, shouts.json + - `src/data/enemies/` — enemies.json + - `src/data/quests/` — quests.json + - `src/data/races/` — races.json + - `src/data/skills/` — skills.json, perks.json, werewolf-perks.json, vampire-perks.json + - `src/data/world/` — standing-stones.json + - `src/data/alchemy/` — ingredients.json, potions.json + - `src/data/crafting/` — smithing.json, cooking.json + - `src/data/dialogue/` — trees.json + - `src/data/transforms.json`, `vampire-stages.json`, `game-config.json` +- **系统数据驱动化**: 11 个游戏系统全部从 DataRegistry 读取数据,不再硬编码: + - MagicSystem, CombatSystem, LevelingSystem, RegenSystem + - EnchantingSystem, SmithingSystem, AlchemySystem, CookingSystem + - TransformationSystem, VampireSystem, DialogueSystem +- **循环依赖解决**: CombatSystem/MagicSystem/QuestSystem/DialogueSystem 通过 `globalThis` 注册,ScriptContext lazy 访问 - **地图数据**: 由 MapManager 从 `zones.json` 加载,支持 Mod 覆盖 - **UI 组件**: CharacterCreationUI/SkillTreeUI 从 DataRegistry 读取数据,不再直接导入 JSON @@ -211,11 +228,14 @@ src/ - **高级系统**: 狼人变形 + 吸血鬼感染 + 传奇技能 ## Mod 生态 -- Mod 格式: JSON 数据文件 + manifest.json 清单 -- 加载方式: 深度合并,后加载覆盖先加载 -- 事件钩子: GameEvents.on('event:name', callback) +- Mod 格式: `ModPackage` (manifest + data + scripts),通过 `modLoader.loadMod()` 加载 +- 加载方式: 深度合并,按 priority 排序后加载,后加载覆盖先加载 +- 依赖检查: `manifest.dependencies` 声明前置依赖,未满足则拒绝加载 +- 冲突检测: 同域同 ID 多个 Mod 声明冲突,最后一个赢 +- 事件钩子: `mod:loaded`, `mod:dataResolved` 事件 - Mod 管理器: 启用/禁用/排序/冲突检测 -- **全部数据可覆盖**: 所有游戏数据 (物品/武器/护甲/敌人/任务/种族/技能/天赋/站立之石/法术/配方/地图) 均通过 DataRegistry 或 MapManager 加载,Mod 可覆盖任何数据 +- **19 个数据域全部可覆盖**: items, armor, enemies, races, skills, perkTrees, spells, shouts, standingStones, quests, recipes, enchantments, soulGems, smithing, cooking, dialogue, transforms, vampireStages, gameConfig +- **E2E 测试验证**: `src/mods/ModE2E.test.ts` — 10 个测试覆盖数据覆盖、新数据添加、优先级排序、依赖检查、系统热重载 ## Mod 脚本系统 Mod 可为游戏实体附加自定义脚本逻辑,与原版上古卷轴的脚本系统一致。 diff --git a/src/data/DataRegistry.ts b/src/data/DataRegistry.ts index 22792c9..3cbc4ed 100644 --- a/src/data/DataRegistry.ts +++ b/src/data/DataRegistry.ts @@ -53,12 +53,25 @@ export interface EnemyLootTable { items?: { id: string; chance: number; quantity?: number }[]; } +export interface EnemyAbility { + id: string; + spellId?: string; + type: 'spell' | 'attack' | 'buff' | 'passive'; + cooldown: number; + condition?: 'hp_below_50' | 'hp_below_25' | 'on_spawn' | 'on_hit' | 'always'; + damage?: number; + effect?: string; + effectDuration?: number; + effectMagnitude?: number; +} + export interface EnemyData { id: string; name: string; level: number; health: number; stamina: number; + magicka?: number; damage: number; armor: number; detectionRange: number; @@ -67,6 +80,8 @@ export interface EnemyData { size: number; color: number; loot?: EnemyLootTable; + abilities?: EnemyAbility[]; + aiBehavior?: 'melee' | 'ranged' | 'caster' | 'mixed'; } export interface SpellData { @@ -441,7 +456,7 @@ export class DataRegistry { this.loadItemRecord(asRecord(data.items), 'item'); this.loadArmorRecord(asRecord(data.armor)); this.loadEnemyRecord(asRecord(data.enemies)); - this.loadSpellRecord(asRecord(data.spells)); + this.loadFullSpellRecord(asRecord(data.spells)); this.loadRecipeRecord(asRecord(data.recipes)); this.loadQuestRecord(asRecord(data.quests)); this.loadRaceRecord(asArray(data.races)); @@ -481,16 +496,6 @@ export class DataRegistry { } } - private loadSpellRecord(record: UnknownRecord): void { - for (const [id, value] of Object.entries(record)) { - if (!isRecord(value)) continue; - const spell = normalizeSpell(id, value); - if (spell) { - this.spells.set(spell.id, spell); - } - } - } - private loadRecipeRecord(record: UnknownRecord): void { for (const [id, value] of Object.entries(record)) { if (!isRecord(value)) continue; @@ -688,25 +693,31 @@ export class DataRegistry { private loadGameConfig(record: UnknownRecord): void { if (isRecord(record.combat) && isRecord(record.leveling) && isRecord(record.regen)) { + const combat = record.combat; + const ar = isRecord(combat.attackRanges) ? combat.attackRanges : {}; + const sc = isRecord(combat.staminaCosts) ? combat.staminaCosts : {}; + const wdt = isRecord(combat.weaponDamageThresholds) ? combat.weaponDamageThresholds : {}; + const sia = isRecord(combat.skillImprovementAmounts) ? combat.skillImprovementAmounts : {}; + const dv = Array.isArray(combat.damageVariance) ? combat.damageVariance : [0.9, 1.1]; this.gameConfig = { combat: { - baseDamage: readNumber(record.combat, 'baseDamage', 10), - skillBonus: readNumber(record.combat, 'skillBonus', 0.5), - powerAttackMultiplier: readNumber(record.combat, 'powerAttackMultiplier', 1.5), - critBaseChance: readNumber(record.combat, 'critBaseChance', 0.10), - critPerSneakSkill: readNumber(record.combat, 'critPerSneakSkill', 0.005), - armorDivisor: readNumber(record.combat, 'armorDivisor', 100), - damageVariance: [0.9, 1.1], - blockDamageMultiplier: readNumber(record.combat, 'blockDamageMultiplier', 0.2), - blockBaseChance: readNumber(record.combat, 'blockBaseChance', 0.3), - blockSkillBonus: readNumber(record.combat, 'blockSkillBonus', 0.5), - blockStaminaThreshold: readNumber(record.combat, 'blockStaminaThreshold', 10), - blockStaminaPenalty: readNumber(record.combat, 'blockStaminaPenalty', -0.2), - blockStaminaCost: readNumber(record.combat, 'blockStaminaCost', 10), - attackRanges: { melee: 60, unarmed: 45 }, - staminaCosts: { powerAttack: 25, normal: 5 }, - weaponDamageThresholds: { twoHanded: 6, powerAttack: 15 }, - skillImprovementAmounts: { attack: 0.5, armor: 0.3 }, + baseDamage: readNumber(combat, 'baseDamage', 10), + skillBonus: readNumber(combat, 'skillBonus', 0.5), + powerAttackMultiplier: readNumber(combat, 'powerAttackMultiplier', 1.5), + critBaseChance: readNumber(combat, 'critBaseChance', 0.10), + critPerSneakSkill: readNumber(combat, 'critPerSneakSkill', 0.005), + armorDivisor: readNumber(combat, 'armorDivisor', 100), + damageVariance: [Number(dv[0]) || 0.9, Number(dv[1]) || 1.1], + blockDamageMultiplier: readNumber(combat, 'blockDamageMultiplier', 0.2), + blockBaseChance: readNumber(combat, 'blockBaseChance', 0.3), + blockSkillBonus: readNumber(combat, 'blockSkillBonus', 0.5), + blockStaminaThreshold: readNumber(combat, 'blockStaminaThreshold', 10), + blockStaminaPenalty: readNumber(combat, 'blockStaminaPenalty', -0.2), + blockStaminaCost: readNumber(combat, 'blockStaminaCost', 10), + attackRanges: { melee: readNumber(ar, 'melee', 60), unarmed: readNumber(ar, 'unarmed', 45) }, + staminaCosts: { powerAttack: readNumber(sc, 'powerAttack', 25), normal: readNumber(sc, 'normal', 5) }, + weaponDamageThresholds: { twoHanded: readNumber(wdt, 'twoHanded', 6), powerAttack: readNumber(wdt, 'powerAttack', 15) }, + skillImprovementAmounts: { attack: readNumber(sia, 'attack', 0.5), armor: readNumber(sia, 'armor', 0.3) }, }, leveling: { defaultSkillLevel: readNumber(record.leveling, 'defaultSkillLevel', 15), @@ -750,15 +761,15 @@ export class DataRegistry { return Array.from(this.enemies.values()); } - getSpell(id: string): SpellData | undefined { - return this.spells.get(id); + getSpell(id: string): FullSpellData | undefined { + return this.fullSpells.get(id); } - getAllSpells(): SpellData[] { - return Array.from(this.spells.values()); + getAllSpells(): FullSpellData[] { + return Array.from(this.fullSpells.values()); } - getSpellsBySchool(school: string): SpellData[] { + getSpellsBySchool(school: string): FullSpellData[] { return this.getAllSpells().filter((spell) => spell.school === school); } @@ -1001,12 +1012,28 @@ function normalizeItem(id: string, value: unknown, source: 'item' | 'weapon'): I function normalizeEnemy(id: string, value: unknown): EnemyData | null { if (!isRecord(value)) return null; + const magicka = readOptionalNumber(value, 'magicka'); + const abilities = Array.isArray(value.abilities) + ? value.abilities.filter(isRecord).map((a) => ({ + id: readString(a, 'id', ''), + spellId: readOptionalString(a, 'spellId'), + type: readString(a, 'type', 'attack') as EnemyAbility['type'], + cooldown: readNumber(a, 'cooldown', 3000), + condition: readOptionalString(a, 'condition') as EnemyAbility['condition'], + damage: readOptionalNumber(a, 'damage'), + effect: readOptionalString(a, 'effect'), + effectDuration: readOptionalNumber(a, 'effectDuration'), + effectMagnitude: readOptionalNumber(a, 'effectMagnitude'), + })) + : undefined; + return { id: readString(value, 'id', id), name: readString(value, 'name', id), level: readNumber(value, 'level', 1), health: readNumber(value, 'health', 50), stamina: readNumber(value, 'stamina', 50), + ...(magicka !== undefined ? { magicka } : {}), damage: readNumber(value, 'damage', 8), armor: readNumber(value, 'armor', 0), detectionRange: readNumber(value, 'detectionRange', 150), @@ -1015,22 +1042,8 @@ function normalizeEnemy(id: string, value: unknown): EnemyData | null { size: readNumber(value, 'size', 24), color: parseColor(value.color, 0xff0000), loot: normalizeLoot(value.loot), - }; -} - -function normalizeSpell(id: string, value: UnknownRecord): SpellData | null { - const school = readOptionalString(value, 'school'); - if (!school) return null; - - return { - id: readString(value, 'id', id), - name: readString(value, 'name', id), - school, - cost: readNumber(value, 'cost', readNumber(value, 'magickaCost', 0)), - magnitude: readNumber(value, 'magnitude', 0), - duration: readOptionalNumber(value, 'duration'), - range: readOptionalNumber(value, 'range'), - description: readOptionalString(value, 'description'), + ...(abilities ? { abilities } : {}), + aiBehavior: readOptionalString(value, 'aiBehavior') as EnemyData['aiBehavior'], }; } diff --git a/src/data/crafting/smithing.json b/src/data/crafting/smithing.json index ef3f1d8..5c52b97 100644 --- a/src/data/crafting/smithing.json +++ b/src/data/crafting/smithing.json @@ -14,164 +14,161 @@ "forge": { "type": "forge", "name": "锻造炉", - "availableRecipes": ["iron_sword", "iron_waraxe", "iron_mace", "iron_dagger", "iron_greatsword", "iron_warhammer", "iron_shield", "iron_helmet", "iron_chestplate", "iron_gauntlets", "iron_boots", "steel_sword", "steel_greatsword", "steel_shield", "hunting_bow", "iron_arrow"] + "availableRecipes": [ + "iron_sword", "iron_waraxe", "iron_mace", "iron_dagger", "iron_greatsword", "iron_warhammer", + "iron_helmet", "iron_chestplate", "iron_gauntlets", "iron_boots", "iron_shield", + "hunting_bow", "iron_arrow", + "steel_sword", "steel_waraxe", "steel_mace", "steel_dagger", "steel_greatsword", "steel_warhammer", + "steel_helmet", "steel_chestplate", "steel_gauntlets", "steel_boots", "steel_shield", + "long_bow", + "corundum_sword", "corundum_waraxe", "corundum_mace", "corundum_dagger", "corundum_greatsword", "corundum_warhammer", + "corundum_helmet", "corundum_chestplate", "corundum_gauntlets", "corundum_boots", "corundum_shield", "corundum_bow", + "orichalcum_sword", "orichalcum_waraxe", "orichalcum_mace", "orichalcum_dagger", "orichalcum_greatsword", "orichalcum_warhammer", + "orichalcum_helmet", "orichalcum_chestplate", "orichalcum_gauntlets", "orichalcum_boots", "orichalcum_shield", "orichalcum_bow", + "moonstone_sword", "moonstone_waraxe", "moonstone_mace", "moonstone_dagger", "moonstone_greatsword", "moonstone_warhammer", + "moonstone_helmet", "moonstone_chestplate", "moonstone_gauntlets", "moonstone_boots", "moonstone_shield", "moonstone_bow", + "ebony_sword", "ebony_waraxe", "ebony_mace", "ebony_dagger", "ebony_greatsword", "ebony_warhammer", + "ebony_helmet", "ebony_chestplate", "ebony_gauntlets", "ebony_boots", "ebony_shield", "ebony_bow", + "daedric_sword", "daedric_waraxe", "daedric_mace", "daedric_dagger", "daedric_greatsword", "daedric_warhammer", + "daedric_helmet", "daedric_chestplate", "daedric_gauntlets", "daedric_boots", "daedric_shield", "daedric_bow", + "dragon_sword", "dragon_waraxe", "dragon_mace", "dragon_dagger", "dragon_greatsword", "dragon_warhammer", + "dragon_helmet", "dragon_chestplate", "dragon_gauntlets", "dragon_boots", "dragon_shield", "dragon_bow" + ] }, "workbench": { "type": "workbench", "name": "工作台", - "availableRecipes": ["iron_helmet", "iron_chestplate", "iron_gauntlets", "iron_boots", "iron_shield", "steel_shield"] + "availableRecipes": [ + "iron_helmet", "iron_chestplate", "iron_gauntlets", "iron_boots", "iron_shield", + "steel_helmet", "steel_chestplate", "steel_gauntlets", "steel_boots", "steel_shield", + "corundum_helmet", "corundum_chestplate", "corundum_gauntlets", "corundum_boots", "corundum_shield", + "orichalcum_helmet", "orichalcum_chestplate", "orichalcum_gauntlets", "orichalcum_boots", "orichalcum_shield", + "moonstone_helmet", "moonstone_chestplate", "moonstone_gauntlets", "moonstone_boots", "moonstone_shield", + "ebony_helmet", "ebony_chestplate", "ebony_gauntlets", "ebony_boots", "ebony_shield", + "daedric_helmet", "daedric_chestplate", "daedric_gauntlets", "daedric_boots", "daedric_shield", + "dragon_helmet", "dragon_chestplate", "dragon_gauntlets", "dragon_boots", "dragon_shield" + ] }, "grindstone": { "type": "grindstone", "name": "砂轮", - "availableRecipes": ["iron_sword", "iron_waraxe", "iron_mace", "iron_dagger", "iron_greatsword", "iron_warhammer", "steel_sword", "steel_greatsword"] + "availableRecipes": [ + "iron_sword", "iron_waraxe", "iron_mace", "iron_dagger", "iron_greatsword", "iron_warhammer", + "steel_sword", "steel_waraxe", "steel_mace", "steel_dagger", "steel_greatsword", "steel_warhammer", + "corundum_sword", "corundum_waraxe", "corundum_mace", "corundum_dagger", "corundum_greatsword", "corundum_warhammer", + "orichalcum_sword", "orichalcum_waraxe", "orichalcum_mace", "orichalcum_dagger", "orichalcum_greatsword", "orichalcum_warhammer", + "moonstone_sword", "moonstone_waraxe", "moonstone_mace", "moonstone_dagger", "moonstone_greatsword", "moonstone_warhammer", + "ebony_sword", "ebony_waraxe", "ebony_mace", "ebony_dagger", "ebony_greatsword", "ebony_warhammer", + "daedric_sword", "daedric_waraxe", "daedric_mace", "daedric_dagger", "daedric_greatsword", "daedric_warhammer", + "dragon_sword", "dragon_waraxe", "dragon_mace", "dragon_dagger", "dragon_greatsword", "dragon_warhammer" + ] } }, "recipes": { - "iron_sword": { - "id": "iron_sword", - "name": "铁剑", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], - "result": { "id": "iron_sword", "name": "铁剑", "type": "one_handed_sword", "damage": 10, "weight": 10, "value": 50 }, - "skillRequired": 0 - }, - "iron_waraxe": { - "id": "iron_waraxe", - "name": "铁战斧", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], - "result": { "id": "iron_waraxe", "name": "铁战斧", "type": "one_handed_axe", "damage": 9, "weight": 12, "value": 45 }, - "skillRequired": 0 - }, - "iron_mace": { - "id": "iron_mace", - "name": "铁钉锤", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 3 }], - "result": { "id": "iron_mace", "name": "铁钉锤", "type": "one_handed_mace", "damage": 11, "weight": 14, "value": 55 }, - "skillRequired": 0 - }, - "iron_dagger": { - "id": "iron_dagger", - "name": "铁匕首", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], - "result": { "id": "iron_dagger", "name": "铁匕首", "type": "dagger", "damage": 6, "weight": 3, "value": 25 }, - "skillRequired": 0 - }, - "iron_greatsword": { - "id": "iron_greatsword", - "name": "铁制大剑", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], - "result": { "id": "iron_greatsword", "name": "铁制大剑", "type": "two_handed_sword", "damage": 18, "weight": 20, "value": 100 }, - "skillRequired": 20 - }, - "iron_warhammer": { - "id": "iron_warhammer", - "name": "铁战锤", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 5 }], - "result": { "id": "iron_warhammer", "name": "铁战锤", "type": "two_handed_mace", "damage": 20, "weight": 25, "value": 120 }, - "skillRequired": 20 - }, - "iron_shield": { - "id": "iron_shield", - "name": "铁盾", - "type": "shield", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], - "result": { "id": "iron_shield", "name": "铁盾", "type": "shield", "armor": 20, "weight": 12, "value": 60 }, - "skillRequired": 0 - }, - "iron_helmet": { - "id": "iron_helmet", - "name": "铁头盔", - "type": "armor", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 2 }], - "result": { "id": "iron_helmet", "name": "铁头盔", "type": "head", "armor": 15, "weight": 5, "value": 40 }, - "skillRequired": 0 - }, - "iron_chestplate": { - "id": "iron_chestplate", - "name": "铁胸甲", - "type": "armor", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 4 }], - "result": { "id": "iron_chestplate", "name": "铁胸甲", "type": "chest", "armor": 25, "weight": 15, "value": 80 }, - "skillRequired": 0 - }, - "iron_gauntlets": { - "id": "iron_gauntlets", - "name": "铁护手", - "type": "armor", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 2 }], - "result": { "id": "iron_gauntlets", "name": "铁护手", "type": "hands", "armor": 10, "weight": 4, "value": 30 }, - "skillRequired": 0 - }, - "iron_boots": { - "id": "iron_boots", - "name": "铁靴", - "type": "armor", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 2 }], - "result": { "id": "iron_boots", "name": "铁靴", "type": "feet", "armor": 10, "weight": 4, "value": 30 }, - "skillRequired": 0 - }, - "steel_sword": { - "id": "steel_sword", - "name": "钢剑", - "type": "weapon", - "tier": "steel", - "materials": [{ "id": "steel_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], - "result": { "id": "steel_sword", "name": "钢剑", "type": "one_handed_sword", "damage": 14, "weight": 11, "value": 120 }, - "skillRequired": 20 - }, - "steel_greatsword": { - "id": "steel_greatsword", - "name": "钢制大剑", - "type": "weapon", - "tier": "steel", - "materials": [{ "id": "steel_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], - "result": { "id": "steel_greatsword", "name": "钢制大剑", "type": "two_handed_sword", "damage": 24, "weight": 22, "value": 250 }, - "skillRequired": 30 - }, - "steel_shield": { - "id": "steel_shield", - "name": "钢盾", - "type": "shield", - "tier": "steel", - "materials": [{ "id": "steel_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], - "result": { "id": "steel_shield", "name": "钢盾", "type": "shield", "armor": 30, "weight": 13, "value": 150 }, - "skillRequired": 20 - }, - "hunting_bow": { - "id": "hunting_bow", - "name": "猎弓", - "type": "weapon", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 1 }, { "id": "leather", "quantity": 2 }], - "result": { "id": "hunting_bow", "name": "猎弓", "type": "bow", "damage": 8, "weight": 6, "value": 50 }, - "skillRequired": 10 - }, - "iron_arrow": { - "id": "iron_arrow", - "name": "铁箭", - "type": "material", - "tier": "iron", - "materials": [{ "id": "iron_ingot", "quantity": 1 }], - "result": { "id": "iron_arrow", "name": "铁箭", "type": "ammo", "damage": 8, "weight": 0.1, "value": 5 }, - "skillRequired": 0 - } + "iron_sword": { "id": "iron_sword", "name": "铁剑", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "iron_sword", "name": "铁剑", "type": "one_handed_sword", "damage": 10, "weight": 10, "value": 50 }, "skillRequired": 0 }, + "iron_waraxe": { "id": "iron_waraxe", "name": "铁战斧", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "iron_waraxe", "name": "铁战斧", "type": "one_handed_axe", "damage": 9, "weight": 12, "value": 45 }, "skillRequired": 0 }, + "iron_mace": { "id": "iron_mace", "name": "铁钉锤", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 3 }], "result": { "id": "iron_mace", "name": "铁钉锤", "type": "one_handed_mace", "damage": 11, "weight": 14, "value": 55 }, "skillRequired": 0 }, + "iron_dagger": { "id": "iron_dagger", "name": "铁匕首", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "iron_dagger", "name": "铁匕首", "type": "dagger", "damage": 6, "weight": 3, "value": 25 }, "skillRequired": 0 }, + "iron_greatsword": { "id": "iron_greatsword", "name": "铁制大剑", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "iron_greatsword", "name": "铁制大剑", "type": "two_handed_sword", "damage": 18, "weight": 20, "value": 100 }, "skillRequired": 20 }, + "iron_warhammer": { "id": "iron_warhammer", "name": "铁战锤", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 5 }], "result": { "id": "iron_warhammer", "name": "铁战锤", "type": "two_handed_mace", "damage": 20, "weight": 25, "value": 120 }, "skillRequired": 20 }, + "iron_shield": { "id": "iron_shield", "name": "铁盾", "type": "shield", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "iron_shield", "name": "铁盾", "type": "shield", "armor": 20, "weight": 12, "value": 60 }, "skillRequired": 0 }, + "iron_helmet": { "id": "iron_helmet", "name": "铁头盔", "type": "armor", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 2 }], "result": { "id": "iron_helmet", "name": "铁头盔", "type": "head", "armor": 15, "weight": 5, "value": 40 }, "skillRequired": 0 }, + "iron_chestplate": { "id": "iron_chestplate", "name": "铁胸甲", "type": "armor", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 4 }], "result": { "id": "iron_chestplate", "name": "铁胸甲", "type": "chest", "armor": 25, "weight": 15, "value": 80 }, "skillRequired": 0 }, + "iron_gauntlets": { "id": "iron_gauntlets", "name": "铁护手", "type": "armor", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 2 }], "result": { "id": "iron_gauntlets", "name": "铁护手", "type": "hands", "armor": 10, "weight": 4, "value": 30 }, "skillRequired": 0 }, + "iron_boots": { "id": "iron_boots", "name": "铁靴", "type": "armor", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 2 }], "result": { "id": "iron_boots", "name": "铁靴", "type": "feet", "armor": 10, "weight": 4, "value": 30 }, "skillRequired": 0 }, + "hunting_bow": { "id": "hunting_bow", "name": "猎弓", "type": "weapon", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 1 }, { "id": "leather", "quantity": 2 }], "result": { "id": "hunting_bow", "name": "猎弓", "type": "bow", "damage": 8, "weight": 6, "value": 50 }, "skillRequired": 10 }, + "iron_arrow": { "id": "iron_arrow", "name": "铁箭", "type": "material", "tier": "iron", "materials": [{ "id": "iron_ingot", "quantity": 1 }], "result": { "id": "iron_arrow", "name": "铁箭", "type": "ammo", "damage": 8, "weight": 0.1, "value": 5 }, "skillRequired": 0 }, + + "steel_sword": { "id": "steel_sword", "name": "钢剑", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "steel_sword", "name": "钢剑", "type": "one_handed_sword", "damage": 14, "weight": 11, "value": 120 }, "skillRequired": 20 }, + "steel_waraxe": { "id": "steel_waraxe", "name": "钢战斧", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "steel_waraxe", "name": "钢战斧", "type": "one_handed_axe", "damage": 13, "weight": 13, "value": 110 }, "skillRequired": 20 }, + "steel_mace": { "id": "steel_mace", "name": "钢钉锤", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 3 }], "result": { "id": "steel_mace", "name": "钢钉锤", "type": "one_handed_mace", "damage": 15, "weight": 15, "value": 130 }, "skillRequired": 20 }, + "steel_dagger": { "id": "steel_dagger", "name": "钢匕首", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "steel_dagger", "name": "钢匕首", "type": "dagger", "damage": 8, "weight": 3, "value": 60 }, "skillRequired": 20 }, + "steel_greatsword": { "id": "steel_greatsword", "name": "钢制大剑", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "steel_greatsword", "name": "钢制大剑", "type": "two_handed_sword", "damage": 24, "weight": 22, "value": 250 }, "skillRequired": 30 }, + "steel_warhammer": { "id": "steel_warhammer", "name": "钢战锤", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 5 }], "result": { "id": "steel_warhammer", "name": "钢战锤", "type": "two_handed_mace", "damage": 26, "weight": 27, "value": 270 }, "skillRequired": 30 }, + "steel_shield": { "id": "steel_shield", "name": "钢盾", "type": "shield", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "steel_shield", "name": "钢盾", "type": "shield", "armor": 30, "weight": 13, "value": 150 }, "skillRequired": 20 }, + "steel_helmet": { "id": "steel_helmet", "name": "钢头盔", "type": "armor", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 2 }], "result": { "id": "steel_helmet", "name": "钢头盔", "type": "head", "armor": 20, "weight": 5, "value": 100 }, "skillRequired": 20 }, + "steel_chestplate": { "id": "steel_chestplate", "name": "钢胸甲", "type": "armor", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 4 }], "result": { "id": "steel_chestplate", "name": "钢胸甲", "type": "chest", "armor": 35, "weight": 18, "value": 200 }, "skillRequired": 20 }, + "steel_gauntlets": { "id": "steel_gauntlets", "name": "钢护手", "type": "armor", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 2 }], "result": { "id": "steel_gauntlets", "name": "钢护手", "type": "hands", "armor": 15, "weight": 4, "value": 80 }, "skillRequired": 20 }, + "steel_boots": { "id": "steel_boots", "name": "钢靴", "type": "armor", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 2 }], "result": { "id": "steel_boots", "name": "钢靴", "type": "feet", "armor": 15, "weight": 7, "value": 100 }, "skillRequired": 20 }, + "long_bow": { "id": "long_bow", "name": "长弓", "type": "weapon", "tier": "steel", "materials": [{ "id": "steel_ingot", "quantity": 1 }, { "id": "leather", "quantity": 3 }], "result": { "id": "long_bow", "name": "长弓", "type": "bow", "damage": 12, "weight": 8, "value": 120 }, "skillRequired": 20 }, + + "corundum_sword": { "id": "corundum_sword", "name": "硬化铁剑", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "corundum_sword", "name": "硬化铁剑", "type": "one_handed_sword", "damage": 18, "weight": 12, "value": 250 }, "skillRequired": 30 }, + "corundum_waraxe": { "id": "corundum_waraxe", "name": "硬化铁战斧", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "corundum_waraxe", "name": "硬化铁战斧", "type": "one_handed_axe", "damage": 17, "weight": 14, "value": 230 }, "skillRequired": 30 }, + "corundum_mace": { "id": "corundum_mace", "name": "硬化铁钉锤", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 3 }], "result": { "id": "corundum_mace", "name": "硬化铁钉锤", "type": "one_handed_mace", "damage": 19, "weight": 16, "value": 270 }, "skillRequired": 30 }, + "corundum_dagger": { "id": "corundum_dagger", "name": "硬化铁匕首", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "corundum_dagger", "name": "硬化铁匕首", "type": "dagger", "damage": 10, "weight": 4, "value": 130 }, "skillRequired": 30 }, + "corundum_greatsword": { "id": "corundum_greatsword", "name": "硬化铁大剑", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "corundum_greatsword", "name": "硬化铁大剑", "type": "two_handed_sword", "damage": 30, "weight": 24, "value": 500 }, "skillRequired": 40 }, + "corundum_warhammer": { "id": "corundum_warhammer", "name": "硬化铁战锤", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 5 }], "result": { "id": "corundum_warhammer", "name": "硬化铁战锤", "type": "two_handed_mace", "damage": 34, "weight": 30, "value": 550 }, "skillRequired": 40 }, + "corundum_bow": { "id": "corundum_bow", "name": "硬化铁弓", "type": "weapon", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 2 }, { "id": "leather", "quantity": 3 }], "result": { "id": "corundum_bow", "name": "硬化铁弓", "type": "bow", "damage": 16, "weight": 10, "value": 250 }, "skillRequired": 30 }, + "corundum_helmet": { "id": "corundum_helmet", "name": "硬化铁头盔", "type": "armor", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 2 }], "result": { "id": "corundum_helmet", "name": "硬化铁头盔", "type": "head", "armor": 24, "weight": 6, "value": 200 }, "skillRequired": 30 }, + "corundum_chestplate": { "id": "corundum_chestplate", "name": "硬化铁胸甲", "type": "armor", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 4 }], "result": { "id": "corundum_chestplate", "name": "硬化铁胸甲", "type": "chest", "armor": 40, "weight": 20, "value": 400 }, "skillRequired": 30 }, + "corundum_gauntlets": { "id": "corundum_gauntlets", "name": "硬化铁护手", "type": "armor", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 2 }], "result": { "id": "corundum_gauntlets", "name": "硬化铁护手", "type": "hands", "armor": 18, "weight": 5, "value": 180 }, "skillRequired": 30 }, + "corundum_boots": { "id": "corundum_boots", "name": "硬化铁靴", "type": "armor", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 2 }], "result": { "id": "corundum_boots", "name": "硬化铁靴", "type": "feet", "armor": 18, "weight": 7, "value": 180 }, "skillRequired": 30 }, + "corundum_shield": { "id": "corundum_shield", "name": "硬化铁盾", "type": "shield", "tier": "corundum", "materials": [{ "id": "corundum_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "corundum_shield", "name": "硬化铁盾", "type": "shield", "armor": 35, "weight": 14, "value": 300 }, "skillRequired": 30 }, + + "orichalcum_sword": { "id": "orichalcum_sword", "name": "精金剑", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "orichalcum_sword", "name": "精金剑", "type": "one_handed_sword", "damage": 22, "weight": 11, "value": 400 }, "skillRequired": 40 }, + "orichalcum_waraxe": { "id": "orichalcum_waraxe", "name": "精金战斧", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "orichalcum_waraxe", "name": "精金战斧", "type": "one_handed_axe", "damage": 21, "weight": 13, "value": 380 }, "skillRequired": 40 }, + "orichalcum_mace": { "id": "orichalcum_mace", "name": "精金钉锤", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 3 }], "result": { "id": "orichalcum_mace", "name": "精金钉锤", "type": "one_handed_mace", "damage": 23, "weight": 15, "value": 420 }, "skillRequired": 40 }, + "orichalcum_dagger": { "id": "orichalcum_dagger", "name": "精金匕首", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "orichalcum_dagger", "name": "精金匕首", "type": "dagger", "damage": 13, "weight": 3, "value": 200 }, "skillRequired": 40 }, + "orichalcum_greatsword": { "id": "orichalcum_greatsword", "name": "精金大剑", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "orichalcum_greatsword", "name": "精金大剑", "type": "two_handed_sword", "damage": 36, "weight": 22, "value": 800 }, "skillRequired": 50 }, + "orichalcum_warhammer": { "id": "orichalcum_warhammer", "name": "精金战锤", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 5 }], "result": { "id": "orichalcum_warhammer", "name": "精金战锤", "type": "two_handed_mace", "damage": 40, "weight": 28, "value": 850 }, "skillRequired": 50 }, + "orichalcum_bow": { "id": "orichalcum_bow", "name": "精金弓", "type": "weapon", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 2 }, { "id": "leather", "quantity": 3 }], "result": { "id": "orichalcum_bow", "name": "精金弓", "type": "bow", "damage": 20, "weight": 9, "value": 400 }, "skillRequired": 40 }, + "orichalcum_helmet": { "id": "orichalcum_helmet", "name": "精金头盔", "type": "armor", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 2 }], "result": { "id": "orichalcum_helmet", "name": "精金头盔", "type": "head", "armor": 28, "weight": 6, "value": 350 }, "skillRequired": 40 }, + "orichalcum_chestplate": { "id": "orichalcum_chestplate", "name": "精金胸甲", "type": "armor", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 4 }], "result": { "id": "orichalcum_chestplate", "name": "精金胸甲", "type": "chest", "armor": 48, "weight": 22, "value": 700 }, "skillRequired": 40 }, + "orichalcum_gauntlets": { "id": "orichalcum_gauntlets", "name": "精金护手", "type": "armor", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 2 }], "result": { "id": "orichalcum_gauntlets", "name": "精金护手", "type": "hands", "armor": 22, "weight": 5, "value": 320 }, "skillRequired": 40 }, + "orichalcum_boots": { "id": "orichalcum_boots", "name": "精金靴", "type": "armor", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 2 }], "result": { "id": "orichalcum_boots", "name": "精金靴", "type": "feet", "armor": 22, "weight": 7, "value": 320 }, "skillRequired": 40 }, + "orichalcum_shield": { "id": "orichalcum_shield", "name": "精金盾", "type": "shield", "tier": "orichalcum", "materials": [{ "id": "orichalcum_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "orichalcum_shield", "name": "精金盾", "type": "shield", "armor": 40, "weight": 14, "value": 500 }, "skillRequired": 40 }, + + "moonstone_sword": { "id": "moonstone_sword", "name": "月石剑", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "moonstone_sword", "name": "月石剑", "type": "one_handed_sword", "damage": 26, "weight": 10, "value": 600 }, "skillRequired": 50 }, + "moonstone_waraxe": { "id": "moonstone_waraxe", "name": "月石战斧", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "moonstone_waraxe", "name": "月石战斧", "type": "one_handed_axe", "damage": 25, "weight": 12, "value": 570 }, "skillRequired": 50 }, + "moonstone_mace": { "id": "moonstone_mace", "name": "月石钉锤", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 3 }], "result": { "id": "moonstone_mace", "name": "月石钉锤", "type": "one_handed_mace", "damage": 27, "weight": 14, "value": 630 }, "skillRequired": 50 }, + "moonstone_dagger": { "id": "moonstone_dagger", "name": "月石匕首", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "moonstone_dagger", "name": "月石匕首", "type": "dagger", "damage": 16, "weight": 3, "value": 300 }, "skillRequired": 50 }, + "moonstone_greatsword": { "id": "moonstone_greatsword", "name": "月石大剑", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "moonstone_greatsword", "name": "月石大剑", "type": "two_handed_sword", "damage": 42, "weight": 20, "value": 1200 }, "skillRequired": 60 }, + "moonstone_warhammer": { "id": "moonstone_warhammer", "name": "月石战锤", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 5 }], "result": { "id": "moonstone_warhammer", "name": "月石战锤", "type": "two_handed_mace", "damage": 46, "weight": 26, "value": 1250 }, "skillRequired": 60 }, + "moonstone_bow": { "id": "moonstone_bow", "name": "月石弓", "type": "weapon", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 2 }, { "id": "leather", "quantity": 3 }], "result": { "id": "moonstone_bow", "name": "月石弓", "type": "bow", "damage": 24, "weight": 8, "value": 600 }, "skillRequired": 50 }, + "moonstone_helmet": { "id": "moonstone_helmet", "name": "月石头盔", "type": "armor", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 2 }], "result": { "id": "moonstone_helmet", "name": "月石头盔", "type": "head", "armor": 32, "weight": 5, "value": 500 }, "skillRequired": 50 }, + "moonstone_chestplate": { "id": "moonstone_chestplate", "name": "月石胸甲", "type": "armor", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 4 }], "result": { "id": "moonstone_chestplate", "name": "月石胸甲", "type": "chest", "armor": 55, "weight": 18, "value": 1000 }, "skillRequired": 50 }, + "moonstone_gauntlets": { "id": "moonstone_gauntlets", "name": "月石护手", "type": "armor", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 2 }], "result": { "id": "moonstone_gauntlets", "name": "月石护手", "type": "hands", "armor": 26, "weight": 4, "value": 450 }, "skillRequired": 50 }, + "moonstone_boots": { "id": "moonstone_boots", "name": "月石靴", "type": "armor", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 2 }], "result": { "id": "moonstone_boots", "name": "月石靴", "type": "feet", "armor": 26, "weight": 6, "value": 450 }, "skillRequired": 50 }, + "moonstone_shield": { "id": "moonstone_shield", "name": "月石盾", "type": "shield", "tier": "moonstone", "materials": [{ "id": "moonstone_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "moonstone_shield", "name": "月石盾", "type": "shield", "armor": 45, "weight": 12, "value": 700 }, "skillRequired": 50 }, + + "ebony_sword": { "id": "ebony_sword", "name": "乌木剑", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "ebony_sword", "name": "乌木剑", "type": "one_handed_sword", "damage": 30, "weight": 11, "value": 900 }, "skillRequired": 60 }, + "ebony_waraxe": { "id": "ebony_waraxe", "name": "乌木战斧", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 2 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "ebony_waraxe", "name": "乌木战斧", "type": "one_handed_axe", "damage": 29, "weight": 13, "value": 850 }, "skillRequired": 60 }, + "ebony_mace": { "id": "ebony_mace", "name": "乌木钉锤", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 3 }], "result": { "id": "ebony_mace", "name": "乌木钉锤", "type": "one_handed_mace", "damage": 31, "weight": 15, "value": 950 }, "skillRequired": 60 }, + "ebony_dagger": { "id": "ebony_dagger", "name": "乌木匕首", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 1 }, { "id": "leather_strips", "quantity": 1 }], "result": { "id": "ebony_dagger", "name": "乌木匕首", "type": "dagger", "damage": 19, "weight": 3, "value": 450 }, "skillRequired": 60 }, + "ebony_greatsword": { "id": "ebony_greatsword", "name": "乌木大剑", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "ebony_greatsword", "name": "乌木大剑", "type": "two_handed_sword", "damage": 48, "weight": 22, "value": 1800 }, "skillRequired": 70 }, + "ebony_warhammer": { "id": "ebony_warhammer", "name": "乌木战锤", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 5 }], "result": { "id": "ebony_warhammer", "name": "乌木战锤", "type": "two_handed_mace", "damage": 52, "weight": 28, "value": 1900 }, "skillRequired": 70 }, + "ebony_bow": { "id": "ebony_bow", "name": "乌木弓", "type": "weapon", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 2 }, { "id": "leather", "quantity": 3 }], "result": { "id": "ebony_bow", "name": "乌木弓", "type": "bow", "damage": 28, "weight": 9, "value": 900 }, "skillRequired": 60 }, + "ebony_helmet": { "id": "ebony_helmet", "name": "乌木头盔", "type": "armor", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "ebony_helmet", "name": "乌木头盔", "type": "head", "armor": 36, "weight": 6, "value": 800 }, "skillRequired": 60 }, + "ebony_chestplate": { "id": "ebony_chestplate", "name": "乌木胸甲", "type": "armor", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 4 }], "result": { "id": "ebony_chestplate", "name": "乌木胸甲", "type": "chest", "armor": 62, "weight": 20, "value": 1600 }, "skillRequired": 60 }, + "ebony_gauntlets": { "id": "ebony_gauntlets", "name": "乌木护手", "type": "armor", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "ebony_gauntlets", "name": "乌木护手", "type": "hands", "armor": 30, "weight": 4, "value": 750 }, "skillRequired": 60 }, + "ebony_boots": { "id": "ebony_boots", "name": "乌木靴", "type": "armor", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "ebony_boots", "name": "乌木靴", "type": "feet", "armor": 30, "weight": 6, "value": 750 }, "skillRequired": 60 }, + "ebony_shield": { "id": "ebony_shield", "name": "乌木盾", "type": "shield", "tier": "ebony", "materials": [{ "id": "ebony_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "ebony_shield", "name": "乌木盾", "type": "shield", "armor": 50, "weight": 12, "value": 1000 }, "skillRequired": 60 }, + + "daedric_sword": { "id": "daedric_sword", "name": "魔族剑", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "daedric_sword", "name": "魔族剑", "type": "one_handed_sword", "damage": 35, "weight": 12, "value": 1500 }, "skillRequired": 70 }, + "daedric_waraxe": { "id": "daedric_waraxe", "name": "魔族战斧", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "daedric_waraxe", "name": "魔族战斧", "type": "one_handed_axe", "damage": 34, "weight": 14, "value": 1400 }, "skillRequired": 70 }, + "daedric_mace": { "id": "daedric_mace", "name": "魔族钉锤", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 3 }], "result": { "id": "daedric_mace", "name": "魔族钉锤", "type": "one_handed_mace", "damage": 36, "weight": 16, "value": 1600 }, "skillRequired": 70 }, + "daedric_dagger": { "id": "daedric_dagger", "name": "魔族匕首", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 1 }], "result": { "id": "daedric_dagger", "name": "魔族匕首", "type": "dagger", "damage": 22, "weight": 4, "value": 750 }, "skillRequired": 70 }, + "daedric_greatsword": { "id": "daedric_greatsword", "name": "魔族大剑", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 4 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "daedric_greatsword", "name": "魔族大剑", "type": "two_handed_sword", "damage": 55, "weight": 24, "value": 3000 }, "skillRequired": 80 }, + "daedric_warhammer": { "id": "daedric_warhammer", "name": "魔族战锤", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 5 }], "result": { "id": "daedric_warhammer", "name": "魔族战锤", "type": "two_handed_mace", "damage": 60, "weight": 30, "value": 3200 }, "skillRequired": 80 }, + "daedric_bow": { "id": "daedric_bow", "name": "魔族弓", "type": "weapon", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 2 }, { "id": "leather", "quantity": 3 }], "result": { "id": "daedric_bow", "name": "魔族弓", "type": "bow", "damage": 32, "weight": 10, "value": 1500 }, "skillRequired": 70 }, + "daedric_helmet": { "id": "daedric_helmet", "name": "魔族头盔", "type": "armor", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "daedric_helmet", "name": "魔族头盔", "type": "head", "armor": 42, "weight": 8, "value": 2000 }, "skillRequired": 70 }, + "daedric_chestplate": { "id": "daedric_chestplate", "name": "魔族胸甲", "type": "armor", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 4 }], "result": { "id": "daedric_chestplate", "name": "魔族胸甲", "type": "chest", "armor": 72, "weight": 25, "value": 4000 }, "skillRequired": 70 }, + "daedric_gauntlets": { "id": "daedric_gauntlets", "name": "魔族护手", "type": "armor", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "daedric_gauntlets", "name": "魔族护手", "type": "hands", "armor": 36, "weight": 6, "value": 1800 }, "skillRequired": 70 }, + "daedric_boots": { "id": "daedric_boots", "name": "魔族靴", "type": "armor", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 2 }], "result": { "id": "daedric_boots", "name": "魔族靴", "type": "feet", "armor": 36, "weight": 8, "value": 1800 }, "skillRequired": 70 }, + "daedric_shield": { "id": "daedric_shield", "name": "魔族盾", "type": "shield", "tier": "daedric", "materials": [{ "id": "daedric_heart", "quantity": 1 }, { "id": "ebony_ingot", "quantity": 4 }, { "id": "leather", "quantity": 1 }], "result": { "id": "daedric_shield", "name": "魔族盾", "type": "shield", "armor": 60, "weight": 14, "value": 2500 }, "skillRequired": 70 }, + + "dragon_sword": { "id": "dragon_sword", "name": "龙骨剑", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 2 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_sword", "name": "龙骨剑", "type": "one_handed_sword", "damage": 40, "weight": 13, "value": 2500 }, "skillRequired": 80 }, + "dragon_waraxe": { "id": "dragon_waraxe", "name": "龙骨战斧", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 2 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_waraxe", "name": "龙骨战斧", "type": "one_handed_axe", "damage": 39, "weight": 15, "value": 2400 }, "skillRequired": 80 }, + "dragon_mace": { "id": "dragon_mace", "name": "龙骨钉锤", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 3 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_mace", "name": "龙骨钉锤", "type": "one_handed_mace", "damage": 41, "weight": 17, "value": 2600 }, "skillRequired": 80 }, + "dragon_dagger": { "id": "dragon_dagger", "name": "龙骨匕首", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 1 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_dagger", "name": "龙骨匕首", "type": "dagger", "damage": 25, "weight": 4, "value": 1200 }, "skillRequired": 80 }, + "dragon_greatsword": { "id": "dragon_greatsword", "name": "龙骨大剑", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 4 }, { "id": "dragon_scale", "quantity": 2 }, { "id": "leather_strips", "quantity": 2 }], "result": { "id": "dragon_greatsword", "name": "龙骨大剑", "type": "two_handed_sword", "damage": 62, "weight": 26, "value": 5000 }, "skillRequired": 90 }, + "dragon_warhammer": { "id": "dragon_warhammer", "name": "龙骨战锤", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 5 }, { "id": "dragon_scale", "quantity": 2 }], "result": { "id": "dragon_warhammer", "name": "龙骨战锤", "type": "two_handed_mace", "damage": 68, "weight": 32, "value": 5500 }, "skillRequired": 90 }, + "dragon_bow": { "id": "dragon_bow", "name": "龙骨弓", "type": "weapon", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 2 }, { "id": "dragon_scale", "quantity": 2 }, { "id": "leather", "quantity": 3 }], "result": { "id": "dragon_bow", "name": "龙骨弓", "type": "bow", "damage": 36, "weight": 11, "value": 2500 }, "skillRequired": 80 }, + "dragon_helmet": { "id": "dragon_helmet", "name": "龙头盔", "type": "armor", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 2 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_helmet", "name": "龙头盔", "type": "head", "armor": 48, "weight": 10, "value": 3500 }, "skillRequired": 80 }, + "dragon_chestplate": { "id": "dragon_chestplate", "name": "龙胸甲", "type": "armor", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 4 }, { "id": "dragon_scale", "quantity": 3 }], "result": { "id": "dragon_chestplate", "name": "龙胸甲", "type": "chest", "armor": 82, "weight": 30, "value": 7000 }, "skillRequired": 80 }, + "dragon_gauntlets": { "id": "dragon_gauntlets", "name": "龙护手", "type": "armor", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 2 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_gauntlets", "name": "龙护手", "type": "hands", "armor": 40, "weight": 8, "value": 3000 }, "skillRequired": 80 }, + "dragon_boots": { "id": "dragon_boots", "name": "龙靴", "type": "armor", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 2 }, { "id": "dragon_scale", "quantity": 1 }], "result": { "id": "dragon_boots", "name": "龙靴", "type": "feet", "armor": 40, "weight": 10, "value": 3000 }, "skillRequired": 80 }, + "dragon_shield": { "id": "dragon_shield", "name": "龙盾", "type": "shield", "tier": "dragon", "materials": [{ "id": "dragon_bone", "quantity": 4 }, { "id": "dragon_scale", "quantity": 2 }, { "id": "leather", "quantity": 1 }], "result": { "id": "dragon_shield", "name": "龙盾", "type": "shield", "armor": 70, "weight": 16, "value": 5000 }, "skillRequired": 80 } } } } diff --git a/src/data/enemies/enemies.json b/src/data/enemies/enemies.json index 5198280..42aeb2f 100644 --- a/src/data/enemies/enemies.json +++ b/src/data/enemies/enemies.json @@ -11,6 +11,7 @@ "detectionRange": 150, "attackRange": 45, "attackSpeed": 1.0, + "aiBehavior": "melee", "loot": { "gold": { "min": 10, "max": 30 }, "items": [ @@ -32,6 +33,16 @@ "detectionRange": 200, "attackRange": 35, "attackSpeed": 1.5, + "aiBehavior": "melee", + "abilities": [ + { + "id": "wolf_frost_breath", + "spellId": "frost_breath", + "type": "spell", + "cooldown": 6000, + "condition": "hp_below_50" + } + ], "loot": { "gold": { "min": 0, "max": 5 }, "items": [ @@ -52,6 +63,7 @@ "detectionRange": 120, "attackRange": 45, "attackSpeed": 0.8, + "aiBehavior": "melee", "loot": { "gold": { "min": 5, "max": 15 }, "items": [ @@ -73,6 +85,25 @@ "detectionRange": 140, "attackRange": 45, "attackSpeed": 0.9, + "aiBehavior": "mixed", + "abilities": [ + { + "id": "draugr_frost_breath", + "spellId": "frost_breath", + "type": "spell", + "cooldown": 5000, + "condition": "always" + }, + { + "id": "draugr_rage", + "type": "buff", + "cooldown": 30000, + "condition": "hp_below_50", + "effect": "undead_rage", + "effectDuration": 10000, + "effectMagnitude": 6 + } + ], "loot": { "gold": { "min": 10, "max": 25 }, "items": [ @@ -89,11 +120,29 @@ "level": 16, "health": 180, "stamina": 50, + "magicka": 80, "damage": 22, "armor": 25, "detectionRange": 160, "attackRange": 50, "attackSpeed": 1.0, + "aiBehavior": "caster", + "abilities": [ + { + "id": "wight_ice_storm", + "spellId": "ice_storm", + "type": "spell", + "cooldown": 8000, + "condition": "always" + }, + { + "id": "wight_frost_breath", + "spellId": "frost_breath", + "type": "spell", + "cooldown": 5000, + "condition": "always" + } + ], "loot": { "gold": { "min": 20, "max": 50 }, "items": [ @@ -115,6 +164,18 @@ "detectionRange": 180, "attackRange": 50, "attackSpeed": 0.6, + "aiBehavior": "melee", + "abilities": [ + { + "id": "bear_slam", + "type": "attack", + "cooldown": 7000, + "condition": "hp_below_50", + "damage": 27, + "effect": "stun", + "effectDuration": 1000 + } + ], "loot": { "gold": { "min": 0, "max": 0 }, "items": [ @@ -136,6 +197,26 @@ "detectionRange": 170, "attackRange": 50, "attackSpeed": 0.65, + "aiBehavior": "melee", + "abilities": [ + { + "id": "cave_bear_slam", + "type": "attack", + "cooldown": 6000, + "condition": "hp_below_50", + "damage": 33, + "effect": "stun", + "effectDuration": 1500 + }, + { + "id": "cave_bear_frost_resist", + "type": "passive", + "cooldown": 0, + "condition": "on_spawn", + "effect": "frost_resist", + "effectMagnitude": 50 + } + ], "loot": { "gold": { "min": 0, "max": 10 }, "items": [ @@ -157,6 +238,19 @@ "detectionRange": 160, "attackRange": 40, "attackSpeed": 1.2, + "aiBehavior": "melee", + "abilities": [ + { + "id": "spider_poison", + "type": "attack", + "cooldown": 4000, + "condition": "always", + "damage": 3, + "effect": "poison", + "effectDuration": 3000, + "effectMagnitude": 3 + } + ], "loot": { "gold": { "min": 0, "max": 10 }, "items": [ @@ -173,11 +267,32 @@ "level": 12, "health": 80, "stamina": 50, + "magicka": 40, "damage": 14, "armor": 12, "detectionRange": 170, "attackRange": 42, "attackSpeed": 1.3, + "aiBehavior": "mixed", + "abilities": [ + { + "id": "frost_spider_poison", + "type": "attack", + "cooldown": 4000, + "condition": "always", + "damage": 5, + "effect": "poison", + "effectDuration": 4000, + "effectMagnitude": 5 + }, + { + "id": "frost_spider_breath", + "spellId": "frost_breath", + "type": "spell", + "cooldown": 6000, + "condition": "hp_below_50" + } + ], "loot": { "gold": { "min": 5, "max": 15 }, "items": [ @@ -199,6 +314,16 @@ "detectionRange": 150, "attackRange": 45, "attackSpeed": 1.1, + "aiBehavior": "melee", + "abilities": [ + { + "id": "outlaw_power_attack", + "type": "attack", + "cooldown": 6000, + "condition": "always", + "damage": 18 + } + ], "loot": { "gold": { "min": 15, "max": 40 }, "items": [ @@ -220,6 +345,18 @@ "detectionRange": 150, "attackRange": 50, "attackSpeed": 0.85, + "aiBehavior": "melee", + "abilities": [ + { + "id": "thug_slam", + "type": "attack", + "cooldown": 5000, + "condition": "always", + "damage": 24, + "effect": "stun", + "effectDuration": 1000 + } + ], "loot": { "gold": { "min": 25, "max": 60 }, "items": [ @@ -242,6 +379,30 @@ "detectionRange": 180, "attackRange": 120, "attackSpeed": 0.7, + "aiBehavior": "caster", + "abilities": [ + { + "id": "necro_flames", + "spellId": "flames", + "type": "spell", + "cooldown": 1000, + "condition": "always" + }, + { + "id": "necro_conjure", + "spellId": "conjure_familiar", + "type": "spell", + "cooldown": 15000, + "condition": "hp_below_60" + }, + { + "id": "necro_heal", + "spellId": "healing", + "type": "spell", + "cooldown": 5000, + "condition": "hp_below_40" + } + ], "loot": { "gold": { "min": 20, "max": 50 }, "items": [ diff --git a/src/data/items/armor.json b/src/data/items/armor.json index 211e5c6..f5cba46 100644 --- a/src/data/items/armor.json +++ b/src/data/items/armor.json @@ -204,6 +204,30 @@ "material": "moonstone_ingot", "description": "精致的精灵盾" }, + "elven_gauntlets": { + "id": "elven_gauntlets", + "name": "精灵护手", + "type": "armor", + "subtype": "gauntlets", + "tier": "elven", + "armor": 12, + "weight": 2, + "value": 180, + "material": "moonstone_ingot", + "description": "轻盈的精灵护手" + }, + "elven_boots": { + "id": "elven_boots", + "name": "精灵靴", + "type": "armor", + "subtype": "boots", + "tier": "elven", + "armor": 14, + "weight": 3, + "value": 200, + "material": "moonstone_ingot", + "description": "优雅的精灵靴" + }, "orcish_helmet": { "id": "orcish_helmet", "name": "兽人头盔", @@ -240,6 +264,30 @@ "material": "orichalcum_ingot", "description": "粗犷的兽人盾牌" }, + "orcish_gauntlets": { + "id": "orcish_gauntlets", + "name": "兽人护手", + "type": "armor", + "subtype": "gauntlets", + "tier": "orcish", + "armor": 14, + "weight": 5, + "value": 270, + "material": "orichalcum_ingot", + "description": "厚重的兽人护手" + }, + "orcish_boots": { + "id": "orcish_boots", + "name": "兽人靴", + "type": "armor", + "subtype": "boots", + "tier": "orcish", + "armor": 16, + "weight": 7, + "value": 300, + "material": "orichalcum_ingot", + "description": "坚固的兽人战靴" + }, "ebony_helmet": { "id": "ebony_helmet", "name": "乌木头盔", @@ -276,6 +324,30 @@ "material": "ebony_ingot", "description": "坚硬的乌木盾" }, + "ebony_gauntlets": { + "id": "ebony_gauntlets", + "name": "乌木护手", + "type": "armor", + "subtype": "gauntlets", + "tier": "ebony", + "armor": 18, + "weight": 4, + "value": 550, + "material": "ebony_ingot", + "description": "精致的乌木护手" + }, + "ebony_boots": { + "id": "ebony_boots", + "name": "乌木靴", + "type": "armor", + "subtype": "boots", + "tier": "ebony", + "armor": 20, + "weight": 6, + "value": 600, + "material": "ebony_ingot", + "description": "华丽的乌木靴" + }, "daedric_helmet": { "id": "daedric_helmet", "name": "魔族头盔", @@ -312,6 +384,30 @@ "material": "daedric_heart", "description": "邪恶的魔族盾" }, + "daedric_gauntlets": { + "id": "daedric_gauntlets", + "name": "魔族护手", + "type": "armor", + "subtype": "gauntlets", + "tier": "daedric", + "armor": 24, + "weight": 6, + "value": 1400, + "material": "daedric_heart", + "description": "恐怖的魔族护手" + }, + "daedric_boots": { + "id": "daedric_boots", + "name": "魔族靴", + "type": "armor", + "subtype": "boots", + "tier": "daedric", + "armor": 26, + "weight": 8, + "value": 1500, + "material": "daedric_heart", + "description": "邪恶的魔族靴" + }, "dragon_helmet": { "id": "dragon_helmet", "name": "龙头盔", @@ -347,6 +443,30 @@ "value": 3000, "material": "dragon_bone", "description": "龙骨盾牌" + }, + "dragon_gauntlets": { + "id": "dragon_gauntlets", + "name": "龙护手", + "type": "armor", + "subtype": "gauntlets", + "tier": "dragon", + "armor": 28, + "weight": 8, + "value": 2400, + "material": "dragon_bone", + "description": "龙骨打造的护手" + }, + "dragon_boots": { + "id": "dragon_boots", + "name": "龙靴", + "type": "armor", + "subtype": "boots", + "tier": "dragon", + "armor": 30, + "weight": 10, + "value": 2500, + "material": "dragon_bone", + "description": "龙骨打造的战靴" } } } diff --git a/src/data/items/weapons.json b/src/data/items/weapons.json index c862217..0df300e 100644 --- a/src/data/items/weapons.json +++ b/src/data/items/weapons.json @@ -10,6 +10,7 @@ "value": 0, "description": "空手战斗" }, + "iron_sword": { "id": "iron_sword", "name": "铁剑", @@ -58,6 +59,43 @@ "value": 25, "description": "小巧的铁匕首" }, + "iron_greatsword": { + "id": "iron_greatsword", + "name": "铁制大剑", + "type": "two_handed_sword", + "material": "iron", + "tier": 1, + "damage": 18, + "speed": 0.7, + "weight": 20, + "value": 100, + "description": "沉重的铁制大剑" + }, + "iron_warhammer": { + "id": "iron_warhammer", + "name": "铁战锤", + "type": "two_handed_mace", + "material": "iron", + "tier": 1, + "damage": 20, + "speed": 0.6, + "weight": 25, + "value": 120, + "description": "巨大的铁战锤" + }, + "hunting_bow": { + "id": "hunting_bow", + "name": "猎弓", + "type": "bow", + "material": "wood", + "tier": 1, + "damage": 8, + "speed": 0.9, + "weight": 6, + "value": 50, + "description": "简单的猎弓" + }, + "steel_sword": { "id": "steel_sword", "name": "钢剑", @@ -94,17 +132,17 @@ "value": 130, "description": "坚固的钢钉锤" }, - "iron_greatsword": { - "id": "iron_greatsword", - "name": "铁制大剑", - "type": "two_handed_sword", - "material": "iron", - "tier": 1, - "damage": 18, - "speed": 0.7, - "weight": 20, - "value": 100, - "description": "沉重的铁制大剑" + "steel_dagger": { + "id": "steel_dagger", + "name": "钢匕首", + "type": "dagger", + "material": "steel", + "tier": 2, + "damage": 8, + "speed": 1.6, + "weight": 3, + "value": 60, + "description": "锋利的钢匕首" }, "steel_greatsword": { "id": "steel_greatsword", @@ -118,29 +156,17 @@ "value": 250, "description": "精钢大剑" }, - "iron_warhammer": { - "id": "iron_warhammer", - "name": "铁战锤", + "steel_warhammer": { + "id": "steel_warhammer", + "name": "钢战锤", "type": "two_handed_mace", - "material": "iron", - "tier": 1, - "damage": 20, + "material": "steel", + "tier": 2, + "damage": 26, "speed": 0.6, - "weight": 25, - "value": 120, - "description": "巨大的铁战锤" - }, - "hunting_bow": { - "id": "hunting_bow", - "name": "猎弓", - "type": "bow", - "material": "wood", - "tier": 1, - "damage": 8, - "speed": 0.9, - "weight": 6, - "value": 50, - "description": "简单的猎弓" + "weight": 27, + "value": 270, + "description": "沉重的钢制战锤" }, "long_bow": { "id": "long_bow", @@ -153,6 +179,516 @@ "weight": 8, "value": 120, "description": "射程更远的长弓" + }, + + "corundum_sword": { + "id": "corundum_sword", + "name": "硬化铁剑", + "type": "one_handed_sword", + "material": "corundum", + "tier": 3, + "damage": 18, + "speed": 1.0, + "weight": 12, + "value": 250, + "description": "硬化铁锻造的利剑" + }, + "corundum_waraxe": { + "id": "corundum_waraxe", + "name": "硬化铁战斧", + "type": "one_handed_axe", + "material": "corundum", + "tier": 3, + "damage": 17, + "speed": 1.1, + "weight": 14, + "value": 230, + "description": "坚硬的硬化铁战斧" + }, + "corundum_mace": { + "id": "corundum_mace", + "name": "硬化铁钉锤", + "type": "one_handed_mace", + "material": "corundum", + "tier": 3, + "damage": 19, + "speed": 0.9, + "weight": 16, + "value": 270, + "description": "沉重的硬化铁钉锤" + }, + "corundum_dagger": { + "id": "corundum_dagger", + "name": "硬化铁匕首", + "type": "dagger", + "material": "corundum", + "tier": 3, + "damage": 10, + "speed": 1.6, + "weight": 4, + "value": 130, + "description": "锋利的硬化铁匕首" + }, + "corundum_greatsword": { + "id": "corundum_greatsword", + "name": "硬化铁大剑", + "type": "two_handed_sword", + "material": "corundum", + "tier": 3, + "damage": 30, + "speed": 0.7, + "weight": 24, + "value": 500, + "description": "沉重的硬化铁大剑" + }, + "corundum_warhammer": { + "id": "corundum_warhammer", + "name": "硬化铁战锤", + "type": "two_handed_mace", + "material": "corundum", + "tier": 3, + "damage": 34, + "speed": 0.6, + "weight": 30, + "value": 550, + "description": "巨大的硬化铁战锤" + }, + "corundum_bow": { + "id": "corundum_bow", + "name": "硬化铁弓", + "type": "bow", + "material": "corundum", + "tier": 3, + "damage": 16, + "speed": 0.8, + "weight": 10, + "value": 250, + "description": "坚固的硬化铁弓" + }, + + "orichalcum_sword": { + "id": "orichalcum_sword", + "name": "精金剑", + "type": "one_handed_sword", + "material": "orichalcum", + "tier": 4, + "damage": 22, + "speed": 1.0, + "weight": 11, + "value": 400, + "description": "精金锻造的利剑" + }, + "orichalcum_waraxe": { + "id": "orichalcum_waraxe", + "name": "精金战斧", + "type": "one_handed_axe", + "material": "orichalcum", + "tier": 4, + "damage": 21, + "speed": 1.1, + "weight": 13, + "value": 380, + "description": "锋利的精金战斧" + }, + "orichalcum_mace": { + "id": "orichalcum_mace", + "name": "精金钉锤", + "type": "one_handed_mace", + "material": "orichalcum", + "tier": 4, + "damage": 23, + "speed": 0.9, + "weight": 15, + "value": 420, + "description": "沉重的精金钉锤" + }, + "orichalcum_dagger": { + "id": "orichalcum_dagger", + "name": "精金匕首", + "type": "dagger", + "material": "orichalcum", + "tier": 4, + "damage": 13, + "speed": 1.6, + "weight": 3, + "value": 200, + "description": "精致的精金匕首" + }, + "orichalcum_greatsword": { + "id": "orichalcum_greatsword", + "name": "精金大剑", + "type": "two_handed_sword", + "material": "orichalcum", + "tier": 4, + "damage": 36, + "speed": 0.7, + "weight": 22, + "value": 800, + "description": "精金铸造的大剑" + }, + "orichalcum_warhammer": { + "id": "orichalcum_warhammer", + "name": "精金战锤", + "type": "two_handed_mace", + "material": "orichalcum", + "tier": 4, + "damage": 40, + "speed": 0.6, + "weight": 28, + "value": 850, + "description": "巨大的精金战锤" + }, + "orichalcum_bow": { + "id": "orichalcum_bow", + "name": "精金弓", + "type": "bow", + "material": "orichalcum", + "tier": 4, + "damage": 20, + "speed": 0.8, + "weight": 9, + "value": 400, + "description": "精致的精金弓" + }, + + "moonstone_sword": { + "id": "moonstone_sword", + "name": "月石剑", + "type": "one_handed_sword", + "material": "moonstone", + "tier": 5, + "damage": 26, + "speed": 1.0, + "weight": 10, + "value": 600, + "description": "月石锻造的优雅长剑" + }, + "moonstone_waraxe": { + "id": "moonstone_waraxe", + "name": "月石战斧", + "type": "one_handed_axe", + "material": "moonstone", + "tier": 5, + "damage": 25, + "speed": 1.1, + "weight": 12, + "value": 570, + "description": "月石打造的战斧" + }, + "moonstone_mace": { + "id": "moonstone_mace", + "name": "月石钉锤", + "type": "one_handed_mace", + "material": "moonstone", + "tier": 5, + "damage": 27, + "speed": 0.9, + "weight": 14, + "value": 630, + "description": "沉重的月石钉锤" + }, + "moonstone_dagger": { + "id": "moonstone_dagger", + "name": "月石匕首", + "type": "dagger", + "material": "moonstone", + "tier": 5, + "damage": 16, + "speed": 1.6, + "weight": 3, + "value": 300, + "description": "精致的月石匕首" + }, + "moonstone_greatsword": { + "id": "moonstone_greatsword", + "name": "月石大剑", + "type": "two_handed_sword", + "material": "moonstone", + "tier": 5, + "damage": 42, + "speed": 0.7, + "weight": 20, + "value": 1200, + "description": "月石铸造的大剑" + }, + "moonstone_warhammer": { + "id": "moonstone_warhammer", + "name": "月石战锤", + "type": "two_handed_mace", + "material": "moonstone", + "tier": 5, + "damage": 46, + "speed": 0.6, + "weight": 26, + "value": 1250, + "description": "巨大的月石战锤" + }, + "moonstone_bow": { + "id": "moonstone_bow", + "name": "月石弓", + "type": "bow", + "material": "moonstone", + "tier": 5, + "damage": 24, + "speed": 0.8, + "weight": 8, + "value": 600, + "description": "优雅的月石弓" + }, + + "ebony_sword": { + "id": "ebony_sword", + "name": "乌木剑", + "type": "one_handed_sword", + "material": "ebony", + "tier": 6, + "damage": 30, + "speed": 1.0, + "weight": 11, + "value": 900, + "description": "乌木锻造的黑色利剑" + }, + "ebony_waraxe": { + "id": "ebony_waraxe", + "name": "乌木战斧", + "type": "one_handed_axe", + "material": "ebony", + "tier": 6, + "damage": 29, + "speed": 1.1, + "weight": 13, + "value": 850, + "description": "乌木打造的战斧" + }, + "ebony_mace": { + "id": "ebony_mace", + "name": "乌木钉锤", + "type": "one_handed_mace", + "material": "ebony", + "tier": 6, + "damage": 31, + "speed": 0.9, + "weight": 15, + "value": 950, + "description": "沉重的乌木钉锤" + }, + "ebony_dagger": { + "id": "ebony_dagger", + "name": "乌木匕首", + "type": "dagger", + "material": "ebony", + "tier": 6, + "damage": 19, + "speed": 1.6, + "weight": 3, + "value": 450, + "description": "致命的乌木匕首" + }, + "ebony_greatsword": { + "id": "ebony_greatsword", + "name": "乌木大剑", + "type": "two_handed_sword", + "material": "ebony", + "tier": 6, + "damage": 48, + "speed": 0.7, + "weight": 22, + "value": 1800, + "description": "乌木铸造的大剑" + }, + "ebony_warhammer": { + "id": "ebony_warhammer", + "name": "乌木战锤", + "type": "two_handed_mace", + "material": "ebony", + "tier": 6, + "damage": 52, + "speed": 0.6, + "weight": 28, + "value": 1900, + "description": "巨大的乌木战锤" + }, + "ebony_bow": { + "id": "ebony_bow", + "name": "乌木弓", + "type": "bow", + "material": "ebony", + "tier": 6, + "damage": 28, + "speed": 0.8, + "weight": 9, + "value": 900, + "description": "精致的乌木弓" + }, + + "daedric_sword": { + "id": "daedric_sword", + "name": "魔族剑", + "type": "one_handed_sword", + "material": "daedric", + "tier": 7, + "damage": 35, + "speed": 1.0, + "weight": 12, + "value": 1500, + "description": "魔族锻造的邪恶利剑" + }, + "daedric_waraxe": { + "id": "daedric_waraxe", + "name": "魔族战斧", + "type": "one_handed_axe", + "material": "daedric", + "tier": 7, + "damage": 34, + "speed": 1.1, + "weight": 14, + "value": 1400, + "description": "魔族打造的战斧" + }, + "daedric_mace": { + "id": "daedric_mace", + "name": "魔族钉锤", + "type": "one_handed_mace", + "material": "daedric", + "tier": 7, + "damage": 36, + "speed": 0.9, + "weight": 16, + "value": 1600, + "description": "沉重的魔族钉锤" + }, + "daedric_dagger": { + "id": "daedric_dagger", + "name": "魔族匕首", + "type": "dagger", + "material": "daedric", + "tier": 7, + "damage": 22, + "speed": 1.6, + "weight": 4, + "value": 750, + "description": "致命的魔族匕首" + }, + "daedric_greatsword": { + "id": "daedric_greatsword", + "name": "魔族大剑", + "type": "two_handed_sword", + "material": "daedric", + "tier": 7, + "damage": 55, + "speed": 0.7, + "weight": 24, + "value": 3000, + "description": "魔族铸造的大剑" + }, + "daedric_warhammer": { + "id": "daedric_warhammer", + "name": "魔族战锤", + "type": "two_handed_mace", + "material": "daedric", + "tier": 7, + "damage": 60, + "speed": 0.6, + "weight": 30, + "value": 3200, + "description": "巨大的魔族战锤" + }, + "daedric_bow": { + "id": "daedric_bow", + "name": "魔族弓", + "type": "bow", + "material": "daedric", + "tier": 7, + "damage": 32, + "speed": 0.8, + "weight": 10, + "value": 1500, + "description": "邪恶的魔族弓" + }, + + "dragon_sword": { + "id": "dragon_sword", + "name": "龙骨剑", + "type": "one_handed_sword", + "material": "dragon", + "tier": 8, + "damage": 40, + "speed": 1.0, + "weight": 13, + "value": 2500, + "description": "龙骨锻造的传奇利剑" + }, + "dragon_waraxe": { + "id": "dragon_waraxe", + "name": "龙骨战斧", + "type": "one_handed_axe", + "material": "dragon", + "tier": 8, + "damage": 39, + "speed": 1.1, + "weight": 15, + "value": 2400, + "description": "龙骨打造的战斧" + }, + "dragon_mace": { + "id": "dragon_mace", + "name": "龙骨钉锤", + "type": "one_handed_mace", + "material": "dragon", + "tier": 8, + "damage": 41, + "speed": 0.9, + "weight": 17, + "value": 2600, + "description": "沉重的龙骨钉锤" + }, + "dragon_dagger": { + "id": "dragon_dagger", + "name": "龙骨匕首", + "type": "dagger", + "material": "dragon", + "tier": 8, + "damage": 25, + "speed": 1.6, + "weight": 4, + "value": 1200, + "description": "致命的龙骨匕首" + }, + "dragon_greatsword": { + "id": "dragon_greatsword", + "name": "龙骨大剑", + "type": "two_handed_sword", + "material": "dragon", + "tier": 8, + "damage": 62, + "speed": 0.7, + "weight": 26, + "value": 5000, + "description": "龙骨铸造的传奇大剑" + }, + "dragon_warhammer": { + "id": "dragon_warhammer", + "name": "龙骨战锤", + "type": "two_handed_mace", + "material": "dragon", + "tier": 8, + "damage": 68, + "speed": 0.6, + "weight": 32, + "value": 5500, + "description": "巨大的龙骨战锤" + }, + "dragon_bow": { + "id": "dragon_bow", + "name": "龙骨弓", + "type": "bow", + "material": "dragon", + "tier": 8, + "damage": 36, + "speed": 0.8, + "weight": 11, + "value": 2500, + "description": "传奇的龙骨弓" } } } diff --git a/src/data/quests/quests.json b/src/data/quests/quests.json index 6020490..678c3e9 100644 --- a/src/data/quests/quests.json +++ b/src/data/quests/quests.json @@ -178,6 +178,99 @@ "xp": 250, "items": [{ "id": "bear_pelt", "quantity": 2 }] } + }, + "main_04_dragoncall": { + "id": "main_04_dragoncall", + "name": "龙之呼唤", + "type": "main", + "level": 15, + "prerequisites": ["main_03_dragonsreach"], + "description": "古老的预言指向天际省深处的一座龙墓。据说那里沉睡着一条远古巨龙,而你体内流淌的龙血将唤醒它。", + "objectives": [ + { "id": "find_wordwall", "description": "寻找龙语墙", "type": "reach", "target": "ancient_ruins", "completed": false }, + { "id": "learn_word", "description": "学习龙吼词语", "type": "interact", "target": "wordwall", "completed": false }, + { "id": "kill_ancient_dragon", "description": "击败远古巨龙", "type": "kill", "target": "dragon", "quantity": 1, "current": 0, "completed": false }, + { "id": "return_jarls", "description": "向领主报告胜利", "type": "talk", "target": "whiterun_jarls", "completed": false } + ], + "rewards": { + "gold": 2000, + "xp": 3000, + "items": [{ "id": "dragon_sword", "quantity": 1 }], + "faction": { "whiterun": 30 } + } + }, + "side_missing_cargo": { + "id": "side_missing_cargo", + "name": "失踪的货物", + "type": "side", + "level": 3, + "description": "溪木镇的商人说他的一批货物在运输途中失踪了,可能被强盗劫走了。你需要找回这批货物。", + "objectives": [ + { "id": "find_camp", "description": "寻找强盗营地", "type": "reach", "target": "whiterun_exterior", "completed": false }, + { "id": "kill_bandits", "description": "消灭强盗", "type": "kill", "target": "bandit", "quantity": 3, "current": 0, "completed": false }, + { "id": "find_cargo", "description": "找到失踪的货物", "type": "collect", "target": "missing_cargo", "quantity": 1, "completed": false }, + { "id": "return_merchant", "description": "将货物归还给商人", "type": "talk", "target": "riverwood_merchant", "completed": false } + ], + "rewards": { + "gold": 250, + "xp": 300, + "items": [{ "id": "health_potion", "quantity": 5 }] + } + }, + "guild_mage_trial": { + "id": "guild_mage_trial", + "name": "大法师的试炼", + "type": "guild", + "guild": "mages_guild", + "level": 12, + "description": "大法师要求你前往古代遗迹,证明你对魔法的掌握。遗迹中盘踞着强大的不死生物。", + "objectives": [ + { "id": "enter_ruins", "description": "进入古代遗迹", "type": "reach", "target": "ancient_ruins", "completed": false }, + { "id": "kill_draugr", "description": "消灭尸鬼亡灵", "type": "kill", "target": "draugr_wight", "quantity": 3, "current": 0, "completed": false }, + { "id": "find_tome", "description": "找到古代魔法书", "type": "collect", "target": "ancient_tome", "quantity": 1, "completed": false }, + { "id": "return_mage", "description": "将魔法书交给大法师", "type": "talk", "target": "archmage", "completed": false } + ], + "rewards": { + "gold": 600, + "xp": 800, + "items": [{ "id": "magicka_potion", "quantity": 5 }], + "faction": { "mages_guild": 20 } + } + }, + "daedric_02_star": { + "id": "daedric_02_star", + "name": "黑色星辰", + "type": "daedric", + "level": 18, + "description": "一位疯狂的法师向你提及一颗被污染的星辰——黑色星辰。它蕴含着强大的灵魂能量,但也被魔神的黑暗力量所侵蚀。", + "objectives": [ + { "id": "find_mage", "description": "找到疯狂的法师", "type": "talk", "target": "mad_mage", "completed": false }, + { "id": "enter_cave", "description": "进入暗光洞穴", "type": "reach", "target": "darklight_cave", "completed": false }, + { "id": "kill_daedra", "description": "消灭洞穴中的魔族", "type": "kill", "target": "draugr_wight", "quantity": 5, "current": 0, "completed": false }, + { "id": "find_star", "description": "找到黑色星辰", "type": "collect", "target": "black_star", "quantity": 1, "completed": false }, + { "id": "choose", "description": "做出选择:净化或保留", "type": "interact", "target": "black_star", "completed": false } + ], + "rewards": { + "gold": 1500, + "xp": 2000, + "items": [{ "id": "black_star", "quantity": 1 }] + } + }, + "radiant_bounty_bandits": { + "id": "radiant_bounty_bandits", + "name": "悬赏: 强盗", + "type": "radiant", + "level": 5, + "description": "白漫城的守卫发布了一则悬赏,要求清除出没在城外的强盗团伙。", + "objectives": [ + { "id": "kill_outlaws", "description": "消灭强盗逃犯", "type": "kill", "target": "bandit_outlaw", "quantity": 5, "current": 0, "completed": false }, + { "id": "report_guard", "description": "向守卫报告", "type": "talk", "target": "whiterun_guard", "completed": false } + ], + "rewards": { + "gold": 300, + "xp": 400, + "items": [{ "id": "steel_sword", "quantity": 1 }] + } } } } diff --git a/src/data/spells/spells.json b/src/data/spells/spells.json index 0bbe753..a895d48 100644 --- a/src/data/spells/spells.json +++ b/src/data/spells/spells.json @@ -220,6 +220,58 @@ "level": 10, "description": "大幅增加护甲值", "effects": [{ "type": "fortify", "attribute": "armor", "magnitude": 40, "duration": 60000 }] + }, + "frost_breath": { + "id": "frost_breath", + "name": "寒霜吐息", + "school": "destruction", + "type": "target", + "magickaCost": 15, + "magnitude": 5, + "duration": 0, + "cooldown": 500, + "level": 1, + "description": "喷出寒霜之息,造成冰霜伤害并减速", + "effects": [{ "type": "damage", "magnitude": 5 }, { "type": "slow", "magnitude": 0.3, "duration": 3000 }] + }, + "poison_spit": { + "id": "poison_spit", + "name": "毒液喷射", + "school": "destruction", + "type": "target", + "magickaCost": 0, + "magnitude": 7, + "duration": 3000, + "cooldown": 4000, + "level": 1, + "description": "喷射毒液,造成持续毒素伤害", + "effects": [{ "type": "damage", "magnitude": 7 }, { "type": "slow", "magnitude": 0.2, "duration": 3000 }] + }, + "power_slam": { + "id": "power_slam", + "name": "猛击", + "school": "destruction", + "type": "target", + "magickaCost": 0, + "magnitude": 24, + "duration": 0, + "cooldown": 5000, + "level": 1, + "description": "猛烈一击,造成大量伤害并击晕目标", + "effects": [{ "type": "damage", "magnitude": 24 }] + }, + "undead_rage": { + "id": "undead_rage", + "name": "尸鬼狂暴", + "school": "alteration", + "type": "self", + "magickaCost": 0, + "magnitude": 6, + "duration": 10000, + "cooldown": 30000, + "level": 1, + "description": "激发远古怒火,提升攻击力", + "effects": [{ "type": "fortify", "attribute": "damage", "magnitude": 6, "duration": 10000 }] } } } diff --git a/src/data/world/zones.json b/src/data/world/zones.json index 122eb21..0fe38b7 100644 --- a/src/data/world/zones.json +++ b/src/data/world/zones.json @@ -196,7 +196,9 @@ "treeTile": 14, "bushTile": 15 }, - "doors": [], + "doors": [ + { "x": 0, "y": 20, "targetZone": "whiterun_exterior", "targetX": 20, "targetY": 0, "width": 2, "label": "通往白漫城外" } + ], "entities": [ { "type": "enemy", "id": "wolf_over_01", "x": 10, "y": 10, "data": { "type": "wolf" } }, { "type": "enemy", "id": "wolf_over_02", "x": 12, "y": 11, "data": { "type": "wolf" } }, diff --git a/src/mods/ModE2E.test.ts b/src/mods/ModE2E.test.ts new file mode 100644 index 0000000..4792b4b --- /dev/null +++ b/src/mods/ModE2E.test.ts @@ -0,0 +1,377 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { dataRegistry } from '../data/DataRegistry'; +import { modLoader } from './ModLoader'; + +describe('Mod System E2E — Data Override Pipeline', () => { + beforeEach(() => { + modLoader.clearForTests(); + dataRegistry.resetForTests(); + }); + + // ── 1. Override existing spells ────────────────────────────────── + + it('overrides an existing spell via mod data', async () => { + // Base flames spell has magnitude 8 + const base = dataRegistry.getSpell('flames'); + expect(base?.magnitude).toBe(8); + + await modLoader.loadMod( + { id: 'spell-rebalance', name: 'Spell Rebalance', version: '1.0.0' }, + { + spells: { + flames: { + id: 'flames', + name: '烈焰 (增强)', + school: 'destruction', + type: 'target', + magickaCost: 15, + magnitude: 20, + duration: 0, + cooldown: 400, + level: 1, + description: '增强版烈焰', + effects: [{ type: 'damage', magnitude: 20 }], + }, + }, + } + ); + + await dataRegistry.loadAll(); + + const overridden = dataRegistry.getSpell('flames'); + expect(overridden?.name).toBe('烈焰 (增强)'); + expect(overridden?.magnitude).toBe(20); + expect(overridden?.magickaCost).toBe(15); + }); + + // ── 2. Add new items ───────────────────────────────────────────── + + it('adds new items without affecting existing ones', async () => { + const baseCount = dataRegistry.getAllItems().length; + + await modLoader.loadMod( + { id: 'extra-gear', name: 'Extra Gear', version: '1.0.0' }, + { + items: { + diamond_sword: { + name: '钻石剑', + type: 'weapon', + subtype: 'one_handed_sword', + damage: 25, + speed: 1.2, + weight: 6, + value: 500, + }, + diamond_armor: { + name: '钻石甲', + type: 'armor', + subtype: 'heavy_chest', + rating: 30, + weight: 18, + value: 800, + }, + }, + } + ); + + await dataRegistry.loadAll(); + + const newCount = dataRegistry.getAllItems().length; + expect(newCount).toBe(baseCount + 2); + + const diamondSword = dataRegistry.getItem('diamond_sword'); + expect(diamondSword?.name).toBe('钻石剑'); + expect(diamondSword?.effects).toContainEqual({ type: 'damage', magnitude: 25 }); + + // Original items still exist + const ironSword = dataRegistry.getItem('iron_sword'); + expect(ironSword).toBeDefined(); + }); + + // ── 3. Override enemy data ─────────────────────────────────────── + + it('overrides enemy stats via mod data', async () => { + const baseWolf = dataRegistry.getEnemy('wolf'); + expect(baseWolf?.health).toBe(40); + expect(baseWolf?.name).toBe('狼'); + + await modLoader.loadMod( + { id: 'hardcore-wolves', name: 'Hardcore Wolves', version: '1.0.0' }, + { + enemies: { + wolf: { + name: '暗影狼', + health: 120, + damage: 18, + level: 10, + stamina: 80, + armor: 15, + detectionRange: 250, + attackRange: 45, + attackSpeed: 1.3, + color: '#440044', + size: 30, + loot: { + gold: { min: 20, max: 60 }, + items: [{ id: 'health_potion', chance: 0.5 }], + }, + }, + }, + } + ); + + await dataRegistry.loadAll(); + + const wolf = dataRegistry.getEnemy('wolf'); + expect(wolf?.name).toBe('暗影狼'); + expect(wolf?.health).toBe(120); + expect(wolf?.damage).toBe(18); + expect(wolf?.level).toBe(10); + + // Other enemies unaffected + const bandit = dataRegistry.getEnemy('bandit'); + expect(bandit?.name).toBe('强盗'); + }); + + // ── 4. Override game config ────────────────────────────────────── + + it('overrides game config combat parameters', async () => { + const baseConfig = dataRegistry.getGameConfig(); + expect(baseConfig.combat.baseDamage).toBe(10); + expect(baseConfig.combat.critBaseChance).toBe(0.1); + + await modLoader.loadMod( + { id: 'combat-overhaul', name: 'Combat Overhaul', version: '1.0.0' }, + { + gameConfig: { + combat: { + baseDamage: 15, + skillBonus: 0.8, + powerAttackMultiplier: 2.0, + critBaseChance: 0.15, + critPerSneakSkill: 0.008, + armorDivisor: 80, + damageVariance: [0.85, 1.15], + blockDamageMultiplier: 0.15, + blockBaseChance: 0.35, + blockSkillBonus: 0.6, + blockStaminaThreshold: 12, + blockStaminaPenalty: -0.25, + blockStaminaCost: 8, + attackRanges: { melee: 65, unarmed: 50 }, + staminaCosts: { powerAttack: 30, normal: 8 }, + weaponDamageThresholds: { twoHanded: 8, powerAttack: 18 }, + skillImprovementAmounts: { attack: 0.6, armor: 0.4 }, + }, + leveling: baseConfig.leveling, + regen: baseConfig.regen, + }, + } + ); + + await dataRegistry.loadAll(); + + const config = dataRegistry.getGameConfig(); + expect(config.combat.baseDamage).toBe(15); + expect(config.combat.critBaseChance).toBe(0.15); + expect(config.combat.powerAttackMultiplier).toBe(2.0); + expect(config.combat.attackRanges.melee).toBe(65); + }); + + // ── 5. Multiple mods with priority ordering ────────────────────── + + it('applies mods in priority order — later mods win', async () => { + // Mod A (priority 200) sets flames magnitude to 30 + await modLoader.loadMod( + { id: 'mod-a', name: 'Mod A', version: '1.0.0', priority: 200 }, + { + spells: { + flames: { + id: 'flames', + name: 'Mod A 烈焰', + school: 'destruction', + type: 'target', + magickaCost: 20, + magnitude: 30, + duration: 0, + cooldown: 500, + level: 1, + description: 'Mod A version', + effects: [{ type: 'damage', magnitude: 30 }], + }, + }, + } + ); + + // Mod B (priority 100) sets flames magnitude to 50 — loads first but lower priority number = earlier + // Wait — lower priority number = loaded FIRST, so it gets overwritten by higher priority + // Actually: mods sorted by priority ascending. So priority 100 loads before priority 200. + // Priority 200 (mod-a) loads after priority 100 (mod-b), so mod-a wins. + await modLoader.loadMod( + { id: 'mod-b', name: 'Mod B', version: '1.0.0', priority: 100 }, + { + spells: { + flames: { + id: 'flames', + name: 'Mod B 烈焰', + school: 'destruction', + type: 'target', + magickaCost: 20, + magnitude: 50, + duration: 0, + cooldown: 500, + level: 1, + description: 'Mod B version', + effects: [{ type: 'damage', magnitude: 50 }], + }, + }, + } + ); + + await dataRegistry.loadAll(); + + const spell = dataRegistry.getSpell('flames'); + // Priority 200 (mod-a) loads after priority 100 (mod-b), so mod-a wins + expect(spell?.name).toBe('Mod A 烈焰'); + expect(spell?.magnitude).toBe(30); + }); + + // ── 6. System reloads after mod data change ────────────────────── + + it('systems pick up mod data after mod:dataResolved event', async () => { + // Verify the magic system loads from DataRegistry + const { magicSystem } = await import('../systems/MagicSystem'); + + const baseSpell = magicSystem.getSpell('flames'); + expect(baseSpell?.magnitude).toBe(8); + + // Load mod that overrides flames + await modLoader.loadMod( + { id: 'system-test', name: 'System Test', version: '1.0.0' }, + { + spells: { + flames: { + id: 'flames', + name: '超级烈焰', + school: 'destruction', + type: 'target', + magickaCost: 10, + magnitude: 99, + duration: 0, + cooldown: 100, + level: 1, + description: '系统测试版', + effects: [{ type: 'damage', magnitude: 99 }], + }, + }, + } + ); + + await dataRegistry.loadAll(); + + // After loadAll → applyModData → mod:dataResolved event → MagicSystem reloads + const moddedSpell = magicSystem.getSpell('flames'); + expect(moddedSpell?.name).toBe('超级烈焰'); + expect(moddedSpell?.magnitude).toBe(99); + }); + + // ── 7. Mod adding shouts ───────────────────────────────────────── + + it('adds new shouts via mod data', async () => { + const baseShouts = dataRegistry.getAllShouts(); + const baseCount = baseShouts.length; + + await modLoader.loadMod( + { id: 'extra-shouts', name: 'Extra Shouts', version: '1.0.0' }, + { + shouts: { + custom_shout: { + id: 'custom_shout', + name: '自定义龙吼', + words: ['自定义', '龙吼', '力量'], + wordCount: 3, + cooldown: 10, + effects: [{ type: 'damage', magnitude: 50 }, { type: 'push', magnitude: 200 }], + }, + }, + } + ); + + await dataRegistry.loadAll(); + + const newShouts = dataRegistry.getAllShouts(); + expect(newShouts.length).toBe(baseCount + 1); + + const custom = dataRegistry.getShout('custom_shout'); + expect(custom?.name).toBe('自定义龙吼'); + expect(custom?.wordCount).toBe(3); + }); + + // ── 8. Mod overriding transforms ───────────────────────────────── + + it('overrides transform data via mod data', async () => { + const baseTransform = dataRegistry.getTransform('werewolf'); + expect(baseTransform).toBeDefined(); + + await modLoader.loadMod( + { id: 'werewolf-rebalance', name: 'Werewolf Rebalance', version: '1.0.0' }, + { + transforms: { + werewolf: { + id: 'werewolf', + name: '超级狼人', + healthBonus: 200, + staminaBonus: 100, + armorBonus: 25, + speedBonus: 100, + damageBonus: 40, + durationMs: 240000, + cooldownMs: 60000, + weaponDamage: 30, + }, + }, + } + ); + + await dataRegistry.loadAll(); + + const werewolf = dataRegistry.getTransform('werewolf'); + expect(werewolf?.name).toBe('超级狼人'); + expect(werewolf?.healthBonus).toBe(200); + expect(werewolf?.durationMs).toBe(240000); + }); + + // ── 9. Mod with dependency loads correctly ─────────────────────── + + it('loads mod with satisfied dependency', async () => { + // Load dependency first + await modLoader.loadMod( + { id: 'base-mod', name: 'Base Mod', version: '1.0.0' }, + { items: { base_item: { name: '基础物品', type: 'misc', weight: 1, value: 10 } } } + ); + + // Load mod that depends on base-mod + const result = await modLoader.loadMod( + { id: 'addon-mod', name: 'Addon Mod', version: '1.0.0', dependencies: ['base-mod'] }, + { items: { addon_item: { name: '附属物品', type: 'misc', weight: 2, value: 20 } } } + ); + + expect(result).toBe(true); + + await dataRegistry.loadAll(); + + expect(dataRegistry.getItem('base_item')).toBeDefined(); + expect(dataRegistry.getItem('addon_item')).toBeDefined(); + }); + + // ── 10. Mod with unsatisfied dependency fails ──────────────────── + + it('rejects mod with unsatisfied dependency', async () => { + const result = await modLoader.loadMod( + { id: 'orphan-mod', name: 'Orphan Mod', version: '1.0.0', dependencies: ['nonexistent'] }, + { items: { orphan_item: { name: '孤儿物品', type: 'misc', weight: 1, value: 10 } } } + ); + + expect(result).toBe(false); + }); +}); diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 6610e17..7fe9f63 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -23,6 +23,14 @@ import { combatUI } from '../ui/components/CombatUI'; import { uiManager } from '../ui/UIManager'; import { worldMapUI } from '../ui/components/WorldMapUI'; import { craftingUI } from '../ui/components/CraftingUI'; +import { questJournalUI } from '../ui/components/QuestJournalUI'; +import { magicUI } from '../ui/components/MagicUI'; +import { statusEffectHUD } from '../ui/components/StatusEffectHUD'; +import { timeDisplayHUD } from '../ui/components/TimeDisplayHUD'; +import { radialMenuUI } from '../ui/components/RadialMenuUI'; +import { compassHUD } from '../ui/components/CompassHUD'; +import { T } from '../ui/theme'; +import { skillTreeUI } from '../ui/components/SkillTreeUI'; export class GameScene extends Phaser.Scene { private player!: Phaser.GameObjects.Rectangle; @@ -40,10 +48,13 @@ export class GameScene extends Phaser.Scene { private shoutKey!: Phaser.Input.Keyboard.Key; private craftingKey!: Phaser.Input.Keyboard.Key; private transformKey!: Phaser.Input.Keyboard.Key; + private journalKey!: Phaser.Input.Keyboard.Key; + private magicKey!: Phaser.Input.Keyboard.Key; private nearbyEntities = { enemy: null, corpse: null, item: null, container: null, npc: null } as { enemy: Entity | null; corpse: Entity | null; item: Entity | null; container: Entity | null; npc: Entity | null }; private mapTiles: Phaser.GameObjects.GameObject[] = []; private currentZone: MapZone | null = null; private isDead: boolean = false; + private isUIOpen: boolean = false; constructor() { super({ key: 'GameScene' }); @@ -59,6 +70,18 @@ export class GameScene extends Phaser.Scene { regenSystem.setupWithEventBus(eventBus); + // Initialize always-on HUDs (singletons auto-register on import) + void statusEffectHUD; + void timeDisplayHUD; + compassHUD.start(); + + radialMenuUI.registerItems([ + { id: 'skills', label: '技能', key: '↑', color: T.textGold, action: () => skillTreeUI.toggle() }, + { id: 'magic', label: '魔法', key: '←', color: '#7050c0', action: () => magicUI.toggle() }, + { id: 'items', label: '物品', key: '→', color: T.success, action: () => uiManager.toggleInventory() }, + { id: 'map', label: '地图', key: '↓', color: T.smithing, action: () => worldMapUI.toggle() }, + ]); + scriptSystem.initialize(); inventorySystem.initializeInventory(this.playerEntity); @@ -222,9 +245,42 @@ export class GameScene extends Phaser.Scene { entityManager.addComponent(enemy.id, { type: 'position', x: cx, y: cy }); entityManager.addComponent(enemy.id, { type: 'health', current: stats.health, max: stats.health }); entityManager.addComponent(enemy.id, { type: 'enemyType', name: stats.name, id: stats.id }); - entityManager.addComponent(enemy.id, { type: 'ai', state: 'idle', detectionRange: stats.detectionRange, attackRange: stats.attackRange, attackCooldown: Math.max(250, 1000 / stats.attackSpeed), lastAttackTime: 0 }); + entityManager.addComponent(enemy.id, { type: 'ai', state: 'idle', detectionRange: stats.detectionRange, attackRange: stats.attackRange, attackCooldown: Math.max(250, 1000 / stats.attackSpeed), lastAttackTime: 0, aiBehavior: stats.aiBehavior }); entityManager.addComponent(enemy.id, { type: 'weapon', id: 'fists', damage: stats.damage, speed: 1.0 }); entityManager.addComponent(enemy.id, { type: 'armor', rating: stats.armor }); + entityManager.addComponent(enemy.id, { type: 'level', level: stats.level }); + if (stats.magicka) { + entityManager.addComponent(enemy.id, { type: 'magicka', current: stats.magicka, max: stats.magicka }); + } + const isCaster = stats.aiBehavior === 'caster' || stats.aiBehavior === 'mixed'; + entityManager.addComponent(enemy.id, { + type: 'skills', + oneHanded: isCaster ? stats.level : stats.level * 2, + twoHanded: isCaster ? stats.level : stats.level * 2, + archery: stats.level, + block: stats.level, + heavyArmor: stats.level, + lightArmor: stats.level, + destruction: isCaster ? stats.level * 2 : stats.level, + conjuration: isCaster ? stats.level : 0, + illusion: 0, + alteration: 0, + restoration: isCaster ? stats.level : 0, + enchanting: 0, + sneak: stats.level, + lockpicking: 0, + pickpocket: 0, + speech: 0, + alchemy: 0, + smithing: 0, + }); + if (stats.abilities && stats.abilities.length > 0) { + entityManager.addComponent(enemy.id, { + type: 'enemyAbility', + abilities: stats.abilities.map(a => ({ ...a })), + cooldowns: Object.fromEntries(stats.abilities.map(a => [a.id, 0])), + }); + } if (typeof entityData.data.script === 'string') { entityManager.addComponent(enemy.id, { type: 'script', scriptId: entityData.data.script }); } @@ -271,6 +327,8 @@ export class GameScene extends Phaser.Scene { this.shoutKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q); this.craftingKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.C); this.transformKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.T); + this.journalKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.J); + this.magicKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.X); // Mouse — left click attack, right click block this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => { @@ -367,6 +425,8 @@ export class GameScene extends Phaser.Scene { update(_time: number, delta: number): void { if (this.isDead) return; + this.isUIOpen = radialMenuUI.getIsOpen() || uiManager.getIsInventoryOpen() || worldMapUI.getIsOpen() || craftingUI.getIsOpen() || questJournalUI.getIsOpen() || magicUI.getIsOpen() || skillTreeUI.getIsOpen(); + eventBus.emit('game:update', { delta, time: _time }); regenSystem.update(delta); @@ -379,12 +439,14 @@ export class GameScene extends Phaser.Scene { this.applyDayNightTint(); // Movement — keyboard - movementSystem.movePlayer(this.playerEntity, { - up: this.cursors?.up.isDown || this.wasd?.W.isDown || false, - down: this.cursors?.down.isDown || this.wasd?.S.isDown || false, - left: this.cursors?.left.isDown || this.wasd?.A.isDown || false, - right: this.cursors?.right.isDown || this.wasd?.D.isDown || false, - }, delta); + if (!this.isUIOpen) { + movementSystem.movePlayer(this.playerEntity, { + up: this.cursors?.up.isDown || this.wasd?.W.isDown || false, + down: this.cursors?.down.isDown || this.wasd?.S.isDown || false, + left: this.cursors?.left.isDown || this.wasd?.A.isDown || false, + right: this.cursors?.right.isDown || this.wasd?.D.isDown || false, + }, delta); + } // Sync sprite const pos = entityManager.getComponent<{ x: number; y: number }>(this.playerEntity.id, 'position'); @@ -499,6 +561,7 @@ export class GameScene extends Phaser.Scene { } private handleCombat(): void { + if (this.isUIOpen) return; // Space bar attack (keyboard fallback) if (!this.nearbyEntities.enemy) return; if (this.attackKey && Phaser.Input.Keyboard.JustDown(this.attackKey)) { @@ -571,9 +634,18 @@ export class GameScene extends Phaser.Scene { } private handleInputToggles(): void { - if (this.inventoryKey && Phaser.Input.Keyboard.JustDown(this.inventoryKey)) uiManager.toggleInventory(); + if (this.isUIOpen) { + if (this.inventoryKey && Phaser.Input.Keyboard.JustDown(this.inventoryKey)) { + radialMenuUI.hide(); + uiManager.toggleInventory(); + } + return; + } + if (this.inventoryKey && Phaser.Input.Keyboard.JustDown(this.inventoryKey)) radialMenuUI.toggle(); if (this.mapKey && Phaser.Input.Keyboard.JustDown(this.mapKey)) worldMapUI.toggle(); if (this.craftingKey && Phaser.Input.Keyboard.JustDown(this.craftingKey)) craftingUI.toggle('smithing'); + if (this.journalKey && Phaser.Input.Keyboard.JustDown(this.journalKey)) questJournalUI.toggle(); + if (this.magicKey && Phaser.Input.Keyboard.JustDown(this.magicKey)) magicUI.toggle(); if (this.transformKey && Phaser.Input.Keyboard.JustDown(this.transformKey)) { if (transformationSystem.isTransformed(this.playerEntity.id)) { transformationSystem.revert(this.playerEntity.id); diff --git a/src/systems/AISystem.ts b/src/systems/AISystem.ts index 64c03dd..4eb8016 100644 --- a/src/systems/AISystem.ts +++ b/src/systems/AISystem.ts @@ -1,8 +1,9 @@ import { entityManager, type Entity } from '../core/EntityManager'; import { combatSystem } from './CombatSystem'; import { corpseSystem } from './CorpseSystem'; +import type { EnemyAbility } from '../data/DataRegistry'; -export type AIState = 'idle' | 'chase' | 'attack' | 'retreat' | 'patrol'; +export type AIState = 'idle' | 'chase' | 'attack' | 'retreat' | 'patrol' | 'stunned'; export interface AIComponent { type: 'ai'; @@ -12,6 +13,10 @@ export interface AIComponent { attackCooldown: number; lastAttackTime: number; targetId?: string; + spawnPosition?: { x: number; y: number }; + patrolTarget?: { x: number; y: number }; + patrolWaitUntil?: number; + aiBehavior?: 'melee' | 'ranged' | 'caster' | 'mixed'; } export class AISystem { @@ -55,19 +60,39 @@ export class AISystem { delta: number, player: Entity ): void { + // Record spawn position on first tick + if (!ai.spawnPosition) { + ai.spawnPosition = { x: pos.x, y: pos.y }; + } + + const health = entityManager.getComponent<{ current: number; max: number }>(enemy.id, 'health'); + const hpRatio = health ? health.current / health.max : 1; + switch (ai.state) { + case 'stunned': + break; case 'idle': + if (hpRatio < 0.25) { + ai.state = 'retreat'; + break; + } if (distanceToPlayer < ai.detectionRange) { ai.state = 'chase'; + break; } + // Patrol: wander near spawn + this.updatePatrol(ai, pos, delta); break; case 'chase': + if (hpRatio < 0.25) { + ai.state = 'retreat'; + break; + } if (distanceToPlayer > ai.detectionRange * 1.5) { ai.state = 'idle'; break; } - if (distanceToPlayer <= ai.attackRange) { ai.state = 'attack'; } else { @@ -78,30 +103,155 @@ export class AISystem { const dy = playerPos.y - pos.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 0) { - const nx = dx / dist; - const ny = dy / dist; - pos.x += nx * speed * (delta / 1000); - pos.y += ny * speed * (delta / 1000); + pos.x += (dx / dist) * speed * (delta / 1000); + pos.y += (dy / dist) * speed * (delta / 1000); } } } break; case 'attack': + if (hpRatio < 0.25) { + ai.state = 'retreat'; + break; + } if (distanceToPlayer > ai.attackRange * 1.2) { ai.state = 'chase'; break; } - const now = Date.now(); if (now - ai.lastAttackTime > ai.attackCooldown) { - combatSystem.performAttack(enemy, player, false); + const abilityUsed = this.tryUseAbility(enemy, ai, player, hpRatio); + if (!abilityUsed) { + combatSystem.performAttack(enemy, player, false); + } ai.lastAttackTime = now; } break; + + case 'retreat': { + const safeDistance = ai.detectionRange * 1.5; + if (distanceToPlayer >= safeDistance || hpRatio >= 0.5) { + ai.state = 'idle'; + ai.patrolWaitUntil = Date.now() + 2000; + break; + } + this.tryRetreatAbility(enemy, ai, player, hpRatio); + const retreatSpeed = 100; + const pp = entityManager.getComponent<{ x: number; y: number }>(player.id, 'position'); + if (pp) { + const dx = pos.x - pp.x; + const dy = pos.y - pp.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 0) { + pos.x += (dx / dist) * retreatSpeed * (delta / 1000); + pos.y += (dy / dist) * retreatSpeed * (delta / 1000); + } + } + break; + } + + case 'patrol': + this.updatePatrol(ai, pos, delta); + if (distanceToPlayer < ai.detectionRange) { + ai.state = 'chase'; + } + break; } } + private tryUseAbility(enemy: Entity, _ai: AIComponent, player: Entity, hpRatio: number): boolean { + const enemyAbility = entityManager.getComponent<{ abilities: EnemyAbility[]; cooldowns: Record }>(enemy.id, 'enemyAbility'); + if (!enemyAbility) return false; + + const now = Date.now(); + for (const ability of enemyAbility.abilities) { + if (ability.type === 'passive') continue; + const lastUse = enemyAbility.cooldowns[ability.id] || 0; + if (now - lastUse < ability.cooldown) continue; + + if (!this.checkAbilityCondition(ability.condition, hpRatio)) continue; + + if (ability.type === 'spell' && ability.spellId) { + const magicka = entityManager.getComponent<{ current: number }>(enemy.id, 'magicka'); + const spell = (globalThis as any).__oesMagicSystem?.getSpell?.(ability.spellId); + if (!magicka || (spell && magicka.current < spell.magickaCost)) continue; + } + + enemyAbility.cooldowns[ability.id] = now; + combatSystem.performAbilityCaster(enemy, player, ability); + return true; + } + return false; + } + + private tryRetreatAbility(enemy: Entity, _ai: AIComponent, _player: Entity, _hpRatio: number): void { + const enemyAbility = entityManager.getComponent<{ abilities: EnemyAbility[]; cooldowns: Record }>(enemy.id, 'enemyAbility'); + if (!enemyAbility) return; + + const now = Date.now(); + for (const ability of enemyAbility.abilities) { + if (ability.type !== 'spell' || !ability.spellId) continue; + if (ability.effect !== 'heal' && ability.spellId !== 'healing') continue; + + const lastUse = enemyAbility.cooldowns[ability.id] || 0; + if (now - lastUse < ability.cooldown) continue; + + const magicka = entityManager.getComponent<{ current: number }>(enemy.id, 'magicka'); + const spell = (globalThis as any).__oesMagicSystem?.getSpell?.(ability.spellId); + if (!magicka || (spell && magicka.current < spell.magickaCost)) continue; + + enemyAbility.cooldowns[ability.id] = now; + combatSystem.performAbilityCaster(enemy, enemy, ability); + return; + } + } + + private checkAbilityCondition(condition: string | undefined, hpRatio: number): boolean { + switch (condition) { + case 'hp_below_50': return hpRatio < 0.5; + case 'hp_below_25': return hpRatio < 0.25; + case 'hp_below_40': return hpRatio < 0.4; + case 'hp_below_60': return hpRatio < 0.6; + case 'on_spawn': return false; + case 'on_hit': return false; + case 'always': return true; + default: return true; + } + } + + private updatePatrol(ai: AIComponent, pos: { x: number; y: number }, delta: number): void { + const now = Date.now(); + + // Wait if we just finished something + if (ai.patrolWaitUntil && now < ai.patrolWaitUntil) return; + + // Pick a new patrol target if we don't have one or reached it + if (!ai.patrolTarget) { + const spawn = ai.spawnPosition || pos; + const angle = Math.random() * Math.PI * 2; + const radius = Math.random() * 100; + ai.patrolTarget = { x: spawn.x + Math.cos(angle) * radius, y: spawn.y + Math.sin(angle) * radius }; + ai.patrolWaitUntil = now + 500; + return; + } + + const dx = ai.patrolTarget.x - pos.x; + const dy = ai.patrolTarget.y - pos.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 5) { + // Reached target, wait then pick new one + ai.patrolTarget = undefined; + ai.patrolWaitUntil = now + 2000 + Math.random() * 3000; + return; + } + + const patrolSpeed = 30; + pos.x += (dx / dist) * patrolSpeed * (delta / 1000); + pos.y += (dy / dist) * patrolSpeed * (delta / 1000); + } + setEnemyAI(entity: Entity, config: Partial): void { entityManager.addComponent(entity.id, { type: 'ai', @@ -110,6 +260,7 @@ export class AISystem { attackRange: config.attackRange || 45, attackCooldown: config.attackCooldown || 1000, lastAttackTime: 0, + aiBehavior: config.aiBehavior, ...config, }); } diff --git a/src/systems/CombatSystem.ts b/src/systems/CombatSystem.ts index bf611e6..c02de60 100644 --- a/src/systems/CombatSystem.ts +++ b/src/systems/CombatSystem.ts @@ -14,7 +14,7 @@ export class CombatSystem { private static instance: CombatSystem; private attackCooldown: number = 500; private lastAttackTime: number = 0; - private config = dataRegistry.getGameConfig().combat; + private config!: ReturnType['combat']; static getInstance(): CombatSystem { if (!CombatSystem.instance) { @@ -24,6 +24,7 @@ export class CombatSystem { } constructor() { + this.config = dataRegistry.getGameConfig().combat; eventBus.on('mod:dataResolved', () => { this.config = dataRegistry.getGameConfig().combat; }); @@ -277,6 +278,73 @@ export class CombatSystem { entityManager.addComponent(entityId, { type: 'blocking', isBlocking }); } } + + performAbilityCaster(enemy: Entity, target: Entity, ability: { id: string; spellId?: string; type: string; damage?: number; effect?: string; effectDuration?: number; effectMagnitude?: number }): boolean { + if (ability.type === 'spell' && ability.spellId) { + const magicSystem = (globalThis as any).__oesMagicSystem; + if (magicSystem) { + return magicSystem.castSpell(enemy, ability.spellId, target); + } + return false; + } + + if (ability.type === 'attack') { + const damage = ability.damage || 0; + const targetHealth = entityManager.getComponent<{ current: number }>(target.id, 'health'); + if (targetHealth) { + const armor = entityManager.getComponent<{ rating: number }>(target.id, 'armor'); + const armorReduction = armor ? armor.rating / (armor.rating + 100) : 0; + const finalDamage = Math.max(1, Math.round(damage * (1 - armorReduction))); + targetHealth.current = Math.max(0, targetHealth.current - finalDamage); + eventBus.emit('combat:hit', { attacker: enemy, target, damage: finalDamage, isCritical: false }); + if (ability.effect === 'stun' && ability.effectDuration) { + const ai = entityManager.getComponent<{ state: string }>(target.id, 'ai'); + if (ai) { + ai.state = 'stunned'; + setTimeout(() => { + if (ai.state === 'stunned') ai.state = 'chase'; + }, ability.effectDuration); + } + } + if (ability.effect === 'poison' && ability.effectDuration && ability.effectMagnitude) { + const tickDamage = ability.effectMagnitude; + const interval = 1000; + const ticks = Math.floor(ability.effectDuration / interval); + for (let i = 0; i < ticks; i++) { + setTimeout(() => { + const h = entityManager.getComponent<{ current: number }>(target.id, 'health'); + if (h && h.current > 0) { + h.current = Math.max(0, h.current - tickDamage); + if (h.current <= 0) { + eventBus.emit('entity:killed', { entity: target, killer: enemy }); + } + } + }, (i + 1) * interval); + } + } + if (targetHealth.current <= 0) { + eventBus.emit('entity:killed', { entity: target, killer: enemy }); + } + return true; + } + return false; + } + + if (ability.type === 'buff') { + const weapon = entityManager.getComponent<{ damage: number }>(enemy.id, 'weapon'); + if (weapon && ability.effectMagnitude) { + weapon.damage += ability.effectMagnitude; + if (ability.effectDuration) { + setTimeout(() => { + weapon.damage -= ability.effectMagnitude!; + }, ability.effectDuration); + } + } + return true; + } + + return false; + } } function randomInt(min: number, max: number): number { diff --git a/src/systems/CorpseSystem.ts b/src/systems/CorpseSystem.ts index 2281701..1982857 100644 --- a/src/systems/CorpseSystem.ts +++ b/src/systems/CorpseSystem.ts @@ -1,5 +1,6 @@ import { eventBus } from '../core/EventBus'; import { entityManager, type Entity } from '../core/EntityManager'; +import { dataRegistry } from '../data/DataRegistry'; export type CorpseState = 'fresh' | 'looted' | 'decaying' | 'skeleton' | 'gone'; @@ -77,28 +78,34 @@ export class CorpseSystem { private generateLoot(entity: Entity): any[] { const loot: any[] = []; - const enemyType = entityManager.getComponent<{ name: string }>(entity.id, 'enemyType'); + const enemyType = entityManager.getComponent<{ id: string }>(entity.id, 'enemyType'); + if (!enemyType) return loot; - if (enemyType) { - const gold = Math.floor(Math.random() * 20) + 5; - loot.push({ type: 'gold', amount: gold }); + const enemyData = dataRegistry.getEnemy(enemyType.id); + if (!enemyData?.loot) { + // Fallback: basic gold drop + loot.push({ type: 'gold', amount: Math.floor(Math.random() * 10) + 3 }); + return loot; + } - if (Math.random() < 0.3) { - loot.push({ - type: 'item', - id: 'health_potion', - name: '生命药水', - quantity: 1, - }); - } + const { gold, items } = enemyData.loot; - if (Math.random() < 0.1) { - loot.push({ - type: 'item', - id: 'iron_sword', - name: '铁剑', - quantity: 1, - }); + if (gold) { + const amount = Math.floor(Math.random() * (gold.max - gold.min + 1)) + gold.min; + if (amount > 0) loot.push({ type: 'gold', amount }); + } + + if (items) { + for (const entry of items) { + if (Math.random() < entry.chance) { + const itemData = dataRegistry.getItem(entry.id); + loot.push({ + type: 'item', + id: entry.id, + name: itemData?.name || entry.id, + quantity: entry.quantity || 1, + }); + } } } diff --git a/src/systems/GroundItemSystem.ts b/src/systems/GroundItemSystem.ts index caf5ad7..3f5cddf 100644 --- a/src/systems/GroundItemSystem.ts +++ b/src/systems/GroundItemSystem.ts @@ -1,5 +1,6 @@ import { eventBus } from '../core/EventBus'; import { entityManager, type Entity } from '../core/EntityManager'; +import { dataRegistry } from '../data/DataRegistry'; export interface GroundItem { type: 'groundItem'; @@ -154,45 +155,18 @@ export class GroundItemSystem { } private getItemName(itemId: string): string { - const names: Record = { - health_potion: '生命药水', - magicka_potion: '魔力药水', - stamina_potion: '耐力药水', - iron_sword: '铁剑', - steel_sword: '钢剑', - iron_dagger: '铁匕首', - bread: '面包', - gold_coin: '金币', - }; - return names[itemId] || itemId; + const item = dataRegistry.getItem(itemId); + return item?.name || itemId; } private getItemWeight(itemId: string): number { - const weights: Record = { - health_potion: 0.5, - magicka_potion: 0.5, - stamina_potion: 0.5, - iron_sword: 10, - steel_sword: 11, - iron_dagger: 3, - bread: 0.2, - gold_coin: 0, - }; - return weights[itemId] || 1; + const item = dataRegistry.getItem(itemId); + return item?.weight ?? 1; } private getItemValue(itemId: string): number { - const values: Record = { - health_potion: 25, - magicka_potion: 30, - stamina_potion: 20, - iron_sword: 50, - steel_sword: 120, - iron_dagger: 25, - bread: 5, - gold_coin: 1, - }; - return values[itemId] || 10; + const item = dataRegistry.getItem(itemId); + return item?.value ?? 10; } getAllItems(): Entity[] { diff --git a/src/systems/LegendarySystem.ts b/src/systems/LegendarySystem.ts index 2d53ffc..3edea97 100644 --- a/src/systems/LegendarySystem.ts +++ b/src/systems/LegendarySystem.ts @@ -21,8 +21,8 @@ export class LegendarySystem { this.onPlayerCreated(data.entity.id); }); - eventBus.on('skill:legendaryize', (data: { entityId: string; skillId: string }) => { - this.legendaryize(data.entityId, data.skillId); + eventBus.on('skill:legendary', (data: { entityId: string; skill: string }) => { + this.legendaryize(data.entityId, data.skill); }); } diff --git a/src/systems/LevelingSystem.ts b/src/systems/LevelingSystem.ts index 31788ca..af78867 100644 --- a/src/systems/LevelingSystem.ts +++ b/src/systems/LevelingSystem.ts @@ -4,7 +4,7 @@ import { dataRegistry } from '../data/DataRegistry'; export class LevelingSystem { private static instance: LevelingSystem; - private config = dataRegistry.getGameConfig().leveling; + private config!: ReturnType['leveling']; static getInstance(): LevelingSystem { if (!LevelingSystem.instance) { @@ -14,6 +14,7 @@ export class LevelingSystem { } constructor() { + this.config = dataRegistry.getGameConfig().leveling; eventBus.on('mod:dataResolved', () => { this.config = dataRegistry.getGameConfig().leveling; }); diff --git a/src/systems/MagicSystem.ts b/src/systems/MagicSystem.ts index 19bd64e..c15878f 100644 --- a/src/systems/MagicSystem.ts +++ b/src/systems/MagicSystem.ts @@ -19,7 +19,7 @@ export interface Spell { } export interface SpellEffect { - type: 'damage' | 'heal' | 'fortify' | 'weakness' | 'fear' | 'calm' | 'frenzy' | 'invisibility' | 'conjure' | 'bound' | 'transmute' | 'slow'; + type: 'damage' | 'heal' | 'fortify' | 'weakness' | 'fear' | 'calm' | 'frenzy' | 'invisibility' | 'conjure' | 'bound' | 'transmute' | 'slow' | 'stun' | 'poison'; attribute?: string; magnitude: number; duration?: number; @@ -244,6 +244,42 @@ export class MagicSystem { } break; } + case 'stun': { + if (target) { + const ai = entityManager.getComponent<{ state: string; lastAttackTime: number }>(target.id, 'ai'); + if (ai) { + ai.state = 'stunned'; + const duration = effect.duration || 1000; + setTimeout(() => { + if (ai.state === 'stunned') ai.state = 'chase'; + }, duration); + } + } + break; + } + case 'poison': { + if (target) { + const health = entityManager.getComponent<{ current: number }>(target.id, 'health'); + if (health && effect.duration) { + const tickDamage = effect.magnitude; + const interval = 1000; + const ticks = Math.floor(effect.duration / interval); + for (let i = 0; i < ticks; i++) { + setTimeout(() => { + const h = entityManager.getComponent<{ current: number }>(target.id, 'health'); + if (h && h.current > 0) { + h.current = Math.max(0, h.current - tickDamage); + eventBus.emit('combat:poisonDamage', { caster, target, damage: tickDamage }); + if (h.current <= 0) { + eventBus.emit('entity:killed', { entity: target, killer: caster }); + } + } + }, (i + 1) * interval); + } + } + } + break; + } } } diff --git a/src/systems/RegenSystem.ts b/src/systems/RegenSystem.ts index 2931a1d..67dd72a 100644 --- a/src/systems/RegenSystem.ts +++ b/src/systems/RegenSystem.ts @@ -9,7 +9,7 @@ import { dataRegistry } from '../data/DataRegistry'; */ export class RegenSystem { private static instance: RegenSystem; - private config = dataRegistry.getGameConfig().regen; + private config!: ReturnType['regen']; private lastDamageTime = 0; private lastStaminaUseTime = 0; @@ -23,6 +23,7 @@ export class RegenSystem { } constructor() { + this.config = dataRegistry.getGameConfig().regen; eventBus.on('mod:dataResolved', () => { this.config = dataRegistry.getGameConfig().regen; }); diff --git a/src/ui/UIManager.ts b/src/ui/UIManager.ts index c779255..33cc5fa 100644 --- a/src/ui/UIManager.ts +++ b/src/ui/UIManager.ts @@ -6,16 +6,12 @@ import { T, FONT, panelStyle } from './theme'; export class UIManager { private static instance: UIManager; private container: HTMLDivElement; - private hud: HTMLDivElement; private healthBar: HTMLDivElement; private magickaBar: HTMLDivElement; private staminaBar: HTMLDivElement; private interactPrompt: HTMLDivElement; - private goldDisplay: HTMLDivElement; - private weightDisplay: HTMLDivElement; private inventoryPanel: HTMLDivElement; private isInventoryOpen: boolean = false; - private levelDisplay: HTMLDivElement; private zoneDisplay: HTMLDivElement; static getInstance(): UIManager { @@ -39,17 +35,11 @@ export class UIManager { `; document.body.appendChild(this.container); - this.hud = this.createHUD(); - this.container.appendChild(this.hud); - - this.healthBar = this.createBar('health', 'oes-bar-fill--health', 240); - this.magickaBar = this.createBar('magicka', 'oes-bar-fill--magicka', 180); - this.staminaBar = this.createBar('stamina', 'oes-bar-fill--stamina', 180); + this.healthBar = this.createBar('health', 'oes-bar-fill--health', 240, { bottom: '20px', left: '20px' }); + this.staminaBar = this.createBar('stamina', 'oes-bar-fill--stamina', 240, { bottom: '20px', right: '20px' }); + this.magickaBar = this.createBar('magicka', 'oes-bar-fill--magicka', 180, { bottom: '50px', left: '20px' }); this.interactPrompt = this.createInteractPrompt(); - this.goldDisplay = this.createGoldDisplay(); - this.weightDisplay = this.createWeightDisplay(); this.inventoryPanel = this.createInventoryPanel(); - this.levelDisplay = this.createLevelDisplay(); this.zoneDisplay = this.createZoneDisplay(); this.createCrosshair(); @@ -57,26 +47,16 @@ export class UIManager { this.updateBars(); } - private createHUD(): HTMLDivElement { - const hud = document.createElement('div'); - hud.id = 'hud'; - hud.style.cssText = ` - position: absolute; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 3px; - `; - return hud; - } - - private createBar(name: string, fillClass: string, width: number): HTMLDivElement { + private createBar(name: string, fillClass: string, width: number, position: { bottom?: string; left?: string; right?: string }): HTMLDivElement { const outer = document.createElement('div'); outer.className = 'oes-bar-outer'; - outer.style.width = `${width}px`; + outer.style.cssText = ` + position: absolute; + width: ${width}px; + bottom: ${position.bottom || 'auto'}; + left: ${position.left || 'auto'}; + right: ${position.right || 'auto'}; + `; const fill = document.createElement('div'); fill.id = `${name}-bar-fill`; @@ -90,35 +70,11 @@ export class UIManager { outer.appendChild(fill); outer.appendChild(text); - this.hud.appendChild(outer); + this.container.appendChild(outer); return fill; } - private createLevelDisplay(): HTMLDivElement { - const el = document.createElement('div'); - el.id = 'level-display'; - el.style.cssText = ` - position: absolute; - top: 16px; - left: 50%; - transform: translateX(-50%); - padding: 6px 24px; - background: linear-gradient(180deg, #2a2420 0%, #1a1510 100%); - border: 1px solid ${T.borderBronze}; - border-radius: 3px; - color: ${T.textGold}; - font-size: 14px; - font-family: ${FONT.title}; - letter-spacing: 1px; - text-shadow: 0 1px 3px rgba(0,0,0,0.6); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - `; - el.textContent = '等级 1'; - this.container.appendChild(el); - return el; - } - private createZoneDisplay(): HTMLDivElement { const el = document.createElement('div'); el.id = 'zone-display'; @@ -171,56 +127,26 @@ export class UIManager { private createInteractPrompt(): HTMLDivElement { const prompt = document.createElement('div'); prompt.id = 'interact-prompt'; - prompt.className = 'oes-prompt'; - prompt.style.display = 'none'; - prompt.textContent = '[E] Interact'; - this.container.appendChild(prompt); - return prompt; - } - - private createGoldDisplay(): HTMLDivElement { - const display = document.createElement('div'); - display.id = 'gold-display'; - display.style.cssText = ` - position: absolute; - top: 16px; - right: 16px; - padding: 6px 14px; - background: linear-gradient(180deg, #2a2420 0%, #1a1510 100%); + prompt.style.cssText = ` + position: fixed; + bottom: 120px; + left: 50%; + transform: translateX(-50%); + padding: 8px 22px; + background: linear-gradient(180deg, #2a2420, #1a1510); border: 1px solid ${T.borderBronze}; border-radius: 3px; - color: ${T.goldAccent}; - font-size: 13px; - font-weight: 600; + color: ${T.textGold}; + font-size: 14px; font-family: ${FONT.body}; - text-shadow: 0 1px 2px rgba(0,0,0,0.6); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); + letter-spacing: 0.3px; + pointer-events: none; + z-index: 200; + display: none; `; - display.textContent = '金币: 0'; - this.container.appendChild(display); - return display; - } - - private createWeightDisplay(): HTMLDivElement { - const display = document.createElement('div'); - display.id = 'weight-display'; - display.style.cssText = ` - position: absolute; - top: 46px; - right: 16px; - padding: 6px 14px; - background: linear-gradient(180deg, #2a2420 0%, #1a1510 100%); - border: 1px solid ${T.borderIron}; - border-radius: 3px; - color: ${T.textMuted}; - font-size: 13px; - font-family: ${FONT.body}; - text-shadow: 0 1px 2px rgba(0,0,0,0.6); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - `; - display.textContent = '负重: 0/300'; - this.container.appendChild(display); - return display; + prompt.textContent = 'E 交互'; + this.container.appendChild(prompt); + return prompt; } private createInventoryPanel(): HTMLDivElement { @@ -254,35 +180,16 @@ export class UIManager { }); eventBus.on('inventory:updated', () => { - this.updateGoldDisplay(); - this.updateWeightDisplay(); if (this.isInventoryOpen) { this.renderInventory(); } }); - eventBus.on('player:created', () => { - this.updateGoldDisplay(); - this.updateWeightDisplay(); - this.updateLevelDisplay(); - }); - - eventBus.on('player:levelUp', (_data: { entityId: string; level: number }) => { - this.updateLevelDisplay(); - }); - eventBus.on('game:zoneChanged', (data: { zoneId: string }) => { this.updateZoneDisplay(data.zoneId); }); } - updateLevelDisplay(): void { - const player = entityManager.getEntitiesByType('player')[0]; - if (!player) return; - const level = entityManager.getComponent<{ level: number }>(player.id, 'level'); - if (level) this.levelDisplay.textContent = `等级 ${level.level}`; - } - updateZoneDisplay(zoneId: string): void { const zoneNames: Record = { whiterun: '白漫城', @@ -345,29 +252,6 @@ export class UIManager { setTimeout(() => toast.remove(), 2600); } - private updateGoldDisplay(): void { - const player = entityManager.getEntitiesByType('player')[0]; - if (!player) return; - - const inventory = inventorySystem.getInventory(player); - if (inventory) { - this.goldDisplay.textContent = `金币: ${inventory.gold}`; - } - } - - private updateWeightDisplay(): void { - const player = entityManager.getEntitiesByType('player')[0]; - if (!player) return; - - const inventory = inventorySystem.getInventory(player); - if (inventory) { - this.weightDisplay.textContent = `负重: ${inventory.carryWeight.toFixed(1)}/${inventory.maxCarryWeight}`; - this.weightDisplay.style.color = inventory.carryWeight > inventory.maxCarryWeight - ? T.danger - : T.textMuted; - } - } - toggleInventory(): void { this.isInventoryOpen = !this.isInventoryOpen; this.inventoryPanel.style.display = this.isInventoryOpen ? 'block' : 'none'; @@ -377,6 +261,10 @@ export class UIManager { } } + getIsInventoryOpen(): boolean { + return this.isInventoryOpen; + } + private renderInventory(): void { const player = entityManager.getEntitiesByType('player')[0]; if (!player) return; diff --git a/src/ui/components/CompassHUD.ts b/src/ui/components/CompassHUD.ts new file mode 100644 index 0000000..cd7748a --- /dev/null +++ b/src/ui/components/CompassHUD.ts @@ -0,0 +1,182 @@ +import { T, FONT } from '../theme'; +import { entityManager } from '../../core/EntityManager'; + +interface CompassMarker { + id: string; + label: string; + type: 'door' | 'enemy' | 'npc' | 'poi'; + angle: number; +} + +export class CompassHUD { + private static instance: CompassHUD; + private container: HTMLDivElement; + private markerContainer: HTMLDivElement; + private updateTimer: ReturnType | null = null; + + static getInstance(): CompassHUD { + if (!CompassHUD.instance) { + CompassHUD.instance = new CompassHUD(); + } + return CompassHUD.instance; + } + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'compass-hud'; + this.container.style.cssText = ` + position: fixed; + top: 8px; + left: 50%; + transform: translateX(-50%); + width: 400px; + height: 28px; + background: rgba(10,10,15,0.6); + border: 1px solid ${T.borderDark}; + border-radius: 3px; + overflow: hidden; + pointer-events: none; + z-index: 150; + `; + + const centerLine = document.createElement('div'); + centerLine.style.cssText = ` + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 2px; + height: 100%; + background: ${T.borderBronze}; + opacity: 0.6; + `; + this.container.appendChild(centerLine); + + this.markerContainer = document.createElement('div'); + this.markerContainer.style.cssText = ` + position: absolute; + inset: 0; + `; + this.container.appendChild(this.markerContainer); + + this.startUpdate(); + } + + start(): void { + if (!this.container.parentElement) { + document.body.appendChild(this.container); + } + this.startUpdate(); + } + + stop(): void { + if (this.updateTimer) { + clearInterval(this.updateTimer); + this.updateTimer = null; + } + } + + updateMarkers(markers: CompassMarker[]): void { + this.markerContainer.innerHTML = ''; + const width = 400; + const centerX = width / 2; + + for (const marker of markers) { + const offset = Math.max(-180, Math.min(180, marker.angle)); + const x = centerX + (offset / 180) * (width / 2); + + const el = document.createElement('div'); + el.style.cssText = ` + position: absolute; + top: 50%; + left: ${x}px; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + pointer-events: none; + `; + + const dot = document.createElement('div'); + const color = this.getMarkerColor(marker.type); + dot.style.cssText = ` + width: 6px; + height: 6px; + background: ${color}; + border-radius: 50%; + box-shadow: 0 0 4px ${color}; + `; + + const label = document.createElement('div'); + label.style.cssText = ` + font-size: 9px; + color: ${T.textMuted}; + font-family: ${FONT.body}; + white-space: nowrap; + margin-top: 1px; + `; + label.textContent = marker.label; + + el.appendChild(dot); + el.appendChild(label); + this.markerContainer.appendChild(el); + } + } + + private getMarkerColor(type: string): string { + switch (type) { + case 'door': return T.goldAccent; + case 'enemy': return T.danger; + case 'npc': return '#4488cc'; + default: return T.textMuted; + } + } + + private startUpdate(): void { + if (this.updateTimer) return; + this.updateTimer = setInterval(() => { + this.autoUpdate(); + }, 500); + } + + private autoUpdate(): void { + const player = entityManager.getEntitiesByType('player')[0]; + if (!player) return; + + const playerPos = entityManager.getComponent<{ x: number; y: number }>(player.id, 'position'); + if (!playerPos) return; + + const markers: CompassMarker[] = []; + const enemies = entityManager.getEntitiesByType('enemy'); + for (const enemy of enemies) { + const pos = entityManager.getComponent<{ x: number; y: number }>(enemy.id, 'position'); + const health = entityManager.getComponent<{ current: number }>(enemy.id, 'health'); + if (!pos || !health || health.current <= 0) continue; + const dx = pos.x - playerPos.x; + const dy = pos.y - playerPos.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 300) continue; + const angle = Math.atan2(dx, -dy) * (180 / Math.PI); + const name = entityManager.getComponent<{ name: string }>(enemy.id, 'enemyType'); + markers.push({ id: enemy.id, label: name?.name || '敌', type: 'enemy', angle }); + } + + const npcs = entityManager.getEntitiesByType('npc'); + for (const npc of npcs) { + const pos = entityManager.getComponent<{ x: number; y: number }>(npc.id, 'position'); + if (!pos) continue; + const dx = pos.x - playerPos.x; + const dy = pos.y - playerPos.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 300) continue; + const angle = Math.atan2(dx, -dy) * (180 / Math.PI); + const name = entityManager.getComponent<{ name: string }>(npc.id, 'npcData'); + markers.push({ id: npc.id, label: name?.name || 'NPC', type: 'npc', angle }); + } + + markers.sort((a, b) => a.angle - b.angle); + this.updateMarkers(markers); + } +} + +export const compassHUD = CompassHUD.getInstance(); diff --git a/src/ui/components/CraftingUI.ts b/src/ui/components/CraftingUI.ts index 7b991bb..038ede6 100644 --- a/src/ui/components/CraftingUI.ts +++ b/src/ui/components/CraftingUI.ts @@ -3,9 +3,10 @@ import { entityManager } from '../../core/EntityManager'; import { alchemySystem, type AlchemyRecipe } from '../../systems/AlchemySystem'; import { enchantingSystem, type Enchantment } from '../../systems/EnchantingSystem'; import { smithingSystem, type SmithingRecipe } from '../../systems/SmithingSystem'; +import { cookingSystem, type CookingRecipe } from '../../systems/CookingSystem'; import { T, FONT, goldTitleStyle } from '../theme'; -export type CraftingTab = 'alchemy' | 'enchanting' | 'smithing'; +export type CraftingTab = 'alchemy' | 'enchanting' | 'smithing' | 'cooking'; export class CraftingUI { private static instance: CraftingUI; @@ -56,11 +57,16 @@ export class CraftingUI { } } + getIsOpen(): boolean { + return this.isOpen; + } + private render(): void { const tabs: { id: CraftingTab; label: string; color: string }[] = [ { id: 'alchemy', label: '炼金', color: T.alchemy }, { id: 'enchanting', label: '附魔', color: T.enchanting }, { id: 'smithing', label: '锻造', color: T.smithing }, + { id: 'cooking', label: '烹饪', color: '#d4a040' }, ]; this.container.innerHTML = ` @@ -130,6 +136,7 @@ export class CraftingUI { alchemy: T.alchemy, enchanting: T.enchanting, smithing: T.smithing, + cooking: '#d4a040', }; const accentColor = tabColors[this.currentTab]; @@ -224,6 +231,36 @@ export class CraftingUI { } break; } + case 'cooking': { + const recipes = cookingSystem.getAvailableRecipes(playerEntity); + if (recipes.length === 0) { + list.innerHTML = `

没有可用的烹饪配方

`; + return; + } + for (const recipe of recipes) { + const item = document.createElement('div'); + item.style.cssText = ` + padding: 8px 10px; + background: rgba(255,255,255,0.02); + border: 1px solid ${T.borderDark}; + border-radius: 3px; + cursor: pointer; + transition: border-color 0.15s; + `; + item.innerHTML = ` +
${recipe.name}
+
${recipe.ingredients.join(' + ')}
+ `; + item.addEventListener('mouseenter', () => { item.style.borderColor = accentColor; }); + item.addEventListener('mouseleave', () => { item.style.borderColor = T.borderDark; }); + item.addEventListener('click', () => { + this.selectedRecipe = { type: 'cooking', data: recipe }; + this.renderRecipeDetails(); + }); + list.appendChild(item); + } + break; + } } } @@ -238,6 +275,7 @@ export class CraftingUI { alchemy: T.alchemy, enchanting: T.enchanting, smithing: T.smithing, + cooking: '#d4a040', }; const accentColor = tabColors[this.selectedRecipe.type as CraftingTab]; @@ -345,6 +383,55 @@ export class CraftingUI { } break; } + case 'cooking': { + const recipe = this.selectedRecipe.data as CookingRecipe; + const canCook = cookingSystem.canCook(recipe.id, playerEntity); + + details.innerHTML = ` +
+

${recipe.name}

+
类型: ${recipe.result.type === 'food' ? '食物' : '饮品'}
+
+
+
材料
+ ${recipe.ingredients.map((id) => { + const inventory = entityManager.getComponent<{ items: any[] }>(playerEntity.id, 'inventory'); + const owned = inventory?.items.find((i) => i.id === id); + const count = owned?.quantity || 0; + return ` +
+ ${id} + 你有: ${count} ${count > 0 ? '✓' : '✗'} +
+ `; + }).join('')} +
+
+
效果
+ ${recipe.result.effects.map((e) => ` +
+ ${e.type} + ${e.magnitude}${e.duration ? ` (${e.duration / 1000}秒)` : ''} +
+ `).join('')} +
+
价值: ${recipe.result.value} 金币
+ + `; + + if (canCook) { + document.getElementById('cook-btn')?.addEventListener('click', () => { + if (cookingSystem.cook(recipe.id, playerEntity)) { + this.showNotification(`烹饪了 ${recipe.result.name}`); + this.renderRecipeList(); + this.renderRecipeDetails(); + } + }); + } + break; + } } } diff --git a/src/ui/components/MagicUI.ts b/src/ui/components/MagicUI.ts new file mode 100644 index 0000000..7e8c219 --- /dev/null +++ b/src/ui/components/MagicUI.ts @@ -0,0 +1,281 @@ +import { entityManager } from '../../core/EntityManager'; +import { magicSystem, type Spell, type Shout, type MagicSchool } from '../../systems/MagicSystem'; +import { T, FONT, goldTitleStyle } from '../theme'; + +type MagicTab = MagicSchool | 'shouts'; + +export class MagicUI { + private static instance: MagicUI; + private container: HTMLDivElement; + private isOpen = false; + private currentTab: MagicTab = 'destruction'; + private selectedSpell: Spell | Shout | null = null; + private selectedType: 'spell' | 'shout' = 'spell'; + + static getInstance(): MagicUI { + if (!MagicUI.instance) MagicUI.instance = new MagicUI(); + return MagicUI.instance; + } + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'magic-ui'; + this.container.style.cssText = ` + position: fixed; inset: 0; + background: ${T.bgOverlay}; + display: none; z-index: 500; + font-family: ${FONT.body}; color: ${T.textLight}; + `; + } + + show(): void { + this.isOpen = true; + this.container.style.display = 'block'; + this.render(); + document.body.appendChild(this.container); + } + + hide(): void { + this.isOpen = false; + this.container.style.display = 'none'; + } + + toggle(): void { + if (this.isOpen) this.hide(); else this.show(); + } + + getIsOpen(): boolean { + return this.isOpen; + } + + private render(): void { + const tabs: { id: MagicTab; label: string; color: string }[] = [ + { id: 'destruction', label: '毁灭', color: '#e04040' }, + { id: 'restoration', label: '恢复', color: '#e0e060' }, + { id: 'illusion', label: '幻术', color: '#c060e0' }, + { id: 'conjuration', label: '召唤', color: '#6080e0' }, + { id: 'alteration', label: '变化', color: '#60c0a0' }, + { id: 'shouts', label: '龙吼', color: T.textGold }, + ]; + + this.container.innerHTML = ` +
+
+

魔法

+ +
+
+ ${tabs.map((t) => ` + + `).join('')} +
+
+
+
法术列表
+
+
+
+
+

选择一个法术查看详情

+
+
+
+
+ `; + + this.renderSpellList(); + document.getElementById('close-magic')?.addEventListener('click', () => this.hide()); + document.querySelectorAll('.magic-tab').forEach((btn) => { + btn.addEventListener('click', () => { + this.currentTab = (btn as HTMLElement).getAttribute('data-tab') as MagicTab; + this.selectedSpell = null; + this.selectedType = this.currentTab === 'shouts' ? 'shout' : 'spell'; + this.render(); + }); + }); + } + + private renderSpellList(): void { + const list = document.getElementById('spell-list'); + if (!list) return; + list.innerHTML = ''; + + const playerEntity = entityManager.getEntitiesByType('player')[0]; + if (!playerEntity) return; + + if (this.currentTab === 'shouts') { + const shouts = magicSystem.getAllShouts(); + if (shouts.length === 0) { + list.innerHTML = `

没有已学龙吼

`; + return; + } + for (const shout of shouts) { + const item = document.createElement('div'); + item.style.cssText = ` + padding: 8px 10px; background: rgba(255,255,255,0.02); + border: 1px solid ${T.borderDark}; border-radius: 3px; + cursor: pointer; transition: border-color 0.15s; + `; + item.innerHTML = ` +
${shout.name}
+
${shout.words.join(' · ')}
+ `; + item.addEventListener('mouseenter', () => { item.style.borderColor = T.textGold; }); + item.addEventListener('mouseleave', () => { item.style.borderColor = T.borderDark; }); + item.addEventListener('click', () => { + this.selectedSpell = shout; + this.selectedType = 'shout'; + this.renderSpellDetails(); + }); + list.appendChild(item); + } + return; + } + + const school = this.currentTab as MagicSchool; + const spells = magicSystem.getSpellsBySchool(school); + const schoolColors: Record = { + destruction: '#e04040', restoration: '#e0e060', illusion: '#c060e0', conjuration: '#6080e0', alteration: '#60c0a0', + }; + const color = schoolColors[school] || T.textMuted; + + if (spells.length === 0) { + list.innerHTML = `

该学派没有已学法术

`; + return; + } + + for (const spell of spells) { + const item = document.createElement('div'); + item.style.cssText = ` + padding: 8px 10px; background: rgba(255,255,255,0.02); + border: 1px solid ${T.borderDark}; border-radius: 3px; + cursor: pointer; transition: border-color 0.15s; + `; + item.innerHTML = ` +
${spell.name}
+
消耗: ${spell.magickaCost} · 威力: ${spell.magnitude}
+ `; + item.addEventListener('mouseenter', () => { item.style.borderColor = color; }); + item.addEventListener('mouseleave', () => { item.style.borderColor = T.borderDark; }); + item.addEventListener('click', () => { + this.selectedSpell = spell; + this.selectedType = 'spell'; + this.renderSpellDetails(); + }); + list.appendChild(item); + } + } + + private renderSpellDetails(): void { + const details = document.getElementById('spell-details'); + if (!details || !this.selectedSpell) return; + + const playerEntity = entityManager.getEntitiesByType('player')[0]; + if (!playerEntity) return; + + if (this.selectedType === 'shout') { + const shout = this.selectedSpell as Shout; + details.innerHTML = ` +
+

${shout.name}

+
龙吼
+
+
+
词语
+
+ ${shout.words.map((w, i) => ` +
+ ${w} +
+ `).join('')} +
+
+
+
效果
+ ${shout.effects.map((e) => ` +
+ ${e.type} + ${e.magnitude}${e.duration ? ` (${e.duration / 1000}s)` : ''} +
+ `).join('')} +
+
冷却: ${shout.cooldown}s
+ `; + return; + } + + const spell = this.selectedSpell as Spell; + const magicka = entityManager.getComponent<{ current: number; max: number }>(playerEntity.id, 'magicka'); + const hasEnough = (magicka?.current ?? 0) >= spell.magickaCost; + + details.innerHTML = ` +
+

${spell.name}

+
+ ${spell.school === 'destruction' ? '毁灭' : spell.school === 'restoration' ? '恢复' : spell.school === 'illusion' ? '幻术' : spell.school === 'conjuration' ? '召唤' : '变化'} + · ${spell.type === 'self' ? '自身' : spell.type === 'target' ? '目标' : spell.type === 'area' ? '范围' : '远程'} +
+
+
+
+
魔力消耗
+
${spell.magickaCost}
+
+
+
威力
+
${spell.magnitude}
+
+
+
冷却
+
${spell.cooldown / 1000}s
+
+
+

${spell.description}

+
+
效果
+ ${spell.effects.map((e) => ` +
+ ${e.type} + ${e.magnitude}${e.duration ? ` (${e.duration / 1000}s)` : ''} +
+ `).join('')} +
+
+ + + +
+ `; + + document.getElementById('equip-left-btn')?.addEventListener('click', () => { + entityManager.addComponent(playerEntity.id, { type: 'equippedSpells', left: spell.id, right: (entityManager.getComponent<{ right?: string }>(playerEntity.id, 'equippedSpells')?.right) || '' }); + this.showNotification(`${spell.name} 装备到左手`); + }); + document.getElementById('equip-right-btn')?.addEventListener('click', () => { + entityManager.addComponent(playerEntity.id, { type: 'equippedSpells', left: (entityManager.getComponent<{ left?: string }>(playerEntity.id, 'equippedSpells')?.left) || '', right: spell.id }); + this.showNotification(`${spell.name} 装备到右手`); + }); + if (hasEnough) { + document.getElementById('cast-btn')?.addEventListener('click', () => { + if (magicSystem.castSpell(playerEntity, spell.id)) { + this.showNotification(`施放了 ${spell.name}`); + } + }); + } + } + + private showNotification(text: string): void { + const toast = document.createElement('div'); + toast.className = 'oes-toast'; + toast.textContent = text; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2600); + } +} + +export const magicUI = MagicUI.getInstance(); diff --git a/src/ui/components/QuestJournalUI.ts b/src/ui/components/QuestJournalUI.ts new file mode 100644 index 0000000..d2862ae --- /dev/null +++ b/src/ui/components/QuestJournalUI.ts @@ -0,0 +1,216 @@ +import { eventBus } from '../../core/EventBus'; +import { questSystem, type Quest, type QuestObjective } from '../../systems/QuestSystem'; +import { T, FONT, goldTitleStyle } from '../theme'; + +type QuestFilter = 'all' | 'main' | 'side' | 'guild' | 'daedric' | 'radiant'; + +export class QuestJournalUI { + private static instance: QuestJournalUI; + private container: HTMLDivElement; + private isOpen = false; + private currentFilter: QuestFilter = 'all'; + private selectedQuest: Quest | null = null; + + static getInstance(): QuestJournalUI { + if (!QuestJournalUI.instance) QuestJournalUI.instance = new QuestJournalUI(); + return QuestJournalUI.instance; + } + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'quest-journal-ui'; + this.container.style.cssText = ` + position: fixed; inset: 0; + background: ${T.bgOverlay}; + display: none; z-index: 500; + font-family: ${FONT.body}; color: ${T.textLight}; + `; + eventBus.on('quest:activated', () => { if (this.isOpen) this.render(); }); + eventBus.on('quest:completed', () => { if (this.isOpen) this.render(); }); + eventBus.on('quest:failed', () => { if (this.isOpen) this.render(); }); + } + + show(): void { + this.isOpen = true; + this.container.style.display = 'block'; + this.render(); + document.body.appendChild(this.container); + } + + hide(): void { + this.isOpen = false; + this.container.style.display = 'none'; + } + + toggle(): void { + if (this.isOpen) this.hide(); else this.show(); + } + + getIsOpen(): boolean { + return this.isOpen; + } + + private getFilteredQuests(): Quest[] { + const all = [...questSystem.getActiveQuests(), ...questSystem.getCompletedQuests()]; + if (this.currentFilter === 'all') return all; + return all.filter((q) => q.type === this.currentFilter); + } + + private render(): void { + const filters: { id: QuestFilter; label: string; color: string }[] = [ + { id: 'all', label: '全部', color: T.textGold }, + { id: 'main', label: '主线', color: '#e0c060' }, + { id: 'side', label: '支线', color: '#60a0e0' }, + { id: 'guild', label: '公会', color: '#e08040' }, + { id: 'daedric', label: '魔神', color: '#c040c0' }, + { id: 'radiant', label: '辐射', color: '#80c080' }, + ]; + + const quests = this.getFilteredQuests(); + + this.container.innerHTML = ` +
+
+

任务日志

+ +
+
+ ${filters.map((f) => ` + + `).join('')} +
+
+
+
+ 任务 (${quests.length}) +
+
+
+
+
+

选择一个任务查看详情

+
+
+
+
+ `; + + this.renderQuestList(quests); + document.getElementById('close-quest-journal')?.addEventListener('click', () => this.hide()); + document.querySelectorAll('.quest-filter-tab').forEach((btn) => { + btn.addEventListener('click', () => { + this.currentFilter = (btn as HTMLElement).getAttribute('data-filter') as QuestFilter; + this.selectedQuest = null; + this.render(); + }); + }); + } + + private renderQuestList(quests: Quest[]): void { + const list = document.getElementById('quest-list'); + if (!list) return; + list.innerHTML = ''; + + if (quests.length === 0) { + list.innerHTML = `

没有任务

`; + return; + } + + const typeColors: Record = { + main: '#e0c060', side: '#60a0e0', guild: '#e08040', daedric: '#c040c0', radiant: '#80c080', + }; + + for (const quest of quests) { + const item = document.createElement('div'); + const isActive = quest.status === 'active'; + const isCompleted = quest.status === 'completed'; + item.style.cssText = ` + padding: 8px 10px; + background: ${this.selectedQuest?.id === quest.id ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)'}; + border: 1px solid ${this.selectedQuest?.id === quest.id ? T.borderGold : T.borderDark}; + border-radius: 3px; cursor: pointer; transition: border-color 0.15s; + opacity: ${isCompleted ? '0.6' : '1'}; + `; + item.innerHTML = ` +
+ + ${quest.name} +
+
+ Lv.${quest.level} · ${quest.type === 'main' ? '主线' : quest.type === 'side' ? '支线' : quest.type === 'guild' ? '公会' : quest.type === 'daedric' ? '魔神' : '辐射'} + ${isCompleted ? ' ✓' : ''} +
+ `; + item.addEventListener('mouseenter', () => { if (this.selectedQuest?.id !== quest.id) item.style.borderColor = T.borderBronze; }); + item.addEventListener('mouseleave', () => { if (this.selectedQuest?.id !== quest.id) item.style.borderColor = T.borderDark; }); + item.addEventListener('click', () => { + this.selectedQuest = quest; + this.renderQuestDetails(quest); + this.renderQuestList(quests); + }); + list.appendChild(item); + } + } + + private renderQuestDetails(quest: Quest): void { + const details = document.getElementById('quest-details'); + if (!details) return; + + const typeLabels: Record = { + main: '主线任务', side: '支线任务', guild: '公会任务', daedric: '魔神任务', radiant: '辐射任务', + }; + const statusLabels: Record = { active: '进行中', completed: '已完成', failed: '已失败', inactive: '未激活' }; + const statusColors: Record = { + active: T.success, completed: T.textMuted, failed: T.danger, inactive: T.textDim, + }; + + details.innerHTML = ` +
+

${quest.name}

+
+ ${statusLabels[quest.status]} + Lv.${quest.level} + ${typeLabels[quest.type]} +
+
+

${quest.description}

+
+
目标
+ ${quest.objectives.map((obj, i) => this.renderObjective(obj, i, quest)).join('')} +
+
+
奖励
+
+
${quest.rewards.gold} 金币
+
${quest.rewards.xp} 经验
+ ${quest.rewards.items.length > 0 ? quest.rewards.items.map((it) => `
${it.id} x${it.quantity}
`).join('') : ''} +
+
+ `; + } + + private renderObjective(obj: QuestObjective, index: number, quest: Quest): string { + const isCurrent = index === quest.currentObjective; + const isDone = obj.completed || index < quest.currentObjective; + const progress = obj.count != null && obj.currentCount != null ? ` (${obj.currentCount}/${obj.count})` : ''; + return ` +
+ ${isDone ? '✓' : isCurrent ? '›' : '·'} + ${obj.description}${progress} +
+ `; + } +} + +export const questJournalUI = QuestJournalUI.getInstance(); diff --git a/src/ui/components/RadialMenuUI.ts b/src/ui/components/RadialMenuUI.ts new file mode 100644 index 0000000..750f9ae --- /dev/null +++ b/src/ui/components/RadialMenuUI.ts @@ -0,0 +1,204 @@ +import { T, FONT } from '../theme'; +import { eventBus } from '../../core/EventBus'; + +export interface RadialMenuItem { + id: string; + label: string; + key: string; + color: string; + action: () => void; +} + +export class RadialMenuUI { + private static instance: RadialMenuUI; + private container: HTMLDivElement; + private isOpen = false; + private items: RadialMenuItem[] = []; + + static getInstance(): RadialMenuUI { + if (!RadialMenuUI.instance) { + RadialMenuUI.instance = new RadialMenuUI(); + } + return RadialMenuUI.instance; + } + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'radial-menu'; + this.container.style.cssText = ` + position: fixed; + inset: 0; + display: none; + z-index: 500; + pointer-events: auto; + `; + } + + registerItems(items: RadialMenuItem[]): void { + this.items = items; + } + + show(): void { + if (this.isOpen) return; + this.isOpen = true; + this.render(); + this.container.style.display = 'block'; + document.body.appendChild(this.container); + eventBus.emit('ui:menuOpened', { menu: 'radial' }); + } + + hide(): void { + if (!this.isOpen) return; + this.isOpen = false; + this.container.style.display = 'none'; + this.container.remove(); + eventBus.emit('ui:menuClosed', { menu: 'radial' }); + } + + toggle(): void { + if (this.isOpen) this.hide(); + else this.show(); + } + + getIsOpen(): boolean { + return this.isOpen; + } + + private render(): void { + this.container.innerHTML = ''; + + const backdrop = document.createElement('div'); + backdrop.style.cssText = ` + position: absolute; + inset: 0; + background: rgba(5,5,10,0.7); + `; + backdrop.addEventListener('click', () => this.hide()); + this.container.appendChild(backdrop); + + const center = document.createElement('div'); + center.style.cssText = ` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + height: 300px; + `; + this.container.appendChild(center); + + const playerIcon = document.createElement('div'); + playerIcon.style.cssText = ` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, #3a3540, #2a2530); + border: 2px solid ${T.borderBronze}; + display: flex; + align-items: center; + justify-content: center; + color: ${T.textGold}; + font-size: 20px; + font-family: ${FONT.title}; + text-shadow: 0 1px 3px rgba(0,0,0,0.6); + `; + playerIcon.textContent = '⚔'; + center.appendChild(playerIcon); + + const positions = [ + { top: '0', left: '50%', transform: 'translate(-50%, 0)' }, + { top: '50%', left: '0', transform: 'translate(0, -50%)' }, + { top: '50%', left: '100%', transform: 'translate(-100%, -50%)' }, + { top: '100%', left: '50%', transform: 'translate(-50%, -100%)' }, + ]; + + this.items.forEach((item, i) => { + const pos = positions[i]; + if (!pos) return; + + const btn = document.createElement('div'); + btn.style.cssText = ` + position: absolute; + top: ${pos.top}; + left: ${pos.left}; + transform: ${pos.transform}; + width: 80px; + height: 80px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(20,18,30,0.85); + border: 1px solid ${T.borderDark}; + border-radius: 6px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, transform 0.1s; + pointer-events: auto; + `; + + const keyBadge = document.createElement('div'); + keyBadge.style.cssText = ` + font-size: 10px; + color: ${T.textDim}; + font-family: ${FONT.body}; + margin-bottom: 4px; + `; + keyBadge.textContent = item.key; + + const icon = document.createElement('div'); + icon.style.cssText = ` + font-size: 22px; + color: ${item.color}; + margin-bottom: 2px; + `; + icon.textContent = this.getItemIcon(item.id); + + const label = document.createElement('div'); + label.style.cssText = ` + font-size: 12px; + color: ${T.textLight}; + font-family: ${FONT.body}; + letter-spacing: 0.5px; + `; + label.textContent = item.label; + + btn.appendChild(keyBadge); + btn.appendChild(icon); + btn.appendChild(label); + + btn.addEventListener('mouseenter', () => { + btn.style.borderColor = item.color; + btn.style.background = 'rgba(30,28,42,0.95)'; + btn.style.transform = `${pos.transform} scale(1.08)`; + }); + btn.addEventListener('mouseleave', () => { + btn.style.borderColor = T.borderDark; + btn.style.background = 'rgba(20,18,30,0.85)'; + btn.style.transform = pos.transform; + }); + btn.addEventListener('click', (e) => { + e.stopPropagation(); + this.hide(); + item.action(); + }); + + center.appendChild(btn); + }); + } + + private getItemIcon(id: string): string { + switch (id) { + case 'skills': return '✦'; + case 'magic': return '☄'; + case 'items': return '☒'; + case 'map': return '⌖'; + default: return '●'; + } + } +} + +export const radialMenuUI = RadialMenuUI.getInstance(); diff --git a/src/ui/components/SkillTreeUI.ts b/src/ui/components/SkillTreeUI.ts index 85c9712..75e57e0 100644 --- a/src/ui/components/SkillTreeUI.ts +++ b/src/ui/components/SkillTreeUI.ts @@ -31,6 +31,14 @@ export class SkillTreeUI { this.container.style.display = 'none'; } + toggle(): void { + if (this.container.style.display === 'flex') this.hide(); else this.show(); + } + + getIsOpen(): boolean { + return this.container.style.display === 'flex'; + } + private render(): void { this.container.innerHTML = ''; diff --git a/src/ui/components/StatusEffectHUD.ts b/src/ui/components/StatusEffectHUD.ts new file mode 100644 index 0000000..321fefa --- /dev/null +++ b/src/ui/components/StatusEffectHUD.ts @@ -0,0 +1,70 @@ +import { entityManager } from '../../core/EntityManager'; +import { statusEffectSystem, type StatusEffect } from '../../systems/StatusEffectSystem'; +import { T, FONT } from '../theme'; + +export class StatusEffectHUD { + private static instance: StatusEffectHUD; + private container: HTMLDivElement; + private updateInterval: ReturnType | null = null; + + static getInstance(): StatusEffectHUD { + if (!StatusEffectHUD.instance) StatusEffectHUD.instance = new StatusEffectHUD(); + return StatusEffectHUD.instance; + } + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'status-effect-hud'; + this.container.style.cssText = ` + position: fixed; top: 12px; left: 50%; transform: translateX(-50%); + display: flex; gap: 6px; z-index: 150; + font-family: ${FONT.body}; pointer-events: none; + `; + document.body.appendChild(this.container); + this.updateInterval = setInterval(() => this.update(), 500); + } + + private update(): void { + const player = entityManager.getEntitiesByType('player')[0]; + if (!player) { this.container.innerHTML = ''; return; } + + const effects = statusEffectSystem.getEffects(player.id); + if (effects.length === 0) { this.container.innerHTML = ''; return; } + + this.container.innerHTML = effects.map((e) => this.renderEffect(e)).join(''); + } + + private renderEffect(effect: StatusEffect): string { + const isBuff = effect.type === 'buff'; + const color = isBuff ? '#40a060' : '#c83030'; + const bgColor = isBuff ? 'rgba(64,160,96,0.15)' : 'rgba(200,48,48,0.15)'; + const remaining = Math.ceil(effect.remainingMs / 1000); + const ratio = effect.totalMs > 0 ? effect.remainingMs / effect.totalMs : 1; + + return ` +
+
+
+
+
+
${effect.attribute} ${effect.magnitude > 0 ? '+' : ''}${effect.magnitude}
+
${remaining}s
+
+
+ `; + } + + destroy(): void { + if (this.updateInterval) clearInterval(this.updateInterval); + this.container.remove(); + } +} + +export const statusEffectHUD = StatusEffectHUD.getInstance(); diff --git a/src/ui/components/TimeDisplayHUD.ts b/src/ui/components/TimeDisplayHUD.ts new file mode 100644 index 0000000..443805b --- /dev/null +++ b/src/ui/components/TimeDisplayHUD.ts @@ -0,0 +1,59 @@ +import { dayNightSystem } from '../../systems/DayNightSystem'; +import { T, FONT } from '../theme'; + +export class TimeDisplayHUD { + private static instance: TimeDisplayHUD; + private container: HTMLDivElement; + private updateInterval: ReturnType | null = null; + + static getInstance(): TimeDisplayHUD { + if (!TimeDisplayHUD.instance) TimeDisplayHUD.instance = new TimeDisplayHUD(); + return TimeDisplayHUD.instance; + } + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'time-display-hud'; + this.container.style.cssText = ` + position: fixed; top: 12px; right: 16px; + display: flex; align-items: center; gap: 10px; + padding: 6px 14px; + background: rgba(0,0,0,0.5); + border: 1px solid ${T.borderIron}; + border-radius: 3px; + z-index: 150; + font-family: ${FONT.body}; + pointer-events: none; + `; + document.body.appendChild(this.container); + this.updateInterval = setInterval(() => this.update(), 1000); + this.update(); + } + + private update(): void { + const timeStr = dayNightSystem.getTimeString(); + const dayCount = dayNightSystem.getDayCount(); + const isNight = dayNightSystem.isNight(); + const hour = dayNightSystem.getHour(); + + let icon = '☀'; + if (hour >= 20 || hour < 5) icon = '🌙'; + else if (hour >= 17) icon = '🌅'; + else if (hour >= 5 && hour < 7) icon = '🌄'; + + this.container.innerHTML = ` + ${icon} +
+
${timeStr}
+
第 ${dayCount} 天
+
+ `; + } + + destroy(): void { + if (this.updateInterval) clearInterval(this.updateInterval); + this.container.remove(); + } +} + +export const timeDisplayHUD = TimeDisplayHUD.getInstance(); diff --git a/src/ui/components/WorldMapUI.ts b/src/ui/components/WorldMapUI.ts index dee044d..fd43854 100644 --- a/src/ui/components/WorldMapUI.ts +++ b/src/ui/components/WorldMapUI.ts @@ -76,6 +76,10 @@ export class WorldMapUI { } } + getIsOpen(): boolean { + return this.isOpen; + } + discoverLocation(locationId: string): void { const location = this.locations.find((l) => l.id === locationId); if (location) {