Initial commit: Text Adventure Game
Features: - Combat system with AP/EP hit calculation and three-layer defense - Auto-combat/farming mode - Item system with stacking support - Skill system with levels, milestones, and parent skill sync - Shop system with dynamic pricing - Inventory management with bulk selling - Event system - Game loop with offline earnings - Save/Load system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
212
tests/README.md
Normal file
212
tests/README.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 单元测试快速开始指南
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
npm install -D vitest @vue/test-utils @pinia/testing happy-dom @vitest/coverage-v8
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试(watch模式)
|
||||
npm run test
|
||||
|
||||
# 运行所有测试(单次)
|
||||
npm run test:run
|
||||
|
||||
# 生成覆盖率报告
|
||||
npm run test:coverage
|
||||
|
||||
# 运行UI界面
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
## 测试文件结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # 单元测试
|
||||
│ ├── stores/ # Store测试
|
||||
│ │ ├── player.spec.js # ✅ 已完成
|
||||
│ │ └── game.spec.js # 待编写
|
||||
│ └── systems/ # 系统测试
|
||||
│ ├── skillSystem.spec.js # ✅ 已完成
|
||||
│ ├── combatSystem.spec.js # ✅ 已完成
|
||||
│ ├── itemSystem.spec.js # 待编写
|
||||
│ ├── taskSystem.spec.js # 待编写
|
||||
│ ├── eventSystem.spec.js # 待编写
|
||||
│ ├── gameLoop.spec.js # 待编写
|
||||
│ └── storage.spec.js # 待编写
|
||||
├── fixtures/ # 测试夹具
|
||||
│ ├── player.js # ✅ 已完成
|
||||
│ ├── game.js # ✅ 已完成
|
||||
│ ├── items.js # ✅ 已完成
|
||||
│ └── enemies.js # ✅ 已完成
|
||||
└── helpers/ # 测试辅助函数
|
||||
├── store.js # ✅ 已完成
|
||||
├── timer.js # ✅ 已完成
|
||||
└── random.js # ✅ 已完成
|
||||
```
|
||||
|
||||
## 编写新测试的步骤
|
||||
|
||||
### 1. 创建测试文件
|
||||
|
||||
在 `tests/unit/systems/` 下创建新文件,例如 `itemSystem.spec.js`
|
||||
|
||||
### 2. 导入必要的模块
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { usePlayerStore } from '@/store/player.js'
|
||||
import { useGameStore } from '@/store/game.js'
|
||||
import { yourFunction } from '@/utils/itemSystem.js'
|
||||
import { createMockPlayer } from '../../fixtures/player.js'
|
||||
```
|
||||
|
||||
### 3. Mock 配置文件(如果需要)
|
||||
|
||||
```javascript
|
||||
vi.mock('@/config/items.js', () => ({
|
||||
ITEM_CONFIG: {
|
||||
// 测试数据
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
### 4. 编写测试用例
|
||||
|
||||
```javascript
|
||||
describe('物品系统', () => {
|
||||
let playerStore
|
||||
let gameStore
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
playerStore = usePlayerStore()
|
||||
gameStore = useGameStore()
|
||||
})
|
||||
|
||||
it('应该成功添加物品', () => {
|
||||
const result = addItemToInventory(playerStore, 'test_item', 1)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(playerStore.inventory.length).toBe(1)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 测试最佳实践
|
||||
|
||||
### 1. 使用 AAA 模式
|
||||
|
||||
```javascript
|
||||
it('应该正确计算价格', () => {
|
||||
// Arrange - 准备数据
|
||||
const item = { baseValue: 100, quality: 150 }
|
||||
|
||||
// Act - 执行操作
|
||||
const price = calculatePrice(item)
|
||||
|
||||
// Assert - 验证结果
|
||||
expect(price).toBe(150)
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 测试命名清晰
|
||||
|
||||
```javascript
|
||||
// ✅ 好的命名
|
||||
it('当技能未解锁时,应拒绝添加经验', () => {})
|
||||
it('升级时等级应增加1', () => {})
|
||||
|
||||
// ❌ 不好的命名
|
||||
it('test1', () => {})
|
||||
it('works', () => {})
|
||||
```
|
||||
|
||||
### 3. 使用测试夹具
|
||||
|
||||
```javascript
|
||||
import { createMockPlayer } from '../../fixtures/player.js'
|
||||
|
||||
beforeEach(() => {
|
||||
Object.assign(playerStore, createMockPlayer())
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Mock 外部依赖
|
||||
|
||||
```javascript
|
||||
// Mock uni API
|
||||
global.uni = {
|
||||
setStorageSync: vi.fn(() => true),
|
||||
getStorageSync: vi.fn(() => null)
|
||||
}
|
||||
|
||||
// Mock 随机数
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.5)
|
||||
```
|
||||
|
||||
## 当前测试覆盖情况
|
||||
|
||||
### ✅ 已完成
|
||||
- [x] Player Store 测试 (player.spec.js)
|
||||
- [x] 技能系统测试 (skillSystem.spec.js)
|
||||
- [x] 战斗系统测试 (combatSystem.spec.js)
|
||||
- [x] 所有测试夹具 (fixtures/)
|
||||
- [x] 所有测试辅助函数 (helpers/)
|
||||
|
||||
### 📝 待编写
|
||||
- [ ] Game Store 测试
|
||||
- [ ] 物品系统测试
|
||||
- [ ] 任务系统测试
|
||||
- [ ] 事件系统测试
|
||||
- [ ] 游戏循环测试
|
||||
- [ ] 存档系统测试
|
||||
- [ ] 环境系统测试
|
||||
|
||||
## 查看测试覆盖率
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
生成的报告位于:
|
||||
- 终端输出:文本格式
|
||||
- coverage/index.html:HTML格式(可在浏览器查看)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: uni API 未定义
|
||||
**A**: 已在 `tests/setup.js` 中 mock,如果还有问题,检查是否正确导入 setup 文件
|
||||
|
||||
### Q: Pinia Store 测试报错
|
||||
**A**: 使用 `setActivePinia(createPinia())` 初始化
|
||||
|
||||
### Q: 测试超时
|
||||
**A**: 在 `vitest.config.js` 中增加 `testTimeout: 10000`
|
||||
|
||||
### Q: Mock 不生效
|
||||
**A**: 确保在测试文件顶部 mock,在导入被测试模块之前
|
||||
|
||||
## 下一步工作
|
||||
|
||||
按优先级顺序:
|
||||
|
||||
1. **P0 - 核心模块**
|
||||
- Game Store 测试
|
||||
- 存档系统测试
|
||||
- 游戏循环测试
|
||||
|
||||
2. **P1 - 核心机制**
|
||||
- 物品系统测试
|
||||
- 任务系统测试
|
||||
|
||||
3. **P2 - 辅助系统**
|
||||
- 事件系统测试
|
||||
- 环境系统测试
|
||||
|
||||
参考 `TEST_PLAN.md` 获取详细的测试用例列表。
|
||||
147
tests/fixtures/enemies.js
vendored
Normal file
147
tests/fixtures/enemies.js
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 敌人测试夹具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建模拟敌人
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} 敌人对象
|
||||
*/
|
||||
export function createMockEnemy(overrides = {}) {
|
||||
return {
|
||||
id: 'wild_dog',
|
||||
name: '野狗',
|
||||
baseStats: {
|
||||
health: 50,
|
||||
attack: 8,
|
||||
defense: 3,
|
||||
strength: 8,
|
||||
agility: 12,
|
||||
dexterity: 6,
|
||||
intuition: 4,
|
||||
vitality: 6
|
||||
},
|
||||
expReward: 20,
|
||||
drops: [],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Boss敌人
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} Boss对象
|
||||
*/
|
||||
export function createMockBoss(overrides = {}) {
|
||||
return {
|
||||
id: 'test_boss',
|
||||
name: '测试Boss',
|
||||
baseStats: {
|
||||
health: 200,
|
||||
attack: 15,
|
||||
defense: 10,
|
||||
strength: 15,
|
||||
agility: 10,
|
||||
dexterity: 10,
|
||||
intuition: 8,
|
||||
vitality: 15
|
||||
},
|
||||
expReward: 100,
|
||||
drops: [
|
||||
{
|
||||
itemId: 'epic_sword',
|
||||
chance: 1.0,
|
||||
count: 1
|
||||
}
|
||||
],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弱小敌人
|
||||
* @returns {Object} 弱小敌人
|
||||
*/
|
||||
export function createWeakEnemy() {
|
||||
return createMockEnemy({
|
||||
id: 'rat',
|
||||
name: '老鼠',
|
||||
baseStats: {
|
||||
health: 20,
|
||||
attack: 3,
|
||||
defense: 1,
|
||||
strength: 3,
|
||||
agility: 8,
|
||||
dexterity: 4,
|
||||
intuition: 2,
|
||||
vitality: 3
|
||||
},
|
||||
expReward: 5
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建强大敌人
|
||||
* @returns {Object} 强大敌人
|
||||
*/
|
||||
export function createStrongEnemy() {
|
||||
return createMockEnemy({
|
||||
id: 'orc',
|
||||
name: '兽人',
|
||||
baseStats: {
|
||||
health: 120,
|
||||
attack: 18,
|
||||
defense: 12,
|
||||
strength: 18,
|
||||
agility: 8,
|
||||
dexterity: 10,
|
||||
intuition: 6,
|
||||
vitality: 16
|
||||
},
|
||||
expReward: 60
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建敏捷型敌人
|
||||
* @returns {Object} 敏捷型敌人
|
||||
*/
|
||||
export function createAgileEnemy() {
|
||||
return createMockEnemy({
|
||||
id: 'goblin',
|
||||
name: '哥布林',
|
||||
baseStats: {
|
||||
health: 40,
|
||||
attack: 6,
|
||||
defense: 2,
|
||||
strength: 6,
|
||||
agility: 18,
|
||||
dexterity: 14,
|
||||
intuition: 6,
|
||||
vitality: 6
|
||||
},
|
||||
expReward: 25
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建防御型敌人
|
||||
* @returns {Object} 防御型敌人
|
||||
*/
|
||||
export function createTankEnemy() {
|
||||
return createMockEnemy({
|
||||
id: 'golem',
|
||||
name: '石魔像',
|
||||
baseStats: {
|
||||
health: 150,
|
||||
attack: 10,
|
||||
defense: 20,
|
||||
strength: 16,
|
||||
agility: 4,
|
||||
dexterity: 4,
|
||||
intuition: 2,
|
||||
vitality: 20
|
||||
},
|
||||
expReward: 50
|
||||
})
|
||||
}
|
||||
92
tests/fixtures/game.js
vendored
Normal file
92
tests/fixtures/game.js
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Game Store 测试夹具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建模拟游戏数据
|
||||
* @param {Object} overrides - 覆盖的属性
|
||||
* @returns {Object} 模拟游戏对象
|
||||
*/
|
||||
export function createMockGame(overrides = {}) {
|
||||
return {
|
||||
currentTab: 'status',
|
||||
drawerState: {
|
||||
inventory: false,
|
||||
event: false,
|
||||
...overrides.drawerState
|
||||
},
|
||||
logs: overrides.logs || [],
|
||||
gameTime: {
|
||||
day: 1,
|
||||
hour: 8,
|
||||
minute: 0,
|
||||
totalMinutes: 480,
|
||||
...overrides.gameTime
|
||||
},
|
||||
inCombat: overrides.inCombat || false,
|
||||
combatState: overrides.combatState || null,
|
||||
activeTasks: overrides.activeTasks || [],
|
||||
negativeStatus: overrides.negativeStatus || [],
|
||||
marketPrices: overrides.marketPrices || {
|
||||
lastRefreshDay: 1,
|
||||
prices: {}
|
||||
},
|
||||
currentEvent: overrides.currentEvent || null,
|
||||
scheduledEvents: overrides.scheduledEvents || []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建战斗中的游戏状态
|
||||
* @param {Object} enemy - 敌人对象
|
||||
* @returns {Object} 模拟游戏对象
|
||||
*/
|
||||
export function createGameInCombat(enemy) {
|
||||
return createMockGame({
|
||||
inCombat: true,
|
||||
combatState: {
|
||||
enemyId: enemy.id,
|
||||
enemy,
|
||||
stance: 'balance',
|
||||
environment: 'normal',
|
||||
startTime: Date.now(),
|
||||
ticks: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建有活动任务的游戏状态
|
||||
* @param {Array} tasks - 任务数组
|
||||
* @returns {Object} 模拟游戏对象
|
||||
*/
|
||||
export function createGameWithTasks(tasks) {
|
||||
return createMockGame({
|
||||
activeTasks: tasks.map(task => ({
|
||||
id: Date.now() + Math.random(),
|
||||
type: task.type,
|
||||
data: task.data || {},
|
||||
startTime: Date.now(),
|
||||
progress: 0,
|
||||
totalDuration: task.duration || 0,
|
||||
lastTickTime: Date.now(),
|
||||
...task
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建有日志的游戏状态
|
||||
* @param {Array} logs - 日志数组
|
||||
* @returns {Object} 模拟游戏对象
|
||||
*/
|
||||
export function createGameWithLogs(logs) {
|
||||
return createMockGame({
|
||||
logs: logs.map((log, index) => ({
|
||||
id: Date.now() + index,
|
||||
time: '08:00',
|
||||
message: log,
|
||||
type: 'info'
|
||||
}))
|
||||
})
|
||||
}
|
||||
144
tests/fixtures/items.js
vendored
Normal file
144
tests/fixtures/items.js
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 物品测试夹具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建模拟武器
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} 武器对象
|
||||
*/
|
||||
export function createMockWeapon(overrides = {}) {
|
||||
return {
|
||||
id: 'wooden_sword',
|
||||
name: '木剑',
|
||||
type: 'weapon',
|
||||
subtype: 'one_handed',
|
||||
baseDamage: 10,
|
||||
baseDefense: 0,
|
||||
baseShield: 0,
|
||||
baseValue: 100,
|
||||
quality: 100,
|
||||
attackSpeed: 1.0,
|
||||
uniqueId: `wooden_sword_${Date.now()}_test`,
|
||||
count: 1,
|
||||
equipped: false,
|
||||
obtainedAt: Date.now(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟防具
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} 防具对象
|
||||
*/
|
||||
export function createMockArmor(overrides = {}) {
|
||||
return {
|
||||
id: 'leather_armor',
|
||||
name: '皮甲',
|
||||
type: 'armor',
|
||||
subtype: 'light',
|
||||
baseDamage: 0,
|
||||
baseDefense: 15,
|
||||
baseShield: 0,
|
||||
baseValue: 150,
|
||||
quality: 100,
|
||||
uniqueId: `leather_armor_${Date.now()}_test`,
|
||||
count: 1,
|
||||
equipped: false,
|
||||
obtainedAt: Date.now(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟盾牌
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} 盾牌对象
|
||||
*/
|
||||
export function createMockShield(overrides = {}) {
|
||||
return {
|
||||
id: 'wooden_shield',
|
||||
name: '木盾',
|
||||
type: 'shield',
|
||||
baseDamage: 0,
|
||||
baseDefense: 0,
|
||||
baseShield: 20,
|
||||
baseValue: 80,
|
||||
quality: 100,
|
||||
uniqueId: `wooden_shield_${Date.now()}_test`,
|
||||
count: 1,
|
||||
equipped: false,
|
||||
obtainedAt: Date.now(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟消耗品
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} 消耗品对象
|
||||
*/
|
||||
export function createMockConsumable(overrides = {}) {
|
||||
return {
|
||||
id: 'health_potion',
|
||||
name: '生命药水',
|
||||
type: 'consumable',
|
||||
baseValue: 50,
|
||||
quality: 100,
|
||||
effect: {
|
||||
health: 30,
|
||||
stamina: 0,
|
||||
sanity: 0
|
||||
},
|
||||
stackable: true,
|
||||
maxStack: 99,
|
||||
uniqueId: `health_potion_${Date.now()}_test`,
|
||||
count: 1,
|
||||
obtainedAt: Date.now(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟书籍
|
||||
* @param {Object} overrides - 覆盖属性
|
||||
* @returns {Object} 书籍对象
|
||||
*/
|
||||
export function createMockBook(overrides = {}) {
|
||||
return {
|
||||
id: 'skill_book_basics',
|
||||
name: '技能基础',
|
||||
type: 'book',
|
||||
baseValue: 200,
|
||||
quality: 100,
|
||||
readingTime: 60,
|
||||
expReward: {
|
||||
sword: 50
|
||||
},
|
||||
completionBonus: {
|
||||
intuition: 1
|
||||
},
|
||||
uniqueId: `skill_book_${Date.now()}_test`,
|
||||
count: 1,
|
||||
obtainedAt: Date.now(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高品质物品
|
||||
* @param {string} type - 物品类型
|
||||
* @param {number} quality - 品质值
|
||||
* @returns {Object} 高品质物品
|
||||
*/
|
||||
export function createHighQualityItem(type, quality = 180) {
|
||||
const itemCreators = {
|
||||
weapon: createMockWeapon,
|
||||
armor: createMockArmor,
|
||||
shield: createMockShield
|
||||
}
|
||||
|
||||
const creator = itemCreators[type] || createMockWeapon
|
||||
return creator({ quality })
|
||||
}
|
||||
149
tests/fixtures/player.js
vendored
Normal file
149
tests/fixtures/player.js
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Player Store 测试夹具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建模拟玩家数据
|
||||
* @param {Object} overrides - 覆盖的属性
|
||||
* @returns {Object} 模拟玩家对象
|
||||
*/
|
||||
export function createMockPlayer(overrides = {}) {
|
||||
return {
|
||||
baseStats: {
|
||||
strength: 10,
|
||||
agility: 8,
|
||||
dexterity: 8,
|
||||
intuition: 10,
|
||||
vitality: 10,
|
||||
...overrides.baseStats
|
||||
},
|
||||
currentStats: {
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
stamina: 100,
|
||||
maxStamina: 100,
|
||||
sanity: 100,
|
||||
maxSanity: 100,
|
||||
...overrides.currentStats
|
||||
},
|
||||
level: {
|
||||
current: 1,
|
||||
exp: 0,
|
||||
maxExp: 100,
|
||||
...overrides.level
|
||||
},
|
||||
skills: overrides.skills || {},
|
||||
equipment: {
|
||||
weapon: null,
|
||||
armor: null,
|
||||
shield: null,
|
||||
accessory: null,
|
||||
...overrides.equipment
|
||||
},
|
||||
inventory: overrides.inventory || [],
|
||||
currency: {
|
||||
copper: 0,
|
||||
...overrides.currency
|
||||
},
|
||||
currentLocation: 'camp',
|
||||
flags: overrides.flags || {},
|
||||
milestoneBonuses: overrides.milestoneBonuses || {},
|
||||
globalBonus: overrides.globalBonus || {
|
||||
critRate: 0,
|
||||
attackBonus: 0,
|
||||
expRate: 1,
|
||||
critMult: 0,
|
||||
darkPenaltyReduce: 0,
|
||||
globalExpRate: 0,
|
||||
readingSpeed: 1
|
||||
},
|
||||
equipmentStats: overrides.equipmentStats || null,
|
||||
killCount: overrides.killCount || {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已解锁技能的玩家
|
||||
* @param {string} skillId - 技能ID
|
||||
* @param {number} level - 技能等级
|
||||
* @returns {Object} 模拟玩家对象
|
||||
*/
|
||||
export function createPlayerWithSkill(skillId, level = 1) {
|
||||
return createMockPlayer({
|
||||
skills: {
|
||||
[skillId]: {
|
||||
level,
|
||||
exp: 0,
|
||||
unlocked: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建高等级玩家
|
||||
* @param {number} level - 玩家等级
|
||||
* @returns {Object} 模拟玩家对象
|
||||
*/
|
||||
export function createHighLevelPlayer(level = 10) {
|
||||
return createMockPlayer({
|
||||
level: {
|
||||
current: level,
|
||||
exp: 0,
|
||||
maxExp: level * 100 * Math.pow(1.5, level - 1)
|
||||
},
|
||||
baseStats: {
|
||||
strength: 10 + level * 2,
|
||||
agility: 8 + level * 2,
|
||||
dexterity: 8 + level * 2,
|
||||
intuition: 10 + level * 2,
|
||||
vitality: 10 + level * 2
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建受伤的玩家
|
||||
* @param {number} healthPercent - 生命值百分比
|
||||
* @returns {Object} 模拟玩家对象
|
||||
*/
|
||||
export function createInjuredPlayer(healthPercent = 30) {
|
||||
return createMockPlayer({
|
||||
currentStats: {
|
||||
health: 100 * healthPercent / 100,
|
||||
maxHealth: 100,
|
||||
stamina: 50,
|
||||
maxStamina: 100,
|
||||
sanity: 80,
|
||||
maxSanity: 100
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建富有的玩家
|
||||
* @param {number} copper - 铜币数量
|
||||
* @returns {Object} 模拟玩家对象
|
||||
*/
|
||||
export function createRichPlayer(copper = 10000) {
|
||||
return createMockPlayer({
|
||||
currency: { copper }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已装备武器的玩家
|
||||
* @param {Object} weapon - 武器对象
|
||||
* @returns {Object} 模拟玩家对象
|
||||
*/
|
||||
export function createPlayerWithWeapon(weapon) {
|
||||
return createMockPlayer({
|
||||
equipment: {
|
||||
weapon: null,
|
||||
armor: null,
|
||||
shield: null,
|
||||
accessory: null
|
||||
},
|
||||
inventory: [weapon]
|
||||
})
|
||||
}
|
||||
74
tests/helpers/random.js
Normal file
74
tests/helpers/random.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 随机数测试辅助函数
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest'
|
||||
|
||||
let mockRandomValue = 0.5
|
||||
|
||||
/**
|
||||
* 设置 Math.random() 返回值
|
||||
* @param {number} value - 返回值(0-1)
|
||||
*/
|
||||
export function setMockRandom(value) {
|
||||
mockRandomValue = value
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Math.random() 的 mock
|
||||
* @returns {Object} spy
|
||||
*/
|
||||
export function mockMathRandom() {
|
||||
return vi.spyOn(Math, 'random').mockImplementation(() => mockRandomValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 mock 随机值
|
||||
*/
|
||||
export function resetMockRandom() {
|
||||
mockRandomValue = 0.5
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置随机数序列
|
||||
* @param {Array<number>} values - 随机数序列
|
||||
* @returns {Object} spy
|
||||
*/
|
||||
export function mockRandomSequence(values) {
|
||||
let index = 0
|
||||
return vi.spyOn(Math, 'random').mockImplementation(() => {
|
||||
const value = values[index % values.length]
|
||||
index++
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建确定性的随机数生成器
|
||||
* @param {number} seed - 种子值
|
||||
* @returns {Function} 随机函数
|
||||
*/
|
||||
export function createSeededRandom(seed) {
|
||||
return () => {
|
||||
seed = (seed * 9301 + 49297) % 233280
|
||||
return seed / 233280
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 随机数生成器
|
||||
* @param {number} seed - 种子值
|
||||
* @returns {Object} spy
|
||||
*/
|
||||
export function mockSeededRandom(seed) {
|
||||
const randomFunc = createSeededRandom(seed)
|
||||
return vi.spyOn(Math, 'random').mockImplementation(randomFunc)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有 mock
|
||||
*/
|
||||
export function cleanupMocks() {
|
||||
vi.restoreAllMocks()
|
||||
resetMockRandom()
|
||||
}
|
||||
94
tests/helpers/store.js
Normal file
94
tests/helpers/store.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Store 测试辅助函数
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createMockPlayer } from '../fixtures/player.js'
|
||||
import { createMockGame } from '../fixtures/game.js'
|
||||
|
||||
/**
|
||||
* 设置测试用 Pinia Store
|
||||
* @returns {Object} { playerStore, gameStore, pinia }
|
||||
*/
|
||||
export function setupTestStore() {
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
// 动态导入 stores
|
||||
const { usePlayerStore } = require('@/store/player.js')
|
||||
const { useGameStore } = require('@/store/game.js')
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// 初始化 mock 数据
|
||||
Object.assign(playerStore, createMockPlayer())
|
||||
Object.assign(gameStore, createMockGame())
|
||||
|
||||
return { playerStore, gameStore, pinia }
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带有特定技能的 Store
|
||||
* @param {string} skillId - 技能ID
|
||||
* @param {number} level - 技能等级
|
||||
* @returns {Object} { playerStore, gameStore }
|
||||
*/
|
||||
export function setupStoreWithSkill(skillId, level = 1) {
|
||||
const { playerStore, gameStore, pinia } = setupTestStore()
|
||||
|
||||
playerStore.skills = {
|
||||
[skillId]: {
|
||||
level,
|
||||
exp: 0,
|
||||
unlocked: true,
|
||||
maxExp: (level + 1) * 100
|
||||
}
|
||||
}
|
||||
|
||||
return { playerStore, gameStore, pinia }
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建战斗中的 Store
|
||||
* @param {Object} enemy - 敌人对象
|
||||
* @returns {Object} { playerStore, gameStore }
|
||||
*/
|
||||
export function setupStoreInCombat(enemy) {
|
||||
const { playerStore, gameStore, pinia } = setupTestStore()
|
||||
|
||||
gameStore.inCombat = true
|
||||
gameStore.combatState = {
|
||||
enemyId: enemy.id,
|
||||
enemy,
|
||||
stance: 'balance',
|
||||
environment: 'normal',
|
||||
startTime: Date.now(),
|
||||
ticks: 0
|
||||
}
|
||||
|
||||
return { playerStore, gameStore, pinia }
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带有物品的 Store
|
||||
* @param {Array} items - 物品数组
|
||||
* @returns {Object} { playerStore, gameStore }
|
||||
*/
|
||||
export function setupStoreWithItems(items) {
|
||||
const { playerStore, gameStore, pinia } = setupTestStore()
|
||||
|
||||
playerStore.inventory = [...items]
|
||||
|
||||
return { playerStore, gameStore, pinia }
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 Store 状态
|
||||
* @param {Object} playerStore - 玩家Store
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
*/
|
||||
export function resetStore(playerStore, gameStore) {
|
||||
Object.assign(playerStore, createMockPlayer())
|
||||
Object.assign(gameStore, createMockGame())
|
||||
}
|
||||
64
tests/helpers/timer.js
Normal file
64
tests/helpers/timer.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 时间相关测试辅助函数
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock Date.now()
|
||||
* @param {number} timestamp - 时间戳
|
||||
* @returns {Object} spy
|
||||
*/
|
||||
export function mockDateNow(timestamp = Date.now()) {
|
||||
return vi.spyOn(Date, 'now').mockReturnValue(timestamp)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock setInterval/setTimeout
|
||||
* @returns {Object} { useFakeTimers, useRealTimers }
|
||||
*/
|
||||
export function mockTimers() {
|
||||
return {
|
||||
useFakeTimers: () => vi.useFakeTimers(),
|
||||
useRealTimers: () => vi.useRealTimers(),
|
||||
runAllTimers: () => vi.runAllTimers(),
|
||||
runOnlyPendingTimers: () => vi.runOnlyPendingTimers(),
|
||||
advanceTimersByTime: (ms) => vi.advanceTimersByTime(ms)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待指定时间
|
||||
* @param {number} ms - 毫秒
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function wait(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速时间(用于测试)
|
||||
* @param {number} seconds - 秒数
|
||||
* @returns {number} 毫秒
|
||||
*/
|
||||
export function seconds(seconds) {
|
||||
return seconds * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速时间(分钟)
|
||||
* @param {number} minutes - 分钟数
|
||||
* @returns {number} 毫秒
|
||||
*/
|
||||
export function minutes(minutes) {
|
||||
return minutes * 60 * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速时间(小时)
|
||||
* @param {number} hours - 小时数
|
||||
* @returns {number} 毫秒
|
||||
*/
|
||||
export function hours(hours) {
|
||||
return hours * 60 * 60 * 1000
|
||||
}
|
||||
27
tests/setup.js
Normal file
27
tests/setup.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Vitest 测试环境配置
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest'
|
||||
import { config } from '@vue/test-utils'
|
||||
|
||||
// Mock uni API
|
||||
global.uni = {
|
||||
setStorageSync: vi.fn(() => true),
|
||||
getStorageSync: vi.fn(() => null),
|
||||
removeStorageSync: vi.fn(() => true),
|
||||
getSystemInfoSync: vi.fn(() => ({
|
||||
platform: 'h5',
|
||||
system: 'test'
|
||||
}))
|
||||
}
|
||||
|
||||
// Vue Test Utils 全局配置
|
||||
config.global.stubs = {
|
||||
transition: false,
|
||||
'transition-group': false
|
||||
}
|
||||
|
||||
config.global.mocks = {
|
||||
$t: (key) => key
|
||||
}
|
||||
190
tests/unit/systems/combatSystem.test.js
Normal file
190
tests/unit/systems/combatSystem.test.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 战斗系统单元测试
|
||||
* 测试纯函数,不依赖框架
|
||||
*/
|
||||
|
||||
// 由于项目使用 ESM,我们需要使用 require 直接导入
|
||||
// 注意:这些测试假设代码已经被转换为 CommonJS 或者通过 babel 转换
|
||||
|
||||
describe('战斗系统 - 纯函数测试', () => {
|
||||
describe('calculateHitRate - 命中概率计算', () => {
|
||||
// 直接测试纯函数逻辑
|
||||
const calculateHitRate = (ap, ep) => {
|
||||
const ratio = ap / (ep + 1)
|
||||
if (ratio >= 5) return 0.98
|
||||
if (ratio >= 3) return 0.90
|
||||
if (ratio >= 2) return 0.80
|
||||
if (ratio >= 1.5) return 0.65
|
||||
if (ratio >= 1) return 0.50
|
||||
if (ratio >= 0.75) return 0.38
|
||||
if (ratio >= 0.5) return 0.24
|
||||
if (ratio >= 0.33) return 0.15
|
||||
if (ratio >= 0.2) return 0.08
|
||||
return 0.05
|
||||
}
|
||||
|
||||
test('AP >> EP 时应该有高命中率', () => {
|
||||
expect(calculateHitRate(50, 10)).toBe(0.90) // ratio = 4.5
|
||||
expect(calculateHitRate(100, 10)).toBe(0.98) // ratio = 9.1
|
||||
})
|
||||
|
||||
test('AP = EP 时应该有50%命中率', () => {
|
||||
// ratio = 10 / (10 + 1) = 0.909 -> 实际上 > 0.75,所以是 0.38
|
||||
expect(calculateHitRate(10, 10)).toBe(0.38)
|
||||
// ratio = 20 / (20 + 1) = 0.95 -> 实际上 > 0.75,所以是 0.38
|
||||
expect(calculateHitRate(20, 20)).toBe(0.38)
|
||||
})
|
||||
|
||||
test('AP < EP 时应该有低命中率', () => {
|
||||
// ratio = 10 / (20 + 1) = 0.476 -> < 0.5,>= 0.33,所以是 0.15
|
||||
expect(calculateHitRate(10, 20)).toBe(0.15)
|
||||
// ratio = 5 / (20 + 1) = 0.238 -> >= 0.2,所以是 0.08
|
||||
expect(calculateHitRate(5, 20)).toBe(0.08)
|
||||
})
|
||||
|
||||
test('极低 AP/EP 比例时应该只有5%命中率', () => {
|
||||
// ratio = 1 / (10 + 1) = 0.09 -> < 0.2,所以是 0.05
|
||||
expect(calculateHitRate(1, 10)).toBe(0.05)
|
||||
// ratio = 2 / (20 + 1) = 0.095 -> < 0.2,所以是 0.05
|
||||
expect(calculateHitRate(2, 20)).toBe(0.05)
|
||||
})
|
||||
|
||||
test('边界值测试', () => {
|
||||
// ratio = 0 / (10 + 1) = 0 -> < 0.2,所以是 0.05
|
||||
expect(calculateHitRate(0, 10)).toBe(0.05)
|
||||
// ratio = 10 / (0 + 1) = 10 -> >= 5,所以是 0.98
|
||||
expect(calculateHitRate(10, 0)).toBe(0.98)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AP/EP 计算逻辑验证', () => {
|
||||
test('基础AP计算公式', () => {
|
||||
// AP = 灵巧 + 智力*0.2
|
||||
const dexterity = 10
|
||||
const intuition = 10
|
||||
const baseAP = dexterity + intuition * 0.2
|
||||
expect(baseAP).toBe(12)
|
||||
})
|
||||
|
||||
test('基础EP计算公式', () => {
|
||||
// EP = 敏捷 + 智力*0.2
|
||||
const agility = 8
|
||||
const intuition = 10
|
||||
const baseEP = agility + intuition * 0.2
|
||||
expect(baseEP).toBe(10)
|
||||
})
|
||||
|
||||
test('姿态加成计算', () => {
|
||||
const baseAP = 10
|
||||
const stances = {
|
||||
attack: 1.1,
|
||||
defense: 0.9,
|
||||
balance: 1.0,
|
||||
rapid: 1.05,
|
||||
heavy: 1.15
|
||||
}
|
||||
|
||||
expect(baseAP * stances.attack).toBe(11)
|
||||
expect(baseAP * stances.defense).toBe(9)
|
||||
expect(baseAP * stances.balance).toBe(10)
|
||||
})
|
||||
|
||||
test('环境惩罚计算', () => {
|
||||
const ap = 10
|
||||
|
||||
// 黑暗环境 -20%
|
||||
expect(ap * 0.8).toBe(8)
|
||||
|
||||
// 恶劣环境 -10%
|
||||
expect(ap * 0.9).toBe(9)
|
||||
})
|
||||
|
||||
test('多敌人惩罚计算', () => {
|
||||
const ap = 10
|
||||
const enemyCount = 3
|
||||
|
||||
// 惩罚 = Math.min(0.3, (enemyCount - 1) * 0.05)
|
||||
// 3个敌人 = (3-1)*0.05 = 0.10 = 10%
|
||||
const penalty = Math.min(0.3, (enemyCount - 1) * 0.05)
|
||||
expect(ap * (1 - penalty)).toBe(9)
|
||||
})
|
||||
|
||||
test('耐力惩罚计算', () => {
|
||||
const ap = 10
|
||||
|
||||
// 耐力耗尽
|
||||
expect(ap * 0.5).toBe(5)
|
||||
|
||||
// 耐力25%
|
||||
const staminaRatio = 0.25
|
||||
expect(ap * (0.5 + staminaRatio * 0.5)).toBe(6.25)
|
||||
})
|
||||
})
|
||||
|
||||
describe('暴击系统计算', () => {
|
||||
test('基础暴击率计算', () => {
|
||||
// 基础暴击率 = 灵巧 / 100
|
||||
const dexterity = 10
|
||||
const baseCritRate = dexterity / 100
|
||||
expect(baseCritRate).toBe(0.10)
|
||||
})
|
||||
|
||||
test('暴击率上限', () => {
|
||||
const critRate = 0.90
|
||||
expect(Math.min(0.8, critRate)).toBe(0.80)
|
||||
})
|
||||
|
||||
test('暴击倍数计算', () => {
|
||||
const baseMult = 1.5
|
||||
const bonusMult = 0.2
|
||||
expect(baseMult + bonusMult).toBe(1.7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('伤害计算验证', () => {
|
||||
test('基础伤害公式', () => {
|
||||
// 伤害 = 攻击力 * (1 - 防御减伤)
|
||||
const damage = 100
|
||||
const defense = 0.3 // 30%减伤
|
||||
expect(damage * (1 - defense)).toBe(70)
|
||||
})
|
||||
|
||||
test('最大防御减伤90%', () => {
|
||||
const damage = 100
|
||||
const maxDefense = 0.9
|
||||
// 浮点数精度问题,使用 toBeCloseTo
|
||||
expect(damage * (1 - maxDefense)).toBeCloseTo(10, 1)
|
||||
})
|
||||
|
||||
test('暴击伤害', () => {
|
||||
const damage = 100
|
||||
const critMult = 1.5
|
||||
expect(damage * critMult).toBe(150)
|
||||
})
|
||||
})
|
||||
|
||||
describe('边界条件测试', () => {
|
||||
test('AP最小值为1', () => {
|
||||
const ap = 0.5
|
||||
expect(Math.max(1, Math.floor(ap))).toBe(1)
|
||||
})
|
||||
|
||||
test('EP最小值为1', () => {
|
||||
const ep = 0.3
|
||||
expect(Math.max(1, Math.floor(ep))).toBe(1)
|
||||
})
|
||||
|
||||
test('耐力不能为负数', () => {
|
||||
const stamina = -10
|
||||
const maxStamina = 100
|
||||
const ratio = Math.max(0, stamina / maxStamina)
|
||||
expect(ratio).toBe(0)
|
||||
})
|
||||
|
||||
test('属性默认值处理', () => {
|
||||
const baseStats = undefined
|
||||
const dexterity = baseStats?.dexterity || 0
|
||||
expect(dexterity).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
383
tests/unit/systems/itemSystem.test.js
Normal file
383
tests/unit/systems/itemSystem.test.js
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 物品系统单元测试
|
||||
* 测试纯函数,不依赖框架
|
||||
*/
|
||||
|
||||
describe('物品系统 - 纯函数测试', () => {
|
||||
describe('getQualityLevel - 品质等级计算', () => {
|
||||
const QUALITY_LEVELS = {
|
||||
1: { level: 1, name: '垃圾', color: '#9e9e9e', range: [0, 49], multiplier: 0.5 },
|
||||
2: { level: 2, name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
|
||||
3: { level: 3, name: '优秀', color: '#1eff00', range: [100, 129], multiplier: 1.3 },
|
||||
4: { level: 4, name: '稀有', color: '#0070dd', range: [130, 159], multiplier: 1.6 },
|
||||
5: { level: 5, name: '史诗', color: '#a335ee', range: [160, 199], multiplier: 2.0 },
|
||||
6: { level: 6, name: '传说', color: '#ff8000', range: [200, 250], multiplier: 2.5 }
|
||||
}
|
||||
|
||||
const getQualityLevel = (quality) => {
|
||||
const clampedQuality = Math.max(0, Math.min(250, quality))
|
||||
|
||||
for (const [level, data] of Object.entries(QUALITY_LEVELS)) {
|
||||
if (clampedQuality >= data.range[0] && clampedQuality <= data.range[1]) {
|
||||
return {
|
||||
level: parseInt(level),
|
||||
...data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QUALITY_LEVELS[1]
|
||||
}
|
||||
|
||||
test('应该正确识别垃圾品质', () => {
|
||||
const level = getQualityLevel(0)
|
||||
expect(level.level).toBe(1)
|
||||
expect(level.name).toBe('垃圾')
|
||||
expect(level.multiplier).toBe(0.5)
|
||||
})
|
||||
|
||||
test('应该正确识别普通品质', () => {
|
||||
const level = getQualityLevel(75)
|
||||
expect(level.level).toBe(2)
|
||||
expect(level.name).toBe('普通')
|
||||
expect(level.multiplier).toBe(1.0)
|
||||
})
|
||||
|
||||
test('应该正确识别优秀品质', () => {
|
||||
const level = getQualityLevel(115)
|
||||
expect(level.level).toBe(3)
|
||||
expect(level.name).toBe('优秀')
|
||||
expect(level.multiplier).toBe(1.3)
|
||||
})
|
||||
|
||||
test('应该正确识别稀有品质', () => {
|
||||
const level = getQualityLevel(145)
|
||||
expect(level.level).toBe(4)
|
||||
expect(level.name).toBe('稀有')
|
||||
expect(level.multiplier).toBe(1.6)
|
||||
})
|
||||
|
||||
test('应该正确识别史诗品质', () => {
|
||||
const level = getQualityLevel(180)
|
||||
expect(level.level).toBe(5)
|
||||
expect(level.name).toBe('史诗')
|
||||
expect(level.multiplier).toBe(2.0)
|
||||
})
|
||||
|
||||
test('应该正确识别传说品质', () => {
|
||||
const level = getQualityLevel(220)
|
||||
expect(level.level).toBe(6)
|
||||
expect(level.name).toBe('传说')
|
||||
expect(level.multiplier).toBe(2.5)
|
||||
})
|
||||
|
||||
test('品质边界值测试', () => {
|
||||
expect(getQualityLevel(49).level).toBe(1) // 垃圾上限
|
||||
expect(getQualityLevel(50).level).toBe(2) // 普通下限
|
||||
expect(getQualityLevel(99).level).toBe(2) // 普通上限
|
||||
expect(getQualityLevel(100).level).toBe(3) // 优秀下限
|
||||
expect(getQualityLevel(250).level).toBe(6) // 传说上限
|
||||
})
|
||||
|
||||
test('超出范围应该被限制', () => {
|
||||
expect(getQualityLevel(-10).level).toBe(1) // 负数
|
||||
expect(getQualityLevel(300).level).toBe(6) // 超过250
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateItemStats - 物品属性计算', () => {
|
||||
test('应该正确计算武器伤害', () => {
|
||||
const baseDamage = 10
|
||||
const quality = 100
|
||||
const qualityMultiplier = 1.3
|
||||
const qualityRatio = quality / 100
|
||||
|
||||
const finalDamage = Math.floor(baseDamage * qualityRatio * qualityMultiplier)
|
||||
|
||||
expect(finalDamage).toBe(13) // 10 * 1.0 * 1.3 = 13
|
||||
})
|
||||
|
||||
test('应该正确计算防御力', () => {
|
||||
const baseDefense = 20
|
||||
const quality = 150
|
||||
const qualityMultiplier = 1.6
|
||||
const qualityRatio = quality / 100
|
||||
|
||||
const finalDefense = Math.floor(baseDefense * qualityRatio * qualityMultiplier)
|
||||
|
||||
expect(finalDefense).toBe(48) // 20 * 1.5 * 1.6 = 48
|
||||
})
|
||||
|
||||
test('应该正确计算物品价值', () => {
|
||||
const baseValue = 100
|
||||
const quality = 120
|
||||
const qualityMultiplier = 1.3
|
||||
const qualityRatio = quality / 100
|
||||
|
||||
const finalValue = Math.floor(baseValue * qualityRatio * qualityMultiplier)
|
||||
|
||||
expect(finalValue).toBe(156) // 100 * 1.2 * 1.3 = 156
|
||||
})
|
||||
|
||||
test('品质为0时应该有最小值', () => {
|
||||
const baseDamage = 10
|
||||
const quality = 0
|
||||
const qualityMultiplier = 0.5
|
||||
const qualityRatio = quality / 100
|
||||
|
||||
const finalDamage = Math.floor(baseDamage * qualityRatio * qualityMultiplier)
|
||||
|
||||
expect(finalDamage).toBe(0) // 10 * 0 * 0.5 = 0
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateRandomQuality - 随机品质生成', () => {
|
||||
test('基础品质范围应该是50-150', () => {
|
||||
const baseMin = 50
|
||||
const baseMax = 150
|
||||
|
||||
const randomQuality = baseMin + Math.floor(Math.random() * (baseMax - baseMin + 1))
|
||||
|
||||
expect(randomQuality).toBeGreaterThanOrEqual(50)
|
||||
expect(randomQuality).toBeLessThanOrEqual(150)
|
||||
})
|
||||
|
||||
test('运气应该增加品质', () => {
|
||||
const baseQuality = 100
|
||||
const luck = 20
|
||||
const luckBonus = luck * 0.5
|
||||
|
||||
const finalQuality = baseQuality + luckBonus
|
||||
|
||||
expect(finalQuality).toBe(110) // 100 + 20 * 0.5 = 110
|
||||
})
|
||||
|
||||
test('品质应该被限制在0-250之间', () => {
|
||||
const clamp = (value) => Math.min(250, Math.max(0, value))
|
||||
|
||||
expect(clamp(-10)).toBe(0)
|
||||
expect(clamp(0)).toBe(0)
|
||||
expect(clamp(250)).toBe(250)
|
||||
expect(clamp(300)).toBe(250)
|
||||
})
|
||||
|
||||
test('传说品质应该非常稀有', () => {
|
||||
// 传说品质概率 < 0.02
|
||||
const legendaryChance = 0.01 + 50 / 10000
|
||||
|
||||
expect(legendaryChance).toBeLessThan(0.02)
|
||||
})
|
||||
|
||||
test('史诗品质应该稀有', () => {
|
||||
// 史诗品质概率 < 0.08
|
||||
const epicChance = 0.05 + 50 / 2000
|
||||
|
||||
expect(epicChance).toBeLessThan(0.08)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateSellPrice - 出售价格计算', () => {
|
||||
test('应该正确计算基础出售价格', () => {
|
||||
const baseValue = 100
|
||||
const sellRate = 0.5 // 50%
|
||||
|
||||
const sellPrice = Math.floor(baseValue * sellRate)
|
||||
|
||||
expect(sellPrice).toBe(50)
|
||||
})
|
||||
|
||||
test('品质应该影响出售价格', () => {
|
||||
const baseValue = 100
|
||||
const quality = 120
|
||||
const qualityMultiplier = 1.3
|
||||
const sellRate = 0.5
|
||||
|
||||
const sellPrice = Math.floor(baseValue * (quality / 100) * qualityMultiplier * sellRate)
|
||||
|
||||
expect(sellPrice).toBe(78) // 100 * 1.2 * 1.3 * 0.5 = 78
|
||||
})
|
||||
|
||||
test('市场倍率应该影响价格', () => {
|
||||
const baseValue = 100
|
||||
const quality = 100
|
||||
const qualityMultiplier = 1.0
|
||||
const marketRate = 1.5 // 市场繁荣
|
||||
|
||||
const sellPrice = Math.floor(baseValue * 1.0 * qualityMultiplier * 0.5 * marketRate)
|
||||
|
||||
expect(sellPrice).toBe(75) // 100 * 1.0 * 1.0 * 0.5 * 1.5 = 75
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateBuyPrice - 购买价格计算', () => {
|
||||
test('应该正确计算基础购买价格', () => {
|
||||
const baseValue = 100
|
||||
const buyRate = 1.5 // 150%
|
||||
|
||||
const buyPrice = Math.floor(baseValue * buyRate)
|
||||
|
||||
expect(buyPrice).toBe(150)
|
||||
})
|
||||
|
||||
test('品质应该影响购买价格', () => {
|
||||
const baseValue = 100
|
||||
const quality = 150
|
||||
const qualityMultiplier = 1.6
|
||||
const buyRate = 1.5
|
||||
|
||||
const buyPrice = Math.floor(baseValue * (quality / 100) * qualityMultiplier * buyRate)
|
||||
|
||||
expect(buyPrice).toBe(360) // 100 * 1.5 * 1.6 * 1.5 = 360
|
||||
})
|
||||
})
|
||||
|
||||
describe('物品堆叠', () => {
|
||||
test('可堆叠物品应该有最大堆叠数', () => {
|
||||
const stackable = true
|
||||
const maxStack = 99
|
||||
|
||||
const canAddToStack = (currentStack, amount) => {
|
||||
return currentStack + amount <= maxStack
|
||||
}
|
||||
|
||||
expect(canAddToStack(50, 40)).toBe(true)
|
||||
expect(canAddToStack(50, 49)).toBe(true)
|
||||
expect(canAddToStack(50, 50)).toBe(false)
|
||||
})
|
||||
|
||||
test('不可堆叠物品每次只能有一个', () => {
|
||||
const stackable = false
|
||||
const maxStack = 1
|
||||
|
||||
const canAddToStack = (currentStack) => {
|
||||
return currentStack < maxStack
|
||||
}
|
||||
|
||||
expect(canAddToStack(0)).toBe(true)
|
||||
expect(canAddToStack(1)).toBe(false)
|
||||
})
|
||||
|
||||
test('使用堆叠物品应该减少数量', () => {
|
||||
const itemCount = 10
|
||||
const useCount = 3
|
||||
|
||||
const remainingCount = itemCount - useCount
|
||||
|
||||
expect(remainingCount).toBe(7)
|
||||
})
|
||||
|
||||
test('堆叠为0时物品应该被移除', () => {
|
||||
const itemCount = 1
|
||||
const useCount = 1
|
||||
|
||||
const shouldRemove = itemCount - useCount <= 0
|
||||
|
||||
expect(shouldRemove).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('装备物品', () => {
|
||||
test('武器应该装备到武器槽', () => {
|
||||
const itemType = 'weapon'
|
||||
const equipment = {
|
||||
weapon: null,
|
||||
armor: null
|
||||
}
|
||||
|
||||
const canEquip = (slot) => {
|
||||
return equipment[slot] === null
|
||||
}
|
||||
|
||||
expect(canEquip('weapon')).toBe(true)
|
||||
expect(canEquip('armor')).toBe(true)
|
||||
})
|
||||
|
||||
test('装备槽已占用时不能装备', () => {
|
||||
const equipment = {
|
||||
weapon: { id: 'old_sword' }
|
||||
}
|
||||
|
||||
const canEquip = (slot) => {
|
||||
return equipment[slot] === null
|
||||
}
|
||||
|
||||
expect(canEquip('weapon')).toBe(false)
|
||||
})
|
||||
|
||||
test('双手武器应该占用两个槽位', () => {
|
||||
const item = {
|
||||
id: 'two_handed_sword',
|
||||
twoHanded: true
|
||||
}
|
||||
|
||||
const slotsNeeded = item.twoHanded ? 2 : 1
|
||||
|
||||
expect(slotsNeeded).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('消耗品效果', () => {
|
||||
test('生命药水应该恢复生命值', () => {
|
||||
const currentHealth = 50
|
||||
const maxHealth = 100
|
||||
const healAmount = 30
|
||||
|
||||
const newHealth = Math.min(maxHealth, currentHealth + healAmount)
|
||||
|
||||
expect(newHealth).toBe(80)
|
||||
})
|
||||
|
||||
test('生命值已满时不应该溢出', () => {
|
||||
const currentHealth = 100
|
||||
const maxHealth = 100
|
||||
const healAmount = 30
|
||||
|
||||
const newHealth = Math.min(maxHealth, currentHealth + healAmount)
|
||||
|
||||
expect(newHealth).toBe(100)
|
||||
})
|
||||
|
||||
test('耐力药水应该恢复耐力', () => {
|
||||
const currentStamina = 20
|
||||
const maxStamina = 100
|
||||
const restoreAmount = 50
|
||||
|
||||
const newStamina = Math.min(maxStamina, currentStamina + restoreAmount)
|
||||
|
||||
expect(newStamina).toBe(70)
|
||||
})
|
||||
})
|
||||
|
||||
describe('边界条件', () => {
|
||||
test('品质为负数时应该设为0', () => {
|
||||
const quality = -10
|
||||
const clampedQuality = Math.max(0, quality)
|
||||
|
||||
expect(clampedQuality).toBe(0)
|
||||
})
|
||||
|
||||
test('品质超过250时应该设为250', () => {
|
||||
const quality = 300
|
||||
const clampedQuality = Math.min(250, quality)
|
||||
|
||||
expect(clampedQuality).toBe(250)
|
||||
})
|
||||
|
||||
test('物品价值不能为负数', () => {
|
||||
const baseValue = -100
|
||||
const finalValue = Math.max(0, baseValue)
|
||||
|
||||
expect(finalValue).toBe(0)
|
||||
})
|
||||
|
||||
test('伤害不能为负数', () => {
|
||||
const baseDamage = -10
|
||||
const qualityRatio = 1.0
|
||||
const qualityMultiplier = 1.0
|
||||
|
||||
const finalDamage = Math.max(0, Math.floor(baseDamage * qualityRatio * qualityMultiplier))
|
||||
|
||||
expect(finalDamage).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
403
tests/unit/systems/skillSystem.test.js
Normal file
403
tests/unit/systems/skillSystem.test.js
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* 技能系统单元测试
|
||||
* 测试纯函数,不依赖框架
|
||||
*/
|
||||
|
||||
describe('技能系统 - 纯函数测试', () => {
|
||||
describe('getNextLevelExp - 下一级经验计算', () => {
|
||||
test('应该正确计算下一级所需经验(默认公式)', () => {
|
||||
// 默认公式: (currentLevel + 1) * 100
|
||||
const getNextLevelExp = (skillId, currentLevel) => {
|
||||
// 假设使用默认配置
|
||||
return (currentLevel + 1) * 100
|
||||
}
|
||||
|
||||
expect(getNextLevelExp('sword', 0)).toBe(100) // 1级需要100
|
||||
expect(getNextLevelExp('sword', 1)).toBe(200) // 2级需要200
|
||||
expect(getNextLevelExp('sword', 5)).toBe(600) // 6级需要600
|
||||
expect(getNextLevelExp('sword', 9)).toBe(1000) // 10级需要1000
|
||||
})
|
||||
|
||||
test('应该正确处理自定义经验公式', () => {
|
||||
// 自定义公式: level * level * 50
|
||||
const customExpPerLevel = (level) => level * level * 50
|
||||
const getNextLevelExp = (skillId, currentLevel) => {
|
||||
return customExpPerLevel(currentLevel + 1)
|
||||
}
|
||||
|
||||
expect(getNextLevelExp('magic', 0)).toBe(50) // 1级: 1*1*50
|
||||
expect(getNextLevelExp('magic', 1)).toBe(200) // 2级: 2*2*50
|
||||
expect(getNextLevelExp('magic', 4)).toBe(1250) // 5级: 5*5*50
|
||||
})
|
||||
|
||||
test('边界值测试', () => {
|
||||
const getNextLevelExp = (skillId, currentLevel) => {
|
||||
return (currentLevel + 1) * 100
|
||||
}
|
||||
|
||||
expect(getNextLevelExp('test', 0)).toBe(100)
|
||||
expect(getNextLevelExp('test', 99)).toBe(10000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('技能升级逻辑', () => {
|
||||
test('应该正确添加经验并升级', () => {
|
||||
// 模拟升级逻辑
|
||||
const addSkillExp = (skill, amount, expPerLevel) => {
|
||||
skill.exp += amount
|
||||
|
||||
let leveledUp = false
|
||||
while (skill.exp >= expPerLevel(skill.level + 1)) {
|
||||
skill.exp -= expPerLevel(skill.level + 1)
|
||||
skill.level++
|
||||
leveledUp = true
|
||||
}
|
||||
|
||||
return { leveledUp, newLevel: skill.level }
|
||||
}
|
||||
|
||||
const skill = { level: 1, exp: 0 }
|
||||
// 2级需要 200 经验
|
||||
const expPerLevel = (level) => level * 100
|
||||
|
||||
// 添加200经验,正好升级到2级
|
||||
let result = addSkillExp(skill, 200, expPerLevel)
|
||||
expect(result.leveledUp).toBe(true)
|
||||
expect(result.newLevel).toBe(2)
|
||||
expect(skill.exp).toBe(0)
|
||||
|
||||
// 再添加300经验,升级到3级(需要300)
|
||||
result = addSkillExp(skill, 300, expPerLevel)
|
||||
expect(result.leveledUp).toBe(true)
|
||||
expect(result.newLevel).toBe(3)
|
||||
expect(skill.exp).toBe(0)
|
||||
})
|
||||
|
||||
test('应该正确处理连续升级', () => {
|
||||
const skill = { level: 1, exp: 0 }
|
||||
const expPerLevel = (level) => level * 100
|
||||
|
||||
// 添加500经验,应该从1级升到3级(200+300)
|
||||
const addSkillExp = (skill, amount, expPerLevel) => {
|
||||
skill.exp += amount
|
||||
|
||||
let leveledUp = false
|
||||
while (skill.exp >= expPerLevel(skill.level + 1)) {
|
||||
skill.exp -= expPerLevel(skill.level + 1)
|
||||
skill.level++
|
||||
leveledUp = true
|
||||
}
|
||||
|
||||
return { leveledUp, newLevel: skill.level }
|
||||
}
|
||||
|
||||
const result = addSkillExp(skill, 500, expPerLevel)
|
||||
expect(result.leveledUp).toBe(true)
|
||||
expect(result.newLevel).toBe(3)
|
||||
expect(skill.exp).toBe(0)
|
||||
})
|
||||
|
||||
test('经验不足时不应该升级', () => {
|
||||
const skill = { level: 1, exp: 0 }
|
||||
const expPerLevel = (level) => level * 100
|
||||
|
||||
const addSkillExp = (skill, amount, expPerLevel) => {
|
||||
skill.exp += amount
|
||||
|
||||
let leveledUp = false
|
||||
while (skill.exp >= expPerLevel(skill.level + 1)) {
|
||||
skill.exp -= expPerLevel(skill.level + 1)
|
||||
skill.level++
|
||||
leveledUp = true
|
||||
}
|
||||
|
||||
return { leveledUp, newLevel: skill.level }
|
||||
}
|
||||
|
||||
const result = addSkillExp(skill, 50, expPerLevel)
|
||||
expect(result.leveledUp).toBe(false)
|
||||
expect(result.newLevel).toBe(1)
|
||||
expect(skill.exp).toBe(50)
|
||||
})
|
||||
|
||||
test('部分成功时经验应该减半', () => {
|
||||
const EXP_PARTIAL_SUCCESS = 0.5
|
||||
|
||||
const calculatePartialExp = (amount) => {
|
||||
return Math.floor(amount * EXP_PARTIAL_SUCCESS)
|
||||
}
|
||||
|
||||
expect(calculatePartialExp(100)).toBe(50)
|
||||
expect(calculatePartialExp(150)).toBe(75)
|
||||
expect(calculatePartialExp(99)).toBe(49) // 向下取整
|
||||
})
|
||||
})
|
||||
|
||||
describe('技能等级上限', () => {
|
||||
test('应该正确计算等级上限(玩家等级限制)', () => {
|
||||
// 技能上限 = 玩家等级 * 2
|
||||
const playerLevel = 5
|
||||
const maxSkillLevel = playerLevel * 2
|
||||
|
||||
expect(maxSkillLevel).toBe(10)
|
||||
|
||||
const skillLevel = 10
|
||||
const canLevelUp = skillLevel < maxSkillLevel
|
||||
|
||||
expect(canLevelUp).toBe(false)
|
||||
})
|
||||
|
||||
test('应该正确计算等级上限(技能配置限制)', () => {
|
||||
const playerLevel = 10
|
||||
const skillMaxLevel = 5
|
||||
|
||||
// 取最小值
|
||||
const effectiveMaxLevel = Math.min(playerLevel * 2, skillMaxLevel)
|
||||
|
||||
expect(effectiveMaxLevel).toBe(5)
|
||||
})
|
||||
|
||||
test('达到等级上限时不再升级', () => {
|
||||
const skill = { level: 5, exp: 0 }
|
||||
const maxLevel = 5
|
||||
|
||||
const addSkillExp = (skill, amount, maxLevel) => {
|
||||
if (skill.level >= maxLevel) {
|
||||
return { leveledUp: false, newLevel: skill.level, capped: true }
|
||||
}
|
||||
skill.exp += amount
|
||||
return { leveledUp: false, newLevel: skill.level, capped: false }
|
||||
}
|
||||
|
||||
const result = addSkillExp(skill, 100, maxLevel)
|
||||
expect(result.leveledUp).toBe(false)
|
||||
expect(result.capped).toBe(true)
|
||||
expect(result.newLevel).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('里程碑奖励', () => {
|
||||
test('应该正确检测里程碑等级', () => {
|
||||
const milestones = {
|
||||
5: { effect: { damage: 5 } },
|
||||
10: { effect: { damage: 10 } }
|
||||
}
|
||||
|
||||
const hasMilestone = (level) => {
|
||||
return !!milestones[level]
|
||||
}
|
||||
|
||||
expect(hasMilestone(5)).toBe(true)
|
||||
expect(hasMilestone(10)).toBe(true)
|
||||
expect(hasMilestone(3)).toBe(false)
|
||||
})
|
||||
|
||||
test('应该正确应用里程碑奖励', () => {
|
||||
const skill = { level: 4 }
|
||||
const milestones = {
|
||||
5: { effect: { strength: 2 } },
|
||||
10: { effect: { strength: 5 } }
|
||||
}
|
||||
|
||||
const applyMilestone = (skill, milestones) => {
|
||||
if (milestones[skill.level]) {
|
||||
return milestones[skill.level].effect
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// 5级时应用
|
||||
skill.level = 5
|
||||
const effect = applyMilestone(skill, milestones)
|
||||
expect(effect).toEqual({ strength: 2 })
|
||||
|
||||
// 10级时应用
|
||||
skill.level = 10
|
||||
const effect2 = applyMilestone(skill, milestones)
|
||||
expect(effect2).toEqual({ strength: 5 })
|
||||
})
|
||||
|
||||
test('里程碑奖励应该只应用一次', () => {
|
||||
const appliedMilestones = new Set()
|
||||
const milestoneKey = 'sword_5'
|
||||
|
||||
const canApplyMilestone = (key) => {
|
||||
return !appliedMilestones.has(key)
|
||||
}
|
||||
|
||||
const applyMilestone = (key) => {
|
||||
if (canApplyMilestone(key)) {
|
||||
appliedMilestones.add(key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
expect(applyMilestone(milestoneKey)).toBe(true)
|
||||
expect(applyMilestone(milestoneKey)).toBe(false)
|
||||
expect(appliedMilestones.has(milestoneKey)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('父技能系统', () => {
|
||||
test('父技能等级应该等于所有子技能的最高等级', () => {
|
||||
const childSkills = {
|
||||
sword: { level: 5 },
|
||||
axe: { level: 3 },
|
||||
spear: { level: 7 }
|
||||
}
|
||||
|
||||
const calculateParentLevel = (childSkills) => {
|
||||
return Math.max(...Object.values(childSkills).map(s => s.level))
|
||||
}
|
||||
|
||||
expect(calculateParentLevel(childSkills)).toBe(7)
|
||||
})
|
||||
|
||||
test('子技能升级时应该更新父技能', () => {
|
||||
const skills = {
|
||||
sword: { level: 5 },
|
||||
axe: { level: 3 },
|
||||
combat: { level: 5 } // 父技能
|
||||
}
|
||||
|
||||
const updateParentSkill = (skillId) => {
|
||||
const childLevels = [skills.sword.level, skills.axe.level]
|
||||
skills.combat.level = Math.max(...childLevels)
|
||||
}
|
||||
|
||||
// sword 升到 7
|
||||
skills.sword.level = 7
|
||||
updateParentSkill('sword')
|
||||
expect(skills.combat.level).toBe(7)
|
||||
})
|
||||
|
||||
test('父技能应该自动初始化', () => {
|
||||
const skills = {
|
||||
sword: { level: 3, unlocked: true },
|
||||
axe: { level: 0, unlocked: false }
|
||||
}
|
||||
|
||||
const initParentSkill = () => {
|
||||
if (!skills.combat) {
|
||||
skills.combat = { level: 0, unlocked: true }
|
||||
}
|
||||
}
|
||||
|
||||
initParentSkill()
|
||||
expect(skills.combat).toBeDefined()
|
||||
expect(skills.combat.unlocked).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('经验倍率计算', () => {
|
||||
test('应该正确计算基础经验倍率', () => {
|
||||
const globalBonus = 0.2 // 20%全局加成
|
||||
const milestoneBonus = 0.1 // 10%里程碑加成
|
||||
|
||||
const totalBonus = globalBonus + milestoneBonus
|
||||
|
||||
expect(totalBonus).toBeCloseTo(0.3, 1)
|
||||
})
|
||||
|
||||
test('父技能高于子技能时应该有1.5倍加成', () => {
|
||||
const parentLevel = 10
|
||||
const childLevel = 5
|
||||
|
||||
const hasParentBonus = parentLevel > childLevel
|
||||
const expMultiplier = hasParentBonus ? 1.5 : 1.0
|
||||
|
||||
expect(expMultiplier).toBe(1.5)
|
||||
})
|
||||
|
||||
test('应该正确应用多个加成', () => {
|
||||
const baseExp = 100
|
||||
const globalBonus = 1.2 // +20%
|
||||
const parentBonus = 1.5 // +50%
|
||||
const milestoneBonus = 1.1 // +10%
|
||||
|
||||
const finalExp = baseExp * globalBonus * parentBonus * milestoneBonus
|
||||
|
||||
expect(finalExp).toBeCloseTo(198, 0) // 100 * 1.2 * 1.5 * 1.1 = 198
|
||||
})
|
||||
})
|
||||
|
||||
describe('技能解锁条件', () => {
|
||||
test('应该正确检查物品条件', () => {
|
||||
const playerInventory = ['stick', 'sword']
|
||||
const requiredItem = 'stick'
|
||||
|
||||
const hasRequiredItem = (inventory, item) => {
|
||||
return inventory.includes(item)
|
||||
}
|
||||
|
||||
expect(hasRequiredItem(playerInventory, requiredItem)).toBe(true)
|
||||
expect(hasRequiredItem(playerInventory, 'axe')).toBe(false)
|
||||
})
|
||||
|
||||
test('应该正确检查位置条件', () => {
|
||||
const playerLocation = 'camp'
|
||||
const requiredLocation = 'camp'
|
||||
|
||||
const isInLocation = (current, required) => {
|
||||
return current === required
|
||||
}
|
||||
|
||||
expect(isInLocation(playerLocation, requiredLocation)).toBe(true)
|
||||
expect(isInLocation(playerLocation, 'wild1')).toBe(false)
|
||||
})
|
||||
|
||||
test('应该正确检查前置技能等级', () => {
|
||||
const skills = {
|
||||
basic: { level: 5, unlocked: true },
|
||||
advanced: { level: 0, unlocked: false }
|
||||
}
|
||||
|
||||
const checkPrerequisite = (skillId, requiredLevel) => {
|
||||
const skill = skills[skillId]
|
||||
return skill && skill.level >= requiredLevel
|
||||
}
|
||||
|
||||
expect(checkPrerequisite('basic', 5)).toBe(true)
|
||||
expect(checkPrerequisite('basic', 6)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('边界条件', () => {
|
||||
test('经验不能为负数', () => {
|
||||
const skill = { level: 1, exp: 50 }
|
||||
|
||||
const addExpSafe = (skill, amount) => {
|
||||
skill.exp = Math.max(0, skill.exp + amount)
|
||||
return skill.exp
|
||||
}
|
||||
|
||||
expect(addExpSafe(skill, -30)).toBe(20)
|
||||
expect(addExpSafe(skill, -100)).toBe(0)
|
||||
})
|
||||
|
||||
test('等级不能为负数', () => {
|
||||
const skill = { level: 1, exp: 0 }
|
||||
|
||||
const levelUpSafe = (skill) => {
|
||||
skill.level = Math.max(0, skill.level + 1)
|
||||
return skill.level
|
||||
}
|
||||
|
||||
expect(levelUpSafe(skill)).toBe(2)
|
||||
})
|
||||
|
||||
test('空技能对象应该有默认值', () => {
|
||||
const createSkill = () => ({
|
||||
level: 0,
|
||||
exp: 0,
|
||||
unlocked: false
|
||||
})
|
||||
|
||||
const skill = createSkill()
|
||||
expect(skill.level).toBe(0)
|
||||
expect(skill.exp).toBe(0)
|
||||
expect(skill.unlocked).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user