fix: 修复武器经验获取并完善义体系统
- 修复战斗胜利后未获得武器技能经验的问题 (initCombat未传递skillExpReward) - 每级武器技能提供5%武器伤害加成(已实现,无需修改) - 实现义体安装/卸载功能,支持NPC对话交互 - StatusPanel添加义体装备槽显示 - MapPanel修复NPC对话import问题 - 新增成就系统框架 - 添加项目文档CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
177
CLAUDE.md
Normal file
177
CLAUDE.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a text-based idle adventure game (文字冒险游戏) built with uni-app, Vue 3, and Pinia. The game features skill-driven progression, idle mechanics, combat, crafting, and a dark post-apocalyptic theme.
|
||||||
|
|
||||||
|
**Tech Stack:**
|
||||||
|
- Framework: uni-app (Vue 3 with Composition API)
|
||||||
|
- State Management: Pinia
|
||||||
|
- Testing: Jest + Vitest + Playwright
|
||||||
|
- Style: SCSS with global variables
|
||||||
|
|
||||||
|
## Build and Run Commands
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- Use HBuilderX IDE to run the project (uni-app standard)
|
||||||
|
- Run to different platforms: mp-weixin (WeChat Mini Program), h5, app
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
npm test # Run Jest tests in watch mode
|
||||||
|
npm run test:watch # Run tests in watch mode
|
||||||
|
npm run test:coverage # Generate coverage report
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
├── components/
|
||||||
|
│ ├── common/ # Reusable UI components (ProgressBar, StatItem, etc.)
|
||||||
|
│ ├── layout/ # Layout components (TabBar, Drawer)
|
||||||
|
│ ├── panels/ # Main game panels (StatusPanel, MapPanel, LogPanel)
|
||||||
|
│ └── drawers/ # Slide-out drawers (Inventory, Event, Shop, Crafting)
|
||||||
|
├── config/ # Game configuration (skills, items, enemies, locations, etc.)
|
||||||
|
├── store/ # Pinia stores (player.js, game.js)
|
||||||
|
├── utils/ # Game systems (combat, skill, item, task, event, etc.)
|
||||||
|
├── pages/ # Uni-app pages
|
||||||
|
└── tests/ # Unit tests with fixtures and helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Systems (utils/)
|
||||||
|
- **skillSystem.js**: Skill unlocking, XP, leveling, milestone rewards, parent skills
|
||||||
|
- **combatSystem.js**: AP/EP hit calculation, three-layer defense (evade/shield/armor), damage
|
||||||
|
- **itemSystem.js**: Item usage, equipment, quality calculation, inventory management
|
||||||
|
- **taskSystem.js**: Idle task management, mutual exclusion checks
|
||||||
|
- **eventSystem.js**: Event triggering, NPC dialogue handling
|
||||||
|
- **environmentSystem.js**: Environment penalties, passive skill XP
|
||||||
|
- **gameLoop.js**: Main game tick (1-second interval), time progression, auto-save
|
||||||
|
- **storage.js**: Save/load using uni.setStorageSync, offline earnings
|
||||||
|
- **craftingSystem.js**: Recipe-based crafting with quality calculation
|
||||||
|
- **levelingSystem.js**: Player level progression and stat scaling
|
||||||
|
- **mapLayout.js**: Visual map representation
|
||||||
|
|
||||||
|
### State Management (Pinia Stores)
|
||||||
|
- **player.js**: Base stats, current stats (HP/stamina/sanity), level, skills, equipment, inventory, currency, location, flags
|
||||||
|
- **game.js**: Current tab, drawer states, logs, game time, combat state, active tasks, market prices, current event
|
||||||
|
|
||||||
|
### Configuration (config/)
|
||||||
|
- **constants.js**: Game constants (quality levels, stat scaling, time scale, etc.)
|
||||||
|
- **skills.js**: All skill definitions with milestones and unlock conditions
|
||||||
|
- **items.js**: All item definitions (weapons, consumables, materials, books)
|
||||||
|
- **enemies.js**: Enemy stats, drops, spawn conditions
|
||||||
|
- **locations.js**: Area definitions, connections, activities, unlock conditions
|
||||||
|
- **npcs.js**: NPC dialogue trees and interactions
|
||||||
|
- **events.js**: Event definitions and triggers
|
||||||
|
- **recipes.js**: Crafting recipes
|
||||||
|
- **shop.js**: Shop inventory and pricing
|
||||||
|
- **formulas.js**: Mathematical formulas for stat calculations
|
||||||
|
|
||||||
|
## Key Game Concepts
|
||||||
|
|
||||||
|
### Quality System (品质系统)
|
||||||
|
Items have quality (0-250) that affects their power:
|
||||||
|
- 0-49: 垃圾 (Trash)
|
||||||
|
- 50-99: 普通 (Common)
|
||||||
|
- 100-139: 优秀 (Good)
|
||||||
|
- 140-169: 稀有 (Rare)
|
||||||
|
- 170-199: 史诗 (Epic)
|
||||||
|
- 200-250: 传说 (Legendary)
|
||||||
|
|
||||||
|
Each tier has a multiplier that increases item stats.
|
||||||
|
|
||||||
|
### AP/EP Combat System
|
||||||
|
- AP (Attack Points) = 灵巧 + 智力×0.2 + skills + equipment + stance - enemyCountPenalty
|
||||||
|
- EP (Evasion Points) = 敏捷 + 智力×0.2 + skills + equipment + stance - environmentPenalty
|
||||||
|
- Hit rate is non-linear: AP=5×EP → ~98%, AP=EP → ~50%, AP=0.5×EP → ~24%
|
||||||
|
|
||||||
|
### Three-Layer Defense
|
||||||
|
1. Evasion (complete dodge)
|
||||||
|
2. Shield absorption
|
||||||
|
3. Defense reduction (min 10% damage)
|
||||||
|
|
||||||
|
### Skill Milestones
|
||||||
|
Skills provide permanent global bonuses at specific levels (e.g., all weapons +crit rate at level 5). Parent skills auto-level to match highest child skill level.
|
||||||
|
|
||||||
|
### Time System
|
||||||
|
- 1 real second = 5 game minutes
|
||||||
|
- Day/night cycle affects gameplay
|
||||||
|
- Markets refresh daily
|
||||||
|
- Offline earnings available (0.5h base, 2.5h with ad)
|
||||||
|
|
||||||
|
### Idle Tasks
|
||||||
|
Active tasks (reading, training, working) and passive tasks (environment adaptation). Combat is mutually exclusive with most tasks.
|
||||||
|
|
||||||
|
## Important Patterns
|
||||||
|
|
||||||
|
### Adding New Skills
|
||||||
|
1. Add config to `config/skills.js`
|
||||||
|
2. Set unlockCondition (item, location, or skill level)
|
||||||
|
3. Add milestones with permanent bonuses
|
||||||
|
4. Link to parentSkill if applicable
|
||||||
|
|
||||||
|
### Adding New Items
|
||||||
|
1. Add config to `config/items.js`
|
||||||
|
2. Set baseValue, baseDamage/defense, type
|
||||||
|
3. Add to shop or enemy drop tables
|
||||||
|
4. For equipment, quality affects final stats
|
||||||
|
|
||||||
|
### Adding New Locations
|
||||||
|
1. Add to `config/locations.js`
|
||||||
|
2. Set connections, enemies, activities
|
||||||
|
3. Add unlockCondition if needed
|
||||||
|
4. Update `utils/mapLayout.js` for visual representation
|
||||||
|
|
||||||
|
### Store Usage
|
||||||
|
```javascript
|
||||||
|
import { usePlayerStore } from '@/store/player.js'
|
||||||
|
import { useGameStore } from '@/store/game.js'
|
||||||
|
|
||||||
|
const player = usePlayerStore()
|
||||||
|
const game = useGameStore()
|
||||||
|
|
||||||
|
// Access state
|
||||||
|
player.baseStats.strength
|
||||||
|
game.logs
|
||||||
|
|
||||||
|
// Call actions
|
||||||
|
game.addLog('message', 'info')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Variables (uni.scss)
|
||||||
|
- $bg-primary: #0a0a0f (main background)
|
||||||
|
- $accent: #4ecdc4 (primary accent)
|
||||||
|
- $danger: #ff6b6b (combat/HP low)
|
||||||
|
- Quality colors: $quality-trash through $quality-legend
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Game Loop Timing
|
||||||
|
- Main loop runs every 1 second via `gameLoop.js`
|
||||||
|
- Time advances by TIME_SCALE (5) minutes per tick
|
||||||
|
- Auto-save triggers every 30 game minutes
|
||||||
|
|
||||||
|
### Saving
|
||||||
|
- `App.vue` handles onLaunch/onShow/onHide
|
||||||
|
- Auto-saves on app hide
|
||||||
|
- Manual save via `window.manualSave()`
|
||||||
|
- Logs are NOT persisted (cleared on load)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Tests in `tests/unit/` use Vitest
|
||||||
|
- Fixtures in `tests/fixtures/` provide mock data
|
||||||
|
- Helpers in `tests/helpers/` mock timers, stores, random
|
||||||
|
- See `tests/README.md` for testing guidelines
|
||||||
|
|
||||||
|
## Known Gotchas
|
||||||
|
|
||||||
|
1. **uni API availability**: Some uni APIs only work in specific platforms (H5 vs mini-program)
|
||||||
|
2. **Store timing**: Always use `setActivePinia(createPinia())` in tests
|
||||||
|
3. **Logs array**: Limited to 200 entries, shifts old entries
|
||||||
|
4. **Skill level cap**: Cannot exceed player level × 2
|
||||||
|
5. **Equipment quality**: Calculated as base × quality% × tierMultiplier
|
||||||
|
6. **Combat stance**: Switching stance resets attack progress
|
||||||
|
7. **Market saturation**: Selling same item reduces price (encourages exploration)
|
||||||
@@ -124,7 +124,7 @@ import { NPC_CONFIG } from '@/config/npcs.js'
|
|||||||
import { ENEMY_CONFIG, getRandomEnemyForLocation } from '@/config/enemies.js'
|
import { ENEMY_CONFIG, getRandomEnemyForLocation } from '@/config/enemies.js'
|
||||||
import { getShopConfig } from '@/config/shop.js'
|
import { getShopConfig } from '@/config/shop.js'
|
||||||
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
||||||
import { triggerExploreEvent, tryFlee } from '@/utils/eventSystem.js'
|
import { triggerExploreEvent, tryFlee, startNPCDialogue } from '@/utils/eventSystem.js'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
@@ -252,16 +252,8 @@ const availableNPCs = computed(() => {
|
|||||||
function talkToNPC(npc) {
|
function talkToNPC(npc) {
|
||||||
game.addLog(`与 ${npc.name} 开始对话...`, 'info')
|
game.addLog(`与 ${npc.name} 开始对话...`, 'info')
|
||||||
|
|
||||||
// 打开 NPC 对话
|
// 使用 eventSystem 的 startNPCDialogue 来处理对话
|
||||||
if (npc.dialogue && npc.dialogue.first) {
|
startNPCDialogue(game, npc.id, 'first', player)
|
||||||
game.drawerState.event = true
|
|
||||||
game.currentEvent = {
|
|
||||||
type: 'npc_dialogue',
|
|
||||||
npcId: npc.id,
|
|
||||||
dialogue: npc.dialogue.first,
|
|
||||||
choices: npc.dialogue.first.choices || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openInventory() {
|
function openInventory() {
|
||||||
@@ -360,14 +352,15 @@ function startCombat() {
|
|||||||
// 获取环境类型
|
// 获取环境类型
|
||||||
const environment = getEnvironmentType(player.currentLocation)
|
const environment = getEnvironmentType(player.currentLocation)
|
||||||
|
|
||||||
// 使用 initCombat 初始化战斗
|
// 使用 initCombat 初始化战斗,传入偏好的战斗姿态
|
||||||
game.combatState = initCombat(enemyConfig.id, enemyConfig, environment)
|
game.combatState = initCombat(enemyConfig.id, enemyConfig, environment, game.preferredStance)
|
||||||
game.inCombat = true
|
game.inCombat = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeStance(stance) {
|
function changeStance(stance) {
|
||||||
if (game.combatState) {
|
if (game.combatState) {
|
||||||
game.combatState.stance = stance
|
game.combatState.stance = stance
|
||||||
|
game.preferredStance = stance // 记住玩家选择的姿态
|
||||||
game.addLog(`切换到${stanceTabs.find(t => t.id === stance)?.label}姿态`, 'combat')
|
game.addLog(`切换到${stanceTabs.find(t => t.id === stance)?.label}姿态`, 'combat')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,19 @@
|
|||||||
</view>
|
</view>
|
||||||
<text v-else class="equipment-slot__empty">空</text>
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.prosthetic }">
|
||||||
|
<text class="equipment-slot__label">义体</text>
|
||||||
|
<view v-if="player.equipment.prosthetic" class="equipment-slot__item">
|
||||||
|
<text class="equipment-slot__name" style="color: #ff6b9d;">
|
||||||
|
{{ player.equipment.prosthetic.icon }} {{ player.equipment.prosthetic.name }}
|
||||||
|
</text>
|
||||||
|
<text v-if="player.equipment.prosthetic.stats" class="equipment-slot__stats">
|
||||||
|
{{ formatProstheticStats(player.equipment.prosthetic.stats) }}
|
||||||
|
</text>
|
||||||
|
<text v-if="player.equipment.prosthetic.skill" class="equipment-slot__quality">[{{ getProstheticSkillName(player.equipment.prosthetic.skill) }}]</text>
|
||||||
|
</view>
|
||||||
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -575,6 +588,26 @@ function cancelActiveTask(task) {
|
|||||||
endTask(game, player, task.id, false)
|
endTask(game, player, task.id, false)
|
||||||
game.addLog(`取消了 ${task.name}`, 'info')
|
game.addLog(`取消了 ${task.name}`, 'info')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 义体相关辅助函数
|
||||||
|
function formatProstheticStats(stats) {
|
||||||
|
if (!stats) return ''
|
||||||
|
const parts = []
|
||||||
|
if (stats.strength) parts.push(`力+${stats.strength}`)
|
||||||
|
if (stats.agility) parts.push(`敏+${stats.agility}`)
|
||||||
|
if (stats.dexterity) parts.push(`灵+${stats.dexterity}`)
|
||||||
|
if (stats.intuition) parts.push(`智+${stats.intuition}`)
|
||||||
|
if (stats.vitality) parts.push(`体+${stats.vitality}`)
|
||||||
|
if (stats.attack) parts.push(`攻+${stats.attack}`)
|
||||||
|
if (stats.defense) parts.push(`防+${stats.defense}`)
|
||||||
|
return parts.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProstheticSkillName(skillId) {
|
||||||
|
const { SKILL_CONFIG } = require('@/config/skills')
|
||||||
|
const skill = SKILL_CONFIG[skillId]
|
||||||
|
return skill ? skill.name : skillId
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
110
config/achievements.js
Normal file
110
config/achievements.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 成就系统配置
|
||||||
|
*/
|
||||||
|
export const ACHIEVEMENT_CONFIG = {
|
||||||
|
// ===== 战斗成就 =====
|
||||||
|
first_blood: {
|
||||||
|
id: 'first_blood',
|
||||||
|
name: '初露锋芒',
|
||||||
|
description: '击败第一个敌人',
|
||||||
|
icon: '⚔️',
|
||||||
|
condition: { type: 'kill', count: 1 },
|
||||||
|
reward: { copper: 100, exp: 20 }
|
||||||
|
},
|
||||||
|
|
||||||
|
hunter: {
|
||||||
|
id: 'hunter',
|
||||||
|
name: '狩猎者',
|
||||||
|
description: '击败10个敌人',
|
||||||
|
icon: '🏹',
|
||||||
|
condition: { type: 'kill', count: 10 },
|
||||||
|
reward: { copper: 500, exp: 100 }
|
||||||
|
},
|
||||||
|
|
||||||
|
slayer: {
|
||||||
|
id: 'slayer',
|
||||||
|
name: '屠戮者',
|
||||||
|
description: '击败100个敌人',
|
||||||
|
icon: '💀',
|
||||||
|
condition: { type: 'kill', count: 100 },
|
||||||
|
reward: { copper: 5000, exp: 1000, item: 'iron_sword' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Boss成就 =====
|
||||||
|
boss_slayer: {
|
||||||
|
id: 'boss_slayer',
|
||||||
|
name: '弑君者',
|
||||||
|
description: '击败一个Boss',
|
||||||
|
icon: '👑',
|
||||||
|
condition: { type: 'boss_kill', count: 1 },
|
||||||
|
reward: { copper: 2000, exp: 500 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 义体成就 =====
|
||||||
|
prosthetic_user: {
|
||||||
|
id: 'prosthetic_user',
|
||||||
|
name: '机械飞升',
|
||||||
|
description: '安装第一个义体',
|
||||||
|
icon: '🦾',
|
||||||
|
condition: { type: 'equip_prosthetic', count: 1 },
|
||||||
|
reward: { copper: 1000, exp: 200 }
|
||||||
|
},
|
||||||
|
|
||||||
|
full_conversion: {
|
||||||
|
id: 'full_conversion',
|
||||||
|
name: '完全机械化',
|
||||||
|
description: '同时装备4个义体',
|
||||||
|
icon: '🤖',
|
||||||
|
condition: { type: 'equip_prosthetic', count: 4 },
|
||||||
|
reward: { copper: 10000, exp: 2000 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 技能成就 =====
|
||||||
|
skill_master: {
|
||||||
|
id: 'skill_master',
|
||||||
|
name: '宗师',
|
||||||
|
description: '任意技能达到20级',
|
||||||
|
icon: '📜',
|
||||||
|
condition: { type: 'skill_level', level: 20 },
|
||||||
|
reward: { copper: 3000, exp: 500 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 探索成就 =====
|
||||||
|
explorer: {
|
||||||
|
id: 'explorer',
|
||||||
|
name: '探险家',
|
||||||
|
description: '访问所有区域',
|
||||||
|
icon: '🗺️',
|
||||||
|
condition: { type: 'visit_locations', count: 99 },
|
||||||
|
reward: { copper: 2000, exp: 300 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 财富成就 =====
|
||||||
|
wealthy: {
|
||||||
|
id: 'wealthy',
|
||||||
|
name: '小有积蓄',
|
||||||
|
description: '累计获得10000铜币',
|
||||||
|
icon: '💰',
|
||||||
|
condition: { type: 'total_earned', amount: 10000 },
|
||||||
|
reward: { item: 'lucky_ring' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== 生存成就 =====
|
||||||
|
survivor: {
|
||||||
|
id: 'survivor',
|
||||||
|
name: '幸存者',
|
||||||
|
description: '存活10天',
|
||||||
|
icon: '🌅',
|
||||||
|
condition: { type: 'survive_days', days: 10 },
|
||||||
|
reward: { copper: 1000, exp: 200 }
|
||||||
|
},
|
||||||
|
|
||||||
|
veteran: {
|
||||||
|
id: 'veteran',
|
||||||
|
name: '老兵',
|
||||||
|
description: '存活30天',
|
||||||
|
icon: '🎖️',
|
||||||
|
condition: { type: 'survive_days', days: 30 },
|
||||||
|
reward: { copper: 5000, exp: 1000, item: 'iron_armor' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,5 +40,53 @@ export const NPC_CONFIG = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ripper_doc: {
|
||||||
|
id: 'ripper_doc',
|
||||||
|
name: '义体医生',
|
||||||
|
location: 'blackmarket',
|
||||||
|
icon: '👨⚕️',
|
||||||
|
dialogue: {
|
||||||
|
first: {
|
||||||
|
text: '嗯...你是新面孔。我看你的身体还很"原始",需要做点升级吗?',
|
||||||
|
choices: [
|
||||||
|
{ text: '什么是义体?', next: 'explain_prosthetic' },
|
||||||
|
{ text: '我想安装义体', next: 'install_prosthetic' },
|
||||||
|
{ text: '我想卸下义体', next: 'remove_prosthetic' },
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explain_prosthetic: {
|
||||||
|
text: '义体是机械植入物,可以大幅增强你的能力。它们通常只有从强大的敌人身上才能获得。装备义体后,你还能获得特殊的攻击技能。',
|
||||||
|
choices: [
|
||||||
|
{ text: '我想安装义体', next: 'install_prosthetic' },
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
install_prosthetic: {
|
||||||
|
text: '让我看看...你有以下义体可以安装:\n{prosthetic_list}\n\n选择一件装备吧。',
|
||||||
|
choices: [
|
||||||
|
{ text: '{refresh}', next: 'install_prosthetic', action: 'refresh_list' },
|
||||||
|
{ text: '返回', next: 'first' }
|
||||||
|
],
|
||||||
|
dynamicChoices: 'prosthetic_inventory'
|
||||||
|
},
|
||||||
|
remove_prosthetic: {
|
||||||
|
text: '卸下义体是个精细的手术...你确定吗?当前装备的义体:\n{equipped_prosthetic}',
|
||||||
|
choices: [
|
||||||
|
{ text: '确定卸下', next: null, action: 'remove_prosthetic' },
|
||||||
|
{ text: '返回', next: 'first' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
install_prosthetic: {
|
||||||
|
cost: 500 // 安装费用
|
||||||
|
},
|
||||||
|
remove_prosthetic: {
|
||||||
|
cost: 200 // 卸下费用
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,22 +143,27 @@ export function calculateEP(defender, stance = 'balance', environment = 'normal'
|
|||||||
*
|
*
|
||||||
* @param {Number} ap - 攻击点数
|
* @param {Number} ap - 攻击点数
|
||||||
* @param {Number} ep - 闪避点数
|
* @param {Number} ep - 闪避点数
|
||||||
|
* @param {Boolean} isPlayer - 是否是玩家攻击(玩家享受最低命中率加成)
|
||||||
* @returns {Number} 命中概率 (0-1)
|
* @returns {Number} 命中概率 (0-1)
|
||||||
*/
|
*/
|
||||||
export function calculateHitRate(ap, ep) {
|
export function calculateHitRate(ap, ep, isPlayer = false) {
|
||||||
const ratio = ap / (ep + 1)
|
const ratio = ap / (ep + 1)
|
||||||
|
|
||||||
// 非线性命中概率表
|
// 非线性命中概率表
|
||||||
if (ratio >= 5) return 0.98
|
let hitRate = 0.05
|
||||||
if (ratio >= 3) return 0.90
|
if (ratio >= 5) hitRate = 0.98
|
||||||
if (ratio >= 2) return 0.80
|
else if (ratio >= 3) hitRate = 0.90
|
||||||
if (ratio >= 1.5) return 0.65
|
else if (ratio >= 2) hitRate = 0.80
|
||||||
if (ratio >= 1) return 0.50
|
else if (ratio >= 1.5) hitRate = 0.65
|
||||||
if (ratio >= 0.75) return 0.38
|
else if (ratio >= 1) hitRate = 0.50
|
||||||
if (ratio >= 0.5) return 0.24
|
else if (ratio >= 0.75) hitRate = 0.38
|
||||||
if (ratio >= 0.33) return 0.15
|
else if (ratio >= 0.5) hitRate = 0.24
|
||||||
if (ratio >= 0.2) return 0.08
|
else if (ratio >= 0.33) hitRate = 0.15
|
||||||
return 0.05
|
else if (ratio >= 0.2) hitRate = 0.08
|
||||||
|
|
||||||
|
// 最低命中率:敌人攻击玩家时至少40%命中率,玩家攻击敌人时至少15%
|
||||||
|
const minHitRate = isPlayer ? 0.15 : 0.40
|
||||||
|
return Math.max(minHitRate, hitRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,13 +207,13 @@ export function calculateCritMultiplier(attacker) {
|
|||||||
* @param {Object} defenderBonuses - 防御者加成
|
* @param {Object} defenderBonuses - 防御者加成
|
||||||
* @returns {Object} { hit: boolean, damage: number, crit: boolean, evaded: boolean, shieldAbsorbed: number }
|
* @returns {Object} { hit: boolean, damage: number, crit: boolean, evaded: boolean, shieldAbsorbed: number }
|
||||||
*/
|
*/
|
||||||
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}) {
|
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}, isPlayerAttacker = false) {
|
||||||
// 计算AP和EP
|
// 计算AP和EP
|
||||||
const ap = calculateAP(attacker, stance, 1, environment, attackerBonuses)
|
const ap = calculateAP(attacker, stance, 1, environment, attackerBonuses)
|
||||||
const ep = calculateEP(defender, 'balance', environment, defenderBonuses)
|
const ep = calculateEP(defender, 'balance', environment, defenderBonuses)
|
||||||
|
|
||||||
// 计算命中概率
|
// 计算命中概率(玩家攻击时isPlayerAttacker=true,敌人攻击时isPlayerAttacker=false)
|
||||||
const hitRate = calculateHitRate(ap, ep)
|
const hitRate = calculateHitRate(ap, ep, isPlayerAttacker)
|
||||||
|
|
||||||
// 第一步:闪避判定
|
// 第一步:闪避判定
|
||||||
const roll = Math.random()
|
const roll = Math.random()
|
||||||
@@ -391,7 +396,8 @@ export function combatTick(gameStore, playerStore, combatState) {
|
|||||||
stance,
|
stance,
|
||||||
environment,
|
environment,
|
||||||
playerBonuses,
|
playerBonuses,
|
||||||
{}
|
{},
|
||||||
|
true // 玩家攻击
|
||||||
)
|
)
|
||||||
|
|
||||||
if (playerAttack.hit) {
|
if (playerAttack.hit) {
|
||||||
@@ -441,7 +447,8 @@ export function combatTick(gameStore, playerStore, combatState) {
|
|||||||
'balance',
|
'balance',
|
||||||
environment,
|
environment,
|
||||||
{},
|
{},
|
||||||
playerBonuses
|
playerBonuses,
|
||||||
|
false // 敌人攻击
|
||||||
)
|
)
|
||||||
|
|
||||||
if (enemyAttack.hit) {
|
if (enemyAttack.hit) {
|
||||||
@@ -516,9 +523,10 @@ export function getStaminaCost(stance) {
|
|||||||
* @param {String} enemyId - 敌人ID
|
* @param {String} enemyId - 敌人ID
|
||||||
* @param {Object} enemyConfig - 敌人配置
|
* @param {Object} enemyConfig - 敌人配置
|
||||||
* @param {String} environment - 环境类型
|
* @param {String} environment - 环境类型
|
||||||
|
* @param {String} preferredStance - 玩家偏好的战斗姿态
|
||||||
* @returns {Object} 战斗状态
|
* @returns {Object} 战斗状态
|
||||||
*/
|
*/
|
||||||
export function initCombat(enemyId, enemyConfig, environment = 'normal') {
|
export function initCombat(enemyId, enemyConfig, environment = 'normal', preferredStance = 'balance') {
|
||||||
return {
|
return {
|
||||||
enemyId,
|
enemyId,
|
||||||
enemy: {
|
enemy: {
|
||||||
@@ -530,9 +538,10 @@ export function initCombat(enemyId, enemyConfig, environment = 'normal') {
|
|||||||
defense: enemyConfig.baseStats.defense,
|
defense: enemyConfig.baseStats.defense,
|
||||||
baseStats: enemyConfig.baseStats,
|
baseStats: enemyConfig.baseStats,
|
||||||
expReward: enemyConfig.expReward,
|
expReward: enemyConfig.expReward,
|
||||||
|
skillExpReward: enemyConfig.skillExpReward || 0,
|
||||||
drops: enemyConfig.drops || []
|
drops: enemyConfig.drops || []
|
||||||
},
|
},
|
||||||
stance: 'balance',
|
stance: preferredStance, // 使用玩家偏好的战斗姿态
|
||||||
environment,
|
environment,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
ticks: 0
|
ticks: 0
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { EVENT_CONFIG } from '@/config/events.js'
|
|||||||
import { LOCATION_CONFIG } from '@/config/locations.js'
|
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||||
import { SKILL_CONFIG } from '@/config/skills.js'
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
import { ENEMY_CONFIG } from '@/config/enemies.js'
|
import { ENEMY_CONFIG } from '@/config/enemies.js'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
import { unlockSkill } from './skillSystem.js'
|
import { unlockSkill } from './skillSystem.js'
|
||||||
import { addItemToInventory } from './itemSystem.js'
|
import { addItemToInventory } from './itemSystem.js'
|
||||||
import { initCombat, getEnvironmentType } from './combatSystem.js'
|
import { initCombat, getEnvironmentType } from './combatSystem.js'
|
||||||
|
import { installProsthetic, removeProsthetic } from './prostheticSystem.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查并触发事件
|
* 检查并触发事件
|
||||||
@@ -198,8 +200,9 @@ function renderTemplate(template, context) {
|
|||||||
* @param {Object} gameStore - 游戏Store
|
* @param {Object} gameStore - 游戏Store
|
||||||
* @param {String} npcId - NPC ID
|
* @param {String} npcId - NPC ID
|
||||||
* @param {String} dialogueKey - 对话键名
|
* @param {String} dialogueKey - 对话键名
|
||||||
|
* @param {Object} playerStore - 玩家Store(用于动态选项生成)
|
||||||
*/
|
*/
|
||||||
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first', playerStore = null) {
|
||||||
const npc = NPC_CONFIG[npcId]
|
const npc = NPC_CONFIG[npcId]
|
||||||
if (!npc) {
|
if (!npc) {
|
||||||
console.warn(`NPC ${npcId} not found`)
|
console.warn(`NPC ${npcId} not found`)
|
||||||
@@ -212,14 +215,27 @@ export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理模板变量
|
||||||
|
let text = dialogue.text
|
||||||
|
if (playerStore) {
|
||||||
|
text = renderDialogueTemplate(text, playerStore, dialogue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成选项列表(包括动态选项)
|
||||||
|
let choices = dialogue.choices || []
|
||||||
|
if (dialogue.dynamicChoices && playerStore) {
|
||||||
|
const dynamicChoices = generateDynamicChoices(dialogue.dynamicChoices, playerStore)
|
||||||
|
choices = [...dynamicChoices, ...choices]
|
||||||
|
}
|
||||||
|
|
||||||
// 构建事件数据
|
// 构建事件数据
|
||||||
const eventData = {
|
const eventData = {
|
||||||
id: `npc_${npcId}_${dialogueKey}`,
|
id: `npc_${npcId}_${dialogueKey}`,
|
||||||
type: 'dialogue',
|
type: 'dialogue',
|
||||||
title: npc.name,
|
title: npc.name,
|
||||||
text: dialogue.text,
|
text: text,
|
||||||
npc: npc,
|
npc: npc,
|
||||||
choices: dialogue.choices || [],
|
choices: choices,
|
||||||
currentDialogue: dialogueKey,
|
currentDialogue: dialogueKey,
|
||||||
npcId
|
npcId
|
||||||
}
|
}
|
||||||
@@ -228,6 +244,61 @@ export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
|||||||
gameStore.drawerState.event = true
|
gameStore.drawerState.event = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染对话模板(替换变量)
|
||||||
|
*/
|
||||||
|
function renderDialogueTemplate(template, playerStore, dialogue) {
|
||||||
|
let text = template
|
||||||
|
|
||||||
|
// 替换义体列表
|
||||||
|
if (text.includes('{prosthetic_list}')) {
|
||||||
|
const prostheticList = getProstheticInventoryList(playerStore)
|
||||||
|
text = text.replace('{prosthetic_list}', prostheticList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换已装备的义体
|
||||||
|
if (text.includes('{equipped_prosthetic}')) {
|
||||||
|
const equipped = playerStore.equipment?.prosthetic
|
||||||
|
const equippedText = equipped ? `${equipped.name} (${equipped.description})` : '无'
|
||||||
|
text = text.replace('{equipped_prosthetic}', equippedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取玩家背包中的义体列表
|
||||||
|
*/
|
||||||
|
function getProstheticInventoryList(playerStore) {
|
||||||
|
const prosthetics = playerStore.inventory.filter(item => item.type === 'prosthetic')
|
||||||
|
if (prosthetics.length === 0) {
|
||||||
|
return '(你没有义体可以安装)'
|
||||||
|
}
|
||||||
|
return prosthetics.map(p => `• ${p.name}: ${p.description}`).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成动态选项
|
||||||
|
*/
|
||||||
|
function generateDynamicChoices(type, playerStore) {
|
||||||
|
switch (type) {
|
||||||
|
case 'prosthetic_inventory':
|
||||||
|
// 生成义体安装选项
|
||||||
|
const prosthetics = playerStore.inventory.filter(item => item.type === 'prosthetic')
|
||||||
|
if (prosthetics.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return prosthetics.map(p => ({
|
||||||
|
text: `安装 ${p.name}`,
|
||||||
|
action: 'install_prosthetic',
|
||||||
|
actionData: { itemId: p.id }
|
||||||
|
}))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理对话选择
|
* 处理对话选择
|
||||||
* @param {Object} gameStore - 游戏Store
|
* @param {Object} gameStore - 游戏Store
|
||||||
@@ -259,7 +330,7 @@ export function handleDialogueChoice(gameStore, playerStore, choice) {
|
|||||||
// 继续对话
|
// 继续对话
|
||||||
const currentEvent = gameStore.currentEvent
|
const currentEvent = gameStore.currentEvent
|
||||||
if (currentEvent && currentEvent.npc) {
|
if (currentEvent && currentEvent.npc) {
|
||||||
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next)
|
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next, playerStore)
|
||||||
result.closeEvent = false
|
result.closeEvent = false
|
||||||
}
|
}
|
||||||
} else if (!choice.action) {
|
} else if (!choice.action) {
|
||||||
@@ -418,6 +489,60 @@ export function processDialogueAction(gameStore, playerStore, action, actionData
|
|||||||
// 直接关闭
|
// 直接关闭
|
||||||
return { success: true, message: '', closeEvent: true }
|
return { success: true, message: '', closeEvent: true }
|
||||||
|
|
||||||
|
case 'install_prosthetic':
|
||||||
|
// 安装义体
|
||||||
|
if (actionData.itemId) {
|
||||||
|
const item = playerStore.inventory.find(i => i.id === actionData.itemId)
|
||||||
|
if (!item) {
|
||||||
|
return { success: false, message: '找不到该义体', closeEvent: false }
|
||||||
|
}
|
||||||
|
const installResult = installProsthetic(playerStore, gameStore, item)
|
||||||
|
if (installResult.success) {
|
||||||
|
// 从背包移除义体
|
||||||
|
const index = playerStore.inventory.findIndex(i => i.id === actionData.itemId)
|
||||||
|
if (index > -1) {
|
||||||
|
playerStore.inventory.splice(index, 1)
|
||||||
|
}
|
||||||
|
// 解锁义体技能
|
||||||
|
if (item.grantedSkill) {
|
||||||
|
unlockSkill(playerStore, item.grantedSkill)
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog(`解锁了技能: ${SKILL_CONFIG[item.grantedSkill]?.name || item.grantedSkill}`, 'system')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 更新成就进度
|
||||||
|
gameStore.achievementProgress.prostheticEquipped = 1
|
||||||
|
gameStore.checkAchievements('equip_prosthetic', { prostheticCount: 1 })
|
||||||
|
// 重新显示对话框
|
||||||
|
const currentEvent = gameStore.currentEvent
|
||||||
|
if (currentEvent && currentEvent.npc) {
|
||||||
|
startNPCDialogue(gameStore, currentEvent.npc.id, 'first', playerStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: installResult.success, message: installResult.message, closeEvent: false }
|
||||||
|
}
|
||||||
|
return { success: false, message: '义体参数错误', closeEvent: false }
|
||||||
|
|
||||||
|
case 'remove_prosthetic':
|
||||||
|
// 卸下义体
|
||||||
|
const removeResult = removeProsthetic(playerStore, gameStore)
|
||||||
|
if (removeResult.success) {
|
||||||
|
// 重新显示对话框
|
||||||
|
const currentEvent = gameStore.currentEvent
|
||||||
|
if (currentEvent && currentEvent.npc) {
|
||||||
|
startNPCDialogue(gameStore, currentEvent.npc.id, 'first', playerStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: removeResult.success, message: removeResult.message, closeEvent: false }
|
||||||
|
|
||||||
|
case 'refresh_list':
|
||||||
|
// 刷新列表(重新显示当前对话框)
|
||||||
|
const evt = gameStore.currentEvent
|
||||||
|
if (evt && evt.npc && evt.currentDialogue) {
|
||||||
|
startNPCDialogue(gameStore, evt.npc.id, evt.currentDialogue, playerStore)
|
||||||
|
}
|
||||||
|
return { success: true, message: '', closeEvent: false }
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown dialogue action: ${action}`)
|
console.warn(`Unknown dialogue action: ${action}`)
|
||||||
return { success: false, message: '未知动作', closeEvent: false }
|
return { success: false, message: '未知动作', closeEvent: false }
|
||||||
|
|||||||
142
utils/prostheticSystem.js
Normal file
142
utils/prostheticSystem.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 义体系统 - 安装、卸下、义体适应性
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装义体
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} prostheticItem - 义体物品
|
||||||
|
* @returns {Object} { success: boolean, message: string }
|
||||||
|
*/
|
||||||
|
export function installProsthetic(playerStore, gameStore, prostheticItem) {
|
||||||
|
// 检查是否已有义体
|
||||||
|
if (playerStore.equipment.prosthetic) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '你已经装备了义体,需要先卸下当前的义体。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查义体类型
|
||||||
|
if (prostheticItem.type !== 'prosthetic') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '这不是义体装备。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 装备义体
|
||||||
|
playerStore.equipment.prosthetic = { ...prostheticItem }
|
||||||
|
|
||||||
|
// 解锁义体适应性技能
|
||||||
|
if (!playerStore.skills['prosthetic_adaptation']) {
|
||||||
|
playerStore.skills['prosthetic_adaptation'] = { level: 0, exp: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStore.addLog(`安装了 ${prostheticItem.name}`, 'reward')
|
||||||
|
gameStore.addLog(`获得了被动技能: 义体适应性`, 'info')
|
||||||
|
|
||||||
|
// 检查成就
|
||||||
|
gameStore.checkAchievements('equip_prosthetic', { prostheticCount: 1 })
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `成功安装 ${prostheticItem.name}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸下义体
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Object} { success: boolean, message: string, removedItem: Object }
|
||||||
|
*/
|
||||||
|
export function removeProsthetic(playerStore, gameStore) {
|
||||||
|
const currentProsthetic = playerStore.equipment.prosthetic
|
||||||
|
|
||||||
|
if (!currentProsthetic) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '你没有装备任何义体。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将义体放回背包
|
||||||
|
playerStore.inventory.push({ ...currentProsthetic })
|
||||||
|
|
||||||
|
// 卸下义体
|
||||||
|
playerStore.equipment.prosthetic = null
|
||||||
|
|
||||||
|
gameStore.addLog(`卸下了 ${currentProsthetic.name}`, 'info')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `成功卸下 ${currentProsthetic.name}`,
|
||||||
|
removedItem: currentProsthetic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取义体提供的属性加成
|
||||||
|
* @param {Object} prosthetic - 义体装备
|
||||||
|
* @returns {Object} 属性加成
|
||||||
|
*/
|
||||||
|
export function getProstheticBonus(prosthetic) {
|
||||||
|
if (!prosthetic) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: prosthetic.stats || {},
|
||||||
|
skill: prosthetic.skill || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查义体技能是否可用
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {String} skillId - 技能ID
|
||||||
|
* @returns {Boolean} 是否可用
|
||||||
|
*/
|
||||||
|
export function isProstheticSkillAvailable(playerStore, skillId) {
|
||||||
|
const prosthetic = playerStore.equipment.prosthetic
|
||||||
|
if (!prosthetic) return false
|
||||||
|
|
||||||
|
// 检查义体是否提供该技能
|
||||||
|
return prosthetic.skill === skillId || prosthetic.grantedSkills?.includes(skillId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加义体适应性经验
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Number} exp - 经验值
|
||||||
|
*/
|
||||||
|
export function addProstheticExp(playerStore, gameStore, exp) {
|
||||||
|
const skill = playerStore.skills['prosthetic_adaptation']
|
||||||
|
if (!skill) return
|
||||||
|
|
||||||
|
const { SKILL_CONFIG } = require('@/config/skills.js')
|
||||||
|
const skillConfig = SKILL_CONFIG['prosthetic_adaptation']
|
||||||
|
const maxLevel = skillConfig.maxLevel
|
||||||
|
|
||||||
|
if (skill.level >= maxLevel) return
|
||||||
|
|
||||||
|
skill.exp += exp
|
||||||
|
const expNeeded = skillConfig.expPerLevel(skill.level)
|
||||||
|
|
||||||
|
while (skill.exp >= expNeeded && skill.level < maxLevel) {
|
||||||
|
skill.exp -= expNeeded
|
||||||
|
skill.level++
|
||||||
|
gameStore.addLog(`义体适应性提升到 Lv.${skill.level}!`, 'reward')
|
||||||
|
|
||||||
|
// 检查里程碑奖励
|
||||||
|
if (skillConfig.milestones[skill.level]) {
|
||||||
|
const milestone = skillConfig.milestones[skill.level]
|
||||||
|
gameStore.addLog(`解锁效果: ${milestone.desc}`, 'info')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skill.level < maxLevel) {
|
||||||
|
expNeeded = skillConfig.expPerLevel(skill.level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user