531 lines
18 KiB
JavaScript
531 lines
18 KiB
JavaScript
|
|
/**
|
|||
|
|
* 数值模型测试
|
|||
|
|
* 验证数学模型文档中的各种计算
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 导入公式模块 - 使用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)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
})
|