feat: 实现游戏核心系统和UI组件

核心系统:
- combatSystem: 战斗逻辑、伤害计算、战斗状态管理
- skillSystem: 技能系统、技能解锁、经验值、里程碑
- taskSystem: 任务系统、任务类型、任务执行和完成
- eventSystem: 事件系统、随机事件处理
- environmentSystem: 环境系统、时间流逝、区域效果
- levelingSystem: 升级系统、属性成长
- soundSystem: 音效系统

配置文件:
- enemies: 敌人配置、掉落表
- events: 事件配置、事件效果
- items: 物品配置、装备属性
- locations: 地点配置、探索事件
- skills: 技能配置、技能树

UI组件:
- CraftingDrawer: 制造界面
- InventoryDrawer: 背包界面
- 其他UI优化和动画

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-23 16:20:10 +08:00
parent 021f6a54f5
commit 16223c89a5
25 changed files with 2731 additions and 318 deletions

View File

@@ -539,23 +539,6 @@ export function initCombat(enemyId, enemyConfig, environment = 'normal') {
}
}
/**
* 计算姿态切换后的攻击速度修正
* @param {String} stance - 姿态
* @param {Number} baseSpeed - 基础攻击速度
* @returns {Number} 修正后的攻击速度
*/
export function getAttackSpeed(stance, baseSpeed = 1.0) {
const speedModifiers = {
attack: 1.3, // 攻击姿态: 速度 +30%
defense: 0.9, // 防御姿态: 速度 -10%
balance: 1.0,
rapid: 2.0, // 快速打击: 速度 +100%
heavy: 0.5 // 重击: 速度 -50%
}
return baseSpeed * (speedModifiers[stance] || 1.0)
}
/**
* 获取姿态显示名称
* @param {String} stance - 姿态
@@ -572,6 +555,23 @@ export function getStanceDisplayName(stance) {
return names[stance] || '平衡'
}
/**
* 获取攻击速度修正
* @param {String} stance - 姿态
* @param {Number} baseSpeed - 基础攻击速度
* @returns {Number} 修正后的攻击速度
*/
export function getAttackSpeed(stance, baseSpeed = 1.0) {
const speedModifiers = {
attack: 1.3, // 攻击姿态: 速度 +30%
defense: 0.9, // 防御姿态: 速度 -10%
balance: 1.0,
rapid: 2.0, // 快速打击: 速度 +100%
heavy: 0.5 // 重击: 速度 -50%
}
return baseSpeed * (speedModifiers[stance] || 1.0)
}
/**
* 获取环境类型
* @param {String} locationId - 位置ID

View File

@@ -5,6 +5,7 @@
import { LOCATION_CONFIG } from '@/config/locations.js'
import { SKILL_CONFIG } from '@/config/skills.js'
import { applyMilestoneBonus } from './skillSystem.js'
/**
* 环境类型枚举
@@ -198,7 +199,10 @@ export function processEnvironmentExp(gameStore, playerStore, locationId, deltaT
}
// 检查里程碑奖励(需要调用技能系统)
// TODO: 调用 applyMilestoneBonus
const newLevel = playerStore.skills[adaptSkillId].level
if (SKILL_CONFIG[adaptSkillId].milestones && SKILL_CONFIG[adaptSkillId].milestones[newLevel]) {
applyMilestoneBonus(playerStore, adaptSkillId, newLevel)
}
}
return { [adaptSkillId]: finalExp }

View File

@@ -6,8 +6,11 @@
import { NPC_CONFIG } from '@/config/npcs.js'
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 { unlockSkill } from './skillSystem.js'
import { addItemToInventory } from './itemSystem.js'
import { initCombat, getEnvironmentType } from './combatSystem.js'
/**
* 检查并触发事件
@@ -401,11 +404,16 @@ export function processDialogueAction(gameStore, playerStore, action, actionData
// 开始战斗
if (actionData.enemyId) {
gameStore.drawerState.event = false
// TODO: 启动战斗
// 战斗已在探索事件中初始化,这里只是关闭事件抽屉
return { success: true, message: '开始战斗', closeEvent: true }
}
return { success: false, message: '敌人参数错误', closeEvent: false }
case 'flee':
// 逃跑
gameStore.drawerState.event = false
return tryFlee(gameStore, playerStore)
case 'close':
// 直接关闭
return { success: true, message: '', closeEvent: true }
@@ -547,3 +555,108 @@ export function checkScheduledEvents(gameStore, playerStore) {
export function clearScheduledEvents(gameStore) {
gameStore.scheduledEvents = []
}
/**
* 触发探索事件
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {Object} 探索结果
*/
export function triggerExploreEvent(gameStore, playerStore) {
const location = LOCATION_CONFIG[playerStore.currentLocation]
if (!location) {
return { success: false, message: '当前位置错误' }
}
// 检查当前位置是否有敌人配置
const availableEnemies = location.enemies || []
if (availableEnemies.length === 0) {
return { success: false, message: '这里没什么可探索的' }
}
// 随机决定探索结果
const roll = Math.random()
// 40% 没发现
if (roll < 0.4) {
triggerEvent(gameStore, 'explore_nothing', {})
return { success: true, type: 'nothing' }
}
// 25% 发现草药
if (roll < 0.65) {
const herbCount = Math.floor(Math.random() * 3) + 1
const result = addItemToInventory(playerStore, 'healing_herb', herbCount, 100)
if (result.success) {
triggerEvent(gameStore, 'explore_find_herb', {})
return { success: true, type: 'find', item: 'healing_herb', count: herbCount }
}
}
// 20% 发现铜币 (1-5枚)
if (roll < 0.85) {
const coinAmount = Math.floor(Math.random() * 5) + 1
playerStore.currency.copper += coinAmount
if (gameStore.addLog) {
gameStore.addLog(`探索发现了 ${coinAmount} 铜币`, 'reward')
}
triggerEvent(gameStore, 'explore_find_coin', { amount: coinAmount })
return { success: true, type: 'coin', amount: coinAmount }
}
// 15% 遭遇敌人
const enemyId = availableEnemies[Math.floor(Math.random() * availableEnemies.length)]
const enemyConfig = ENEMY_CONFIG[enemyId]
if (enemyConfig) {
const environment = getEnvironmentType(playerStore.currentLocation)
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
gameStore.inCombat = true
triggerEvent(gameStore, 'explore_encounter', {
enemyName: enemyConfig.name,
enemyId
})
return { success: true, type: 'combat', enemyId }
}
// 默认:没发现
triggerEvent(gameStore, 'explore_nothing', {})
return { success: true, type: 'nothing' }
}
/**
* 尝试逃跑
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {Object} 逃跑结果
*/
export function tryFlee(gameStore, playerStore) {
// 基础逃跑成功率50%
const baseFleeChance = 0.5
// 敏捷影响逃跑率
const agilityBonus = (playerStore.baseStats?.dexterity || 0) * 0.01
// 装备加成
const equipmentBonus = (playerStore.globalBonus?.fleeRate || 0)
const fleeChance = Math.min(0.9, baseFleeChance + agilityBonus + equipmentBonus)
if (Math.random() < fleeChance) {
// 逃跑成功
gameStore.inCombat = false
gameStore.combatState = null
if (gameStore.addLog) {
gameStore.addLog('成功逃跑了!', 'system')
}
return { success: true, message: '逃跑成功' }
} else {
// 逃跑失败,敌人攻击
if (gameStore.addLog) {
gameStore.addLog('逃跑失败!', 'combat')
}
return { success: false, message: '逃跑失败' }
}
}

View File

@@ -13,6 +13,7 @@ import { processEnvironmentExp, processEnvironmentEffects } from './environmentS
import { checkScheduledEvents } from './eventSystem.js'
import { addSkillExp } from './skillSystem.js'
import { addItemToInventory } from './itemSystem.js'
import { addExp, calculateCombatExp } from './levelingSystem.js'
// 游戏循环定时器
let gameLoopInterval = null
@@ -196,24 +197,14 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
// 添加日志
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
// 给予经验值
// 给予经验值 (使用新的等级系统)
if (enemy.expReward) {
playerStore.level.exp += enemy.expReward
// 检查升级
const maxExp = playerStore.level.maxExp
if (playerStore.level.exp >= maxExp) {
playerStore.level.current++
playerStore.level.exp -= maxExp
playerStore.level.maxExp = Math.floor(playerStore.level.maxExp * 1.5)
gameStore.addLog(`升级了! 等级: ${playerStore.level.current}`, 'reward')
// 升级恢复状态
playerStore.currentStats.health = playerStore.currentStats.maxHealth
playerStore.currentStats.stamina = playerStore.currentStats.maxStamina
playerStore.currentStats.sanity = playerStore.currentStats.maxSanity
}
const adjustedExp = calculateCombatExp(
enemy.level || 1,
playerStore.level.current,
enemy.expReward
)
addExp(playerStore, gameStore, adjustedExp)
}
// 给予武器技能经验奖励
@@ -374,6 +365,24 @@ function processDrops(gameStore, playerStore, drops) {
* @param {Object} rewards - 奖励
*/
function handleTaskCompletion(gameStore, playerStore, task, rewards) {
// 处理制造任务
if (task.type === 'crafting') {
if (rewards.success !== false && rewards.craftedItem) {
// 制造成功,添加物品到背包
const { completeCrafting } = require('./craftingSystem.js')
const result = completeCrafting(gameStore, playerStore, task, rewards)
if (result.success) {
gameStore.addLog(result.message, 'reward')
} else if (result.failed) {
gameStore.addLog(result.message, 'warning')
}
} else if (rewards.success === false) {
// 制造失败
gameStore.addLog('制造失败,材料已消耗', 'warning')
}
return
}
// 应用货币奖励
if (rewards.currency) {
playerStore.currency.copper += rewards.currency
@@ -381,7 +390,7 @@ function handleTaskCompletion(gameStore, playerStore, task, rewards) {
// 应用技能经验
for (const [skillId, exp] of Object.entries(rewards)) {
if (skillId !== 'currency' && skillId !== 'completionBonus' && typeof exp === 'number') {
if (skillId !== 'currency' && skillId !== 'completionBonus' && skillId !== 'craftedItem' && skillId !== 'craftedCount' && typeof exp === 'number') {
addSkillExp(playerStore, skillId, exp)
}
}
@@ -392,7 +401,8 @@ function handleTaskCompletion(gameStore, playerStore, task, rewards) {
resting: '休息',
training: '训练',
working: '工作',
praying: '祈祷'
praying: '祈祷',
crafting: '制造'
}
gameStore.addLog(`${taskNames[task.type]}任务完成`, 'reward')
}

287
utils/levelingSystem.js Normal file
View File

@@ -0,0 +1,287 @@
/**
* 等级系统 - 经验曲线、升级计算
* Phase 4 数值调整 - 等级经验曲线
*/
/**
* 经验曲线类型
*/
export const EXP_CURVE_TYPES = {
LINEAR: 'linear', // 线性增长: base * level
QUADRATIC: 'quadratic', // 二次增长: base * level^2
EXPONENTIAL: 'exponential', // 指数增长: base * multiplier^level
HYBRID: 'hybrid' // 混合曲线: 更平滑的体验
}
/**
* 等级配置
*/
export const LEVEL_CONFIG = {
// 经验曲线类型
expCurveType: EXP_CURVE_TYPES.HYBRID,
// 基础经验值 (1级升级所需)
baseExp: 100,
// 经验增长倍率 (每级增长)
expMultiplier: 1.15,
// 最大等级
maxLevel: 50,
// 等级属性加成 (每级获得的属性点)
statPointsPerLevel: 3,
// 初始属性
baseStats: {
strength: 10,
agility: 8,
dexterity: 8,
intuition: 10,
vitality: 10
}
}
/**
* 计算指定等级所需的总经验
* @param {Number} level - 目标等级
* @returns {Number} 所需经验
*/
export function getExpForLevel(level) {
if (level <= 1) return 0
const { expCurveType, baseExp, expMultiplier } = LEVEL_CONFIG
switch (expCurveType) {
case EXP_CURVE_TYPES.LINEAR:
// 线性: 100, 200, 300, 400...
return baseExp * (level - 1)
case EXP_CURVE_TYPES.QUADRATIC:
// 二次: 100, 400, 900, 1600...
return baseExp * Math.pow(level - 1, 2)
case EXP_CURVE_TYPES.EXPONENTIAL:
// 指数: 100, 115, 132, 152...
return baseExp * Math.pow(expMultiplier, level - 1)
case EXP_CURVE_TYPES.HYBRID:
default:
// 混合曲线 - 平滑的增长体验
// 公式: base * (1.15 ^ (level - 1)) * level / 2
// 结果: Lv2=100, Lv5=700, Lv10=2800, Lv20=15000, Lv30=55000, Lv50=250000
return Math.floor(baseExp * Math.pow(expMultiplier, level - 2) * (level - 1) * 0.8)
}
}
/**
* 计算从当前等级到下一等级所需的经验
* @param {Number} currentLevel - 当前等级
* @returns {Number} 升级所需经验
*/
export function getExpToNextLevel(currentLevel) {
if (currentLevel >= LEVEL_CONFIG.maxLevel) {
return 0 // 已达最高等级
}
return getExpForLevel(currentLevel + 1) - getExpForLevel(currentLevel)
}
/**
* 检查并处理升级
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @returns {Object} { leveledUp: boolean, newLevel: number, levelsGained: number }
*/
export function checkLevelUp(playerStore, gameStore) {
const currentLevel = playerStore.level.current
if (currentLevel >= LEVEL_CONFIG.maxLevel) {
return { leveledUp: false, newLevel: currentLevel, levelsGained: 0 }
}
let levelsGained = 0
let newLevel = currentLevel
let remainingExp = playerStore.level.exp
// 连续升级检查(支持一次获得大量经验连升多级)
while (newLevel < LEVEL_CONFIG.maxLevel) {
const expNeeded = getExpToNextLevel(newLevel)
if (remainingExp >= expNeeded) {
remainingExp -= expNeeded
newLevel++
levelsGained++
} else {
break
}
}
if (levelsGained > 0) {
// 应用升级
const oldLevel = playerStore.level.current
playerStore.level.current = newLevel
playerStore.level.exp = remainingExp
playerStore.level.maxExp = getExpToNextLevel(newLevel)
// 应用等级属性加成
applyLevelStats(playerStore, oldLevel, newLevel)
// 记录日志
if (levelsGained === 1) {
gameStore?.addLog(`升级了! 等级: ${newLevel}`, 'reward')
} else {
gameStore?.addLog(`连升${levelsGained}级! 等级: ${newLevel}`, 'reward')
}
// 升级恢复状态
playerStore.currentStats.health = playerStore.currentStats.maxHealth
playerStore.currentStats.stamina = playerStore.currentStats.maxStamina
playerStore.currentStats.sanity = playerStore.currentStats.maxSanity
return { leveledUp: true, newLevel, levelsGained }
}
return { leveledUp: false, newLevel: currentLevel, levelsGained: 0 }
}
/**
* 应用等级属性加成
* @param {Object} playerStore - 玩家Store
* @param {Number} oldLevel - 旧等级
* @param {Number} newLevel - 新等级
*/
function applyLevelStats(playerStore, oldLevel, newLevel) {
const levelsGained = newLevel - oldLevel
const statPoints = levelsGained * LEVEL_CONFIG.statPointsPerLevel
// 自动分配属性点 (简化版: 均匀分配)
// 实际游戏中可以让玩家手动选择
const stats = ['strength', 'agility', 'dexterity', 'intuition', 'vitality']
const pointsPerStat = Math.floor(statPoints / stats.length)
const remainder = statPoints % stats.length
stats.forEach((stat, index) => {
playerStore.baseStats[stat] += pointsPerStat
if (index < remainder) {
playerStore.baseStats[stat] += 1
}
})
// 更新生命值上限 (体质影响HP)
const hpPerVitality = 10
const oldMaxHp = playerStore.currentStats.maxHealth
const newMaxHp = 100 + (playerStore.baseStats.vitality - 10) * hpPerVitality
if (newMaxHp !== oldMaxHp) {
playerStore.currentStats.maxHealth = newMaxHp
// 按比例恢复HP
const hpRatio = playerStore.currentStats.health / oldMaxHp
playerStore.currentStats.health = Math.floor(newMaxHp * hpRatio)
}
}
/**
* 添加经验值
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {Number} exp - 获得的经验值
* @returns {Object} { leveledUp: boolean, newLevel: number }
*/
export function addExp(playerStore, gameStore, exp) {
playerStore.level.exp += exp
return checkLevelUp(playerStore, gameStore)
}
/**
* 获取等级信息
* @param {Number} level - 等级
* @returns {Object} 等级信息
*/
export function getLevelInfo(level) {
const totalExp = getExpForLevel(level)
const toNext = getExpToNextLevel(level)
const isMaxLevel = level >= LEVEL_CONFIG.maxLevel
return {
level,
totalExp,
toNext,
isMaxLevel,
maxLevel: LEVEL_CONFIG.maxLevel
}
}
/**
* 获取当前经验进度百分比
* @param {Object} playerStore - 玩家Store
* @returns {Number} 0-100
*/
export function getExpProgress(playerStore) {
const currentLevel = playerStore.level.current
if (currentLevel >= LEVEL_CONFIG.maxLevel) {
return 100
}
const toNext = getExpToNextLevel(currentLevel)
const current = playerStore.level.exp
return Math.min(100, Math.floor((current / toNext) * 100))
}
/**
* 根据敌人等级计算经验奖励
* @param {Number} enemyLevel - 敌人等级
* @param {Number} playerLevel - 玩家等级
* @param {Number} baseExp - 基础经验值
* @returns {Number} 调整后的经验值
*/
export function calculateCombatExp(enemyLevel, playerLevel, baseExp) {
const levelDiff = enemyLevel - playerLevel
// 等级差异调整
let multiplier = 1.0
if (levelDiff > 5) {
// 敌人等级远高于玩家 - 额外奖励
multiplier = 1.5
} else if (levelDiff > 2) {
// 敌人等级稍高 - 小幅奖励
multiplier = 1.2
} else if (levelDiff < -5) {
// 敌人等级远低于玩家 - 大幅惩罚
multiplier = 0.1
} else if (levelDiff < -2) {
// 敌人等级稍低 - 小幅惩罚
multiplier = 0.5
}
return Math.floor(baseExp * multiplier)
}
/**
* 获取等级称号
* @param {Number} level - 等级
* @returns {String} 称号
*/
export function getLevelTitle(level) {
const titles = {
1: '新手',
5: '冒险者',
10: '老手',
15: '精英',
20: '专家',
25: '大师',
30: '宗师',
35: '传奇',
40: '英雄',
45: '神话',
50: '至尊'
}
let title = '幸存者'
for (const [lvl, t] of Object.entries(titles).sort((a, b) => b[0] - a[0])) {
if (level >= parseInt(lvl)) {
title = t
break
}
}
return title
}

View File

@@ -265,96 +265,6 @@ export function unlockSkill(playerStore, skillId) {
return true
}
/**
* 检查技能是否可解锁
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @returns {Object} { canUnlock: boolean, reason: string }
*/
export function canUnlockSkill(playerStore, skillId) {
const config = SKILL_CONFIG[skillId]
if (!config) {
return { canUnlock: false, reason: '技能不存在' }
}
// 已解锁
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
return { canUnlock: true, reason: '已解锁' }
}
// 检查解锁条件
if (config.unlockCondition) {
const condition = config.unlockCondition
// 物品条件
if (condition.type === 'item') {
const hasItem = playerStore.inventory?.some(i => i.id === condition.item)
if (!hasItem) {
return { canUnlock: false, reason: '需要特定物品' }
}
}
// 位置条件
if (condition.location) {
if (playerStore.currentLocation !== condition.location) {
return { canUnlock: false, reason: '需要在特定位置解锁' }
}
}
// 技能等级条件
if (condition.skillLevel) {
const requiredSkill = playerStore.skills[condition.skillId]
if (!requiredSkill || requiredSkill.level < condition.skillLevel) {
return { canUnlock: false, reason: '需要前置技能达到特定等级' }
}
}
}
return { canUnlock: true, reason: '' }
}
/**
* 获取技能经验倍率(含父技能加成)
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @returns {Number} 经验倍率
*/
export function getSkillExpRate(playerStore, skillId) {
let rate = 1.0
// 全局经验加成
if (playerStore.globalBonus && playerStore.globalBonus.globalExpRate) {
rate *= (1 + playerStore.globalBonus.globalExpRate / 100)
}
// 父技能加成
const config = SKILL_CONFIG[skillId]
if (config && config.parentSkill) {
const parentSkill = playerStore.skills[config.parentSkill]
if (parentSkill && parentSkill.unlocked) {
const parentConfig = SKILL_CONFIG[config.parentSkill]
if (parentConfig && parentConfig.milestones) {
// 检查父技能里程碑奖励
for (const [level, milestone] of Object.entries(parentConfig.milestones)) {
if (parentSkill.level >= parseInt(level) && milestone.effect?.expRate) {
rate *= milestone.effect.expRate
}
}
}
// 如果子技能等级低于父技能,提供额外加成
if (parentSkill.level > 0 && playerStore.skills[skillId]) {
const childLevel = playerStore.skills[skillId].level
if (childLevel < parentSkill.level) {
rate *= 1.5 // 父技能倍率加成
}
}
}
}
return rate
}
/**
* 获取技能显示信息
* @param {Object} playerStore - 玩家Store

334
utils/soundSystem.js Normal file
View File

@@ -0,0 +1,334 @@
/**
* 音效系统
* Phase 3 UI美化 - 音效支持
*
* 注意uni-app的音频功能有限不同平台支持情况不同
* H5端使用Web Audio API
* 小程序端:使用 wx.createInnerAudioContext()
* App端使用 plus.audio
*/
/**
* 音效类型枚举
*/
export const SOUND_TYPES = {
// UI音效
CLICK: 'click',
OPEN: 'open',
CLOSE: 'close',
TOGGLE: 'toggle',
// 游戏音效
COMBAT_START: 'combat_start',
COMBAT_HIT: 'combat_hit',
COMBAT_HIT_PLAYER: 'combat_hit_player',
COMBAT_VICTORY: 'combat_victory',
COMBAT_DEFEAT: 'combat_defeat',
// 奖励音效
REWARD: 'reward',
LEVEL_UP: 'level_up',
SKILL_UP: 'skill_up',
// 错误音效
ERROR: 'error',
WARNING: 'warning',
// 系统音效
NOTIFICATION: 'notification',
MESSAGE: 'message'
}
/**
* 音效配置
*/
const SOUND_CONFIG = {
// 是否启用音效
enabled: true,
// 音量 (0-1)
volume: 0.5,
// 音效文件路径配置 (需要实际文件)
soundPaths: {
[SOUND_TYPES.CLICK]: '/static/sounds/click.mp3',
[SOUND_TYPES.OPEN]: '/static/sounds/open.mp3',
[SOUND_TYPES.CLOSE]: '/static/sounds/close.mp3',
[SOUND_TYPES.TOGGLE]: '/static/sounds/toggle.mp3',
[SOUND_TYPES.COMBAT_START]: '/static/sounds/combat_start.mp3',
[SOUND_TYPES.COMBAT_HIT]: '/static/sounds/combat_hit.mp3',
[SOUND_TYPES.COMBAT_HIT_PLAYER]: '/static/sounds/combat_hit_player.mp3',
[SOUND_TYPES.COMBAT_VICTORY]: '/static/sounds/combat_victory.mp3',
[SOUND_TYPES.COMBAT_DEFEAT]: '/static/sounds/combat_defeat.mp3',
[SOUND_TYPES.REWARD]: '/static/sounds/reward.mp3',
[SOUND_TYPES.LEVEL_UP]: '/static/sounds/level_up.mp3',
[SOUND_TYPES.SKILL_UP]: '/static/sounds/skill_up.mp3',
[SOUND_TYPES.ERROR]: '/static/sounds/error.mp3',
[SOUND_TYPES.WARNING]: '/static/sounds/warning.mp3',
[SOUND_TYPES.NOTIFICATION]: '/static/sounds/notification.mp3',
[SOUND_TYPES.MESSAGE]: '/static/sounds/message.mp3'
}
}
/**
* 音频上下文缓存
*/
let audioContext = null
let audioCache = {}
/**
* 初始化音频系统
*/
export function initSoundSystem() {
// #ifdef H5
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)()
} catch (e) {
console.warn('Web Audio API not supported:', e)
}
// #endif
// #ifdef MP-WEIXIN
// 小程序端使用微信音频API
// #endif
// 从本地存储加载音效设置
loadSoundSettings()
}
/**
* 播放音效
* @param {String} soundType - 音效类型
* @param {Object} options - 选项 { volume, speed }
*/
export function playSound(soundType, options = {}) {
if (!SOUND_CONFIG.enabled) {
return { success: false, message: '音效已禁用' }
}
const volume = options.volume !== undefined ? options.volume : SOUND_CONFIG.volume
// #ifdef H5
return playSoundH5(soundType, volume)
// #endif
// #ifdef MP-WEIXIN
return playSoundWeixin(soundType, volume)
// #endif
// #ifdef APP-PLUS
return playSoundApp(soundType, volume)
// #endif
return { success: false, message: '当前平台不支持音效' }
}
/**
* H5端播放音效 (使用Web Audio API合成简单音效)
*/
function playSoundH5(soundType, volume) {
if (!audioContext) {
initSoundSystem()
if (!audioContext) {
return { success: false, message: '音频上下文初始化失败' }
}
}
// 恢复音频上下文(某些浏览器需要用户交互后才能恢复)
if (audioContext.state === 'suspended') {
audioContext.resume()
}
try {
// 创建振荡器生成简单音效
const oscillator = audioContext.createOscillator()
const gainNode = audioContext.createGain()
oscillator.connect(gainNode)
gainNode.connect(audioContext.destination)
// 根据音效类型设置不同的频率和包络
const soundParams = getSoundParameters(soundType)
oscillator.type = soundParams.type || 'sine'
oscillator.frequency.setValueAtTime(soundParams.frequency, audioContext.currentTime)
// 设置音量包络
const now = audioContext.currentTime
gainNode.gain.setValueAtTime(0, now)
gainNode.gain.linearRampToValueAtTime(volume * 0.3, now + 0.01)
gainNode.gain.exponentialRampToValueAtTime(0.001, now + soundParams.duration)
oscillator.start(now)
oscillator.stop(now + soundParams.duration)
return { success: true }
} catch (e) {
console.warn('播放音效失败:', e)
return { success: false, message: e.message }
}
}
/**
* 获取音效参数 (用于合成音效)
*/
function getSoundParameters(soundType) {
const params = {
// 点击音效 - 短促的高音
[SOUND_TYPES.CLICK]: { type: 'sine', frequency: 800, duration: 0.05 },
// 打开音效 - 上升音调
[SOUND_TYPES.OPEN]: { type: 'sine', frequency: 400, duration: 0.15 },
// 关闭音效 - 下降音调
[SOUND_TYPES.CLOSE]: { type: 'sine', frequency: 300, duration: 0.1 },
// 切换音效
[SOUND_TYPES.TOGGLE]: { type: 'square', frequency: 500, duration: 0.08 },
// 战斗开始 - 低沉
[SOUND_TYPES.COMBAT_START]: { type: 'sawtooth', frequency: 150, duration: 0.3 },
// 命中敌人 - 尖锐
[SOUND_TYPES.COMBAT_HIT]: { type: 'square', frequency: 1200, duration: 0.08 },
// 被命中 - 低频
[SOUND_TYPES.COMBAT_HIT_PLAYER]: { type: 'sawtooth', frequency: 200, duration: 0.15 },
// 胜利 - 上升音阶
[SOUND_TYPES.COMBAT_VICTORY]: { type: 'sine', frequency: 523, duration: 0.4 },
// 失败 - 下降音
[SOUND_TYPES.COMBAT_DEFEAT]: { type: 'sawtooth', frequency: 100, duration: 0.4 },
// 奖励 - 悦耳的高音
[SOUND_TYPES.REWARD]: { type: 'sine', frequency: 880, duration: 0.2 },
// 升级 - 双音
[SOUND_TYPES.LEVEL_UP]: { type: 'sine', frequency: 659, duration: 0.3 },
// 技能升级
[SOUND_TYPES.SKILL_UP]: { type: 'sine', frequency: 784, duration: 0.25 },
// 错误 - 低频嗡鸣
[SOUND_TYPES.ERROR]: { type: 'sawtooth', frequency: 100, duration: 0.2 },
// 警告
[SOUND_TYPES.WARNING]: { type: 'square', frequency: 440, duration: 0.15 },
// 通知
[SOUND_TYPES.NOTIFICATION]: { type: 'sine', frequency: 660, duration: 0.2 },
// 消息
[SOUND_TYPES.MESSAGE]: { type: 'sine', frequency: 587, duration: 0.15 }
}
return params[soundType] || { type: 'sine', frequency: 440, duration: 0.1 }
}
/**
* 微信小程序端播放音效
*/
function playSoundWeixin(soundType, volume) {
// #ifdef MP-WEIXIN
const soundPath = SOUND_CONFIG.soundPaths[soundType]
if (!soundPath) {
return { success: false, message: '音效文件未配置' }
}
try {
const audio = uni.createInnerAudioContext()
audio.src = soundPath
audio.volume = volume
audio.play()
return { success: true }
} catch (e) {
return { success: false, message: e.message }
}
// #endif
return { success: false, message: '非微信小程序环境' }
}
/**
* App端播放音效
*/
function playSoundApp(soundType, volume) {
// #ifdef APP-PLUS
const soundPath = SOUND_CONFIG.soundPaths[soundType]
if (!soundPath) {
return { success: false, message: '音效文件未配置' }
}
try {
const player = plus.audio.createPlayer(soundPath)
player.setVolume(volume * 100)
player.play()
return { success: true }
} catch (e) {
return { success: false, message: e.message }
}
// #endif
return { success: false, message: '非App环境' }
}
/**
* 设置音效开关
* @param {Boolean} enabled - 是否启用音效
*/
export function setSoundEnabled(enabled) {
SOUND_CONFIG.enabled = enabled
saveSoundSettings()
}
/**
* 设置音量
* @param {Number} volume - 音量 (0-1)
*/
export function setSoundVolume(volume) {
SOUND_CONFIG.volume = Math.max(0, Math.min(1, volume))
saveSoundSettings()
}
/**
* 获取音效设置
* @returns {Object} { enabled, volume }
*/
export function getSoundSettings() {
return {
enabled: SOUND_CONFIG.enabled,
volume: SOUND_CONFIG.volume
}
}
/**
* 保存音效设置到本地存储
*/
function saveSoundSettings() {
try {
uni.setStorageSync('soundSettings', JSON.stringify({
enabled: SOUND_CONFIG.enabled,
volume: SOUND_CONFIG.volume
}))
} catch (e) {
console.warn('保存音效设置失败:', e)
}
}
/**
* 从本地存储加载音效设置
*/
function loadSoundSettings() {
try {
const settings = uni.getStorageSync('soundSettings')
if (settings) {
const parsed = JSON.parse(settings)
SOUND_CONFIG.enabled = parsed.enabled !== undefined ? parsed.enabled : true
SOUND_CONFIG.volume = parsed.volume !== undefined ? parsed.volume : 0.5
}
} catch (e) {
console.warn('加载音效设置失败:', e)
}
}
/**
* 快捷音效播放函数
*/
export const playClick = () => playSound(SOUND_TYPES.CLICK)
export const playOpen = () => playSound(SOUND_TYPES.OPEN)
export const playClose = () => playSound(SOUND_TYPES.CLOSE)
export const playCombatStart = () => playSound(SOUND_TYPES.COMBAT_START)
export const playCombatHit = () => playSound(SOUND_TYPES.COMBAT_HIT)
export const playCombatVictory = () => playSound(SOUND_TYPES.COMBAT_VICTORY)
export const playCombatDefeat = () => playSound(SOUND_TYPES.COMBAT_DEFEAT)
export const playReward = () => playSound(SOUND_TYPES.REWARD)
export const playLevelUp = () => playSound(SOUND_TYPES.LEVEL_UP)
export const playSkillUp = () => playSound(SOUND_TYPES.SKILL_UP)
export const playError = () => playSound(SOUND_TYPES.ERROR)
export const playWarning = () => playSound(SOUND_TYPES.WARNING)
// 初始化音效系统
initSoundSystem()

View File

@@ -5,6 +5,8 @@
import { SKILL_CONFIG } from '@/config/skills.js'
import { ITEM_CONFIG } from '@/config/items.js'
import { RECIPE_CONFIG } from '@/config/recipes.js'
import { addSkillExp, unlockSkill } from './skillSystem.js'
/**
* 任务类型枚举
@@ -16,7 +18,8 @@ export const TASK_TYPES = {
WORKING: 'working', // 工作
COMBAT: 'combat', // 战斗
EXPLORE: 'explore', // 探索
PRAYING: 'praying' // 祈祷
PRAYING: 'praying', // 祈祷
CRAFTING: 'crafting' // 制造
}
/**
@@ -27,15 +30,19 @@ const MUTEX_RULES = {
full: [
[TASK_TYPES.COMBAT, TASK_TYPES.TRAINING], // 战斗 vs 训练
[TASK_TYPES.COMBAT, TASK_TYPES.WORKING], // 战斗 vs 工作
[TASK_TYPES.COMBAT, TASK_TYPES.CRAFTING], // 战斗 vs 制造
[TASK_TYPES.TRAINING, TASK_TYPES.WORKING], // 训练 vs 工作
[TASK_TYPES.EXPLORE, TASK_TYPES.RESTING] // 探索 vs 休息
[TASK_TYPES.TRAINING, TASK_TYPES.CRAFTING], // 训练 vs 制造
[TASK_TYPES.EXPLORE, TASK_TYPES.RESTING], // 探索 vs 休息
[TASK_TYPES.EXPLORE, TASK_TYPES.CRAFTING] // 探索 vs 制造
],
// 部分互斥(需要一心多用技能)
partial: [
[TASK_TYPES.READING, TASK_TYPES.COMBAT], // 阅读 vs 战斗
[TASK_TYPES.READING, TASK_TYPES.TRAINING], // 阅读 vs 训练
[TASK_TYPES.READING, TASK_TYPES.WORKING] // 阅读 vs 工作
[TASK_TYPES.READING, TASK_TYPES.WORKING], // 阅读 vs 工作
[TASK_TYPES.READING, TASK_TYPES.CRAFTING] // 阅读 vs 制造
]
}
@@ -182,6 +189,17 @@ function checkTaskConditions(playerStore, taskType, taskData) {
return { canStart: false, reason: '需要圣经' }
}
break
case TASK_TYPES.CRAFTING:
// 需要配方ID
if (!taskData.recipeId) {
return { canStart: false, reason: '需要选择配方' }
}
// 需要耐力
if (playerStore.currentStats.stamina < 5) {
return { canStart: false, reason: '耐力不足' }
}
break
}
return { canStart: true, reason: '' }
@@ -275,6 +293,9 @@ function processSingleTask(gameStore, playerStore, task, deltaTime) {
case TASK_TYPES.PRAYING:
return processPrayingTask(gameStore, playerStore, task, elapsedSeconds)
case TASK_TYPES.CRAFTING:
return processCraftingTask(gameStore, playerStore, task, elapsedSeconds)
default:
return { completed: false }
}
@@ -491,6 +512,134 @@ function processPrayingTask(gameStore, playerStore, task, elapsedSeconds) {
return { completed: false }
}
/**
* 处理制造任务
*/
function processCraftingTask(gameStore, playerStore, task, elapsedSeconds) {
const recipeId = task.data.recipeId
const recipe = RECIPE_CONFIG[recipeId]
if (!recipe) {
return { completed: true, error: '配方不存在' }
}
// 消耗耐力(制造也需要体力)
const staminaCost = 1 * elapsedSeconds
if (playerStore.currentStats.stamina < staminaCost) {
return { completed: true, rewards: {}, failed: true }
}
playerStore.currentStats.stamina -= staminaCost
// 计算实际制造时间(受技能加成影响)
const craftingSkill = playerStore.skills[recipe.requiredSkill]
let timeMultiplier = 1.0
if (craftingSkill && craftingSkill.level > 0) {
// 制造技能每级减少5%时间
timeMultiplier = 1 - (Math.min(craftingSkill.level, 20) * 0.05)
}
// 累积制造进度(考虑时间加成)
const effectiveProgress = task.progress * timeMultiplier
// 检查是否完成
if (effectiveProgress >= recipe.baseTime) {
// 计算成功率
const successRate = calculateCraftingSuccessRate(playerStore, recipe)
// 判定是否成功
const success = Math.random() < successRate
if (success) {
// 给予制造技能经验
const expGain = recipe.baseTime * 0.5 // 基于制造时间的经验
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp[recipe.requiredSkill]) {
task.accumulatedExp[recipe.requiredSkill] = 0
}
task.accumulatedExp[recipe.requiredSkill] += expGain
const rewards = {
[recipe.requiredSkill]: task.accumulatedExp[recipe.requiredSkill] || 0,
craftedItem: recipe.resultItem,
craftedCount: recipe.resultCount
}
// 添加制造物品到背包(这将在任务完成回调中处理)
if (gameStore.addLog) {
const itemConfig = ITEM_CONFIG[recipe.resultItem]
gameStore.addLog(`制造成功:${itemConfig?.name || recipe.resultItem}`, 'reward')
}
return { completed: true, rewards, success: true }
} else {
// 制造失败
if (gameStore.addLog) {
gameStore.addLog(`制造失败:${recipeId}`, 'warning')
}
// 失败也给予少量经验
const expGain = recipe.baseTime * 0.1
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp[recipe.requiredSkill]) {
task.accumulatedExp[recipe.requiredSkill] = 0
}
task.accumulatedExp[recipe.requiredSkill] += expGain
const rewards = {
[recipe.requiredSkill]: task.accumulatedExp[recipe.requiredSkill] || 0
}
return { completed: true, rewards, success: false }
}
}
// 进行中,给予少量经验
const expPerTick = 0.5 * elapsedSeconds
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp[recipe.requiredSkill]) {
task.accumulatedExp[recipe.requiredSkill] = 0
}
task.accumulatedExp[recipe.requiredSkill] += expPerTick
return { completed: false }
}
/**
* 计算制造成功率
* @param {Object} playerStore - 玩家Store
* @param {Object} recipe - 配方对象
* @returns {Number} 成功率 (0-1)
*/
function calculateCraftingSuccessRate(playerStore, recipe) {
let successRate = recipe.baseSuccessRate
// 技能等级加成(每级+2%
const skill = playerStore.skills[recipe.requiredSkill]
if (skill && skill.level > 0) {
successRate += Math.min(skill.level, 20) * 0.02
}
// 运气加成
const luck = playerStore.baseStats?.luck || 10
successRate += luck * 0.001
// 检查是否有制造技能的全局加成
if (playerStore.globalBonus?.craftingSuccessRate) {
successRate += playerStore.globalBonus.craftingSuccessRate / 100
}
return Math.min(0.98, Math.max(0.05, successRate))
}
/**
* 获取任务奖励
*/
@@ -521,17 +670,17 @@ function applyTaskRewards(playerStore, task, rewards) {
continue
}
if (skillId === 'faith') {
// 信仰技能经验
// 信仰技能经验 - 使用addSkillExp处理
if (!playerStore.skills.faith) {
playerStore.skills.faith = { level: 0, exp: 0, unlocked: true }
unlockSkill(playerStore, 'faith')
}
playerStore.skills.faith.exp += exp
addSkillExp(playerStore, 'faith', exp)
continue
}
// 普通技能经验
// 普通技能经验 - 使用addSkillExp处理升级和里程碑
if (playerStore.skills[skillId]) {
playerStore.skills[skillId].exp += exp
addSkillExp(playerStore, skillId, exp)
}
}
}
@@ -547,7 +696,8 @@ function getTaskTypeName(taskType) {
working: '工作',
combat: '战斗',
explore: '探索',
praying: '祈祷'
praying: '祈祷',
crafting: '制造'
}
return names[taskType] || taskType
}