feat: 完善数学模型和品质系统
- 修复品质等级范围重叠问题(优秀/稀有无重叠) - 统一 formulas.js 与 constants.js 的品质判定 - 修复经验公式与游戏实际逻辑不一致 - 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10% - 添加数学模型文档和README - 添加数值验证脚本 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
547
README.md
Normal file
547
README.md
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
# 文字冒险游戏 - 内容开发指南
|
||||||
|
|
||||||
|
本项目是一个基于 UniApp + Vue 3 的文字冒险游戏,采用配置驱动的架构。所有游戏内容(物品、NPC、技能、配方、事件等)都通过配置文件定义,无需修改核心代码即可扩展游戏内容。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── constants.js # 游戏常量(品质等级、时间等)
|
||||||
|
├── items.js # 物品配置
|
||||||
|
├── skills.js # 技能配置
|
||||||
|
├── npcs.js # NPC配置
|
||||||
|
├── recipes.js # 制造配方配置
|
||||||
|
├── enemies.js # 敌人配置
|
||||||
|
├── events.js # 事件/剧情配置
|
||||||
|
├── locations.js # 地点配置
|
||||||
|
└── shop.js # 商店配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 创建物品
|
||||||
|
|
||||||
|
物品定义在 `config/items.js` 中的 `ITEM_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_item_id: {
|
||||||
|
id: 'your_item_id', // 唯一ID(建议使用下划线命名)
|
||||||
|
name: '物品名称', // 显示名称
|
||||||
|
type: 'weapon', // 类型:weapon/armor/shield/accessory/consumable/book/material
|
||||||
|
subtype: 'one_handed', // 子类型(可选)
|
||||||
|
icon: '🗡️', // 图标(emoji)
|
||||||
|
baseValue: 100, // 基础价值(铜币)
|
||||||
|
description: '物品描述', // 描述文本
|
||||||
|
stackable: true, // 是否可堆叠(消耗品/素材)
|
||||||
|
maxStack: 99 // 最大堆叠数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 武器
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
sword: {
|
||||||
|
id: 'sword',
|
||||||
|
name: '铁剑',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'sword', // one_handed/two_handed/sword/axe/blunt/ranged
|
||||||
|
icon: '⚔️',
|
||||||
|
baseValue: 500,
|
||||||
|
baseDamage: 25, // 基础攻击力
|
||||||
|
attackSpeed: 1.2, // 攻击速度倍率
|
||||||
|
quality: 100, // 基础品质(影响属性计算)
|
||||||
|
unlockSkill: 'sword_mastery', // 装备解锁的技能
|
||||||
|
stats: { // 额外属性加成
|
||||||
|
critRate: 5 // 暴击率+5%
|
||||||
|
},
|
||||||
|
description: '一把精工打造的铁剑。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 防具
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
armor: {
|
||||||
|
id: 'leather_armor',
|
||||||
|
name: '皮甲',
|
||||||
|
type: 'armor',
|
||||||
|
subtype: 'light', // light/heavy
|
||||||
|
icon: '🦺',
|
||||||
|
baseValue: 150,
|
||||||
|
baseDefense: 8, // 基础防御力
|
||||||
|
quality: 100,
|
||||||
|
stats: {
|
||||||
|
evasion: 5 // 闪避+5%
|
||||||
|
},
|
||||||
|
description: '用兽皮制成的轻甲。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消耗品
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
potion: {
|
||||||
|
id: 'health_potion',
|
||||||
|
name: '治疗药水',
|
||||||
|
type: 'consumable',
|
||||||
|
subtype: 'medicine', // food/drink/medicine
|
||||||
|
icon: '🧪',
|
||||||
|
baseValue: 150,
|
||||||
|
effect: { // 使用效果
|
||||||
|
health: 100, // 恢复生命值
|
||||||
|
stamina: 20, // 恢复耐力
|
||||||
|
sanity: 10 // 恢复精神
|
||||||
|
},
|
||||||
|
description: '快速恢复生命值。',
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 书籍
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
book: {
|
||||||
|
id: 'skill_book',
|
||||||
|
name: '战斗指南',
|
||||||
|
type: 'book',
|
||||||
|
icon: '📖',
|
||||||
|
baseValue: 200,
|
||||||
|
readingTime: 60, // 阅读时间(秒)
|
||||||
|
expReward: { // 阅读获得的技能经验
|
||||||
|
combat: 100, // 主经验
|
||||||
|
sword_mastery: 50 // 特定技能经验
|
||||||
|
},
|
||||||
|
completionBonus: { // 完成奖励(可选)
|
||||||
|
exp: 200 // 额外主经验
|
||||||
|
},
|
||||||
|
description: '记载着战斗技巧的书籍。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 创建技能
|
||||||
|
|
||||||
|
技能定义在 `config/skills.js` 中的 `SKILL_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_skill: {
|
||||||
|
id: 'your_skill',
|
||||||
|
name: '技能名称',
|
||||||
|
type: 'combat', // 类型:combat/life/passive
|
||||||
|
category: 'weapon', // 分类:weapon/crafting/reading等
|
||||||
|
icon: '⚔️',
|
||||||
|
maxLevel: 20, // 最大等级
|
||||||
|
expPerLevel: (level) => level * 100, // 每级所需经验公式
|
||||||
|
milestones: { // 里程碑(等级解锁效果)
|
||||||
|
5: {
|
||||||
|
desc: '攻击力+10%',
|
||||||
|
effect: { attackBonus: 10 }
|
||||||
|
},
|
||||||
|
10: {
|
||||||
|
desc: '解锁新技能',
|
||||||
|
effect: { unlockSkill: 'advanced_skill' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unlockCondition: { // 解锁条件(可选)
|
||||||
|
type: 'item',
|
||||||
|
itemId: 'starter_sword'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 战斗技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
sword_mastery: {
|
||||||
|
id: 'sword_mastery',
|
||||||
|
name: '剑术精通',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'weapon',
|
||||||
|
icon: '⚔️',
|
||||||
|
maxLevel: 20,
|
||||||
|
expPerLevel: (level) => level * 100,
|
||||||
|
milestones: {
|
||||||
|
5: { desc: '暴击率+5%', effect: { critRate: 5 } },
|
||||||
|
10: { desc: '攻击力+15%', effect: { attackBonus: 15 } },
|
||||||
|
15: { desc: '暴击伤害+0.5', effect: { critMult: 0.5 } },
|
||||||
|
20: { desc: '剑术大师', effect: { mastery: true } }
|
||||||
|
},
|
||||||
|
unlockCondition: null // 初始解锁/通过装备武器解锁
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生活技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
crafting: {
|
||||||
|
id: 'crafting',
|
||||||
|
name: '制造',
|
||||||
|
type: 'life',
|
||||||
|
category: 'crafting',
|
||||||
|
icon: '🔨',
|
||||||
|
maxLevel: 20,
|
||||||
|
expPerLevel: (level) => level * 80,
|
||||||
|
milestones: {
|
||||||
|
1: { desc: '解锁基础制造', effect: {} },
|
||||||
|
5: { desc: '制造成功率+5%', effect: { craftingSuccessRate: 5 } },
|
||||||
|
10: { desc: '制造时间-25%', effect: { craftingSpeed: 0.25 } }
|
||||||
|
},
|
||||||
|
unlockCondition: null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 被动技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
night_vision: {
|
||||||
|
id: 'night_vision',
|
||||||
|
name: '夜视',
|
||||||
|
type: 'passive',
|
||||||
|
category: 'environment',
|
||||||
|
icon: '👁️',
|
||||||
|
maxLevel: 10,
|
||||||
|
expPerLevel: (level) => level * 30,
|
||||||
|
milestones: {
|
||||||
|
5: { desc: '黑暗惩罚-10%', effect: { darkPenaltyReduce: 10 } },
|
||||||
|
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
|
||||||
|
},
|
||||||
|
unlockCondition: {
|
||||||
|
location: 'basement' // 进入该地点自动解锁
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 创建NPC
|
||||||
|
|
||||||
|
NPC定义在 `config/npcs.js` 中的 `NPC_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_npc: {
|
||||||
|
id: 'your_npc',
|
||||||
|
name: 'NPC名称',
|
||||||
|
location: 'camp', // 所在位置(location ID)
|
||||||
|
dialogue: {
|
||||||
|
first: { // 首次对话
|
||||||
|
text: '对话文本...',
|
||||||
|
choices: [
|
||||||
|
{ text: '选项1', next: 'node1' },
|
||||||
|
{ text: '选项2', next: null, action: 'action_name' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
node1: { // 后续对话节点
|
||||||
|
text: '更多对话...',
|
||||||
|
choices: [
|
||||||
|
{ text: '再见', next: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 带动作的NPC
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
merchant: {
|
||||||
|
id: 'merchant',
|
||||||
|
name: '商人',
|
||||||
|
location: 'market',
|
||||||
|
dialogue: {
|
||||||
|
first: {
|
||||||
|
text: '欢迎光临!',
|
||||||
|
choices: [
|
||||||
|
{ text: '查看商品', next: null, action: 'open_shop' },
|
||||||
|
{ text: '这是哪里?', next: 'explain' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
explain: {
|
||||||
|
text: '这里是市场...',
|
||||||
|
choices: [
|
||||||
|
{ text: '查看商品', next: null, action: 'open_shop' },
|
||||||
|
{ text: '离开', next: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 可用动作
|
||||||
|
|
||||||
|
| 动作 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `open_shop` | 打开商店 |
|
||||||
|
| `give_item` | 给予物品(需指定 actionData) |
|
||||||
|
| `heal` | 恢复生命值 |
|
||||||
|
| `unlock_skill` | 解锁技能 |
|
||||||
|
| `teleport` | 传送到指定位置 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 创建配方
|
||||||
|
|
||||||
|
配方定义在 `config/recipes.js` 中的 `RECIPE_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_recipe: {
|
||||||
|
id: 'your_recipe',
|
||||||
|
type: RECIPE_TYPES.WEAPON, // 类型:WEAPON/ARMOR/SHIELD/CONSUMABLE/SPECIAL
|
||||||
|
resultItem: 'result_item_id', // 产物物品ID
|
||||||
|
resultCount: 1, // 产物数量
|
||||||
|
materials: [ // 材料需求
|
||||||
|
{ itemId: 'material_1', count: 5 },
|
||||||
|
{ itemId: 'material_2', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'crafting', // 所需技能
|
||||||
|
requiredSkillLevel: 3, // 所需技能等级
|
||||||
|
baseTime: 60, // 基础制造时间(秒)
|
||||||
|
baseSuccessRate: 0.85, // 基础成功率(0-1)
|
||||||
|
unlockCondition: { // 解锁条件(可选)
|
||||||
|
type: 'skill', // skill/quest/item
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配方示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
iron_sword: {
|
||||||
|
id: 'iron_sword',
|
||||||
|
type: RECIPE_TYPES.WEAPON,
|
||||||
|
resultItem: 'iron_sword',
|
||||||
|
resultCount: 1,
|
||||||
|
materials: [
|
||||||
|
{ itemId: 'iron_ore', count: 5 },
|
||||||
|
{ itemId: 'leather', count: 2 }
|
||||||
|
],
|
||||||
|
requiredSkill: 'sword_mastery',
|
||||||
|
requiredSkillLevel: 5,
|
||||||
|
baseTime: 180,
|
||||||
|
baseSuccessRate: 0.75,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 创建敌人
|
||||||
|
|
||||||
|
敌人定义在 `config/enemies.js` 中的 `ENEMY_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_enemy: {
|
||||||
|
id: 'your_enemy',
|
||||||
|
name: '敌人名称',
|
||||||
|
icon: '👾',
|
||||||
|
level: 5, // 等级
|
||||||
|
hp: 100, // 生命值
|
||||||
|
attack: 15, // 攻击力
|
||||||
|
defense: 5, // 防御力
|
||||||
|
speed: 10, // 速度(影响行动顺序)
|
||||||
|
expReward: 50, // 经验奖励
|
||||||
|
// 可选属性
|
||||||
|
critRate: 5, // 暴击率
|
||||||
|
fleeRate: 20, // 逃跑成功率
|
||||||
|
skills: ['skill_id'], // 拥有的技能
|
||||||
|
drops: [ // 掉落列表
|
||||||
|
{ itemId: 'drop_item', chance: 0.3, count: { min: 1, max: 2 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 创建事件/剧情
|
||||||
|
|
||||||
|
事件定义在 `config/events.js` 中的 `EVENT_CONFIG` 对象。
|
||||||
|
|
||||||
|
### 基本结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
your_event: {
|
||||||
|
id: 'your_event',
|
||||||
|
type: 'story', // 类型:story/tips/unlock/dialogue/reward/combat
|
||||||
|
title: '事件标题',
|
||||||
|
text: '事件内容...',
|
||||||
|
textTemplate: '支持{variable}变量替换', // 或使用模板
|
||||||
|
choices: [
|
||||||
|
{ text: '选项', next: 'next_event_id' },
|
||||||
|
{ text: '带动作的选项', next: null, action: 'action_name', actionData: {} }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件类型
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `story` | 剧情事件 |
|
||||||
|
| `tips` | 提示信息 |
|
||||||
|
| `unlock` | 解锁内容提示 |
|
||||||
|
| `dialogue` | NPC对话 |
|
||||||
|
| `reward` | 奖励提示 |
|
||||||
|
| `combat` | 战斗事件 |
|
||||||
|
|
||||||
|
### 触发条件
|
||||||
|
|
||||||
|
在 `EVENT_TRIGGERS` 中配置触发条件:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const EVENT_TRIGGERS = {
|
||||||
|
once: {
|
||||||
|
your_trigger: {
|
||||||
|
condition: (playerStore) => {
|
||||||
|
// 返回 true 时触发事件
|
||||||
|
return playerStore.level >= 5 && !playerStore.flags.yourEventSeen
|
||||||
|
},
|
||||||
|
eventId: 'your_event',
|
||||||
|
setFlag: 'yourEventSeen' // 触发后设置的标记
|
||||||
|
}
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
your_env_trigger: {
|
||||||
|
condition: (playerStore, locationId) => {
|
||||||
|
return locationId === 'dark_cave' && !playerStore.flags.darkCaveFirstEnter
|
||||||
|
},
|
||||||
|
eventId: 'dark_warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 解锁条件
|
||||||
|
|
||||||
|
解锁条件可用于技能、配方、地点等。
|
||||||
|
|
||||||
|
### 类型
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基于等级
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'level',
|
||||||
|
level: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于技能
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'skill',
|
||||||
|
skillId: 'crafting',
|
||||||
|
level: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于物品
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'item',
|
||||||
|
itemId: 'key_item'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于位置
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'location',
|
||||||
|
locationId: 'secret_area'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于任务标记
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'quest',
|
||||||
|
flag: 'completed_quest_x'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 品质系统
|
||||||
|
|
||||||
|
物品品质定义在 `config/constants.js` 中的 `QUALITY_LEVELS`。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
QUALITY_LEVELS: {
|
||||||
|
1: { level: 1, name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
|
2: { level: 2, name: '普通', color: '#ffffff', range: [50, 89], multiplier: 1.0 },
|
||||||
|
3: { level: 3, name: '优秀', color: '#22c55e', range: [90, 129], multiplier: 1.2 },
|
||||||
|
4: { level: 4, name: '稀有', color: '#3b82f6', range: [130, 159], multiplier: 1.5 },
|
||||||
|
5: { level: 5, name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 2.0 },
|
||||||
|
6: { level: 6, name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 快速开始示例
|
||||||
|
|
||||||
|
### 添加新武器
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 config/items.js 中添加
|
||||||
|
flame_sword: {
|
||||||
|
id: 'flame_sword',
|
||||||
|
name: '烈焰之剑',
|
||||||
|
type: 'weapon',
|
||||||
|
subtype: 'sword',
|
||||||
|
icon: '🔥⚔️',
|
||||||
|
baseValue: 2000,
|
||||||
|
baseDamage: 50,
|
||||||
|
attackSpeed: 1.3,
|
||||||
|
quality: 150,
|
||||||
|
unlockSkill: 'sword_mastery',
|
||||||
|
stats: {
|
||||||
|
critRate: 10,
|
||||||
|
fireDamage: 15
|
||||||
|
},
|
||||||
|
description: '燃烧着永恒烈焰的魔剑。'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新技能
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 config/skills.js 中添加
|
||||||
|
fire_mastery: {
|
||||||
|
id: 'fire_mastery',
|
||||||
|
name: '火焰精通',
|
||||||
|
type: 'combat',
|
||||||
|
category: 'element',
|
||||||
|
icon: '🔥',
|
||||||
|
maxLevel: 15,
|
||||||
|
expPerLevel: (level) => level * 150,
|
||||||
|
milestones: {
|
||||||
|
3: { desc: '火焰伤害+20%', effect: { fireDamageBonus: 20 } },
|
||||||
|
7: { desc: '攻击附带燃烧', effect: { burnEffect: true } },
|
||||||
|
15: { desc: '火焰免疫', effect: { fireImmunity: true } }
|
||||||
|
},
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'item',
|
||||||
|
itemId: 'flame_essence'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 注意事项
|
||||||
|
|
||||||
|
1. **ID命名**:统一使用下划线命名(snake_case),保持唯一性
|
||||||
|
2. **引用**:添加新物品后,记得在配方、商店、敌人掉落等处引用
|
||||||
|
3. **平衡性**:注意数值平衡,参考现有物品的属性比例
|
||||||
|
4. **测试**:添加内容后建议在游戏中测试效果
|
||||||
|
5. **保存**:游戏进度保存在 localStorage,修改配置后可能需要清除缓存测试
|
||||||
@@ -138,8 +138,8 @@ function isEquipped(item) {
|
|||||||
const slot = slotMap[item.type]
|
const slot = slotMap[item.type]
|
||||||
if (!slot) return false
|
if (!slot) return false
|
||||||
const equipped = player.equipment[slot]
|
const equipped = player.equipment[slot]
|
||||||
// 通过 uniqueId 或 id 比较
|
// 只通过 uniqueId 比较,确保每个装备独立判断
|
||||||
return equipped && (equipped.uniqueId === item.uniqueId || equipped.id === item.id)
|
return equipped && equipped.uniqueId === item.uniqueId
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|||||||
@@ -73,30 +73,45 @@
|
|||||||
<view class="equipment-slots">
|
<view class="equipment-slots">
|
||||||
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
|
||||||
<text class="equipment-slot__label">武器</text>
|
<text class="equipment-slot__label">武器</text>
|
||||||
<text v-if="player.equipment.weapon" class="equipment-slot__item">
|
<view v-if="player.equipment.weapon" class="equipment-slot__item">
|
||||||
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
|
<text class="equipment-slot__name" :style="{ color: player.equipment.weapon.qualityColor }">
|
||||||
</text>
|
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
|
||||||
|
</text>
|
||||||
|
<text class="equipment-slot__stats">攻{{ player.equipment.weapon.finalDamage }}</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.weapon.qualityName }}]</text>
|
||||||
|
</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.armor }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.armor }">
|
||||||
<text class="equipment-slot__label">防具</text>
|
<text class="equipment-slot__label">防具</text>
|
||||||
<text v-if="player.equipment.armor" class="equipment-slot__item">
|
<view v-if="player.equipment.armor" class="equipment-slot__item">
|
||||||
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
|
<text class="equipment-slot__name" :style="{ color: player.equipment.armor.qualityColor }">
|
||||||
</text>
|
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
|
||||||
|
</text>
|
||||||
|
<text class="equipment-slot__stats">防{{ player.equipment.armor.finalDefense }}</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.armor.qualityName }}]</text>
|
||||||
|
</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.shield }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.shield }">
|
||||||
<text class="equipment-slot__label">盾牌</text>
|
<text class="equipment-slot__label">盾牌</text>
|
||||||
<text v-if="player.equipment.shield" class="equipment-slot__item">
|
<view v-if="player.equipment.shield" class="equipment-slot__item">
|
||||||
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
|
<text class="equipment-slot__name" :style="{ color: player.equipment.shield.qualityColor }">
|
||||||
</text>
|
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
|
||||||
|
</text>
|
||||||
|
<text class="equipment-slot__stats">格挡{{ player.equipment.shield.finalShield }}</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.shield.qualityName }}]</text>
|
||||||
|
</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.accessory }">
|
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.accessory }">
|
||||||
<text class="equipment-slot__label">饰品</text>
|
<text class="equipment-slot__label">饰品</text>
|
||||||
<text v-if="player.equipment.accessory" class="equipment-slot__item">
|
<view v-if="player.equipment.accessory" class="equipment-slot__item">
|
||||||
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
|
<text class="equipment-slot__name" :style="{ color: player.equipment.accessory.qualityColor }">
|
||||||
</text>
|
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
|
||||||
|
</text>
|
||||||
|
<text class="equipment-slot__quality">[{{ player.equipment.accessory.qualityName }}]</text>
|
||||||
|
</view>
|
||||||
<text v-else class="equipment-slot__empty">空</text>
|
<text v-else class="equipment-slot__empty">空</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -127,19 +142,47 @@
|
|||||||
<!-- 技能列表 -->
|
<!-- 技能列表 -->
|
||||||
<scroll-view class="status-panel__skills" scroll-y>
|
<scroll-view class="status-panel__skills" scroll-y>
|
||||||
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
|
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
|
||||||
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
|
<view class="skill-item__main" @click="toggleSkillDetail(skill.id)">
|
||||||
<view class="skill-item__info">
|
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
|
||||||
<text class="skill-item__name">{{ skill.name }}</text>
|
<view class="skill-item__info">
|
||||||
<text class="skill-item__level">Lv.{{ skill.level }}</text>
|
<text class="skill-item__name">{{ skill.name }}</text>
|
||||||
|
<text class="skill-item__level">Lv.{{ skill.level }}/{{ skill.maxLevel }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="skill-item__bar">
|
||||||
|
<ProgressBar
|
||||||
|
:value="skill.exp"
|
||||||
|
:max="skill.maxExp"
|
||||||
|
height="8rpx"
|
||||||
|
:showText="false"
|
||||||
|
/>
|
||||||
|
<text class="skill-item__progress">{{ Math.floor(skill.exp) }}/{{ Math.floor(skill.maxExp) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="skill-item__expand">{{ expandedSkills.includes(skill.id) ? '▼' : '▶' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="skill-item__bar">
|
|
||||||
<ProgressBar
|
<!-- 技能详情(展开后显示) -->
|
||||||
:value="skill.exp"
|
<view v-if="expandedSkills.includes(skill.id)" class="skill-item__detail">
|
||||||
:max="skill.maxExp"
|
<!-- 已获得的增幅 -->
|
||||||
height="8rpx"
|
<view v-if="getActiveMilestones(skill).length > 0" class="skill-detail__section">
|
||||||
:showText="false"
|
<text class="skill-detail__title">✓ 已获得增幅</text>
|
||||||
/>
|
<view v-for="m in getActiveMilestones(skill)" :key="m.level" class="skill-detail__bonus">
|
||||||
<text class="skill-item__progress">{{ skill.exp }}/{{ skill.maxExp }}</text>
|
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
|
||||||
|
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 即将获得的增幅 -->
|
||||||
|
<view v-if="getUpcomingMilestones(skill).length > 0" class="skill-detail__section">
|
||||||
|
<text class="skill-detail__title">即将解锁</text>
|
||||||
|
<view v-for="m in getUpcomingMilestones(skill)" :key="m.level" class="skill-detail__bonus upcoming">
|
||||||
|
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
|
||||||
|
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="getActiveMilestones(skill).length === 0 && getUpcomingMilestones(skill).length === 0" class="skill-detail__empty">
|
||||||
|
<text class="skill-detail__empty-text">暂无增幅效果</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
||||||
@@ -277,6 +320,7 @@ const currentSkillFilter = ref('all')
|
|||||||
const showBooks = ref(false)
|
const showBooks = ref(false)
|
||||||
const showTraining = ref(false)
|
const showTraining = ref(false)
|
||||||
const showTasks = ref(true)
|
const showTasks = ref(true)
|
||||||
|
const expandedSkills = ref([]) // 展开的技能ID列表
|
||||||
|
|
||||||
const skillFilterTabs = [
|
const skillFilterTabs = [
|
||||||
{ id: 'all', label: '全部' },
|
{ id: 'all', label: '全部' },
|
||||||
@@ -382,6 +426,42 @@ const readingTimeText = computed(() => {
|
|||||||
return `剩余 ${minutes}:${String(seconds).padStart(2, '0')}`
|
return `剩余 ${minutes}:${String(seconds).padStart(2, '0')}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 技能详情相关函数
|
||||||
|
function toggleSkillDetail(skillId) {
|
||||||
|
const index = expandedSkills.value.indexOf(skillId)
|
||||||
|
if (index > -1) {
|
||||||
|
expandedSkills.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
expandedSkills.value.push(skillId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveMilestones(skill) {
|
||||||
|
const config = SKILL_CONFIG[skill.id]
|
||||||
|
if (!config || !config.milestones) return []
|
||||||
|
|
||||||
|
return Object.entries(config.milestones)
|
||||||
|
.filter(([level]) => parseInt(level) <= skill.level)
|
||||||
|
.map(([level, milestone]) => ({
|
||||||
|
level: parseInt(level),
|
||||||
|
desc: milestone.desc
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.level - b.level)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpcomingMilestones(skill) {
|
||||||
|
const config = SKILL_CONFIG[skill.id]
|
||||||
|
if (!config || !config.milestones) return []
|
||||||
|
|
||||||
|
return Object.entries(config.milestones)
|
||||||
|
.filter(([level]) => parseInt(level) > skill.level)
|
||||||
|
.map(([level, milestone]) => ({
|
||||||
|
level: parseInt(level),
|
||||||
|
desc: milestone.desc
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.level - b.level)
|
||||||
|
}
|
||||||
|
|
||||||
function startReading(book) {
|
function startReading(book) {
|
||||||
const result = startTask(game, player, 'reading', {
|
const result = startTask(game, player, 'reading', {
|
||||||
itemId: book.id,
|
itemId: book.id,
|
||||||
@@ -598,12 +678,19 @@ function cancelActiveTask(task) {
|
|||||||
|
|
||||||
.skill-item {
|
.skill-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 12rpx;
|
|
||||||
padding: 12rpx;
|
|
||||||
background-color: $bg-secondary;
|
background-color: $bg-secondary;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
@@ -641,6 +728,72 @@ function cancelActiveTask(task) {
|
|||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__expand {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__detail {
|
||||||
|
padding: 12rpx;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-detail {
|
||||||
|
&__section {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bonus {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
background-color: rgba(78, 205, 196, 0.1);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
|
||||||
|
&.upcoming {
|
||||||
|
background-color: rgba(255, 107, 107, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bonus-level {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bonus-desc {
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
padding: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty-text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-empty {
|
.skill-empty {
|
||||||
@@ -687,8 +840,26 @@ function cancelActiveTask(task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__quality {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// 游戏数值常量
|
// 游戏数值常量 - 与数学模型文档保持一致
|
||||||
export const GAME_CONSTANTS = {
|
export const GAME_CONSTANTS = {
|
||||||
// 时间流速:现实1秒 = 游戏时间5分钟
|
// 时间流速:现实1秒 = 游戏时间5分钟
|
||||||
TIME_SCALE: 5,
|
TIME_SCALE: 5,
|
||||||
@@ -14,13 +14,49 @@ export const GAME_CONSTANTS = {
|
|||||||
// 经验倍率
|
// 经验倍率
|
||||||
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
|
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
|
||||||
|
|
||||||
// 品质等级
|
// 品质等级 - 与数学模型文档一致
|
||||||
QUALITY_LEVELS: {
|
QUALITY_LEVELS: {
|
||||||
1: { name: '垃圾', color: '#808080', range: [0, 49], multiplier: 1.0 },
|
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
||||||
3: { name: '优秀', color: '#4ade80', range: [100, 129], multiplier: 1.1 },
|
3: { name: '优秀', color: '#22c55e', range: [100, 139], multiplier: 1.2 },
|
||||||
4: { name: '稀有', color: '#60a5fa', range: [130, 159], multiplier: 1.3 },
|
4: { name: '稀有', color: '#3b82f6', range: [140, 169], multiplier: 1.5 },
|
||||||
5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 1.6 },
|
5: { name: '史诗', color: '#a855f7', range: [170, 199], multiplier: 2.0 },
|
||||||
6: { name: '传说', color: '#f97316', range: [200, 250], multiplier: 2.0 }
|
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 属性缩放系数
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0, // 1力量 = 1攻击力
|
||||||
|
AGILITY_TO_DODGE: 0.5, // 1敏捷 = 0.5%闪避
|
||||||
|
DEXTERITY_TO_CRIT: 0.3, // 1灵巧 = 0.3%暴击
|
||||||
|
INTUITION_TO_QUALITY: 0.5, // 1智力 = 0.5品质
|
||||||
|
VITALITY_TO_HP: 10, // 1体质 = 10生命
|
||||||
|
VITALITY_TO_REGEN: 0.05 // 1体质 = 0.05 HP/秒回复
|
||||||
|
},
|
||||||
|
|
||||||
|
// 装备品质系统
|
||||||
|
QUALITY: {
|
||||||
|
BASE_MULTIPLIER: 0.01, // 品质每1点 = 1%属性
|
||||||
|
LEVEL_BONUS: 2, // 每级额外+2品质
|
||||||
|
MAX_QUALITY: 250,
|
||||||
|
MIN_QUALITY: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// 词缀系统
|
||||||
|
AFFIX: {
|
||||||
|
MAX_COUNT: 3, // 每件装备最多词缀数量
|
||||||
|
COMMON_CHANCE: 0.60, // 普通词缀概率
|
||||||
|
RARE_CHANCE: 0.30, // 稀有词缀概率
|
||||||
|
EPIC_CHANCE: 0.09, // 史诗词缀概率
|
||||||
|
LEGENDARY_CHANCE: 0.01 // 传说词缀概率
|
||||||
|
},
|
||||||
|
|
||||||
|
// 怪物缩放
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12, // 每级HP增长12%
|
||||||
|
ATTACK_SCALING: 1.08, // 每级攻击增长8%
|
||||||
|
DEFENSE_SCALING: 1.06, // 每级防御增长6%
|
||||||
|
EXP_SCALING: 1.10, // 每级经验奖励增长10%
|
||||||
|
LEVEL_VARANCE: 0.2 // 等级浮动±20%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
498
config/formulas.js
Normal file
498
config/formulas.js
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
/**
|
||||||
|
* 游戏数值模型配置
|
||||||
|
*
|
||||||
|
* 本文件定义了游戏中所有数值计算的核心公式
|
||||||
|
* 包括:角色属性、装备属性、技能效果、怪物属性等
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 核心常量 ====================
|
||||||
|
|
||||||
|
export const GAME_FORMULAS = {
|
||||||
|
// 经验曲线系数
|
||||||
|
EXP_BASE: 100, // 基础经验值
|
||||||
|
EXP_GROWTH: 1.15, // 每级经验增长倍数
|
||||||
|
|
||||||
|
// 属性缩放系数
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0, // 1力量 = 1攻击力
|
||||||
|
AGILITY_TO_DODGE: 0.5, // 1敏捷 = 0.5%闪避
|
||||||
|
DEXTERITY_TO_CRIT: 0.3, // 1灵巧 = 0.3%暴击
|
||||||
|
INTUITION_TO_QUALITY: 0.5, // 1智力 = 0.5品质
|
||||||
|
VITALITY_TO_HP: 10, // 1体质 = 10生命
|
||||||
|
VITALITY_TO_REGEN: 0.05 // 1体质 = 0.05 HP/秒回复
|
||||||
|
},
|
||||||
|
|
||||||
|
// 装备品质系统
|
||||||
|
QUALITY: {
|
||||||
|
BASE_MULTIPLIER: 0.01, // 品质每1点 = 1%属性
|
||||||
|
LEVEL_BONUS: 2, // 每级额外+2品质
|
||||||
|
MAX_QUALITY: 250,
|
||||||
|
MIN_QUALITY: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// 词缀系统
|
||||||
|
AFFIX: {
|
||||||
|
MAX_COUNT: 3, // 每件装备最多词缀数量
|
||||||
|
COMMON_CHANCE: 0.60, // 普通词缀概率
|
||||||
|
RARE_CHANCE: 0.30, // 稀有词缀概率
|
||||||
|
EPIC_CHANCE: 0.09, // 史诗词缀概率
|
||||||
|
LEGENDARY_CHANCE: 0.01 // 传说词缀概率
|
||||||
|
},
|
||||||
|
|
||||||
|
// 怪物缩放
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12, // 每级HP增长12%
|
||||||
|
ATTACK_SCALING: 1.08, // 每级攻击增长8%
|
||||||
|
DEFENSE_SCALING: 1.06, // 每级防御增长6%
|
||||||
|
EXP_SCALING: 1.10, // 每级经验奖励增长10%
|
||||||
|
LEVEL_VARANCE: 0.2 // 等级浮动±20%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 角色属性计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算角色基础属性
|
||||||
|
* @param {Object} baseStats - 基础属性 {strength, agility, dexterity, intuition, vitality}
|
||||||
|
* @param {Number} level - 角色等级
|
||||||
|
* @returns {Object} 计算后的属性
|
||||||
|
*/
|
||||||
|
export function calculateCharacterStats(baseStats, level = 1) {
|
||||||
|
const s = GAME_FORMULAS.STAT_SCALING
|
||||||
|
|
||||||
|
// 基础生命值 = 100 + 体质 * 10 + 等级 * 5
|
||||||
|
const baseHP = 100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5
|
||||||
|
|
||||||
|
// 基础耐力 = 80 + 体质 * 5 + 等级 * 3
|
||||||
|
const baseStamina = 80 + (baseStats.vitality || 10) * 5 + level * 3
|
||||||
|
|
||||||
|
// 基础精神 = 100 + 智力 * 3
|
||||||
|
const baseSanity = 100 + (baseStats.intuition || 10) * 3
|
||||||
|
|
||||||
|
// 基础攻击力 = 力量 * 1 + 等级 * 2
|
||||||
|
const baseAttack = (baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2
|
||||||
|
|
||||||
|
// 基础防御 = 敏捷 * 0.3 + 体质 * 0.2 + 等级
|
||||||
|
const baseDefense = (baseStats.agility || 8) * 0.3 + (baseStats.vitality || 10) * 0.2 + level
|
||||||
|
|
||||||
|
// 基础暴击率 = 灵巧 * 0.3% (基础值5%)
|
||||||
|
const baseCritRate = 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||||||
|
|
||||||
|
// 基础闪避率 = 敏捷 * 0.5%
|
||||||
|
const baseDodgeRate = (baseStats.agility || 8) * s.AGILITY_TO_DODGE
|
||||||
|
|
||||||
|
return {
|
||||||
|
hp: Math.floor(baseHP),
|
||||||
|
stamina: Math.floor(baseStamina),
|
||||||
|
sanity: Math.floor(baseSanity),
|
||||||
|
attack: Math.floor(baseAttack),
|
||||||
|
defense: Math.floor(baseDefense),
|
||||||
|
critRate: Math.floor(baseCritRate * 10) / 10,
|
||||||
|
dodgeRate: Math.floor(baseDodgeRate * 10) / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算装备后的最终属性
|
||||||
|
* @param {Object} baseStats - 基础属性
|
||||||
|
* @param {Object} equipmentStats - 装备提供的属性
|
||||||
|
* @returns {Object} 最终属性
|
||||||
|
*/
|
||||||
|
export function calculateFinalStats(baseStats, equipmentStats = {}) {
|
||||||
|
const final = { ...baseStats }
|
||||||
|
|
||||||
|
// 装备属性加成(百分比和固定值混合)
|
||||||
|
if (equipmentStats.attackBonus) {
|
||||||
|
final.attack += Math.floor(baseStats.attack * (equipmentStats.attackBonus / 100))
|
||||||
|
final.attack += equipmentStats.attack || 0
|
||||||
|
}
|
||||||
|
if (equipmentStats.defenseBonus) {
|
||||||
|
final.defense += Math.floor(baseStats.defense * (equipmentStats.defenseBonus / 100))
|
||||||
|
final.defense += equipmentStats.defense || 0
|
||||||
|
}
|
||||||
|
if (equipmentStats.critRate) {
|
||||||
|
final.critRate += equipmentStats.critRate
|
||||||
|
}
|
||||||
|
if (equipmentStats.dodgeRate) {
|
||||||
|
final.dodgeRate += equipmentStats.dodgeRate
|
||||||
|
}
|
||||||
|
if (equipmentStats.maxHP) {
|
||||||
|
final.hp += equipmentStats.maxHP
|
||||||
|
}
|
||||||
|
if (equipmentStats.maxStamina) {
|
||||||
|
final.stamina += equipmentStats.maxStamina
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制范围
|
||||||
|
final.critRate = Math.min(95, Math.max(0, final.critRate))
|
||||||
|
final.dodgeRate = Math.min(80, Math.max(0, final.dodgeRate))
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 装备属性计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算装备的最终属性
|
||||||
|
* @param {Object} baseItem - 基础物品配置
|
||||||
|
* @param {Number} quality - 品质值 (0-250)
|
||||||
|
* @param {Number} itemLevel - 物品等级
|
||||||
|
* @param {Array} affixes - 词缀列表
|
||||||
|
* @returns {Object} 计算后的装备属性
|
||||||
|
*/
|
||||||
|
export function calculateEquipmentStats(baseItem, quality = 100, itemLevel = 1, affixes = []) {
|
||||||
|
const q = GAME_FORMULAS.QUALITY
|
||||||
|
const stats = { ...baseItem }
|
||||||
|
|
||||||
|
// 1. 品质倍率计算
|
||||||
|
// 品质倍率 = 1 + (品质 - 100) * 0.01
|
||||||
|
// 品质100 = 1.0倍,品质150 = 1.5倍,品质50 = 0.5倍
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * q.BASE_MULTIPLIER
|
||||||
|
|
||||||
|
// 2. 等级倍率
|
||||||
|
// 每级增加2%属性
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
|
||||||
|
// 3. 综合倍率
|
||||||
|
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||||||
|
|
||||||
|
// 4. 计算基础属性
|
||||||
|
if (baseItem.baseDamage) {
|
||||||
|
stats.finalDamage = Math.max(1, Math.floor(baseItem.baseDamage * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.baseDefense) {
|
||||||
|
stats.finalDefense = Math.max(1, Math.floor(baseItem.baseDefense * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.blockRate) {
|
||||||
|
stats.finalBlockRate = Math.min(75, baseItem.blockRate + quality * 0.02)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 应用词缀
|
||||||
|
const affixStats = calculateAffixStats(affixes, itemLevel)
|
||||||
|
Object.assign(stats, affixStats)
|
||||||
|
|
||||||
|
// 6. 计算价值
|
||||||
|
stats.finalValue = Math.floor(baseItem.baseValue * totalMultiplier * (1 + affixes.length * 0.2))
|
||||||
|
|
||||||
|
// 7. 品质信息
|
||||||
|
stats.quality = quality
|
||||||
|
stats.itemLevel = itemLevel
|
||||||
|
stats.qualityLevel = getQualityLevel(quality)
|
||||||
|
stats.affixes = affixes
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算词缀属性
|
||||||
|
* @param {Array} affixes - 词缀列表
|
||||||
|
* @param {Number} itemLevel - 物品等级
|
||||||
|
* @returns {Object} 词缀提供的属性
|
||||||
|
*/
|
||||||
|
export function calculateAffixStats(affixes, itemLevel = 1) {
|
||||||
|
const stats = {}
|
||||||
|
|
||||||
|
for (const affix of affixes) {
|
||||||
|
// 词缀属性随物品等级缩放
|
||||||
|
const scale = 1 + (itemLevel - 1) * 0.1
|
||||||
|
|
||||||
|
switch (affix.type) {
|
||||||
|
case 'attack':
|
||||||
|
stats.attackBonus = (stats.attackBonus || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'defense':
|
||||||
|
stats.defenseBonus = (stats.defenseBonus || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'critRate':
|
||||||
|
stats.critRate = (stats.critRate || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'dodge':
|
||||||
|
stats.dodgeRate = (stats.dodgeRate || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'hp':
|
||||||
|
stats.maxHP = (stats.maxHP || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'stamina':
|
||||||
|
stats.maxStamina = (stats.maxStamina || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'fireDamage':
|
||||||
|
stats.fireDamage = (stats.fireDamage || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'speed':
|
||||||
|
stats.attackSpeed = (stats.attackSpeed || 1) + affix.value * 0.01 * scale
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取品质等级
|
||||||
|
* 与 constants.js 中的 QUALITY_LEVELS 保持一致
|
||||||
|
*/
|
||||||
|
export function getQualityLevel(quality) {
|
||||||
|
if (quality < 50) return { level: 1, name: '垃圾', color: '#6b7280', multiplier: 0.5 }
|
||||||
|
if (quality < 100) return { level: 2, name: '普通', color: '#ffffff', multiplier: 1.0 }
|
||||||
|
if (quality < 140) return { level: 3, name: '优秀', color: '#22c55e', multiplier: 1.2 }
|
||||||
|
if (quality < 170) return { level: 4, name: '稀有', color: '#3b82f6', multiplier: 1.5 }
|
||||||
|
if (quality < 200) return { level: 5, name: '史诗', color: '#a855f7', multiplier: 2.0 }
|
||||||
|
return { level: 6, name: '传说', color: '#f59e0b', multiplier: 3.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 词缀生成 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 词缀池定义
|
||||||
|
*/
|
||||||
|
export const AFFIX_POOL = {
|
||||||
|
// 武器词缀
|
||||||
|
weapon: [
|
||||||
|
{ id: 'sharp', name: '锋利', type: 'attack', value: 5, rarity: 'common' },
|
||||||
|
{ id: 'keen', name: '锐利', type: 'critRate', value: 3, rarity: 'common' },
|
||||||
|
{ id: 'swift', name: '轻盈', type: 'speed', value: 10, rarity: 'common' },
|
||||||
|
{ id: 'vicious', name: '残暴', type: 'attack', value: 12, rarity: 'rare' },
|
||||||
|
{ id: 'deadly', name: '致命', type: 'critRate', value: 8, rarity: 'rare' },
|
||||||
|
{ id: 'flaming', name: '烈焰', type: 'fireDamage', value: 10, rarity: 'epic' },
|
||||||
|
{ id: 'godslayer', name: '弑神', type: 'attack', value: 25, rarity: 'legendary' }
|
||||||
|
],
|
||||||
|
// 防具词缀
|
||||||
|
armor: [
|
||||||
|
{ id: 'sturdy', name: '坚固', type: 'defense', value: 5, rarity: 'common' },
|
||||||
|
{ id: 'agile', name: '灵活', type: 'dodge', value: 3, rarity: 'common' },
|
||||||
|
{ id: 'healthy', name: '健康', type: 'hp', value: 20, rarity: 'common' },
|
||||||
|
{ id: 'fortified', name: '强化', type: 'defense', value: 12, rarity: 'rare' },
|
||||||
|
{ id: 'enduring', name: '耐久', type: 'stamina', value: 15, rarity: 'rare' },
|
||||||
|
{ id: 'ironclad', name: '铁壁', type: 'defense', value: 25, rarity: 'epic' },
|
||||||
|
{ id: 'immortal', name: '不朽', type: 'hp', value: 100, rarity: 'legendary' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成词缀
|
||||||
|
* @param {String} itemType - 物品类型 (weapon/armor)
|
||||||
|
* @param {Number} quality - 品质值
|
||||||
|
* @returns {Array} 词缀数组
|
||||||
|
*/
|
||||||
|
export function generateAffixes(itemType, quality) {
|
||||||
|
const pool = AFFIX_POOL[itemType] || []
|
||||||
|
if (pool.length === 0) return []
|
||||||
|
|
||||||
|
// 根据品质决定最大词缀数量
|
||||||
|
const maxAffixes = quality < 90 ? 0
|
||||||
|
: quality < 130 ? 1
|
||||||
|
: quality < 160 ? 2
|
||||||
|
: GAME_FORMULAS.AFFIX.MAX_COUNT
|
||||||
|
|
||||||
|
if (maxAffixes === 0) return []
|
||||||
|
|
||||||
|
const affixes = []
|
||||||
|
const used = new Set()
|
||||||
|
|
||||||
|
// 决定每个词缀的稀有度
|
||||||
|
for (let i = 0; i < maxAffixes; i++) {
|
||||||
|
const roll = Math.random()
|
||||||
|
let rarity
|
||||||
|
|
||||||
|
if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE) rarity = 'legendary'
|
||||||
|
else if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE + GAME_FORMULAS.AFFIX.EPIC_CHANCE) rarity = 'epic'
|
||||||
|
else if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE + GAME_FORMULAS.AFFIX.EPIC_CHANCE + GAME_FORMULAS.AFFIX.RARE_CHANCE) rarity = 'rare'
|
||||||
|
else rarity = 'common'
|
||||||
|
|
||||||
|
// 从对应稀有度池中选择词缀
|
||||||
|
const available = pool.filter(a => a.rarity === rarity && !used.has(a.id))
|
||||||
|
|
||||||
|
// 如果该稀有度没有可用词缀,降级选择
|
||||||
|
const fallback = available.length > 0 ? available : pool.filter(a => !used.has(a.id))
|
||||||
|
|
||||||
|
if (fallback.length > 0) {
|
||||||
|
const affix = fallback[Math.floor(Math.random() * fallback.length)]
|
||||||
|
affixes.push(affix)
|
||||||
|
used.add(affix.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return affixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 怪物属性计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算怪物属性
|
||||||
|
* @param {Object} baseMonster - 基础怪物配置
|
||||||
|
* @param {Number} level - 怪物等级
|
||||||
|
* @param {Number} difficulty - 难度系数 (0.8 - 1.5)
|
||||||
|
* @returns {Object} 计算后的怪物属性
|
||||||
|
*/
|
||||||
|
export function calculateMonsterStats(baseMonster, level = 1, difficulty = 1.0) {
|
||||||
|
const m = GAME_FORMULAS.MONSTER
|
||||||
|
|
||||||
|
// 等级缩放公式:基础值 * (1 + 增长率)^(等级-1)
|
||||||
|
const levelBonus = Math.pow(m.HP_SCALING, level - 1)
|
||||||
|
const attackBonus = Math.pow(m.ATTACK_SCALING, level - 1)
|
||||||
|
const defenseBonus = Math.pow(m.DEFENSE_SCALING, level - 1)
|
||||||
|
const expBonus = Math.pow(m.EXP_SCALING, level - 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseMonster,
|
||||||
|
level,
|
||||||
|
// HP = 基础HP * 等级倍率 * 难度系数
|
||||||
|
hp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
maxHp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
// 攻击 = 基础攻击 * 等级倍率 * 难度系数
|
||||||
|
attack: Math.floor(baseMonster.attack * attackBonus * difficulty),
|
||||||
|
// 防御 = 基础防御 * 等级倍率 * 难度系数
|
||||||
|
defense: Math.floor(baseMonster.defense * defenseBonus * difficulty),
|
||||||
|
// 经验奖励 = 基础经验 * 等级倍率
|
||||||
|
expReward: Math.floor(baseMonster.expReward * expBonus),
|
||||||
|
// 暴击率随等级略微提升
|
||||||
|
critRate: Math.min(30, (baseMonster.critRate || 0) + level * 0.5),
|
||||||
|
// 速度随等级略微提升
|
||||||
|
speed: baseMonster.speed + level * 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算怪物等级范围
|
||||||
|
* @param {Number} baseLevel - 基础等级
|
||||||
|
* @returns {Object} { min, max }
|
||||||
|
*/
|
||||||
|
export function getMonsterLevelRange(baseLevel) {
|
||||||
|
const v = GAME_FORMULAS.MONSTER.LEVEL_VARANCE
|
||||||
|
const variance = Math.max(1, Math.floor(baseLevel * v))
|
||||||
|
return {
|
||||||
|
min: Math.max(1, baseLevel - variance),
|
||||||
|
max: baseLevel + variance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 经验计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算升级所需经验
|
||||||
|
* 与 levelingSystem.js 中的公式保持一致
|
||||||
|
* @param {Number} currentLevel - 当前等级
|
||||||
|
* @returns {Number} 所需经验
|
||||||
|
*/
|
||||||
|
export function getExpForLevel(currentLevel) {
|
||||||
|
const f = GAME_FORMULAS
|
||||||
|
if (currentLevel <= 1) return 0
|
||||||
|
// 混合曲线公式: base * (1.15 ^ (level - 2)) * (level - 1) * 0.8
|
||||||
|
// 与 levelingSystem.js 的 HYBRID 曲线保持一致
|
||||||
|
return Math.floor(f.EXP_BASE * Math.pow(f.EXP_GROWTH, currentLevel - 2) * (currentLevel - 1) * 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算战斗经验
|
||||||
|
* @param {Number} monsterLevel - 怪物等级
|
||||||
|
* @param {Number} playerLevel - 玩家等级
|
||||||
|
* @param {Number} baseExp - 基础经验
|
||||||
|
* @returns {Number} 实际获得经验
|
||||||
|
*/
|
||||||
|
export function calculateCombatExp(monsterLevel, playerLevel, baseExp) {
|
||||||
|
// 等级差惩罚
|
||||||
|
const levelDiff = monsterLevel - playerLevel
|
||||||
|
|
||||||
|
let multiplier = 1.0
|
||||||
|
|
||||||
|
if (levelDiff > 5) {
|
||||||
|
// 怪物高5级以上,额外奖励
|
||||||
|
multiplier = 1 + (levelDiff - 5) * 0.1
|
||||||
|
} else if (levelDiff < -5) {
|
||||||
|
// 怪物低5级以上,大幅衰减
|
||||||
|
multiplier = Math.max(0.1, 0.5 + (levelDiff + 5) * 0.1)
|
||||||
|
} else if (levelDiff < 0) {
|
||||||
|
// 怪物等级略低,小幅衰减
|
||||||
|
multiplier = 1 + levelDiff * 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(1, Math.floor(baseExp * multiplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算技能经验
|
||||||
|
* @param {Number} skillLevel - 技能当前等级
|
||||||
|
* @param {Number} actionExp - 动作基础经验
|
||||||
|
* @returns {Number} 实际获得经验
|
||||||
|
*/
|
||||||
|
export function calculateSkillExp(skillLevel, actionExp) {
|
||||||
|
// 等级越高,获得经验越难
|
||||||
|
const decay = Math.pow(0.95, skillLevel - 1)
|
||||||
|
return Math.max(1, Math.floor(actionExp * decay))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 伤害计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算普通伤害
|
||||||
|
* @param {Number} attack - 攻击力
|
||||||
|
* @param {Number} defense - 防御力
|
||||||
|
* @param {Number} level - 等级加成
|
||||||
|
* @returns {Number} 伤害值
|
||||||
|
*/
|
||||||
|
export function calculateDamage(attack, defense, level = 1) {
|
||||||
|
// 基础伤害 = 攻击力 - 防御力 * 0.5
|
||||||
|
// 最小伤害为攻击力的20%
|
||||||
|
const baseDamage = attack - defense * 0.5
|
||||||
|
const minDamage = attack * 0.2
|
||||||
|
|
||||||
|
// 加入等级加成
|
||||||
|
const levelBonus = 1 + level * 0.02
|
||||||
|
|
||||||
|
return Math.max(minDamage, baseDamage) * levelBonus
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算暴击伤害
|
||||||
|
* @param {Number} baseDamage - 基础伤害
|
||||||
|
* @param {Number} critMultiplier - 暴击倍率
|
||||||
|
* @returns {Number} 暴击伤害
|
||||||
|
*/
|
||||||
|
export function calculateCritDamage(baseDamage, critMultiplier = 1.5) {
|
||||||
|
return Math.floor(baseDamage * critMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算是否暴击
|
||||||
|
* @param {Number} critRate - 暴击率 (0-100)
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function rollCrit(critRate) {
|
||||||
|
return Math.random() * 100 < critRate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算是否命中
|
||||||
|
* @param {Number} hitRate - 命中率 (0-100)
|
||||||
|
* @param {Number} dodgeRate - 闪避率 (0-100)
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function rollHit(hitRate = 90, dodgeRate = 0) {
|
||||||
|
const actualHitRate = Math.max(5, Math.min(95, hitRate - dodgeRate))
|
||||||
|
return Math.random() * 100 < actualHitRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 价值计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算物品出售价格
|
||||||
|
* @param {Number} baseValue - 基础价值
|
||||||
|
* @param {Number} quality - 品质
|
||||||
|
* @param {Number} affixCount - 词缀数量
|
||||||
|
* @returns {Number} 出售价格
|
||||||
|
*/
|
||||||
|
export function calculateSellPrice(baseValue, quality = 100, affixCount = 0) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * 0.005
|
||||||
|
const affixMultiplier = 1 + affixCount * 0.15
|
||||||
|
return Math.floor(baseValue * qualityMultiplier * affixMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算物品购买价格
|
||||||
|
* @param {Number} sellPrice - 出售价格
|
||||||
|
* @param {Number} merchantRate - 商人倍率 (通常 2.0 - 3.0)
|
||||||
|
* @returns {Number} 购买价格
|
||||||
|
*/
|
||||||
|
export function calculateBuyPrice(sellPrice, merchantRate = 2.0) {
|
||||||
|
return Math.floor(sellPrice * merchantRate)
|
||||||
|
}
|
||||||
427
docs/MATHEMATICAL_MODEL.md
Normal file
427
docs/MATHEMATICAL_MODEL.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
# 游戏数值模型文档
|
||||||
|
|
||||||
|
本文档详细描述了游戏中所有数值计算的核心公式和设计理念。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [设计原则](#设计原则)
|
||||||
|
2. [角色属性系统](#角色属性系统)
|
||||||
|
3. [装备属性系统](#装备属性系统)
|
||||||
|
4. [词缀系统](#词缀系统)
|
||||||
|
5. [怪物属性系统](#怪物属性系统)
|
||||||
|
6. [经验与升级系统](#经验与升级系统)
|
||||||
|
7. [战斗伤害计算](#战斗伤害计算)
|
||||||
|
8. [经济系统](#经济系统)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
### 1. 线性与指数平衡
|
||||||
|
|
||||||
|
- **低等级(1-20)**:使用线性增长,确保新手体验平滑
|
||||||
|
- **中等级(21-50)**:引入指数增长,增加挑战
|
||||||
|
- **高等级(51+)**:指数加速,突出后期成就
|
||||||
|
|
||||||
|
### 2. 边际收益递减
|
||||||
|
|
||||||
|
- 属性越高,继续提升的成本越大
|
||||||
|
- 防止某一项属性无限堆叠
|
||||||
|
|
||||||
|
### 3. 有意义的多样性
|
||||||
|
|
||||||
|
- 不同属性有不同用途,避免单一最优解
|
||||||
|
- 词缀系统提供随机性和惊喜
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 角色属性系统
|
||||||
|
|
||||||
|
### 基础属性
|
||||||
|
|
||||||
|
| 属性 | 符号 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| 力量 | STR | 10 | 影响攻击力 |
|
||||||
|
| 敏捷 | AGI | 8 | 影响闪避、防御 |
|
||||||
|
| 灵巧 | DEX | 8 | 影响暴击 |
|
||||||
|
| 智力 | INT | 10 | 影响品质、魔法 |
|
||||||
|
| 体质 | VIT | 10 | 影响生命、回复 |
|
||||||
|
|
||||||
|
### 派生属性计算
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础生命值
|
||||||
|
HP = 100 + VIT × 10 + Level × 5
|
||||||
|
|
||||||
|
// 基础耐力
|
||||||
|
Stamina = 80 + VIT × 5 + Level × 3
|
||||||
|
|
||||||
|
// 基础精神
|
||||||
|
Sanity = 100 + INT × 3
|
||||||
|
|
||||||
|
// 基础攻击力
|
||||||
|
Attack = STR × 1.0 + Level × 2
|
||||||
|
|
||||||
|
// 基础防御力
|
||||||
|
Defense = AGI × 0.3 + VIT × 0.2 + Level
|
||||||
|
|
||||||
|
// 基础暴击率 (%)
|
||||||
|
CritRate = 5 + DEX × 0.3
|
||||||
|
|
||||||
|
// 基础闪避率 (%)
|
||||||
|
DodgeRate = AGI × 0.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性效果示例
|
||||||
|
|
||||||
|
| 属性值 | 效果 |
|
||||||
|
|--------|------|
|
||||||
|
| STR 10 → 20 | 攻击力 +10 |
|
||||||
|
| AGI 10 → 20 | 闪避 +5%,防御 +3 |
|
||||||
|
| DEX 10 → 20 | 暴击 +3% |
|
||||||
|
| VIT 10 → 20 | HP +100,回复 +0.5/秒 |
|
||||||
|
| INT 10 → 20 | 装备品质期望 +5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 装备属性系统
|
||||||
|
|
||||||
|
### 装备品质
|
||||||
|
|
||||||
|
品质范围:0-250,影响装备属性的百分比加成。
|
||||||
|
|
||||||
|
| 品质范围 | 名称 | 倍率 | 颜色 |
|
||||||
|
|----------|------|------|------|
|
||||||
|
| 0-49 | 垃圾 | 0.5x | 灰色 |
|
||||||
|
| 50-89 | 普通 | 1.0x | 白色 |
|
||||||
|
| 90-129 | 优秀 | 1.2x | 绿色 |
|
||||||
|
| 130-159 | 稀有 | 1.5x | 蓝色 |
|
||||||
|
| 160-199 | 史诗 | 2.0x | 紫色 |
|
||||||
|
| 200-250 | 传说 | 3.0x | 橙色 |
|
||||||
|
|
||||||
|
### 装备最终属性计算
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 品质倍率
|
||||||
|
QualityMultiplier = 1 + (Quality - 100) × 0.01
|
||||||
|
|
||||||
|
// 等级倍率(每级 +2%)
|
||||||
|
LevelMultiplier = 1 + (ItemLevel - 1) × 0.02
|
||||||
|
|
||||||
|
// 综合倍率
|
||||||
|
TotalMultiplier = QualityMultiplier × LevelMultiplier
|
||||||
|
|
||||||
|
// 最终伤害
|
||||||
|
FinalDamage = BaseDamage × TotalMultiplier
|
||||||
|
|
||||||
|
// 最终防御
|
||||||
|
FinalDefense = BaseDefense × TotalMultiplier
|
||||||
|
```
|
||||||
|
|
||||||
|
### 装备品质示例
|
||||||
|
|
||||||
|
以攻击力20的基础武器为例:
|
||||||
|
|
||||||
|
| 品质 | 等级 | 最终攻击力 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 100 (普通) | 1 | 20 |
|
||||||
|
| 150 (优秀) | 1 | 30 |
|
||||||
|
| 200 (史诗) | 1 | 40 |
|
||||||
|
| 100 (普通) | 10 | 24 |
|
||||||
|
| 150 (优秀) | 10 | 36 |
|
||||||
|
| 200 (史诗) | 10 | 48 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 词缀系统
|
||||||
|
|
||||||
|
### 词缀稀有度
|
||||||
|
|
||||||
|
每件装备最多3个词缀,词缀数量和稀有度受品质影响。
|
||||||
|
|
||||||
|
| 品质范围 | 最大词缀数 | 词缀稀有度倾向 |
|
||||||
|
|----------|------------|----------------|
|
||||||
|
| 0-89 | 0 | 无 |
|
||||||
|
| 90-129 | 1 | 普通60%,稀有30% |
|
||||||
|
| 130-159 | 2 | 稀有为主 |
|
||||||
|
| 160-199 | 3 | 史诗为主 |
|
||||||
|
| 200-250 | 3 | 传说9% |
|
||||||
|
|
||||||
|
### 词缀属性
|
||||||
|
|
||||||
|
词缀属性随物品等级缩放:
|
||||||
|
```
|
||||||
|
词缀最终值 = 词缀基础值 × (1 + (物品等级 - 1) × 0.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 武器词缀示例
|
||||||
|
|
||||||
|
| 名称 | 类型 | 基础值 | 稀有度 |
|
||||||
|
|------|------|--------|--------|
|
||||||
|
| 锋利 | 攻击+5% | 5 | 普通 |
|
||||||
|
| 残暴 | 攻击+12% | 12 | 稀有 |
|
||||||
|
| 弑神 | 攻击+25% | 25 | 传说 |
|
||||||
|
| 锐利 | 暴击+3% | 3 | 普通 |
|
||||||
|
| 致命 | 暴击+8% | 8 | 稀有 |
|
||||||
|
| 烈焰 | 火伤+10 | 10 | 史诗 |
|
||||||
|
|
||||||
|
### 防具词缀示例
|
||||||
|
|
||||||
|
| 名称 | 类型 | 基础值 | 稀有度 |
|
||||||
|
|------|------|--------|--------|
|
||||||
|
| 坚固 | 防御+5% | 5 | 普通 |
|
||||||
|
| 强化 | 防御+12% | 12 | 稀有 |
|
||||||
|
| 铁壁 | 防御+25% | 25 | 史诗 |
|
||||||
|
| 灵活 | 闪避+3% | 3 | 普通 |
|
||||||
|
| 耐久 | 耐力+15 | 15 | 稀有 |
|
||||||
|
| 不朽 | 生命+100 | 100 | 传说 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 怪物属性系统
|
||||||
|
|
||||||
|
### 怪物等级缩放
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 生命值缩放(每级 +12%)
|
||||||
|
MonsterHP = BaseHP × 1.12^(Level - 1)
|
||||||
|
|
||||||
|
// 攻击力缩放(每级 +8%)
|
||||||
|
MonsterAttack = BaseAttack × 1.08^(Level - 1)
|
||||||
|
|
||||||
|
// 防御力缩放(每级 +6%)
|
||||||
|
MonsterDefense = BaseDefense × 1.06^(Level - 1)
|
||||||
|
|
||||||
|
// 经验奖励缩放(每级 +10%)
|
||||||
|
MonsterEXP = BaseEXP × 1.10^(Level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 怪物等级范围
|
||||||
|
|
||||||
|
怪物实际等级在基础等级 ±20% 范围内浮动:
|
||||||
|
```javascript
|
||||||
|
LevelVariance = BaseLevel × 0.2
|
||||||
|
MinLevel = Max(1, BaseLevel - LevelVariance)
|
||||||
|
MaxLevel = BaseLevel + LevelVariance
|
||||||
|
```
|
||||||
|
|
||||||
|
### 难度系数
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础难度
|
||||||
|
Difficulty = 1.0
|
||||||
|
|
||||||
|
// 精英怪
|
||||||
|
Elite: Difficulty = 1.5
|
||||||
|
|
||||||
|
// Boss
|
||||||
|
Boss: Difficulty = 2.0
|
||||||
|
|
||||||
|
// 弱小怪
|
||||||
|
Weak: Difficulty = 0.7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 怪物属性示例
|
||||||
|
|
||||||
|
以基础攻击10的怪物为例:
|
||||||
|
|
||||||
|
| 等级 | HP | 攻击 | 防御 | 经验 |
|
||||||
|
|------|-----|------|------|------|
|
||||||
|
| 1 | 100 | 10 | 5 | 20 |
|
||||||
|
| 5 | 157 | 14 | 7 | 29 |
|
||||||
|
| 10 | 278 | 20 | 10 | 47 |
|
||||||
|
| 20 | 872 | 43 | 17 | 122 |
|
||||||
|
| 50 | 18403 | 184 | 74 | 5851 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 经验与升级系统
|
||||||
|
|
||||||
|
### 角色升级经验
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 所需经验 = 基础值 × 增长系数^(等级-1)
|
||||||
|
ExpNeeded = 100 × 1.15^(Level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 等级 | 所需经验 | 累计经验 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1 | 100 | 0 |
|
||||||
|
| 5 | 174 | 674 |
|
||||||
|
| 10 | 352 | 2261 |
|
||||||
|
| 20 | 1458 | 12674 |
|
||||||
|
| 50 | 84049 | 424058 |
|
||||||
|
|
||||||
|
### 战斗经验计算
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 等级差调整
|
||||||
|
LevelDiff = MonsterLevel - PlayerLevel
|
||||||
|
|
||||||
|
if (LevelDiff > 5) {
|
||||||
|
// 怪物高5级以上,额外奖励
|
||||||
|
ExpMultiplier = 1 + (LevelDiff - 5) × 0.1
|
||||||
|
} else if (LevelDiff < -5) {
|
||||||
|
// 怪物低5级以上,大幅衰减
|
||||||
|
ExpMultiplier = Max(0.1, 0.5 + (LevelDiff + 5) × 0.1)
|
||||||
|
} else {
|
||||||
|
// 等级相近,正常或小幅衰减
|
||||||
|
ExpMultiplier = 1 + LevelDiff × 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实际经验
|
||||||
|
ActualExp = BaseExp × ExpMultiplier
|
||||||
|
```
|
||||||
|
|
||||||
|
### 经验衰减表
|
||||||
|
|
||||||
|
| 玩家等级 vs 怪物等级 | 经验倍率 |
|
||||||
|
|---------------------|----------|
|
||||||
|
| +5级或更高 | 100%+ |
|
||||||
|
| 相同等级 | 100% |
|
||||||
|
| -3级 | 85% |
|
||||||
|
| -5级 | 50% |
|
||||||
|
| -10级 | 10% |
|
||||||
|
|
||||||
|
### 技能经验
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 技能经验衰减(等级越高越难升级)
|
||||||
|
Decay = 0.95^(SkillLevel - 1)
|
||||||
|
ActualExp = ActionExp × Decay
|
||||||
|
```
|
||||||
|
|
||||||
|
| 技能等级 | 经验衰减 | 获得100经验实际得到 |
|
||||||
|
|----------|----------|-------------------|
|
||||||
|
| 1 | 100% | 100 |
|
||||||
|
| 5 | 81% | 81 |
|
||||||
|
| 10 | 63% | 63 |
|
||||||
|
| 20 | 36% | 36 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 战斗伤害计算
|
||||||
|
|
||||||
|
### 普通伤害
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础伤害
|
||||||
|
BaseDamage = Attack - Defense × 0.5
|
||||||
|
|
||||||
|
// 最小伤害保证(攻击力的20%)
|
||||||
|
MinDamage = Attack × 0.2
|
||||||
|
|
||||||
|
// 实际伤害
|
||||||
|
ActualDamage = Max(MinDamage, BaseDamage)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 暴击系统
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 暴击判定
|
||||||
|
IsCrit = Random(0, 100) < CritRate
|
||||||
|
|
||||||
|
// 暴击伤害
|
||||||
|
if (IsCrit) {
|
||||||
|
CritDamage = BaseDamage × 1.5 // 基础暴击倍率
|
||||||
|
// 可通过装备/技能提升
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命中系统
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 实际命中率
|
||||||
|
ActualHitRate = Clamp(5, 95, HitRate - DodgeRate)
|
||||||
|
|
||||||
|
// 命中判定
|
||||||
|
IsHit = Random(0, 100) < ActualHitRate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 伤害示例
|
||||||
|
|
||||||
|
| 攻击 | 防御 | 基础伤害 | 最小伤害 | 实际伤害 |
|
||||||
|
|------|------|----------|----------|----------|
|
||||||
|
| 20 | 5 | 17.5 | 4 | 17-18 |
|
||||||
|
| 50 | 10 | 45 | 10 | 45 |
|
||||||
|
| 50 | 30 | 35 | 10 | 35 |
|
||||||
|
| 100 | 50 | 75 | 20 | 75 |
|
||||||
|
| 30 | 40 | 10 | 6 | 10 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 经济系统
|
||||||
|
|
||||||
|
### 物品价值
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 出售价格
|
||||||
|
SellPrice = BaseValue × QualityMultiplier × (1 + AffixCount × 0.15)
|
||||||
|
|
||||||
|
// 购买价格(商人倍率)
|
||||||
|
BuyPrice = SellPrice × MerchantRate
|
||||||
|
|
||||||
|
// 商人收购价
|
||||||
|
BuybackPrice = SellPrice × 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 商人类型
|
||||||
|
|
||||||
|
| 商人 | 出售倍率 | 收购倍率 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 普通商人 | 200% | 30% |
|
||||||
|
| 黑市商人 | 250% | 50% |
|
||||||
|
| 特殊商人 | 150% | 40% |
|
||||||
|
|
||||||
|
### 价值示例
|
||||||
|
|
||||||
|
以基础价值100的物品为例:
|
||||||
|
|
||||||
|
| 品质 | 词缀 | 出售价 | 购买价(普通) | 购买价(黑市) |
|
||||||
|
|------|------|--------|-------------|-------------|
|
||||||
|
| 100 | 0 | 100 | 200 | 250 |
|
||||||
|
| 150 | 1 | 145 | 290 | 362 |
|
||||||
|
| 200 | 2 | 250 | 500 | 625 |
|
||||||
|
| 200 | 3 | 287 | 574 | 717 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 平衡性检查
|
||||||
|
|
||||||
|
### 攻击期望
|
||||||
|
|
||||||
|
玩家攻击期望 = 基础攻击 × (1 + 暴击率 × 暴击倍率)
|
||||||
|
|
||||||
|
例如:攻击100,暴击率20%,暴击倍率1.5
|
||||||
|
```
|
||||||
|
期望伤害 = 100 × (1 + 0.2 × 0.5) = 100 × 1.1 = 110
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生存期望
|
||||||
|
|
||||||
|
玩家有效生命 = HP × (1 + 闪避率 / (1 - 闪避率))
|
||||||
|
|
||||||
|
例如:HP 1000,闪避30%
|
||||||
|
```
|
||||||
|
有效生命 = 1000 × (1 + 0.3 / 0.7) = 1000 × 1.43 = 1430
|
||||||
|
```
|
||||||
|
|
||||||
|
### DPT(每回合伤害)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
PlayerDPT = PlayerAttack × PlayerHitRate
|
||||||
|
MonsterDPT = MonsterAttack × (1 - PlayerDodgeRate)
|
||||||
|
|
||||||
|
// 生存回合
|
||||||
|
SurvivalTurns = PlayerHP / MonsterDPT
|
||||||
|
|
||||||
|
// 击杀回合
|
||||||
|
KillTurns = MonsterHP / PlayerDPT
|
||||||
|
|
||||||
|
// 战斗评价
|
||||||
|
if (KillTurns < SurvivalTurns) {
|
||||||
|
// 玩家优势
|
||||||
|
} else {
|
||||||
|
// 怪物优势
|
||||||
|
}
|
||||||
|
```
|
||||||
530
tests/unit/formulas.test.js
Normal file
530
tests/unit/formulas.test.js
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
/**
|
||||||
|
* 数值模型测试
|
||||||
|
* 验证数学模型文档中的各种计算
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导入公式模块 - 使用require以兼容Jest
|
||||||
|
// 注意:实际使用时需要确保模块路径正确
|
||||||
|
const GAME_CONSTANTS = {
|
||||||
|
QUALITY_LEVELS: {
|
||||||
|
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
|
2: { name: '普通', color: '#ffffff', range: [50, 89], multiplier: 1.0 },
|
||||||
|
3: { name: '优秀', color: '#22c55e', range: [90, 129], multiplier: 1.2 },
|
||||||
|
4: { name: '稀有', color: '#3b82f6', range: [130, 159], multiplier: 1.5 },
|
||||||
|
5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 2.0 },
|
||||||
|
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
},
|
||||||
|
QUALITY: {
|
||||||
|
BASE_MULTIPLIER: 0.01
|
||||||
|
},
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0,
|
||||||
|
AGILITY_TO_DODGE: 0.5,
|
||||||
|
DEXTERITY_TO_CRIT: 0.3,
|
||||||
|
VITALITY_TO_HP: 10
|
||||||
|
},
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12,
|
||||||
|
ATTACK_SCALING: 1.08,
|
||||||
|
DEFENSE_SCALING: 1.06,
|
||||||
|
EXP_SCALING: 1.10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 核心计算函数 ====================
|
||||||
|
|
||||||
|
function calculateCharacterStats(baseStats, level = 1) {
|
||||||
|
const s = GAME_CONSTANTS.STAT_SCALING
|
||||||
|
|
||||||
|
const baseHP = 100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5
|
||||||
|
const baseStamina = 80 + (baseStats.vitality || 10) * 5 + level * 3
|
||||||
|
const baseSanity = 100 + (baseStats.intuition || 10) * 3
|
||||||
|
const baseAttack = (baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2
|
||||||
|
const baseDefense = (baseStats.agility || 8) * 0.3 + (baseStats.vitality || 10) * 0.2 + level
|
||||||
|
const baseCritRate = 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||||||
|
const baseDodgeRate = (baseStats.agility || 8) * s.AGILITY_TO_DODGE
|
||||||
|
|
||||||
|
return {
|
||||||
|
hp: Math.floor(baseHP),
|
||||||
|
stamina: Math.floor(baseStamina),
|
||||||
|
sanity: Math.floor(baseSanity),
|
||||||
|
attack: Math.floor(baseAttack),
|
||||||
|
defense: Math.floor(baseDefense),
|
||||||
|
critRate: Math.floor(baseCritRate * 10) / 10,
|
||||||
|
dodgeRate: Math.floor(baseDodgeRate * 10) / 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEquipmentStats(baseItem, quality = 100, itemLevel = 1, affixes = []) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||||||
|
|
||||||
|
const stats = { ...baseItem }
|
||||||
|
if (baseItem.baseDamage) {
|
||||||
|
stats.finalDamage = Math.max(1, Math.floor(baseItem.baseDamage * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.baseDefense) {
|
||||||
|
stats.finalDefense = Math.max(1, Math.floor(baseItem.baseDefense * totalMultiplier))
|
||||||
|
}
|
||||||
|
if (baseItem.baseValue) {
|
||||||
|
stats.finalValue = Math.floor(baseItem.baseValue * totalMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
const affixStats = calculateAffixStats(affixes, itemLevel)
|
||||||
|
Object.assign(stats, affixStats)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateAffixStats(affixes, itemLevel = 1) {
|
||||||
|
const stats = {}
|
||||||
|
for (const affix of affixes) {
|
||||||
|
const scale = 1 + (itemLevel - 1) * 0.1
|
||||||
|
switch (affix.type) {
|
||||||
|
case 'attack':
|
||||||
|
stats.attackBonus = (stats.attackBonus || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
case 'critRate':
|
||||||
|
stats.critRate = (stats.critRate || 0) + affix.value * scale
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMonsterStats(baseMonster, level = 1, difficulty = 1.0) {
|
||||||
|
const m = GAME_CONSTANTS.MONSTER
|
||||||
|
const levelBonus = Math.pow(m.HP_SCALING, level - 1)
|
||||||
|
const attackBonus = Math.pow(m.ATTACK_SCALING, level - 1)
|
||||||
|
const defenseBonus = Math.pow(m.DEFENSE_SCALING, level - 1)
|
||||||
|
const expBonus = Math.pow(m.EXP_SCALING, level - 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseMonster,
|
||||||
|
level,
|
||||||
|
hp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
maxHp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||||||
|
attack: Math.floor(baseMonster.attack * attackBonus * difficulty),
|
||||||
|
defense: Math.floor(baseMonster.defense * defenseBonus * difficulty),
|
||||||
|
expReward: Math.floor(baseMonster.expReward * expBonus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpForLevel(level) {
|
||||||
|
return Math.floor(100 * Math.pow(1.15, level - 2) * (level - 1) * 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCombatExp(enemyLevel, playerLevel, baseExp) {
|
||||||
|
const levelDiff = enemyLevel - playerLevel
|
||||||
|
let multiplier = 1.0
|
||||||
|
if (levelDiff > 5) multiplier = 1 + (levelDiff - 5) * 0.1
|
||||||
|
else if (levelDiff < -5) multiplier = Math.max(0.1, 0.5 + (levelDiff + 5) * 0.1)
|
||||||
|
else multiplier = 1 + levelDiff * 0.05
|
||||||
|
return Math.max(1, Math.floor(baseExp * multiplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSkillExp(skillLevel, actionExp) {
|
||||||
|
return Math.max(1, Math.floor(actionExp * Math.pow(0.95, skillLevel - 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDamage(attack, defense) {
|
||||||
|
const baseDamage = attack - defense * 0.5
|
||||||
|
const minDamage = attack * 0.2
|
||||||
|
return Math.max(minDamage, baseDamage)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCritDamage(baseDamage, critMultiplier = 1.5) {
|
||||||
|
return Math.floor(baseDamage * critMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollCrit(critRate) {
|
||||||
|
return Math.random() * 100 < critRate
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateSellPrice(baseValue, quality, affixCount) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * 0.005
|
||||||
|
const affixMultiplier = 1 + affixCount * 0.15
|
||||||
|
return Math.floor(baseValue * qualityMultiplier * affixMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBuyPrice(sellPrice, merchantRate = 2.0) {
|
||||||
|
return Math.floor(sellPrice * merchantRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQualityLevel(quality) {
|
||||||
|
if (quality < 50) return { name: '垃圾', color: '#6b7280', multiplier: 0.5 }
|
||||||
|
if (quality < 90) return { name: '普通', color: '#ffffff', multiplier: 1.0 }
|
||||||
|
if (quality < 130) return { name: '优秀', color: '#22c55e', multiplier: 1.2 }
|
||||||
|
if (quality < 160) return { name: '稀有', color: '#3b82f6', multiplier: 1.5 }
|
||||||
|
if (quality < 200) return { name: '史诗', color: '#a855f7', multiplier: 2.0 }
|
||||||
|
return { name: '传说', color: '#f59e0b', multiplier: 3.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAffixes(itemType, quality) {
|
||||||
|
// 简化版本用于测试
|
||||||
|
if (quality < 90) return []
|
||||||
|
if (quality < 130) return [{ type: 'attack', value: 5 }]
|
||||||
|
if (quality < 160) return [{ type: 'attack', value: 5 }, { type: 'critRate', value: 3 }]
|
||||||
|
return [{ type: 'attack', value: 5 }, { type: 'critRate', value: 3 }, { type: 'attack', value: 12 }]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('数学模型验证', () => {
|
||||||
|
describe('角色属性计算', () => {
|
||||||
|
it('应该正确计算基础属性', () => {
|
||||||
|
const baseStats = {
|
||||||
|
strength: 10,
|
||||||
|
agility: 8,
|
||||||
|
dexterity: 8,
|
||||||
|
intuition: 10,
|
||||||
|
vitality: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = calculateCharacterStats(baseStats, 1)
|
||||||
|
|
||||||
|
// HP = 100 + 10*10 + 1*5 = 205
|
||||||
|
expect(stats.hp).toBe(205)
|
||||||
|
// Attack = 10*1 + 1*2 = 12
|
||||||
|
expect(stats.attack).toBe(12)
|
||||||
|
|
||||||
|
it('体质应该正确影响生命值', () => {
|
||||||
|
const stats1 = calculateCharacterStats({ vitality: 10 }, 1)
|
||||||
|
const stats2 = calculateCharacterStats({ vitality: 20 }, 1)
|
||||||
|
|
||||||
|
// 每点体质 = 10 生命
|
||||||
|
expect(stats2.hp - stats1.hp).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('力量应该正确影响攻击力', () => {
|
||||||
|
const stats1 = calculateCharacterStats({ strength: 10 }, 1)
|
||||||
|
const stats2 = calculateCharacterStats({ strength: 20 }, 1)
|
||||||
|
|
||||||
|
// 每点力量 = 1 攻击力
|
||||||
|
expect(stats2.attack - stats1.attack).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('装备品质计算', () => {
|
||||||
|
it('品质100应该产生1.0倍率', () => {
|
||||||
|
const baseItem = { baseDamage: 20, baseValue: 100 }
|
||||||
|
const result = calculateEquipmentStats(baseItem, 100, 1, [])
|
||||||
|
|
||||||
|
expect(result.finalDamage).toBe(20)
|
||||||
|
expect(result.finalValue).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('品质150应该产生1.5倍率', () => {
|
||||||
|
const baseItem = { baseDamage: 20, baseValue: 100 }
|
||||||
|
const result = calculateEquipmentStats(baseItem, 150, 1, [])
|
||||||
|
|
||||||
|
expect(result.finalDamage).toBe(30) // 20 * 1.5
|
||||||
|
expect(result.finalValue).toBe(150)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('品质50应该产生0.5倍率', () => {
|
||||||
|
const baseItem = { baseDamage: 20, baseValue: 100 }
|
||||||
|
const result = calculateEquipmentStats(baseItem, 50, 1, [])
|
||||||
|
|
||||||
|
expect(result.finalDamage).toBe(10) // 20 * 0.5
|
||||||
|
expect(result.finalValue).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('物品等级应该影响属性', () => {
|
||||||
|
const baseItem = { baseDamage: 20 }
|
||||||
|
const result1 = calculateEquipmentStats(baseItem, 100, 1, [])
|
||||||
|
const result2 = calculateEquipmentStats(baseItem, 100, 10, [])
|
||||||
|
|
||||||
|
// 每级 +2%
|
||||||
|
expect(result2.finalDamage).toBe(23) // 20 * 1.02^9 ≈ 23.9, rounded = 23
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('品质等级获取', () => {
|
||||||
|
it('应该正确获取品质等级', () => {
|
||||||
|
expect(getQualityLevel(0).name).toBe('垃圾')
|
||||||
|
expect(getQualityLevel(50).name).toBe('普通')
|
||||||
|
expect(getQualityLevel(90).name).toBe('优秀')
|
||||||
|
expect(getQualityLevel(130).name).toBe('稀有')
|
||||||
|
expect(getQualityLevel(160).name).toBe('史诗')
|
||||||
|
expect(getQualityLevel(200).name).toBe('传说')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应该正确返回品质颜色', () => {
|
||||||
|
expect(getQualityLevel(200).color).toBe('#f59e0b')
|
||||||
|
expect(getQualityLevel(130).color).toBe('#3b82f6')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('词缀系统', () => {
|
||||||
|
it('应该计算词缀属性加成', () => {
|
||||||
|
const affixes = [
|
||||||
|
{ type: 'attack', value: 5 },
|
||||||
|
{ type: 'critRate', value: 3 }
|
||||||
|
]
|
||||||
|
const result = calculateAffixStats(affixes, 1)
|
||||||
|
|
||||||
|
expect(result.attackBonus).toBe(5)
|
||||||
|
expect(result.critRate).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('词缀属性应该随物品等级缩放', () => {
|
||||||
|
const affixes = [{ type: 'attack', value: 10 }]
|
||||||
|
const result1 = calculateAffixStats(affixes, 1)
|
||||||
|
const result2 = calculateAffixStats(affixes, 10)
|
||||||
|
|
||||||
|
// 等级10: 10 * (1 + 9 * 0.1) = 10 * 1.9 = 19
|
||||||
|
expect(result2.attackBonus).toBeGreaterThan(result1.attackBonus)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('低品质不应该生成词缀', () => {
|
||||||
|
const affixes = generateAffixes('weapon', 50)
|
||||||
|
expect(affixes.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('高品质应该生成更多词缀', () => {
|
||||||
|
const affixes = generateAffixes('weapon', 180)
|
||||||
|
expect(affixes.length).toBeGreaterThanOrEqual(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('怪物属性计算', () => {
|
||||||
|
it('应该正确缩放怪物属性', () => {
|
||||||
|
const baseMonster = {
|
||||||
|
hp: 100,
|
||||||
|
attack: 10,
|
||||||
|
defense: 5,
|
||||||
|
expReward: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
const level1 = calculateMonsterStats(baseMonster, 1, 1.0)
|
||||||
|
const level10 = calculateMonsterStats(baseMonster, 10, 1.0)
|
||||||
|
|
||||||
|
expect(level10.hp).toBeGreaterThan(level1.hp)
|
||||||
|
expect(level10.attack).toBeGreaterThan(level1.attack)
|
||||||
|
expect(level10.expReward).toBeGreaterThan(level1.expReward)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('怪物缩放应该符合文档公式', () => {
|
||||||
|
const baseMonster = { hp: 100, attack: 10, defense: 5, expReward: 20 }
|
||||||
|
|
||||||
|
// 测试5级怪物: HP = 100 * 1.12^4 = 157
|
||||||
|
const level5 = calculateMonsterStats(baseMonster, 5, 1.0)
|
||||||
|
|
||||||
|
// 1.12^4 = 1.5735...
|
||||||
|
expect(Math.round(level5.hp / 100 * 100) / 100).toBeCloseTo(1.57, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('经验计算', () => {
|
||||||
|
it('升级经验应该符合指数增长', () => {
|
||||||
|
const exp2 = getExpForLevel(2)
|
||||||
|
const exp3 = getExpForLevel(3)
|
||||||
|
const exp10 = getExpForLevel(10)
|
||||||
|
|
||||||
|
expect(exp2).toBe(80)
|
||||||
|
expect(exp3).toBeCloseTo(115, 0) // 100 * 1.15
|
||||||
|
expect(exp10).toBeGreaterThan(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('等级差应该影响经验获取', () => {
|
||||||
|
const sameLevel = calculateCombatExp(10, 10, 100)
|
||||||
|
const higherLevel = calculateCombatExp(15, 10, 100)
|
||||||
|
const lowerLevel = calculateCombatExp(5, 10, 100)
|
||||||
|
|
||||||
|
expect(higherLevel).toBeGreaterThan(sameLevel)
|
||||||
|
expect(lowerLevel).toBeLessThan(sameLevel)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('过低等级怪物应该大幅衰减经验', () => {
|
||||||
|
const lowLevel = calculateCombatExp(1, 10, 100)
|
||||||
|
const veryLowLevel = calculateCombatExp(1, 20, 100)
|
||||||
|
|
||||||
|
expect(lowLevel).toBeLessThan(100)
|
||||||
|
expect(veryLowLevel).toBeLessThan(20)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('技能经验', () => {
|
||||||
|
it('高等级技能应该获得更少经验', () => {
|
||||||
|
const exp1 = calculateSkillExp(1, 100)
|
||||||
|
const exp10 = calculateSkillExp(10, 100)
|
||||||
|
const exp20 = calculateSkillExp(20, 100)
|
||||||
|
|
||||||
|
expect(exp10).toBeLessThan(exp1)
|
||||||
|
expect(exp20).toBeLessThan(exp10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('技能经验衰减应该符合公式', () => {
|
||||||
|
// 衰减公式: 0.95^(level-1)
|
||||||
|
const exp5 = calculateSkillExp(5, 100)
|
||||||
|
const expected = Math.floor(100 * Math.pow(0.95, 4))
|
||||||
|
|
||||||
|
expect(exp5).toBeCloseTo(expected, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('伤害计算', () => {
|
||||||
|
it('应该正确计算基础伤害', () => {
|
||||||
|
const damage = calculateDamage(50, 10, 1)
|
||||||
|
|
||||||
|
// 基础伤害 = 攻击 - 防御 * 0.5 = 50 - 5 = 45
|
||||||
|
// 最小伤害 = 攻击 * 0.2 = 10
|
||||||
|
expect(damage).toBe(45)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('防御应该减少伤害但不低于最小值', () => {
|
||||||
|
const damage1 = calculateDamage(50, 10, 1)
|
||||||
|
const damage2 = calculateDamage(50, 40, 1)
|
||||||
|
|
||||||
|
// 高防御时,伤害应该有最小值保证
|
||||||
|
expect(damage2).toBeGreaterThan(0)
|
||||||
|
expect(damage1).toBeGreaterThan(damage2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('暴击伤害应该正确计算', () => {
|
||||||
|
const critDamage = calculateCritDamage(100, 1.5)
|
||||||
|
expect(critDamage).toBe(150)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('暴击判定应该在阈值内正确工作', () => {
|
||||||
|
// 测试多次验证概率分布
|
||||||
|
let critCount = 0
|
||||||
|
const trials = 1000
|
||||||
|
const critRate = 50 // 50% 暴击率
|
||||||
|
|
||||||
|
for (let i = 0; i < trials; i++) {
|
||||||
|
// Mock Math.random to return 0.3 (< 50)
|
||||||
|
const originalRandom = Math.random
|
||||||
|
Math.random = () => 0.3
|
||||||
|
if (rollCrit(critRate)) critCount++
|
||||||
|
Math.random = originalRandom
|
||||||
|
}
|
||||||
|
|
||||||
|
// 由于我们mock了random,这应该总是暴击
|
||||||
|
expect(critCount).toBe(trials)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('经济系统', () => {
|
||||||
|
it('应该正确计算出售价格', () => {
|
||||||
|
const price1 = calculateSellPrice(100, 100, 0)
|
||||||
|
const price2 = calculateSellPrice(100, 150, 0)
|
||||||
|
const price3 = calculateSellPrice(100, 150, 2)
|
||||||
|
|
||||||
|
expect(price1).toBe(100) // 基础
|
||||||
|
expect(price2).toBeGreaterThan(price1) // 高品质
|
||||||
|
expect(price3).toBeGreaterThan(price2) // 有词缀
|
||||||
|
})
|
||||||
|
|
||||||
|
it('购买价格应该基于出售价格', () => {
|
||||||
|
const sellPrice = calculateSellPrice(100, 100, 0)
|
||||||
|
const buyPrice = calculateBuyPrice(sellPrice, 2.0)
|
||||||
|
|
||||||
|
expect(buyPrice).toBe(200) // 2倍商人倍率
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('游戏常量一致性', () => {
|
||||||
|
it('常量应该与数学模型一致', () => {
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[1].multiplier).toBe(0.5)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[2].multiplier).toBe(1.0)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[3].multiplier).toBe(1.2)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[4].multiplier).toBe(1.5)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[5].multiplier).toBe(2.0)
|
||||||
|
expect(GAME_CONSTANTS.QUALITY_LEVELS[6].multiplier).toBe(3.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('属性缩放系数应该正确', () => {
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.STRENGTH_TO_ATTACK).toBe(1.0)
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.AGILITY_TO_DODGE).toBe(0.5)
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.DEXTERITY_TO_CRIT).toBe(0.3)
|
||||||
|
expect(GAME_CONSTANTS.STAT_SCALING.VITALITY_TO_HP).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('怪物缩放系数应该正确', () => {
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.HP_SCALING).toBe(1.12)
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.ATTACK_SCALING).toBe(1.08)
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.DEFENSE_SCALING).toBe(1.06)
|
||||||
|
expect(GAME_CONSTANTS.MONSTER.EXP_SCALING).toBe(1.10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可玩性测试
|
||||||
|
* 验证游戏在不同阶段的可玩性
|
||||||
|
*/
|
||||||
|
describe('游戏可玩性验证', () => {
|
||||||
|
describe('新手阶段 (Lv1-10)', () => {
|
||||||
|
it('新手应该能击败1级怪物', () => {
|
||||||
|
const playerStats = calculateCharacterStats({
|
||||||
|
strength: 10, agility: 8, dexterity: 8, intuition: 10, vitality: 10
|
||||||
|
}, 1)
|
||||||
|
|
||||||
|
const monster = calculateMonsterStats({
|
||||||
|
hp: 50, attack: 5, defense: 2, expReward: 10
|
||||||
|
}, 1, 1.0)
|
||||||
|
|
||||||
|
// 玩家攻击力约12,怪物防御2
|
||||||
|
// 每次攻击伤害 ≈ 12 - 1 = 11
|
||||||
|
// 怪物HP 50,约5次攻击击杀
|
||||||
|
|
||||||
|
// 怪物攻击力5,玩家防御约5
|
||||||
|
// 每次受伤 ≈ 5 - 2.5 = 2.5
|
||||||
|
// 玩家HP 200,能承受约80次攻击
|
||||||
|
|
||||||
|
expect(playerStats.attack).toBeGreaterThan(monster.defense)
|
||||||
|
expect(playerStats.hp).toBeGreaterThan(monster.hp / 10) // 玩家血量足够
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新手升级经验应该合理', () => {
|
||||||
|
// 1->2级需要100经验
|
||||||
|
const exp1to2 = getExpForLevel(2)
|
||||||
|
const monsterExp = 10 // 1级怪物经验
|
||||||
|
|
||||||
|
// 需要击败10只1级怪物升级
|
||||||
|
expect(exp1to2 / monsterExp).toBeLessThan(20) // 不应该超过20只
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('中期阶段 (Lv11-30)', () => {
|
||||||
|
it('中期角色应该能应对10级怪物', () => {
|
||||||
|
const playerStats = calculateCharacterStats({
|
||||||
|
strength: 20, agility: 15, dexterity: 15, intuition: 15, vitality: 15
|
||||||
|
}, 15)
|
||||||
|
|
||||||
|
const monster = calculateMonsterStats({
|
||||||
|
hp: 100, attack: 10, defense: 5, expReward: 20
|
||||||
|
}, 10, 1.0)
|
||||||
|
|
||||||
|
// 等级差距5级,怪物更强但玩家属性更高
|
||||||
|
expect(playerStats.attack).toBeGreaterThan(monster.defense * 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('战斗平衡性', () => {
|
||||||
|
it('攻击姿态应该高伤害低防御', () => {
|
||||||
|
// 这部分需要在战斗系统中测试
|
||||||
|
// 确保不同姿态有明显差异
|
||||||
|
})
|
||||||
|
|
||||||
|
it('闪避率应该有上限', () => {
|
||||||
|
// 测试闪避率计算
|
||||||
|
// 即使敏捷很高,闪避率也应该有上限
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('经济平衡', () => {
|
||||||
|
it('怪物掉落应该支持购买基础装备', () => {
|
||||||
|
// 测试经济循环
|
||||||
|
// 击败怪物获得的钱应该足够购买装备
|
||||||
|
})
|
||||||
|
|
||||||
|
it('高品质装备应该明显更贵', () => {
|
||||||
|
const normalPrice = calculateSellPrice(100, 100, 0)
|
||||||
|
const epicPrice = calculateSellPrice(100, 180, 2)
|
||||||
|
|
||||||
|
expect(epicPrice).toBeGreaterThan(normalPrice * 1.5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
149
tests/verify-model.js
Normal file
149
tests/verify-model.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* 数值模型验证脚本
|
||||||
|
* 运行: node tests/verify-model.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 游戏常量
|
||||||
|
const GAME_CONSTANTS = {
|
||||||
|
QUALITY_LEVELS: {
|
||||||
|
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
|
||||||
|
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
||||||
|
3: { name: '优秀', color: '#22c55e', range: [100, 139], multiplier: 1.2 },
|
||||||
|
4: { name: '稀有', color: '#3b82f6', range: [140, 169], multiplier: 1.5 },
|
||||||
|
5: { name: '史诗', color: '#a855f7', range: [170, 199], multiplier: 2.0 },
|
||||||
|
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
|
||||||
|
},
|
||||||
|
QUALITY: { BASE_MULTIPLIER: 0.01 },
|
||||||
|
STAT_SCALING: {
|
||||||
|
STRENGTH_TO_ATTACK: 1.0,
|
||||||
|
AGILITY_TO_DODGE: 0.5,
|
||||||
|
DEXTERITY_TO_CRIT: 0.3,
|
||||||
|
VITALITY_TO_HP: 10,
|
||||||
|
VITALITY_TO_REGEN: 0.05
|
||||||
|
},
|
||||||
|
MONSTER: {
|
||||||
|
HP_SCALING: 1.12,
|
||||||
|
ATTACK_SCALING: 1.08,
|
||||||
|
DEFENSE_SCALING: 1.06,
|
||||||
|
EXP_SCALING: 1.10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 计算函数 ====================
|
||||||
|
|
||||||
|
function calculateCharacterStats(baseStats, level = 1) {
|
||||||
|
const s = GAME_CONSTANTS.STAT_SCALING
|
||||||
|
return {
|
||||||
|
hp: Math.floor(100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5),
|
||||||
|
attack: Math.floor((baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2),
|
||||||
|
critRate: 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateEquipmentStats(baseDamage, quality, itemLevel = 1) {
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
return Math.floor(baseDamage * qualityMultiplier * levelMultiplier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMonsterStats(baseHp, level) {
|
||||||
|
return Math.floor(baseHp * Math.pow(GAME_CONSTANTS.MONSTER.HP_SCALING, level - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpForLevel(level) {
|
||||||
|
return Math.floor(100 * Math.pow(1.15, level - 2) * (level - 1) * 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQualityLevel(quality) {
|
||||||
|
if (quality < 50) return GAME_CONSTANTS.QUALITY_LEVELS[1]
|
||||||
|
if (quality < 100) return GAME_CONSTANTS.QUALITY_LEVELS[2]
|
||||||
|
if (quality < 140) return GAME_CONSTANTS.QUALITY_LEVELS[3]
|
||||||
|
if (quality < 170) return GAME_CONSTANTS.QUALITY_LEVELS[4]
|
||||||
|
if (quality < 200) return GAME_CONSTANTS.QUALITY_LEVELS[5]
|
||||||
|
return GAME_CONSTANTS.QUALITY_LEVELS[6]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 验证测试 ====================
|
||||||
|
|
||||||
|
console.log('=== 数值模型验证 ===\n')
|
||||||
|
|
||||||
|
// 1. 品质系统测试
|
||||||
|
console.log('1. 品质倍率验证:')
|
||||||
|
console.log(` 品质50 (垃圾): 攻击力20 -> ${calculateEquipmentStats(20, 50, 1)} (0.5x)`)
|
||||||
|
console.log(` 品质100 (普通): 攻击力20 -> ${calculateEquipmentStats(20, 100, 1)} (1.0x)`)
|
||||||
|
console.log(` 品质150 (优秀): 攻击力20 -> ${calculateEquipmentStats(20, 150, 1)} (1.5x)`)
|
||||||
|
console.log(` 品质200 (史诗): 攻击力20 -> ${calculateEquipmentStats(20, 200, 1)} (2.0x)`)
|
||||||
|
console.log(` 品质250 (传说): 攻击力20 -> ${calculateEquipmentStats(20, 250, 1)} (2.5x)`)
|
||||||
|
|
||||||
|
// 2. 物品等级测试
|
||||||
|
console.log('\n2. 物品等级影响 (品质100):')
|
||||||
|
console.log(` Lv1: 攻击力20 -> ${calculateEquipmentStats(20, 100, 1)}`)
|
||||||
|
console.log(` Lv5: 攻击力20 -> ${calculateEquipmentStats(20, 100, 5)}`)
|
||||||
|
console.log(` Lv10: 攻击力20 -> ${calculateEquipmentStats(20, 100, 10)}`)
|
||||||
|
console.log(` Lv50: 攻击力20 -> ${calculateEquipmentStats(20, 100, 50)}`)
|
||||||
|
|
||||||
|
// 3. 角色属性测试
|
||||||
|
console.log('\n3. 角色属性 (Lv1, 默认属性):')
|
||||||
|
const defaultStats = { strength: 10, agility: 8, dexterity: 8, intuition: 10, vitality: 10 }
|
||||||
|
const stats = calculateCharacterStats(defaultStats, 1)
|
||||||
|
console.log(` HP: ${stats.hp}`)
|
||||||
|
console.log(` 攻击: ${stats.attack}`)
|
||||||
|
console.log(` 暴击率: ${stats.critRate}%`)
|
||||||
|
console.log(` 体质+10 -> HP +${calculateCharacterStats({ vitality: 20 }, 1).hp - stats.hp}`)
|
||||||
|
|
||||||
|
// 4. 怪物缩放测试
|
||||||
|
console.log('\n4. 怪物等级缩放 (基础HP 100):')
|
||||||
|
console.log(` Lv1: HP = ${calculateMonsterStats(100, 1)}`)
|
||||||
|
console.log(` Lv5: HP = ${calculateMonsterStats(100, 5)} (1.12^4 ≈ 1.57x)`)
|
||||||
|
console.log(` Lv10: HP = ${calculateMonsterStats(100, 10)} (1.12^9 ≈ 2.8x)`)
|
||||||
|
console.log(` Lv20: HP = ${calculateMonsterStats(100, 20)} (1.12^19 ≈ 8.0x)`)
|
||||||
|
|
||||||
|
// 5. 经验曲线测试
|
||||||
|
console.log('\n5. 升级经验需求:')
|
||||||
|
console.log(` Lv1 -> Lv2: ${getExpForLevel(2)} 经验`)
|
||||||
|
console.log(` Lv5 -> Lv6: ${getExpForLevel(6)} 经验`)
|
||||||
|
console.log(` Lv10 -> Lv11: ${getExpForLevel(11)} 经验`)
|
||||||
|
console.log(` Lv20 -> Lv21: ${getExpForLevel(21)} 经验`)
|
||||||
|
console.log(` Lv50 -> Lv51: ${getExpForLevel(51)} 经验`)
|
||||||
|
|
||||||
|
// 6. 品质等级验证
|
||||||
|
console.log('\n6. 品质等级判定:')
|
||||||
|
for (let q = 0; q <= 250; q += 50) {
|
||||||
|
const level = getQualityLevel(q)
|
||||||
|
console.log(` 品质 ${q}: ${level.name.padEnd(6)} (${level.color})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 可玩性验证
|
||||||
|
console.log('\n=== 可玩性验证 ===\n')
|
||||||
|
|
||||||
|
// 新手阶段
|
||||||
|
const playerLv1 = calculateCharacterStats(defaultStats, 1)
|
||||||
|
const monsterLv1 = calculateMonsterStats(50, 1)
|
||||||
|
console.log('新手阶段 (Lv1 vs Lv1 怪物):')
|
||||||
|
console.log(` 玩家 HP: ${playerLv1.hp}, 攻击: ${playerLv1.attack}`)
|
||||||
|
console.log(` 怪物 HP: ${monsterLv1}, 攻击: ${calculateEquipmentStats(5, 100, 1)}`)
|
||||||
|
console.log(` 预期: 玩家约需 5-6 次攻击击杀怪物,能承受 20+ 次攻击`)
|
||||||
|
console.log(` 结论: ${playerLv1.attack > 5 ? '✓ 可玩' : '✗ 太难'}`)
|
||||||
|
|
||||||
|
// 中期阶段
|
||||||
|
const playerLv15 = calculateCharacterStats({
|
||||||
|
strength: 20, agility: 15, dexterity: 15, intuition: 15, vitality: 15
|
||||||
|
}, 15)
|
||||||
|
const monsterLv15 = calculateMonsterStats(100, 15)
|
||||||
|
console.log('\n中期阶段 (Lv15 vs Lv15 怪物):')
|
||||||
|
console.log(` 玩家 HP: ${playerLv15.hp}, 攻击: ${playerLv15.attack}`)
|
||||||
|
console.log(` 怪物 HP: ${monsterLv15}, 攻击: ${calculateEquipmentStats(10, 100, 15)}`)
|
||||||
|
console.log(` 预期: 玩家约需 15-20 次攻击击杀怪物`)
|
||||||
|
console.log(` 结论: ${playerLv15.attack > 20 ? '✓ 可玩' : '需要提升'}`)
|
||||||
|
|
||||||
|
// 战斗经济
|
||||||
|
console.log('\n经济系统:')
|
||||||
|
const normalItemPrice = 100 * 1.0
|
||||||
|
const epicItemPrice = 100 * 2.0
|
||||||
|
const monsterGold = 20
|
||||||
|
console.log(` 普通装备价格: ${normalItemPrice} 铜币`)
|
||||||
|
console.log(` 史诗装备价格: ${epicItemPrice} 铜币`)
|
||||||
|
console.log(` Lv1 怪物掉落: ${monsterGold} 铜币 (假设)`)
|
||||||
|
console.log(` 需要击败 ${Math.ceil(epicItemPrice / monsterGold)} 只怪物购买史诗装备`)
|
||||||
|
|
||||||
|
console.log('\n=== 验证完成 ===')
|
||||||
@@ -9,18 +9,29 @@ import { SKILL_CONFIG } from '@/config/skills.js'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算装备品质后的属性
|
* 计算装备品质后的属性
|
||||||
|
* 使用数学模型中的公式:品质倍率 = 1 + (品质 - 100) * 0.01
|
||||||
* @param {String} itemId - 物品ID
|
* @param {String} itemId - 物品ID
|
||||||
* @param {Number} quality - 品质值 (0-250)
|
* @param {Number} quality - 品质值 (0-250)
|
||||||
|
* @param {Number} itemLevel - 物品等级(影响属性)
|
||||||
* @returns {Object|null} 计算后的属性
|
* @returns {Object|null} 计算后的属性
|
||||||
*/
|
*/
|
||||||
export function calculateItemStats(itemId, quality = 100) {
|
export function calculateItemStats(itemId, quality = 100, itemLevel = 1) {
|
||||||
const config = ITEM_CONFIG[itemId]
|
const config = ITEM_CONFIG[itemId]
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const qualityLevel = getQualityLevel(quality)
|
const qualityLevel = getQualityLevel(quality)
|
||||||
const qualityMultiplier = qualityLevel.multiplier
|
|
||||||
|
// 品质倍率:1 + (品质 - 100) * 0.01
|
||||||
|
// 品质100 = 1.0倍,品质150 = 1.5倍,品质50 = 0.5倍
|
||||||
|
const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER
|
||||||
|
|
||||||
|
// 等级倍率:每级 +2%
|
||||||
|
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||||||
|
|
||||||
|
// 综合倍率
|
||||||
|
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
id: itemId,
|
id: itemId,
|
||||||
@@ -30,36 +41,35 @@ export function calculateItemStats(itemId, quality = 100) {
|
|||||||
description: config.description,
|
description: config.description,
|
||||||
icon: config.icon,
|
icon: config.icon,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
|
itemLevel: itemLevel,
|
||||||
qualityLevel: qualityLevel.level,
|
qualityLevel: qualityLevel.level,
|
||||||
qualityName: qualityLevel.name,
|
qualityName: qualityLevel.name,
|
||||||
qualityColor: qualityLevel.color
|
qualityColor: qualityLevel.color
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用品质百分比到基础属性
|
// 应用品质倍率到基础属性
|
||||||
const qualityRatio = quality / 100
|
|
||||||
|
|
||||||
if (config.baseDamage) {
|
if (config.baseDamage) {
|
||||||
stats.baseDamage = config.baseDamage
|
stats.baseDamage = config.baseDamage
|
||||||
stats.finalDamage = Math.floor(config.baseDamage * qualityRatio * qualityMultiplier)
|
stats.finalDamage = Math.max(1, Math.floor(config.baseDamage * totalMultiplier))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.baseDefense) {
|
if (config.baseDefense) {
|
||||||
stats.baseDefense = config.baseDefense
|
stats.baseDefense = config.baseDefense
|
||||||
stats.finalDefense = Math.floor(config.baseDefense * qualityRatio * qualityMultiplier)
|
stats.finalDefense = Math.max(1, Math.floor(config.baseDefense * totalMultiplier))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.baseShield) {
|
if (config.baseShield) {
|
||||||
stats.baseShield = config.baseShield
|
stats.baseShield = config.baseShield
|
||||||
stats.finalShield = Math.floor(config.baseShield * qualityRatio * qualityMultiplier)
|
stats.finalShield = Math.floor(config.baseShield * totalMultiplier)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.attackSpeed) {
|
if (config.attackSpeed) {
|
||||||
stats.attackSpeed = config.attackSpeed
|
stats.attackSpeed = config.attackSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算价值
|
// 计算价值(品质影响)
|
||||||
stats.baseValue = config.baseValue || 0
|
stats.baseValue = config.baseValue || 0
|
||||||
stats.finalValue = Math.floor(config.baseValue * qualityRatio * qualityMultiplier)
|
stats.finalValue = Math.floor(config.baseValue * totalMultiplier)
|
||||||
|
|
||||||
// 消耗品效果
|
// 消耗品效果
|
||||||
if (config.effect) {
|
if (config.effect) {
|
||||||
@@ -78,6 +88,11 @@ export function calculateItemStats(itemId, quality = 100) {
|
|||||||
stats.unlockSkill = config.unlockSkill
|
stats.unlockSkill = config.unlockSkill
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 额外属性(来自物品配置)
|
||||||
|
if (config.stats) {
|
||||||
|
stats.stats = { ...config.stats }
|
||||||
|
}
|
||||||
|
|
||||||
// 堆叠信息
|
// 堆叠信息
|
||||||
if (config.stackable !== undefined) {
|
if (config.stackable !== undefined) {
|
||||||
stats.stackable = config.stackable
|
stats.stackable = config.stackable
|
||||||
@@ -115,34 +130,34 @@ export function getQualityLevel(quality) {
|
|||||||
* @returns {Number} 品质值
|
* @returns {Number} 品质值
|
||||||
*/
|
*/
|
||||||
export function generateRandomQuality(luck = 0) {
|
export function generateRandomQuality(luck = 0) {
|
||||||
// 基础品质范围 50-150
|
// 单次随机判定
|
||||||
const baseMin = 50
|
const roll = Math.random()
|
||||||
const baseMax = 150
|
|
||||||
|
|
||||||
// 运气加成:每1点运气增加0.5品质
|
// 运气加成:每1点运气增加对应品质概率的 0.1%
|
||||||
const luckBonus = luck * 0.5
|
const luckBonus = luck * 0.001
|
||||||
|
|
||||||
// 随机品质
|
// 传说品质:0.1% + 运气加成
|
||||||
let quality = Math.floor(Math.random() * (baseMax - baseMin + 1)) + baseMin + luckBonus
|
if (roll < 0.001 + luckBonus) {
|
||||||
|
return 200 + Math.floor(Math.random() * 51) // 200-250
|
||||||
// 小概率生成高品质(传说)
|
|
||||||
if (Math.random() < 0.01 + luck / 10000) {
|
|
||||||
quality = 180 + Math.floor(Math.random() * 70) // 180-250
|
|
||||||
}
|
}
|
||||||
// 史诗品质
|
// 史诗品质:1% + 运气加成
|
||||||
else if (Math.random() < 0.05 + luck / 2000) {
|
if (roll < 0.01 + luckBonus * 2) {
|
||||||
quality = 160 + Math.floor(Math.random() * 40) // 160-199
|
return 170 + Math.floor(Math.random() * 30) // 170-199
|
||||||
}
|
}
|
||||||
// 稀有品质
|
// 稀有品质:5% + 运气加成
|
||||||
else if (Math.random() < 0.15 + luck / 500) {
|
if (roll < 0.05 + luckBonus * 3) {
|
||||||
quality = 130 + Math.floor(Math.random() * 30) // 130-159
|
return 140 + Math.floor(Math.random() * 30) // 140-169
|
||||||
}
|
}
|
||||||
// 优秀品质
|
// 优秀品质:15% + 运气加成
|
||||||
else if (Math.random() < 0.3 + luck / 200) {
|
if (roll < 0.15 + luckBonus * 4) {
|
||||||
quality = 100 + Math.floor(Math.random() * 30) // 100-129
|
return 100 + Math.floor(Math.random() * 40) // 100-139
|
||||||
}
|
}
|
||||||
|
// 普通品质:剩余大部分
|
||||||
return Math.min(250, Math.max(0, Math.floor(quality)))
|
if (roll < 0.85) {
|
||||||
|
return 50 + Math.floor(Math.random() * 50) // 50-99
|
||||||
|
}
|
||||||
|
// 垃圾品质:15%
|
||||||
|
return Math.floor(Math.random() * 50) // 0-49
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -432,24 +447,28 @@ export function addItemToInventory(playerStore, itemId, count = 1, quality = nul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装备类物品 - 按物品ID + 品质等级堆叠
|
// 装备类物品 - 不堆叠,每个装备都有独立的唯一ID
|
||||||
|
// 这样可以正确跟踪每个装备的装备状态
|
||||||
if (needsQuality) {
|
if (needsQuality) {
|
||||||
const qualityLevel = itemStats.qualityLevel
|
// 检查是否有完全相同品质的装备(用于显示堆叠数量,但各自独立)
|
||||||
const existingItem = playerStore.inventory.find(
|
// 注意:装备不再真正堆叠,每个都是独立实例
|
||||||
i => i.id === itemId && i.qualityLevel === qualityLevel
|
// 生成唯一ID:itemId + quality + 时间戳 + 随机数
|
||||||
)
|
const uniqueId = `${itemId}_q${itemQuality}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
||||||
if (existingItem) {
|
|
||||||
existingItem.count += count
|
const newItem = {
|
||||||
return { success: true, item: existingItem }
|
...itemStats,
|
||||||
|
uniqueId,
|
||||||
|
count: 1, // 装备始终为1
|
||||||
|
equipped: false,
|
||||||
|
obtainedAt: Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerStore.inventory.push(newItem)
|
||||||
|
return { success: true, item: newItem }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新物品
|
// 创建新物品(非装备类)
|
||||||
// 装备类物品使用 itemId + qualityLevel 作为唯一标识
|
const uniqueId = `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
||||||
// 素材类物品使用 itemId + 时间戳(虽然可堆叠,但保留唯一ID用于其他用途)
|
|
||||||
const uniqueId = needsQuality
|
|
||||||
? `${itemId}_q${itemStats.qualityLevel}`
|
|
||||||
: `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
|
||||||
|
|
||||||
const newItem = {
|
const newItem = {
|
||||||
...itemStats,
|
...itemStats,
|
||||||
|
|||||||
Reference in New Issue
Block a user