diff --git a/README.md b/README.md
new file mode 100644
index 0000000..41d0857
--- /dev/null
+++ b/README.md
@@ -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,修改配置后可能需要清除缓存测试
diff --git a/components/drawers/InventoryDrawer.vue b/components/drawers/InventoryDrawer.vue
index 479ee7f..859e9c4 100644
--- a/components/drawers/InventoryDrawer.vue
+++ b/components/drawers/InventoryDrawer.vue
@@ -138,8 +138,8 @@ function isEquipped(item) {
const slot = slotMap[item.type]
if (!slot) return false
const equipped = player.equipment[slot]
- // 通过 uniqueId 或 id 比较
- return equipped && (equipped.uniqueId === item.uniqueId || equipped.id === item.id)
+ // 只通过 uniqueId 比较,确保每个装备独立判断
+ return equipped && equipped.uniqueId === item.uniqueId
}
function close() {
diff --git a/components/panels/StatusPanel.vue b/components/panels/StatusPanel.vue
index 46ef49f..c2f017c 100644
--- a/components/panels/StatusPanel.vue
+++ b/components/panels/StatusPanel.vue
@@ -73,30 +73,45 @@
武器
-
- {{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
-
+
+
+ {{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
+
+ 攻{{ player.equipment.weapon.finalDamage }}
+ [{{ player.equipment.weapon.qualityName }}]
+
空
防具
-
- {{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
-
+
+
+ {{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
+
+ 防{{ player.equipment.armor.finalDefense }}
+ [{{ player.equipment.armor.qualityName }}]
+
空
盾牌
-
- {{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
-
+
+
+ {{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
+
+ 格挡{{ player.equipment.shield.finalShield }}
+ [{{ player.equipment.shield.qualityName }}]
+
空
饰品
-
- {{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
-
+
+
+ {{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
+
+ [{{ player.equipment.accessory.qualityName }}]
+
空
@@ -127,19 +142,47 @@
- {{ skill.icon || '⚡' }}
-
- {{ skill.name }}
- Lv.{{ skill.level }}
+
+ {{ skill.icon || '⚡' }}
+
+ {{ skill.name }}
+ Lv.{{ skill.level }}/{{ skill.maxLevel }}
+
+
+
+ {{ Math.floor(skill.exp) }}/{{ Math.floor(skill.maxExp) }}
+
+ {{ expandedSkills.includes(skill.id) ? '▼' : '▶' }}
-
-
- {{ skill.exp }}/{{ skill.maxExp }}
+
+
+
+
+
+ ✓ 已获得增幅
+
+ Lv.{{ m.level }}
+ {{ m.desc }}
+
+
+
+
+
+ 即将解锁
+
+ Lv.{{ m.level }}
+ {{ m.desc }}
+
+
+
+
+ 暂无增幅效果
+
@@ -277,6 +320,7 @@ const currentSkillFilter = ref('all')
const showBooks = ref(false)
const showTraining = ref(false)
const showTasks = ref(true)
+const expandedSkills = ref([]) // 展开的技能ID列表
const skillFilterTabs = [
{ id: 'all', label: '全部' },
@@ -382,6 +426,42 @@ const readingTimeText = computed(() => {
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) {
const result = startTask(game, player, 'reading', {
itemId: book.id,
@@ -598,12 +678,19 @@ function cancelActiveTask(task) {
.skill-item {
display: flex;
- align-items: center;
- gap: 12rpx;
- padding: 12rpx;
+ flex-direction: column;
background-color: $bg-secondary;
border-radius: 8rpx;
margin-bottom: 8rpx;
+ overflow: hidden;
+
+ &__main {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ padding: 12rpx;
+ cursor: pointer;
+ }
&__icon {
font-size: 32rpx;
@@ -641,6 +728,72 @@ function cancelActiveTask(task) {
font-size: 20rpx;
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 {
@@ -687,8 +840,26 @@ function cancelActiveTask(task) {
}
&__item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2rpx;
+ }
+
+ &__name {
color: $text-primary;
font-size: 22rpx;
+ text-align: center;
+ }
+
+ &__stats {
+ color: $accent;
+ font-size: 20rpx;
+ }
+
+ &__quality {
+ color: $text-muted;
+ font-size: 18rpx;
}
&__empty {
diff --git a/config/constants.js b/config/constants.js
index f53275d..f56f461 100644
--- a/config/constants.js
+++ b/config/constants.js
@@ -1,4 +1,4 @@
-// 游戏数值常量
+// 游戏数值常量 - 与数学模型文档保持一致
export const GAME_CONSTANTS = {
// 时间流速:现实1秒 = 游戏时间5分钟
TIME_SCALE: 5,
@@ -14,13 +14,49 @@ export const GAME_CONSTANTS = {
// 经验倍率
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
- // 品质等级
+ // 品质等级 - 与数学模型文档一致
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 },
- 3: { name: '优秀', color: '#4ade80', range: [100, 129], multiplier: 1.1 },
- 4: { name: '稀有', color: '#60a5fa', range: [130, 159], multiplier: 1.3 },
- 5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 1.6 },
- 6: { name: '传说', color: '#f97316', range: [200, 250], multiplier: 2.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 }
+ },
+
+ // 属性缩放系数
+ 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%
}
}
diff --git a/config/formulas.js b/config/formulas.js
new file mode 100644
index 0000000..6cbd415
--- /dev/null
+++ b/config/formulas.js
@@ -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)
+}
diff --git a/docs/MATHEMATICAL_MODEL.md b/docs/MATHEMATICAL_MODEL.md
new file mode 100644
index 0000000..8ad330b
--- /dev/null
+++ b/docs/MATHEMATICAL_MODEL.md
@@ -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 {
+ // 怪物优势
+}
+```
diff --git a/tests/unit/formulas.test.js b/tests/unit/formulas.test.js
new file mode 100644
index 0000000..60280d3
--- /dev/null
+++ b/tests/unit/formulas.test.js
@@ -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)
+ })
+ })
+})
diff --git a/tests/verify-model.js b/tests/verify-model.js
new file mode 100644
index 0000000..fb809ea
--- /dev/null
+++ b/tests/verify-model.js
@@ -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=== 验证完成 ===')
diff --git a/utils/itemSystem.js b/utils/itemSystem.js
index acc427f..ec285c4 100644
--- a/utils/itemSystem.js
+++ b/utils/itemSystem.js
@@ -9,18 +9,29 @@ import { SKILL_CONFIG } from '@/config/skills.js'
/**
* 计算装备品质后的属性
+ * 使用数学模型中的公式:品质倍率 = 1 + (品质 - 100) * 0.01
* @param {String} itemId - 物品ID
* @param {Number} quality - 品质值 (0-250)
+ * @param {Number} itemLevel - 物品等级(影响属性)
* @returns {Object|null} 计算后的属性
*/
-export function calculateItemStats(itemId, quality = 100) {
+export function calculateItemStats(itemId, quality = 100, itemLevel = 1) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return null
}
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 = {
id: itemId,
@@ -30,36 +41,35 @@ export function calculateItemStats(itemId, quality = 100) {
description: config.description,
icon: config.icon,
quality: quality,
+ itemLevel: itemLevel,
qualityLevel: qualityLevel.level,
qualityName: qualityLevel.name,
qualityColor: qualityLevel.color
}
- // 应用品质百分比到基础属性
- const qualityRatio = quality / 100
-
+ // 应用品质倍率到基础属性
if (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) {
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) {
stats.baseShield = config.baseShield
- stats.finalShield = Math.floor(config.baseShield * qualityRatio * qualityMultiplier)
+ stats.finalShield = Math.floor(config.baseShield * totalMultiplier)
}
if (config.attackSpeed) {
stats.attackSpeed = config.attackSpeed
}
- // 计算价值
+ // 计算价值(品质影响)
stats.baseValue = config.baseValue || 0
- stats.finalValue = Math.floor(config.baseValue * qualityRatio * qualityMultiplier)
+ stats.finalValue = Math.floor(config.baseValue * totalMultiplier)
// 消耗品效果
if (config.effect) {
@@ -78,6 +88,11 @@ export function calculateItemStats(itemId, quality = 100) {
stats.unlockSkill = config.unlockSkill
}
+ // 额外属性(来自物品配置)
+ if (config.stats) {
+ stats.stats = { ...config.stats }
+ }
+
// 堆叠信息
if (config.stackable !== undefined) {
stats.stackable = config.stackable
@@ -115,34 +130,34 @@ export function getQualityLevel(quality) {
* @returns {Number} 品质值
*/
export function generateRandomQuality(luck = 0) {
- // 基础品质范围 50-150
- const baseMin = 50
- const baseMax = 150
+ // 单次随机判定
+ const roll = Math.random()
- // 运气加成:每1点运气增加0.5品质
- const luckBonus = luck * 0.5
+ // 运气加成:每1点运气增加对应品质概率的 0.1%
+ const luckBonus = luck * 0.001
- // 随机品质
- let quality = Math.floor(Math.random() * (baseMax - baseMin + 1)) + baseMin + luckBonus
-
- // 小概率生成高品质(传说)
- if (Math.random() < 0.01 + luck / 10000) {
- quality = 180 + Math.floor(Math.random() * 70) // 180-250
+ // 传说品质:0.1% + 运气加成
+ if (roll < 0.001 + luckBonus) {
+ return 200 + Math.floor(Math.random() * 51) // 200-250
}
- // 史诗品质
- else if (Math.random() < 0.05 + luck / 2000) {
- quality = 160 + Math.floor(Math.random() * 40) // 160-199
+ // 史诗品质:1% + 运气加成
+ if (roll < 0.01 + luckBonus * 2) {
+ return 170 + Math.floor(Math.random() * 30) // 170-199
}
- // 稀有品质
- else if (Math.random() < 0.15 + luck / 500) {
- quality = 130 + Math.floor(Math.random() * 30) // 130-159
+ // 稀有品质:5% + 运气加成
+ if (roll < 0.05 + luckBonus * 3) {
+ return 140 + Math.floor(Math.random() * 30) // 140-169
}
- // 优秀品质
- else if (Math.random() < 0.3 + luck / 200) {
- quality = 100 + Math.floor(Math.random() * 30) // 100-129
+ // 优秀品质:15% + 运气加成
+ if (roll < 0.15 + luckBonus * 4) {
+ 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) {
- const qualityLevel = itemStats.qualityLevel
- const existingItem = playerStore.inventory.find(
- i => i.id === itemId && i.qualityLevel === qualityLevel
- )
- if (existingItem) {
- existingItem.count += count
- return { success: true, item: existingItem }
+ // 检查是否有完全相同品质的装备(用于显示堆叠数量,但各自独立)
+ // 注意:装备不再真正堆叠,每个都是独立实例
+ // 生成唯一ID:itemId + quality + 时间戳 + 随机数
+ const uniqueId = `${itemId}_q${itemQuality}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
+
+ const newItem = {
+ ...itemStats,
+ uniqueId,
+ count: 1, // 装备始终为1
+ equipped: false,
+ obtainedAt: Date.now()
}
+
+ playerStore.inventory.push(newItem)
+ return { success: true, item: newItem }
}
- // 创建新物品
- // 装备类物品使用 itemId + qualityLevel 作为唯一标识
- // 素材类物品使用 itemId + 时间戳(虽然可堆叠,但保留唯一ID用于其他用途)
- const uniqueId = needsQuality
- ? `${itemId}_q${itemStats.qualityLevel}`
- : `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
+ // 创建新物品(非装备类)
+ const uniqueId = `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
const newItem = {
...itemStats,