fix: 修复武器经验获取并完善义体系统
- 修复战斗胜利后未获得武器技能经验的问题 (initCombat未传递skillExpReward) - 每级武器技能提供5%武器伤害加成(已实现,无需修改) - 实现义体安装/卸载功能,支持NPC对话交互 - StatusPanel添加义体装备槽显示 - MapPanel修复NPC对话import问题 - 新增成就系统框架 - 添加项目文档CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,22 +143,27 @@ export function calculateEP(defender, stance = 'balance', environment = 'normal'
|
||||
*
|
||||
* @param {Number} ap - 攻击点数
|
||||
* @param {Number} ep - 闪避点数
|
||||
* @param {Boolean} isPlayer - 是否是玩家攻击(玩家享受最低命中率加成)
|
||||
* @returns {Number} 命中概率 (0-1)
|
||||
*/
|
||||
export function calculateHitRate(ap, ep) {
|
||||
export function calculateHitRate(ap, ep, isPlayer = false) {
|
||||
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
|
||||
let hitRate = 0.05
|
||||
if (ratio >= 5) hitRate = 0.98
|
||||
else if (ratio >= 3) hitRate = 0.90
|
||||
else if (ratio >= 2) hitRate = 0.80
|
||||
else if (ratio >= 1.5) hitRate = 0.65
|
||||
else if (ratio >= 1) hitRate = 0.50
|
||||
else if (ratio >= 0.75) hitRate = 0.38
|
||||
else if (ratio >= 0.5) hitRate = 0.24
|
||||
else if (ratio >= 0.33) hitRate = 0.15
|
||||
else if (ratio >= 0.2) hitRate = 0.08
|
||||
|
||||
// 最低命中率:敌人攻击玩家时至少40%命中率,玩家攻击敌人时至少15%
|
||||
const minHitRate = isPlayer ? 0.15 : 0.40
|
||||
return Math.max(minHitRate, hitRate)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,13 +207,13 @@ export function calculateCritMultiplier(attacker) {
|
||||
* @param {Object} defenderBonuses - 防御者加成
|
||||
* @returns {Object} { hit: boolean, damage: number, crit: boolean, evaded: boolean, shieldAbsorbed: number }
|
||||
*/
|
||||
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}) {
|
||||
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}, isPlayerAttacker = false) {
|
||||
// 计算AP和EP
|
||||
const ap = calculateAP(attacker, stance, 1, environment, attackerBonuses)
|
||||
const ep = calculateEP(defender, 'balance', environment, defenderBonuses)
|
||||
|
||||
// 计算命中概率
|
||||
const hitRate = calculateHitRate(ap, ep)
|
||||
// 计算命中概率(玩家攻击时isPlayerAttacker=true,敌人攻击时isPlayerAttacker=false)
|
||||
const hitRate = calculateHitRate(ap, ep, isPlayerAttacker)
|
||||
|
||||
// 第一步:闪避判定
|
||||
const roll = Math.random()
|
||||
@@ -391,7 +396,8 @@ export function combatTick(gameStore, playerStore, combatState) {
|
||||
stance,
|
||||
environment,
|
||||
playerBonuses,
|
||||
{}
|
||||
{},
|
||||
true // 玩家攻击
|
||||
)
|
||||
|
||||
if (playerAttack.hit) {
|
||||
@@ -441,7 +447,8 @@ export function combatTick(gameStore, playerStore, combatState) {
|
||||
'balance',
|
||||
environment,
|
||||
{},
|
||||
playerBonuses
|
||||
playerBonuses,
|
||||
false // 敌人攻击
|
||||
)
|
||||
|
||||
if (enemyAttack.hit) {
|
||||
@@ -516,9 +523,10 @@ export function getStaminaCost(stance) {
|
||||
* @param {String} enemyId - 敌人ID
|
||||
* @param {Object} enemyConfig - 敌人配置
|
||||
* @param {String} environment - 环境类型
|
||||
* @param {String} preferredStance - 玩家偏好的战斗姿态
|
||||
* @returns {Object} 战斗状态
|
||||
*/
|
||||
export function initCombat(enemyId, enemyConfig, environment = 'normal') {
|
||||
export function initCombat(enemyId, enemyConfig, environment = 'normal', preferredStance = 'balance') {
|
||||
return {
|
||||
enemyId,
|
||||
enemy: {
|
||||
@@ -530,9 +538,10 @@ export function initCombat(enemyId, enemyConfig, environment = 'normal') {
|
||||
defense: enemyConfig.baseStats.defense,
|
||||
baseStats: enemyConfig.baseStats,
|
||||
expReward: enemyConfig.expReward,
|
||||
skillExpReward: enemyConfig.skillExpReward || 0,
|
||||
drops: enemyConfig.drops || []
|
||||
},
|
||||
stance: 'balance',
|
||||
stance: preferredStance, // 使用玩家偏好的战斗姿态
|
||||
environment,
|
||||
startTime: Date.now(),
|
||||
ticks: 0
|
||||
|
||||
@@ -8,9 +8,11 @@ import { EVENT_CONFIG } from '@/config/events.js'
|
||||
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||
import { ENEMY_CONFIG } from '@/config/enemies.js'
|
||||
import { ITEM_CONFIG } from '@/config/items.js'
|
||||
import { unlockSkill } from './skillSystem.js'
|
||||
import { addItemToInventory } from './itemSystem.js'
|
||||
import { initCombat, getEnvironmentType } from './combatSystem.js'
|
||||
import { installProsthetic, removeProsthetic } from './prostheticSystem.js'
|
||||
|
||||
/**
|
||||
* 检查并触发事件
|
||||
@@ -198,8 +200,9 @@ function renderTemplate(template, context) {
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
* @param {String} npcId - NPC ID
|
||||
* @param {String} dialogueKey - 对话键名
|
||||
* @param {Object} playerStore - 玩家Store(用于动态选项生成)
|
||||
*/
|
||||
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
||||
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first', playerStore = null) {
|
||||
const npc = NPC_CONFIG[npcId]
|
||||
if (!npc) {
|
||||
console.warn(`NPC ${npcId} not found`)
|
||||
@@ -212,14 +215,27 @@ export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理模板变量
|
||||
let text = dialogue.text
|
||||
if (playerStore) {
|
||||
text = renderDialogueTemplate(text, playerStore, dialogue)
|
||||
}
|
||||
|
||||
// 生成选项列表(包括动态选项)
|
||||
let choices = dialogue.choices || []
|
||||
if (dialogue.dynamicChoices && playerStore) {
|
||||
const dynamicChoices = generateDynamicChoices(dialogue.dynamicChoices, playerStore)
|
||||
choices = [...dynamicChoices, ...choices]
|
||||
}
|
||||
|
||||
// 构建事件数据
|
||||
const eventData = {
|
||||
id: `npc_${npcId}_${dialogueKey}`,
|
||||
type: 'dialogue',
|
||||
title: npc.name,
|
||||
text: dialogue.text,
|
||||
text: text,
|
||||
npc: npc,
|
||||
choices: dialogue.choices || [],
|
||||
choices: choices,
|
||||
currentDialogue: dialogueKey,
|
||||
npcId
|
||||
}
|
||||
@@ -228,6 +244,61 @@ export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
|
||||
gameStore.drawerState.event = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染对话模板(替换变量)
|
||||
*/
|
||||
function renderDialogueTemplate(template, playerStore, dialogue) {
|
||||
let text = template
|
||||
|
||||
// 替换义体列表
|
||||
if (text.includes('{prosthetic_list}')) {
|
||||
const prostheticList = getProstheticInventoryList(playerStore)
|
||||
text = text.replace('{prosthetic_list}', prostheticList)
|
||||
}
|
||||
|
||||
// 替换已装备的义体
|
||||
if (text.includes('{equipped_prosthetic}')) {
|
||||
const equipped = playerStore.equipment?.prosthetic
|
||||
const equippedText = equipped ? `${equipped.name} (${equipped.description})` : '无'
|
||||
text = text.replace('{equipped_prosthetic}', equippedText)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家背包中的义体列表
|
||||
*/
|
||||
function getProstheticInventoryList(playerStore) {
|
||||
const prosthetics = playerStore.inventory.filter(item => item.type === 'prosthetic')
|
||||
if (prosthetics.length === 0) {
|
||||
return '(你没有义体可以安装)'
|
||||
}
|
||||
return prosthetics.map(p => `• ${p.name}: ${p.description}`).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成动态选项
|
||||
*/
|
||||
function generateDynamicChoices(type, playerStore) {
|
||||
switch (type) {
|
||||
case 'prosthetic_inventory':
|
||||
// 生成义体安装选项
|
||||
const prosthetics = playerStore.inventory.filter(item => item.type === 'prosthetic')
|
||||
if (prosthetics.length === 0) {
|
||||
return []
|
||||
}
|
||||
return prosthetics.map(p => ({
|
||||
text: `安装 ${p.name}`,
|
||||
action: 'install_prosthetic',
|
||||
actionData: { itemId: p.id }
|
||||
}))
|
||||
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理对话选择
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
@@ -259,7 +330,7 @@ export function handleDialogueChoice(gameStore, playerStore, choice) {
|
||||
// 继续对话
|
||||
const currentEvent = gameStore.currentEvent
|
||||
if (currentEvent && currentEvent.npc) {
|
||||
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next)
|
||||
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next, playerStore)
|
||||
result.closeEvent = false
|
||||
}
|
||||
} else if (!choice.action) {
|
||||
@@ -418,6 +489,60 @@ export function processDialogueAction(gameStore, playerStore, action, actionData
|
||||
// 直接关闭
|
||||
return { success: true, message: '', closeEvent: true }
|
||||
|
||||
case 'install_prosthetic':
|
||||
// 安装义体
|
||||
if (actionData.itemId) {
|
||||
const item = playerStore.inventory.find(i => i.id === actionData.itemId)
|
||||
if (!item) {
|
||||
return { success: false, message: '找不到该义体', closeEvent: false }
|
||||
}
|
||||
const installResult = installProsthetic(playerStore, gameStore, item)
|
||||
if (installResult.success) {
|
||||
// 从背包移除义体
|
||||
const index = playerStore.inventory.findIndex(i => i.id === actionData.itemId)
|
||||
if (index > -1) {
|
||||
playerStore.inventory.splice(index, 1)
|
||||
}
|
||||
// 解锁义体技能
|
||||
if (item.grantedSkill) {
|
||||
unlockSkill(playerStore, item.grantedSkill)
|
||||
if (gameStore.addLog) {
|
||||
gameStore.addLog(`解锁了技能: ${SKILL_CONFIG[item.grantedSkill]?.name || item.grantedSkill}`, 'system')
|
||||
}
|
||||
}
|
||||
// 更新成就进度
|
||||
gameStore.achievementProgress.prostheticEquipped = 1
|
||||
gameStore.checkAchievements('equip_prosthetic', { prostheticCount: 1 })
|
||||
// 重新显示对话框
|
||||
const currentEvent = gameStore.currentEvent
|
||||
if (currentEvent && currentEvent.npc) {
|
||||
startNPCDialogue(gameStore, currentEvent.npc.id, 'first', playerStore)
|
||||
}
|
||||
}
|
||||
return { success: installResult.success, message: installResult.message, closeEvent: false }
|
||||
}
|
||||
return { success: false, message: '义体参数错误', closeEvent: false }
|
||||
|
||||
case 'remove_prosthetic':
|
||||
// 卸下义体
|
||||
const removeResult = removeProsthetic(playerStore, gameStore)
|
||||
if (removeResult.success) {
|
||||
// 重新显示对话框
|
||||
const currentEvent = gameStore.currentEvent
|
||||
if (currentEvent && currentEvent.npc) {
|
||||
startNPCDialogue(gameStore, currentEvent.npc.id, 'first', playerStore)
|
||||
}
|
||||
}
|
||||
return { success: removeResult.success, message: removeResult.message, closeEvent: false }
|
||||
|
||||
case 'refresh_list':
|
||||
// 刷新列表(重新显示当前对话框)
|
||||
const evt = gameStore.currentEvent
|
||||
if (evt && evt.npc && evt.currentDialogue) {
|
||||
startNPCDialogue(gameStore, evt.npc.id, evt.currentDialogue, playerStore)
|
||||
}
|
||||
return { success: true, message: '', closeEvent: false }
|
||||
|
||||
default:
|
||||
console.warn(`Unknown dialogue action: ${action}`)
|
||||
return { success: false, message: '未知动作', closeEvent: false }
|
||||
|
||||
142
utils/prostheticSystem.js
Normal file
142
utils/prostheticSystem.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 义体系统 - 安装、卸下、义体适应性
|
||||
*/
|
||||
|
||||
/**
|
||||
* 安装义体
|
||||
* @param {Object} playerStore - 玩家Store
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
* @param {Object} prostheticItem - 义体物品
|
||||
* @returns {Object} { success: boolean, message: string }
|
||||
*/
|
||||
export function installProsthetic(playerStore, gameStore, prostheticItem) {
|
||||
// 检查是否已有义体
|
||||
if (playerStore.equipment.prosthetic) {
|
||||
return {
|
||||
success: false,
|
||||
message: '你已经装备了义体,需要先卸下当前的义体。'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查义体类型
|
||||
if (prostheticItem.type !== 'prosthetic') {
|
||||
return {
|
||||
success: false,
|
||||
message: '这不是义体装备。'
|
||||
}
|
||||
}
|
||||
|
||||
// 装备义体
|
||||
playerStore.equipment.prosthetic = { ...prostheticItem }
|
||||
|
||||
// 解锁义体适应性技能
|
||||
if (!playerStore.skills['prosthetic_adaptation']) {
|
||||
playerStore.skills['prosthetic_adaptation'] = { level: 0, exp: 0 }
|
||||
}
|
||||
|
||||
gameStore.addLog(`安装了 ${prostheticItem.name}`, 'reward')
|
||||
gameStore.addLog(`获得了被动技能: 义体适应性`, 'info')
|
||||
|
||||
// 检查成就
|
||||
gameStore.checkAchievements('equip_prosthetic', { prostheticCount: 1 })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `成功安装 ${prostheticItem.name}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸下义体
|
||||
* @param {Object} playerStore - 玩家Store
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
* @returns {Object} { success: boolean, message: string, removedItem: Object }
|
||||
*/
|
||||
export function removeProsthetic(playerStore, gameStore) {
|
||||
const currentProsthetic = playerStore.equipment.prosthetic
|
||||
|
||||
if (!currentProsthetic) {
|
||||
return {
|
||||
success: false,
|
||||
message: '你没有装备任何义体。'
|
||||
}
|
||||
}
|
||||
|
||||
// 将义体放回背包
|
||||
playerStore.inventory.push({ ...currentProsthetic })
|
||||
|
||||
// 卸下义体
|
||||
playerStore.equipment.prosthetic = null
|
||||
|
||||
gameStore.addLog(`卸下了 ${currentProsthetic.name}`, 'info')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `成功卸下 ${currentProsthetic.name}`,
|
||||
removedItem: currentProsthetic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取义体提供的属性加成
|
||||
* @param {Object} prosthetic - 义体装备
|
||||
* @returns {Object} 属性加成
|
||||
*/
|
||||
export function getProstheticBonus(prosthetic) {
|
||||
if (!prosthetic) return null
|
||||
|
||||
return {
|
||||
stats: prosthetic.stats || {},
|
||||
skill: prosthetic.skill || null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查义体技能是否可用
|
||||
* @param {Object} playerStore - 玩家Store
|
||||
* @param {String} skillId - 技能ID
|
||||
* @returns {Boolean} 是否可用
|
||||
*/
|
||||
export function isProstheticSkillAvailable(playerStore, skillId) {
|
||||
const prosthetic = playerStore.equipment.prosthetic
|
||||
if (!prosthetic) return false
|
||||
|
||||
// 检查义体是否提供该技能
|
||||
return prosthetic.skill === skillId || prosthetic.grantedSkills?.includes(skillId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加义体适应性经验
|
||||
* @param {Object} playerStore - 玩家Store
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
* @param {Number} exp - 经验值
|
||||
*/
|
||||
export function addProstheticExp(playerStore, gameStore, exp) {
|
||||
const skill = playerStore.skills['prosthetic_adaptation']
|
||||
if (!skill) return
|
||||
|
||||
const { SKILL_CONFIG } = require('@/config/skills.js')
|
||||
const skillConfig = SKILL_CONFIG['prosthetic_adaptation']
|
||||
const maxLevel = skillConfig.maxLevel
|
||||
|
||||
if (skill.level >= maxLevel) return
|
||||
|
||||
skill.exp += exp
|
||||
const expNeeded = skillConfig.expPerLevel(skill.level)
|
||||
|
||||
while (skill.exp >= expNeeded && skill.level < maxLevel) {
|
||||
skill.exp -= expNeeded
|
||||
skill.level++
|
||||
gameStore.addLog(`义体适应性提升到 Lv.${skill.level}!`, 'reward')
|
||||
|
||||
// 检查里程碑奖励
|
||||
if (skillConfig.milestones[skill.level]) {
|
||||
const milestone = skillConfig.milestones[skill.level]
|
||||
gameStore.addLog(`解锁效果: ${milestone.desc}`, 'info')
|
||||
}
|
||||
|
||||
if (skill.level < maxLevel) {
|
||||
expNeeded = skillConfig.expPerLevel(skill.level)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user