Files
text-adventure-game/utils/itemSystem.js
Claude 5d4371ba1f feat: 完善数学模型和品质系统
- 修复品质等级范围重叠问题(优秀/稀有无重叠)
- 统一 formulas.js 与 constants.js 的品质判定
- 修复经验公式与游戏实际逻辑不一致
- 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10%
- 添加数学模型文档和README
- 添加数值验证脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:54:20 +08:00

547 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 物品系统 - 使用/装备/品质计算
* Phase 6 核心系统实现
*/
import { ITEM_CONFIG } from '@/config/items.js'
import { GAME_CONSTANTS } from '@/config/constants.js'
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, itemLevel = 1) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return null
}
const qualityLevel = getQualityLevel(quality)
// 品质倍率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,
name: config.name,
type: config.type,
subtype: config.subtype,
description: config.description,
icon: config.icon,
quality: quality,
itemLevel: itemLevel,
qualityLevel: qualityLevel.level,
qualityName: qualityLevel.name,
qualityColor: qualityLevel.color
}
// 应用品质倍率到基础属性
if (config.baseDamage) {
stats.baseDamage = config.baseDamage
stats.finalDamage = Math.max(1, Math.floor(config.baseDamage * totalMultiplier))
}
if (config.baseDefense) {
stats.baseDefense = config.baseDefense
stats.finalDefense = Math.max(1, Math.floor(config.baseDefense * totalMultiplier))
}
if (config.baseShield) {
stats.baseShield = config.baseShield
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 * totalMultiplier)
// 消耗品效果
if (config.effect) {
stats.effect = { ...config.effect }
}
// 书籍信息
if (config.type === 'book') {
stats.readingTime = config.readingTime
stats.expReward = { ...config.expReward }
stats.completionBonus = config.completionBonus
}
// 解锁技能
if (config.unlockSkill) {
stats.unlockSkill = config.unlockSkill
}
// 额外属性(来自物品配置)
if (config.stats) {
stats.stats = { ...config.stats }
}
// 堆叠信息
if (config.stackable !== undefined) {
stats.stackable = config.stackable
stats.maxStack = config.maxStack || 99
}
return stats
}
/**
* 获取品质等级
* @param {Number} quality - 品质值
* @returns {Object} 品质等级信息
*/
export function getQualityLevel(quality) {
// 确保品质在有效范围内
const clampedQuality = Math.max(0, Math.min(250, quality))
for (const [level, data] of Object.entries(GAME_CONSTANTS.QUALITY_LEVELS)) {
if (clampedQuality >= data.range[0] && clampedQuality <= data.range[1]) {
return {
level: parseInt(level),
...data
}
}
}
// 默认返回垃圾品质
return GAME_CONSTANTS.QUALITY_LEVELS[1]
}
/**
* 生成随机品质
* @param {Number} luck - 运气值 (0-100),影响高品质概率
* @returns {Number} 品质值
*/
export function generateRandomQuality(luck = 0) {
// 单次随机判定
const roll = Math.random()
// 运气加成每1点运气增加对应品质概率的 0.1%
const luckBonus = luck * 0.001
// 传说品质0.1% + 运气加成
if (roll < 0.001 + luckBonus) {
return 200 + Math.floor(Math.random() * 51) // 200-250
}
// 史诗品质1% + 运气加成
if (roll < 0.01 + luckBonus * 2) {
return 170 + Math.floor(Math.random() * 30) // 170-199
}
// 稀有品质5% + 运气加成
if (roll < 0.05 + luckBonus * 3) {
return 140 + Math.floor(Math.random() * 30) // 140-169
}
// 优秀品质15% + 运气加成
if (roll < 0.15 + luckBonus * 4) {
return 100 + Math.floor(Math.random() * 40) // 100-139
}
// 普通品质:剩余大部分
if (roll < 0.85) {
return 50 + Math.floor(Math.random() * 50) // 50-99
}
// 垃圾品质15%
return Math.floor(Math.random() * 50) // 0-49
}
/**
* 使用物品
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {String} itemId - 物品ID
* @param {Number} count - 使用数量
* @returns {Object} { success: boolean, message: string, effects: Object }
*/
export function useItem(playerStore, gameStore, itemId, count = 1) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return { success: false, message: '物品不存在' }
}
// 查找背包中的物品
const itemIndex = playerStore.inventory.findIndex(i => i.id === itemId)
if (itemIndex === -1) {
return { success: false, message: '背包中没有该物品' }
}
const inventoryItem = playerStore.inventory[itemIndex]
// 检查数量
if (inventoryItem.count < count) {
return { success: false, message: '物品数量不足' }
}
// 根据物品类型处理
if (config.type === 'consumable') {
return useConsumable(playerStore, gameStore, config, inventoryItem, count, itemIndex)
}
if (config.type === 'book') {
return useBook(playerStore, gameStore, config, inventoryItem)
}
return { success: false, message: '该物品无法使用' }
}
/**
* 使用消耗品
*/
function useConsumable(playerStore, gameStore, config, inventoryItem, count, itemIndex) {
const effects = {}
const appliedEffects = []
// 应用效果
if (config.effect) {
if (config.effect.health) {
const oldHealth = playerStore.currentStats.health
playerStore.currentStats.health = Math.min(
playerStore.currentStats.maxHealth,
playerStore.currentStats.health + config.effect.health * count
)
effects.health = playerStore.currentStats.health - oldHealth
if (effects.health > 0) appliedEffects.push(`恢复${effects.health}生命`)
}
if (config.effect.stamina) {
const oldStamina = playerStore.currentStats.stamina
playerStore.currentStats.stamina = Math.min(
playerStore.currentStats.maxStamina,
playerStore.currentStats.stamina + config.effect.stamina * count
)
effects.stamina = playerStore.currentStats.stamina - oldStamina
if (effects.stamina > 0) appliedEffects.push(`恢复${effects.stamina}耐力`)
}
if (config.effect.sanity) {
const oldSanity = playerStore.currentStats.sanity
playerStore.currentStats.sanity = Math.min(
playerStore.currentStats.maxSanity,
playerStore.currentStats.sanity + config.effect.sanity * count
)
effects.sanity = playerStore.currentStats.sanity - oldSanity
if (effects.sanity > 0) appliedEffects.push(`恢复${effects.sanity}精神`)
}
}
// 消耗物品
inventoryItem.count -= count
if (inventoryItem.count <= 0) {
playerStore.inventory.splice(itemIndex, 1)
}
// 记录日志
const effectText = appliedEffects.length > 0 ? appliedEffects.join('、') : '使用'
if (gameStore.addLog) {
gameStore.addLog(`使用了 ${config.name} x${count}${effectText}`, 'info')
}
return {
success: true,
message: effectText,
effects
}
}
/**
* 使用书籍(阅读)
*/
function useBook(playerStore, gameStore, config, inventoryItem) {
// 书籍不消耗,返回阅读任务信息
return {
success: true,
message: '开始阅读',
readingTask: {
itemId: config.id,
bookName: config.name,
readingTime: config.readingTime,
expReward: config.expReward,
completionBonus: config.completionBonus
}
}
}
/**
* 装备物品
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {String} uniqueId - 物品唯一ID背包中的物品
* @returns {Object} { success: boolean, message: string }
*/
export function equipItem(playerStore, gameStore, uniqueId) {
const inventoryItem = playerStore.inventory.find(i => i.uniqueId === uniqueId)
if (!inventoryItem) {
return { success: false, message: '物品不存在' }
}
const config = ITEM_CONFIG[inventoryItem.id]
if (!config) {
return { success: false, message: '物品配置不存在' }
}
// 确定装备槽位
let slot = config.type
if (config.subtype === 'one_handed' || config.subtype === 'two_handed') {
slot = 'weapon'
} else if (config.type === 'armor') {
slot = 'armor'
} else if (config.type === 'shield') {
slot = 'shield'
} else if (config.type === 'accessory') {
slot = 'accessory'
}
if (!slot || !playerStore.equipment.hasOwnProperty(slot)) {
return { success: false, message: '无法装备该物品' }
}
// 如果已有装备,先卸下
const currentlyEquipped = playerStore.equipment[slot]
if (currentlyEquipped) {
unequipItemBySlot(playerStore, gameStore, slot)
}
// 装备新物品
playerStore.equipment[slot] = inventoryItem
inventoryItem.equipped = true
// 应用装备属性
applyEquipmentStats(playerStore, slot, inventoryItem)
// 记录日志
if (gameStore.addLog) {
gameStore.addLog(`装备了 ${inventoryItem.name}`, 'info')
}
return { success: true, message: '装备成功' }
}
/**
* 卸下装备
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {String} slot - 装备槽位
* @returns {Object} { success: boolean, message: string }
*/
export function unequipItemBySlot(playerStore, gameStore, slot) {
const equipped = playerStore.equipment[slot]
if (!equipped) {
return { success: false, message: '该槽位没有装备' }
}
// 移除装备属性
removeEquipmentStats(playerStore, slot)
// 标记为未装备
equipped.equipped = false
// 清空槽位
playerStore.equipment[slot] = null
// 记录日志
if (gameStore.addLog) {
gameStore.addLog(`卸下了 ${equipped.name}`, 'info')
}
return { success: true, message: '卸下成功' }
}
/**
* 应用装备属性到玩家
*/
function applyEquipmentStats(playerStore, slot, item) {
if (!playerStore.equipmentStats) {
playerStore.equipmentStats = {
attack: 0,
defense: 0,
shield: 0,
critRate: 0,
attackSpeed: 1
}
}
if (item.finalDamage) {
playerStore.equipmentStats.attack += item.finalDamage
}
if (item.finalDefense) {
playerStore.equipmentStats.defense += item.finalDefense
}
if (item.finalShield) {
playerStore.equipmentStats.shield += item.finalShield
}
if (item.attackSpeed) {
playerStore.equipmentStats.attackSpeed *= item.attackSpeed
}
}
/**
* 移除装备属性
*/
function removeEquipmentStats(playerStore, slot) {
const equipped = playerStore.equipment[slot]
if (!equipped || !playerStore.equipmentStats) {
return
}
if (equipped.finalDamage) {
playerStore.equipmentStats.attack -= equipped.finalDamage
}
if (equipped.finalDefense) {
playerStore.equipmentStats.defense -= equipped.finalDefense
}
if (equipped.finalShield) {
playerStore.equipmentStats.shield -= equipped.finalShield
}
}
/**
* 添加物品到背包
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @param {Number} count - 数量
* @param {Number} quality - 品质值装备用0-250
* @returns {Object} { success: boolean, item: Object }
*/
export function addItemToInventory(playerStore, itemId, count = 1, quality = null) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return { success: false, message: '物品不存在' }
}
// 素材和关键道具不需要品质,使用固定值
// 装备类物品才需要随机品质
const needsQuality = config.type === 'weapon' || config.type === 'armor' ||
config.type === 'shield' || config.type === 'accessory'
const itemQuality = quality !== null ? quality
: (needsQuality ? generateRandomQuality(playerStore.baseStats?.intuition || 0) : 100)
// 计算物品属性(包含品质等级)
const itemStats = calculateItemStats(itemId, itemQuality)
// 素材类物品 - 只按ID堆叠
if (config.stackable) {
const existingItem = playerStore.inventory.find(i => i.id === itemId)
if (existingItem) {
existingItem.count += count
return { success: true, item: existingItem }
}
}
// 装备类物品 - 不堆叠每个装备都有独立的唯一ID
// 这样可以正确跟踪每个装备的装备状态
if (needsQuality) {
// 检查是否有完全相同品质的装备(用于显示堆叠数量,但各自独立)
// 注意:装备不再真正堆叠,每个都是独立实例
// 生成唯一IDitemId + 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 }
}
// 创建新物品(非装备类)
const uniqueId = `${itemId}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
const newItem = {
...itemStats,
uniqueId,
count,
equipped: false,
obtainedAt: Date.now()
}
playerStore.inventory.push(newItem)
return { success: true, item: newItem }
}
/**
* 从背包移除物品
* @param {Object} playerStore - 玩家Store
* @param {String} uniqueId - 物品唯一ID
* @param {Number} count - 移除数量
* @returns {Object} { success: boolean, message: string }
*/
export function removeItemFromInventory(playerStore, uniqueId, count = 1) {
const itemIndex = playerStore.inventory.findIndex(i => i.uniqueId === uniqueId)
if (itemIndex === -1) {
return { success: false, message: '物品不存在' }
}
const item = playerStore.inventory[itemIndex]
if (item.count < count) {
return { success: false, message: '物品数量不足' }
}
item.count -= count
if (item.count <= 0) {
// 如果已装备,先卸下
if (item.equipped) {
for (const [slot, equipped] of Object.entries(playerStore.equipment)) {
if (equipped && equipped.uniqueId === uniqueId) {
playerStore.equipment[slot] = null
break
}
}
}
playerStore.inventory.splice(itemIndex, 1)
}
return { success: true, message: '移除成功' }
}
/**
* 检查是否拥有物品
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @returns {Boolean}
*/
export function hasItem(playerStore, itemId) {
return playerStore.inventory.some(i => i.id === itemId && i.count > 0)
}
/**
* 获取物品数量
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @returns {Number}
*/
export function getItemCount(playerStore, itemId) {
const item = playerStore.inventory.find(i => i.id === itemId)
return item ? item.count : 0
}
// Note: checkSkillUnlock, calculateSellPrice, calculateBuyPrice,
// getQualityColor, getQualityName are now test-only utilities
// and are not exported for production use