diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index a7c3b87..8b3f890 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -12,7 +12,8 @@
"Bash(git config:*)",
"Bash(git remote add:*)",
"Bash(git add:*)",
- "Bash(git commit:*)"
+ "Bash(git commit:*)",
+ "Bash(git push:*)"
]
}
}
diff --git a/App.vue b/App.vue
index 346f9b9..908d84b 100644
--- a/App.vue
+++ b/App.vue
@@ -243,4 +243,185 @@ page {
background-color: $bg-tertiary;
border-radius: 4rpx;
}
+
+/* ==================== 过渡动画 ==================== */
+
+/* 淡入淡出 */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.3s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+/* 滑动进入 - 从右侧 */
+.slide-right-enter-active,
+.slide-right-leave-active {
+ transition: all 0.3s ease;
+}
+
+.slide-right-enter-from {
+ transform: translateX(100%);
+ opacity: 0;
+}
+
+.slide-right-leave-to {
+ transform: translateX(100%);
+ opacity: 0;
+}
+
+/* 滑动进入 - 从左侧 */
+.slide-left-enter-active,
+.slide-left-leave-active {
+ transition: all 0.3s ease;
+}
+
+.slide-left-enter-from {
+ transform: translateX(-100%);
+ opacity: 0;
+}
+
+.slide-left-leave-to {
+ transform: translateX(-100%);
+ opacity: 0;
+}
+
+/* 滑动进入 - 从下方 */
+.slide-up-enter-active,
+.slide-up-leave-active {
+ transition: all 0.3s ease;
+}
+
+.slide-up-enter-from {
+ transform: translateY(100%);
+ opacity: 0;
+}
+
+.slide-up-leave-to {
+ transform: translateY(100%);
+ opacity: 0;
+}
+
+/* 缩放动画 */
+.scale-enter-active,
+.scale-leave-active {
+ transition: all 0.2s ease;
+}
+
+.scale-enter-from,
+.scale-leave-to {
+ transform: scale(0.9);
+ opacity: 0;
+}
+
+/* 弹跳动画 */
+.bounce-enter-active {
+ animation: bounce-in 0.4s ease;
+}
+
+.bounce-leave-active {
+ animation: bounce-in 0.3s ease reverse;
+}
+
+@keyframes bounce-in {
+ 0% {
+ transform: scale(0);
+ opacity: 0;
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+/* 闪烁动画 */
+@keyframes flash {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.flash {
+ animation: flash 0.5s ease;
+}
+
+/* 脉冲动画 */
+@keyframes pulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.05);
+ opacity: 0.8;
+ }
+}
+
+.pulse {
+ animation: pulse 1.5s ease infinite;
+}
+
+/* 摇晃动画 */
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 25% { transform: translateX(-10rpx); }
+ 75% { transform: translateX(10rpx); }
+}
+
+.shake {
+ animation: shake 0.3s ease;
+}
+
+/* 旋转加载 */
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.spin {
+ animation: spin 1s linear infinite;
+}
+
+/* 打字机效果 */
+@keyframes typewriter {
+ from { width: 0; }
+ to { width: 100%; }
+}
+
+/* ==================== 工具类 ==================== */
+
+/* 过渡效果 */
+.transition-all {
+ transition: all 0.3s ease;
+}
+
+.transition-fast {
+ transition: all 0.15s ease;
+}
+
+.transition-slow {
+ transition: all 0.5s ease;
+}
+
+/* 悬停效果 */
+.hover-scale {
+ transition: transform 0.2s ease;
+}
+
+.hover-scale:active {
+ transform: scale(0.95);
+}
+
+.hover-bright {
+ transition: filter 0.2s ease;
+}
+
+.hover-bright:active {
+ filter: brightness(1.2);
+}
diff --git a/components/common/TextButton.vue b/components/common/TextButton.vue
index 06aed80..5d7d96b 100644
--- a/components/common/TextButton.vue
+++ b/components/common/TextButton.vue
@@ -1,10 +1,17 @@
- {{ text }}
+ {{ text }}
+
@@ -12,13 +19,14 @@
const props = defineProps({
text: { type: String, required: true },
type: { type: String, default: 'default' },
- disabled: { type: Boolean, default: false }
+ disabled: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false }
})
const emit = defineEmits(['click'])
function handleClick() {
- if (!props.disabled) {
+ if (!props.disabled && !props.loading) {
emit('click')
}
}
@@ -26,43 +34,133 @@ function handleClick() {
diff --git a/components/drawers/InventoryDrawer.vue b/components/drawers/InventoryDrawer.vue
index 299bbeb..09ad88e 100644
--- a/components/drawers/InventoryDrawer.vue
+++ b/components/drawers/InventoryDrawer.vue
@@ -21,8 +21,9 @@
>
{{ item.icon || '📦' }}
-
+
{{ item.name }}
+ [{{ item.qualityName }}]
x{{ item.count }}
@@ -73,6 +74,7 @@
import { ref, computed } from 'vue'
import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
+import { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
import FilterTabs from '@/components/common/FilterTabs.vue'
import TextButton from '@/components/common/TextButton.vue'
@@ -128,16 +130,6 @@ function isEquipped(item) {
return equipped && (equipped.uniqueId === item.uniqueId || equipped.id === item.id)
}
-function qualityColor(quality) {
- if (!quality) return '#ffffff'
- if (quality >= 200) return '#f97316' // 传说
- if (quality >= 160) return '#a855f7' // 史诗
- if (quality >= 130) return '#60a5fa' // 稀有
- if (quality >= 100) return '#4ade80' // 优秀
- if (quality >= 50) return '#ffffff' // 普通
- return '#808080' // 垃圾
-}
-
function close() {
emit('close')
}
@@ -148,30 +140,10 @@ function selectItem(item) {
function equipItem() {
if (!selectedItem.value) return
- const item = selectedItem.value
- const slot = slotMap[item.type]
-
- if (!slot) return
-
- // 如果该槽位已有装备,先卸下旧装备放回背包
- const oldEquip = player.equipment[slot]
- if (oldEquip) {
- player.inventory.push(oldEquip)
+ const result = equipItemUtil(player, game, selectedItem.value.uniqueId)
+ if (result.success) {
+ selectedItem.value = null
}
-
- // 从背包中移除该物品
- const index = player.inventory.findIndex(i =>
- (i.uniqueId === item.uniqueId) || (i.id === item.id && i.uniqueId === undefined)
- )
- if (index > -1) {
- player.inventory.splice(index, 1)
- }
-
- // 装备该物品
- player.equipment[slot] = item
-
- game.addLog(`装备了 ${item.name}`, 'info')
- selectedItem.value = null
}
function unequipItem() {
@@ -181,57 +153,19 @@ function unequipItem() {
if (!slot) return
- // 从装备槽移除
- player.equipment[slot] = null
-
- // 放回背包
- player.inventory.push(item)
-
- game.addLog(`卸下了 ${item.name}`, 'info')
- selectedItem.value = null
+ const result = unequipItemBySlot(player, game, slot)
+ if (result.success) {
+ selectedItem.value = null
+ }
}
function useItem() {
if (!selectedItem.value) return
- const item = selectedItem.value
-
- // 检查是否是消耗品
- if (item.type !== 'consumable') return
-
- // 应用效果
- if (item.effect) {
- if (item.effect.health) {
- player.currentStats.health = Math.min(
- player.currentStats.maxHealth,
- player.currentStats.health + item.effect.health
- )
- }
- if (item.effect.stamina) {
- player.currentStats.stamina = Math.min(
- player.currentStats.maxStamina,
- player.currentStats.stamina + item.effect.stamina
- )
- }
- if (item.effect.sanity) {
- player.currentStats.sanity = Math.min(
- player.currentStats.maxSanity,
- player.currentStats.sanity + item.effect.sanity
- )
- }
+ const result = useItemUtil(player, game, selectedItem.value.id, 1)
+ if (result.success) {
+ // 如果阅读任务,可以在这里处理
+ selectedItem.value = null
}
-
- // 从背包中移除(如果是堆叠物品,减少数量)
- if (item.count > 1) {
- item.count--
- } else {
- const index = player.inventory.findIndex(i => i.id === item.id)
- if (index > -1) {
- player.inventory.splice(index, 1)
- }
- }
-
- game.addLog(`使用了 ${item.name}`, 'info')
- selectedItem.value = null
}
function sellItem() {
@@ -322,6 +256,13 @@ function sellItem() {
font-size: 22rpx;
margin-top: 4rpx;
}
+}
+
+.quality-badge {
+ font-size: 18rpx;
+ opacity: 0.8;
+ margin-left: 8rpx;
+}
&__equipped-badge {
width: 32rpx;
diff --git a/components/layout/Drawer.vue b/components/layout/Drawer.vue
index 0b59987..2698109 100644
--- a/components/layout/Drawer.vue
+++ b/components/layout/Drawer.vue
@@ -1,5 +1,10 @@
-
+
@@ -9,7 +14,8 @@
diff --git a/components/panels/MapPanel.vue b/components/panels/MapPanel.vue
index b3ff4d8..3cd9134 100644
--- a/components/panels/MapPanel.vue
+++ b/components/panels/MapPanel.vue
@@ -105,9 +105,10 @@ import { useGameStore } from '@/store/game'
import { usePlayerStore } from '@/store/player'
import { LOCATION_CONFIG } from '@/config/locations'
import { NPC_CONFIG } from '@/config/npcs.js'
-import { ENEMY_CONFIG } from '@/config/enemies.js'
+import { ENEMY_CONFIG, getRandomEnemyForLocation } from '@/config/enemies.js'
import { getShopConfig } from '@/config/shop.js'
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
+import { triggerExploreEvent, tryFlee } from '@/utils/eventSystem.js'
import TextButton from '@/components/common/TextButton.vue'
import ProgressBar from '@/components/common/ProgressBar.vue'
import FilterTabs from '@/components/common/FilterTabs.vue'
@@ -151,10 +152,10 @@ const availableLocations = computed(() => {
// 检查解锁条件
if (loc.unlockCondition) {
if (loc.unlockCondition.type === 'kill') {
- // 检查击杀条件(需要追踪击杀数)
- const killCount = player.flags[`kill_${loc.unlockCondition.target}`] || 0
+ // 检查击杀条件(使用统一的killCount存储)
+ const killCount = (player.killCount && player.killCount[loc.unlockCondition.target]) || 0
locked = killCount < loc.unlockCondition.count
- lockReason = `需要击杀${loc.unlockCondition.count}只${getEnemyName(loc.unlockCondition.target)}`
+ lockReason = `需要击杀${loc.unlockCondition.count}只${getEnemyName(loc.unlockCondition.target)} (当前: ${killCount})`
} else if (loc.unlockCondition.type === 'item') {
// 检查物品条件
const hasItem = player.inventory.some(i => i.id === loc.unlockCondition.item)
@@ -191,7 +192,8 @@ const currentActions = computed(() => {
trade: { id: 'trade', label: '交易', type: 'default' },
explore: { id: 'explore', label: '探索', type: 'warning' },
combat: { id: 'combat', label: '战斗', type: 'danger' },
- read: { id: 'read', label: '阅读', type: 'default' }
+ read: { id: 'read', label: '阅读', type: 'default' },
+ crafting: { id: 'crafting', label: '制造', type: 'primary' }
}
// 战斗中只显示战斗相关
@@ -302,33 +304,37 @@ function doAction(actionId) {
break
case 'explore':
game.addLog('开始探索...', 'info')
- // 探索逻辑
+ const exploreResult = triggerExploreEvent(game, player)
+ if (!exploreResult.success) {
+ game.addLog(exploreResult.message, 'info')
+ }
break
case 'combat':
startCombat()
break
case 'flee':
game.addLog('尝试逃跑...', 'combat')
- // 逃跑逻辑
+ const fleeResult = tryFlee(game, player)
+ if (!fleeResult.success) {
+ // 逃跑失败,玩家会多受到一次攻击(在战斗循环中处理)
+ }
break
case 'read':
game.addLog('找个安静的地方阅读...', 'info')
break
+ case 'crafting':
+ game.drawerState.crafting = true
+ game.addLog('打开制造界面...', 'info')
+ break
}
}
function startCombat() {
- const current = LOCATION_CONFIG[player.currentLocation]
- if (!current || !current.enemies || current.enemies.length === 0) {
- game.addLog('这里没有敌人', 'info')
- return
- }
-
- const enemyId = current.enemies[0]
- const enemyConfig = ENEMY_CONFIG[enemyId]
+ // 使用新的敌人获取函数
+ const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
if (!enemyConfig) {
- game.addLog('敌人配置错误', 'error')
+ game.addLog('这里没有敌人', 'info')
return
}
@@ -337,8 +343,8 @@ function startCombat() {
// 获取环境类型
const environment = getEnvironmentType(player.currentLocation)
- // 使用 initCombat 初始化战斗(包含 expReward 和 drops)
- game.combatState = initCombat(enemyId, enemyConfig, environment)
+ // 使用 initCombat 初始化战斗
+ game.combatState = initCombat(enemyConfig.id, enemyConfig, environment)
game.inCombat = true
}
@@ -368,8 +374,8 @@ function toggleAutoCombat() {
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
// 如果当前不在战斗中且在危险区域,立即开始战斗
if (!game.inCombat) {
- const current = LOCATION_CONFIG[player.currentLocation]
- if (current && current.enemies && current.enemies.length > 0) {
+ const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
+ if (enemyConfig) {
startCombat()
}
}
diff --git a/components/panels/StatusPanel.vue b/components/panels/StatusPanel.vue
index 7c53174..dd2f995 100644
--- a/components/panels/StatusPanel.vue
+++ b/components/panels/StatusPanel.vue
@@ -145,21 +145,137 @@
暂无技能
+
+
+
+
+
+ 正在阅读:{{ readingTask.bookName }}
+
+ {{ readingTimeText }}
+
+
+
+
+
+ {{ book.icon || '📖' }}
+
+ {{ book.name }}
+ 阅读需 {{ book.readingTime }}秒
+
+
+
+
+
+
+ 暂无书籍,去商店看看吧
+
+
+
+
+
+
+
+ 正在训练:{{ trainingTask.skillName }}
+
+
+
+
+
+
+
+ {{ skill.icon || '⚔️' }}
+
+ {{ skill.name }}
+ Lv.{{ skill.level }}
+
+
+
+
+
+
+ 暂无可训练技能
+
+
+
+
+
+
+
+
+
+ {{ task.timeText }}
+
+
+
+ 暂无活动任务
+
+
diff --git a/config/enemies.js b/config/enemies.js
index 70a867c..5944c95 100644
--- a/config/enemies.js
+++ b/config/enemies.js
@@ -1,45 +1,246 @@
+/**
+ * 敌人配置
+ * Phase 4 数值调整 - 战斗数值平衡
+ */
+
// 敌人配置
export const ENEMY_CONFIG = {
+ // ===== Lv.1 敌人 =====
wild_dog: {
id: 'wild_dog',
name: '野狗',
level: 1,
baseStats: {
- health: 30,
+ health: 35,
attack: 8,
defense: 2,
- speed: 1.0
+ speed: 1.2
},
derivedStats: {
- ap: 10, // 攻击点数
- ep: 8 // 闪避点数
+ ap: 8,
+ ep: 10
},
- expReward: 15,
- skillExpReward: 10,
+ expReward: 20,
+ skillExpReward: 12,
drops: [
- { itemId: 'dog_skin', chance: 0.8, count: { min: 1, max: 1 } }
+ { itemId: 'dog_skin', chance: 0.7, count: { min: 1, max: 1 } },
+ { itemId: 'healing_herb', chance: 0.15, count: { min: 1, max: 2 } }
]
},
- test_boss: {
- id: 'test_boss',
- name: '测试Boss',
- level: 5,
+ rat: {
+ id: 'rat',
+ name: '老鼠',
+ level: 1,
baseStats: {
- health: 200,
- attack: 25,
- defense: 10,
- speed: 0.8
+ health: 20,
+ attack: 5,
+ defense: 1,
+ speed: 1.5
},
derivedStats: {
- ap: 25,
- ep: 15
+ ap: 6,
+ ep: 12
},
- expReward: 150,
- skillExpReward: 50,
+ expReward: 10,
+ skillExpReward: 6,
drops: [
- { itemId: 'basement_key', chance: 1.0, count: { min: 1, max: 1 } }
+ { itemId: 'healing_herb', chance: 0.2, count: { min: 1, max: 1 } }
+ ]
+ },
+
+ // ===== Lv.3 敌人 =====
+ wolf: {
+ id: 'wolf',
+ name: '灰狼',
+ level: 3,
+ baseStats: {
+ health: 50,
+ attack: 15,
+ defense: 5,
+ speed: 1.3
+ },
+ derivedStats: {
+ ap: 14,
+ ep: 13
+ },
+ expReward: 40,
+ skillExpReward: 20,
+ drops: [
+ { itemId: 'wolf_fang', chance: 0.6, count: { min: 1, max: 2 } },
+ { itemId: 'dog_skin', chance: 0.3, count: { min: 1, max: 1 } }
+ ]
+ },
+
+ bandit: {
+ id: 'bandit',
+ name: '流浪者',
+ level: 3,
+ baseStats: {
+ health: 60,
+ attack: 18,
+ defense: 8,
+ speed: 1.0
+ },
+ derivedStats: {
+ ap: 16,
+ ep: 10
+ },
+ expReward: 50,
+ skillExpReward: 25,
+ drops: [
+ { itemId: 'copper_coin', chance: 0.8, count: { min: 3, max: 8 } },
+ { itemId: 'bread', chance: 0.3, count: { min: 1, max: 2 } }
+ ]
+ },
+
+ // ===== Lv.5 Boss =====
+ test_boss: {
+ id: 'test_boss',
+ name: '流浪狗王',
+ level: 5,
+ baseStats: {
+ health: 250,
+ attack: 30,
+ defense: 15,
+ speed: 1.1
+ },
+ derivedStats: {
+ ap: 30,
+ ep: 18
+ },
+ expReward: 200,
+ skillExpReward: 80,
+ drops: [
+ { itemId: 'basement_key', chance: 1.0, count: { min: 1, max: 1 } },
+ { itemId: 'dog_skin', chance: 0.5, count: { min: 2, max: 4 } }
+ ],
+ isBoss: true
+ },
+
+ // ===== Lv.7 敌人 =====
+ cave_bat: {
+ id: 'cave_bat',
+ name: '洞穴蝙蝠',
+ level: 7,
+ baseStats: {
+ health: 40,
+ attack: 25,
+ defense: 3,
+ speed: 1.8
+ },
+ derivedStats: {
+ ap: 22,
+ ep: 24
+ },
+ expReward: 70,
+ skillExpReward: 35,
+ drops: [
+ { itemId: 'bat_wing', chance: 0.5, count: { min: 1, max: 2 } }
+ ]
+ },
+
+ // ===== Lv.10 Boss =====
+ cave_boss: {
+ id: 'cave_boss',
+ name: '洞穴领主',
+ level: 10,
+ baseStats: {
+ health: 500,
+ attack: 50,
+ defense: 25,
+ speed: 0.9
+ },
+ derivedStats: {
+ ap: 50,
+ ep: 25
+ },
+ expReward: 500,
+ skillExpReward: 200,
+ drops: [
+ { itemId: 'rare_gem', chance: 0.8, count: { min: 1, max: 1 } },
+ { itemId: 'iron_sword', chance: 0.3, count: { min: 1, max: 1 } }
],
isBoss: true
}
}
+
+/**
+ * 根据区域获取可用的敌人列表
+ * @param {String} locationId - 位置ID
+ * @returns {Array} 敌人ID列表
+ */
+export function getEnemiesForLocation(locationId) {
+ const locationEnemies = {
+ 'wild1': ['wild_dog', 'rat'],
+ 'wild2': ['wolf', 'bandit'],
+ 'wild3': ['cave_bat', 'wolf'],
+ 'boss_lair': ['test_boss'],
+ 'cave_depth': ['cave_bat', 'cave_boss']
+ }
+
+ return locationEnemies[locationId] || []
+}
+
+/**
+ * 获取位置的敌人完整配置
+ * @param {String} locationId - 位置ID
+ * @returns {Array} 敌人配置对象列表
+ */
+export function getEnemyConfigsForLocation(locationId) {
+ const enemyIds = getEnemiesForLocation(locationId)
+ return enemyIds.map(id => ENEMY_CONFIG[id]).filter(Boolean)
+}
+
+/**
+ * 根据等级获取推荐的敌人
+ * @param {Number} playerLevel - 玩家等级
+ * @returns {Array} 敌人ID列表
+ */
+export function getRecommendedEnemies(playerLevel) {
+ const enemies = []
+
+ for (const [id, enemy] of Object.entries(ENEMY_CONFIG)) {
+ const levelDiff = Math.abs(enemy.level - playerLevel)
+ // 推荐等级差在3以内的敌人
+ if (levelDiff <= 3 && !enemy.isBoss) {
+ enemies.push({ id, levelDiff, enemy })
+ }
+ }
+
+ // 按等级差排序
+ enemies.sort((a, b) => a.levelDiff - b.levelDiff)
+ return enemies.map(e => e.id)
+}
+
+/**
+ * 随机获取位置的一个敌人
+ * @param {String} locationId - 位置ID
+ * @returns {Object|null} 敌人配置
+ */
+export function getRandomEnemyForLocation(locationId) {
+ const enemyIds = getEnemiesForLocation(locationId)
+ if (enemyIds.length === 0) return null
+
+ const randomIndex = Math.floor(Math.random() * enemyIds.length)
+ const enemyId = enemyIds[randomIndex]
+ return ENEMY_CONFIG[enemyId] || null
+}
+
+/**
+ * 获取所有敌人列表
+ * @returns {Array} 所有敌人ID
+ */
+export function getAllEnemyIds() {
+ return Object.keys(ENEMY_CONFIG)
+}
+
+/**
+ * 获取Boss列表
+ * @returns {Array} Boss敌人ID
+ */
+export function getBossIds() {
+ return Object.entries(ENEMY_CONFIG)
+ .filter(([_, config]) => config.isBoss)
+ .map(([id, _]) => id)
+}
diff --git a/config/events.js b/config/events.js
index 47d940c..cec0b23 100644
--- a/config/events.js
+++ b/config/events.js
@@ -3,6 +3,8 @@
* Phase 6 核心系统实现
*/
+import { LOCATION_CONFIG } from './locations.js'
+
/**
* 事件配置
* 每个事件包含:
@@ -236,9 +238,56 @@ export const EVENT_CONFIG = {
title: '新区域',
textTemplate: '解锁了新区域:{areaName}\n\n{description}',
choices: [
- { text: '前往探索', next: null, action: 'teleport', actionData: { locationId: '{areaId}' } },
{ text: '稍后再说', next: null }
]
+ },
+
+ // 探索事件
+ explore_nothing: {
+ id: 'explore_nothing',
+ type: 'info',
+ title: '探索结果',
+ text: '你仔细搜索了这片区域,但没有发现任何有用的东西。',
+ choices: [
+ { text: '继续', next: null }
+ ]
+ },
+ explore_find_herb: {
+ id: 'explore_find_herb',
+ type: 'reward',
+ title: '探索发现',
+ text: '你在草丛中发现了一些草药!',
+ choices: [
+ { text: '收下', next: null, action: 'give_item', actionData: { itemId: 'healing_herb', count: { min: 1, max: 3 } } }
+ ]
+ },
+ explore_find_coin: {
+ id: 'explore_find_coin',
+ type: 'reward',
+ title: '探索发现',
+ textTemplate: '你在角落里发现了一些铜币!\n\n获得了 {amount} 铜币',
+ choices: [
+ { text: '太好了', next: null }
+ ]
+ },
+ explore_find_trash: {
+ id: 'explore_find_trash',
+ type: 'info',
+ title: '探索发现',
+ text: '你只找到了一些垃圾。',
+ choices: [
+ { text: '离开', next: null }
+ ]
+ },
+ explore_encounter: {
+ id: 'explore_encounter',
+ type: 'combat',
+ title: '遭遇',
+ textTemplate: '探索时突然遇到了{enemyName}!',
+ choices: [
+ { text: '迎战', next: null },
+ { text: '逃跑', next: null, action: 'flee' }
+ ]
}
}
diff --git a/config/items.js b/config/items.js
index 0517182..1c02938 100644
--- a/config/items.js
+++ b/config/items.js
@@ -1,21 +1,172 @@
// 物品配置
+// Phase 5 内容扩展 - 更多物品
export const ITEM_CONFIG = {
- // 武器
+ // ===== 武器 =====
wooden_stick: {
id: 'wooden_stick',
name: '木棍',
type: 'weapon',
subtype: 'one_handed',
icon: '🪵',
- baseValue: 10, // 基础价值(铜币)
+ baseValue: 10,
baseDamage: 5,
attackSpeed: 1.0,
- quality: 100, // 默认品质
+ quality: 100,
unlockSkill: 'stick_mastery',
description: '一根粗糙的木棍,至少比空手强。'
},
- // 消耗品
+ rusty_sword: {
+ id: 'rusty_sword',
+ name: '生锈铁剑',
+ type: 'weapon',
+ subtype: 'sword',
+ icon: '🗡️',
+ baseValue: 100,
+ baseDamage: 12,
+ attackSpeed: 1.1,
+ quality: 50,
+ unlockSkill: 'sword_mastery',
+ stats: { critRate: 2 },
+ description: '一把生锈的铁剑,虽然旧了但依然锋利。'
+ },
+
+ iron_sword: {
+ id: 'iron_sword',
+ name: '铁剑',
+ type: 'weapon',
+ subtype: 'sword',
+ icon: '⚔️',
+ baseValue: 500,
+ baseDamage: 25,
+ attackSpeed: 1.2,
+ quality: 100,
+ unlockSkill: 'sword_mastery',
+ stats: { critRate: 5 },
+ description: '一把精工打造的铁剑。'
+ },
+
+ wooden_club: {
+ id: 'wooden_club',
+ name: '木棒',
+ type: 'weapon',
+ subtype: 'blunt',
+ icon: '🏏',
+ baseValue: 30,
+ baseDamage: 8,
+ attackSpeed: 0.9,
+ quality: 80,
+ unlockSkill: 'blunt_mastery',
+ description: '一根粗大的木棒,攻击力强但速度慢。'
+ },
+
+ stone_axe: {
+ id: 'stone_axe',
+ name: '石斧',
+ type: 'weapon',
+ subtype: 'axe',
+ icon: '🪓',
+ baseValue: 80,
+ baseDamage: 18,
+ attackSpeed: 0.8,
+ quality: 60,
+ unlockSkill: 'axe_mastery',
+ stats: { critRate: 3 },
+ description: '用石头打磨成的斧头,笨重但有效。'
+ },
+
+ hunter_bow: {
+ id: 'hunter_bow',
+ name: '猎弓',
+ type: 'weapon',
+ subtype: 'ranged',
+ icon: '🏹',
+ baseValue: 200,
+ baseDamage: 15,
+ attackSpeed: 1.3,
+ quality: 90,
+ unlockSkill: 'archery',
+ stats: { accuracy: 10 },
+ description: '猎人使用的弓,可以远程攻击。'
+ },
+
+ // ===== 防具 =====
+ rag_armor: {
+ id: 'rag_armor',
+ name: '破布护甲',
+ type: 'armor',
+ subtype: 'light',
+ icon: '👕',
+ baseValue: 20,
+ defense: 3,
+ quality: 50,
+ description: '用破布拼凑成的简易护甲。'
+ },
+
+ leather_armor: {
+ id: 'leather_armor',
+ name: '皮甲',
+ type: 'armor',
+ subtype: 'light',
+ icon: '🦺',
+ baseValue: 150,
+ defense: 8,
+ quality: 100,
+ stats: { evasion: 5 },
+ description: '用兽皮制成的轻甲,提供基础保护。'
+ },
+
+ iron_armor: {
+ id: 'iron_armor',
+ name: '铁甲',
+ type: 'armor',
+ subtype: 'heavy',
+ icon: '🛡️',
+ baseValue: 800,
+ defense: 20,
+ quality: 100,
+ stats: { maxStamina: -10 }, // 重量影响耐力
+ description: '铁制重甲,防御力强但会影响行动。'
+ },
+
+ // ===== 盾牌 =====
+ wooden_shield: {
+ id: 'wooden_shield',
+ name: '木盾',
+ type: 'shield',
+ icon: '🛡️',
+ baseValue: 50,
+ defense: 5,
+ blockRate: 10,
+ quality: 80,
+ description: '简单的木制盾牌。'
+ },
+
+ iron_shield: {
+ id: 'iron_shield',
+ name: '铁盾',
+ type: 'shield',
+ icon: '🛡️',
+ baseValue: 300,
+ defense: 12,
+ blockRate: 20,
+ quality: 100,
+ description: '坚固的铁制盾牌。'
+ },
+
+ // ===== 饰品 =====
+ lucky_ring: {
+ id: 'lucky_ring',
+ name: '幸运戒指',
+ type: 'accessory',
+ icon: '💍',
+ baseValue: 200,
+ quality: 100,
+ stats: { critRate: 5, fleeRate: 5 },
+ description: '一枚带来幸运的戒指。'
+ },
+
+ // ===== 消耗品 - 食物 =====
bread: {
id: 'bread',
name: '面包',
@@ -24,13 +175,63 @@ export const ITEM_CONFIG = {
icon: '🍞',
baseValue: 10,
effect: {
- stamina: 20
+ stamina: 20,
+ health: 5
},
- description: '普通的面包,可以恢复耐力。',
+ description: '普通的面包,可以恢复耐力和少量生命。',
stackable: true,
maxStack: 99
},
+ meat: {
+ id: 'meat',
+ name: '肉干',
+ type: 'consumable',
+ subtype: 'food',
+ icon: '🥩',
+ baseValue: 15,
+ effect: {
+ stamina: 35,
+ health: 10
+ },
+ description: '风干的肉,营养丰富。',
+ stackable: true,
+ maxStack: 50
+ },
+
+ cooked_meat: {
+ id: 'cooked_meat',
+ name: '烤肉',
+ type: 'consumable',
+ subtype: 'food',
+ icon: '🍖',
+ baseValue: 25,
+ effect: {
+ stamina: 50,
+ health: 20
+ },
+ description: '烤制的肉,美味又营养。',
+ stackable: true,
+ maxStack: 50
+ },
+
+ fresh_water: {
+ id: 'fresh_water',
+ name: '清水',
+ type: 'consumable',
+ subtype: 'drink',
+ icon: '💧',
+ baseValue: 5,
+ effect: {
+ stamina: 10,
+ sanity: 5
+ },
+ description: '干净的清水,解渴提神。',
+ stackable: true,
+ maxStack: 99
+ },
+
+ // ===== 消耗品 - 药品 =====
healing_herb: {
id: 'healing_herb',
name: '草药',
@@ -39,30 +240,123 @@ export const ITEM_CONFIG = {
icon: '🌿',
baseValue: 15,
effect: {
- health: 15
+ health: 20
},
description: '常见的治疗草药,可以恢复生命值。',
stackable: true,
maxStack: 99
},
- // 书籍
+ bandage: {
+ id: 'bandage',
+ name: '绷带',
+ type: 'consumable',
+ subtype: 'medicine',
+ icon: '🩹',
+ baseValue: 20,
+ effect: {
+ health: 30,
+ stamina: 5
+ },
+ description: '急救用的绷带,可以止血。',
+ stackable: true,
+ maxStack: 50
+ },
+
+ health_potion_small: {
+ id: 'health_potion_small',
+ name: '小治疗药水',
+ type: 'consumable',
+ subtype: 'medicine',
+ icon: '🧪',
+ baseValue: 50,
+ effect: {
+ health: 50
+ },
+ description: '小瓶治疗药水,快速恢复生命值。',
+ stackable: true,
+ maxStack: 20
+ },
+
+ health_potion: {
+ id: 'health_potion',
+ name: '治疗药水',
+ type: 'consumable',
+ subtype: 'medicine',
+ icon: '🧪',
+ baseValue: 150,
+ effect: {
+ health: 100
+ },
+ description: '治疗药水,大幅恢复生命值。',
+ stackable: true,
+ maxStack: 10
+ },
+
+ // ===== 书籍 =====
old_book: {
id: 'old_book',
name: '破旧书籍',
type: 'book',
icon: '📖',
baseValue: 50,
- readingTime: 60, // 秒
+ readingTime: 60,
expReward: {
reading: 10
},
completionBonus: null,
description: '一本破旧的书籍,记录着一些基础知识。',
- consumable: false // 书籍不消耗
+ consumable: false
},
- // 素材
+ survival_guide: {
+ id: 'survival_guide',
+ name: '生存指南',
+ type: 'book',
+ icon: '📕',
+ baseValue: 100,
+ readingTime: 120,
+ expReward: {
+ reading: 25,
+ survival_instinct: 5
+ },
+ completionBonus: { maxStamina: 10 },
+ description: '荒野生存技巧指南。',
+ consumable: false
+ },
+
+ combat_manual: {
+ id: 'combat_manual',
+ name: '战斗手册',
+ type: 'book',
+ icon: '📗',
+ baseValue: 150,
+ readingTime: 180,
+ expReward: {
+ reading: 30
+ },
+ completionBonus: { critRate: 3 },
+ description: '记录战斗技巧的手册。',
+ consumable: false
+ },
+
+ herbalism_book: {
+ id: 'herbalism_book',
+ name: '草药图鉴',
+ type: 'book',
+ icon: '📙',
+ baseValue: 120,
+ readingTime: 150,
+ expReward: {
+ reading: 20,
+ herbalism: 10
+ },
+ completionBonus: null,
+ description: '识别和采集草药的图鉴。',
+ consumable: false
+ },
+
+ // ===== 素材 =====
dog_skin: {
id: 'dog_skin',
name: '狗皮',
@@ -74,7 +368,74 @@ export const ITEM_CONFIG = {
maxStack: 99
},
- // 关键道具
+ wolf_fang: {
+ id: 'wolf_fang',
+ name: '狼牙',
+ type: 'material',
+ icon: '🦷',
+ baseValue: 20,
+ description: '锋利的狼牙,可用于制作武器。',
+ stackable: true,
+ maxStack: 99
+ },
+
+ bat_wing: {
+ id: 'bat_wing',
+ name: '蝙蝠翼',
+ type: 'material',
+ icon: '🦇',
+ baseValue: 15,
+ description: '蝙蝠的翅膀,有特殊用途。',
+ stackable: true,
+ maxStack: 99
+ },
+
+ leather: {
+ id: 'leather',
+ name: '皮革',
+ type: 'material',
+ icon: '🟤',
+ baseValue: 30,
+ description: '加工过的兽皮,可用于制作装备。',
+ stackable: true,
+ maxStack: 99
+ },
+
+ iron_ore: {
+ id: 'iron_ore',
+ name: '铁矿石',
+ type: 'material',
+ icon: '⛰️',
+ baseValue: 50,
+ description: '含铁的矿石,可以提炼金属。',
+ stackable: true,
+ maxStack: 99
+ },
+
+ rare_gem: {
+ id: 'rare_gem',
+ name: '稀有宝石',
+ type: 'material',
+ icon: '💎',
+ baseValue: 500,
+ description: '闪闪发光的宝石,价值不菲。',
+ stackable: true,
+ maxStack: 10
+ },
+
+ // ===== 货币 =====
+ copper_coin: {
+ id: 'copper_coin',
+ name: '铜币',
+ type: 'currency',
+ icon: '🪙',
+ baseValue: 1,
+ description: '通用的货币单位。',
+ stackable: true,
+ maxStack: 9999
+ },
+
+ // ===== 关键道具 =====
basement_key: {
id: 'basement_key',
name: '地下室钥匙',
@@ -83,5 +444,71 @@ export const ITEM_CONFIG = {
baseValue: 0,
description: '一把生锈的钥匙,上面刻着「B」字母。',
stackable: false
+ },
+
+ cave_key: {
+ id: 'cave_key',
+ name: '洞穴钥匙',
+ type: 'key',
+ icon: '🗝️',
+ baseValue: 0,
+ description: '开启深处洞穴的钥匙。',
+ stackable: false
+ },
+
+ mystic_key: {
+ id: 'mystic_key',
+ name: '神秘钥匙',
+ type: 'key',
+ icon: '🔮',
+ baseValue: 1000,
+ description: '一把散发着神秘光芒的钥匙,似乎能打开某扇重要的门。',
+ stackable: false,
+ keyItem: true
+ },
+
+ // ===== 特殊物品 =====
+ bomb: {
+ id: 'bomb',
+ name: '炸弹',
+ type: 'special',
+ subtype: 'explosive',
+ icon: '💣',
+ baseValue: 100,
+ description: '可以造成范围伤害的爆炸物,在战斗中特别有效。',
+ stackable: true,
+ maxStack: 10,
+ effect: {
+ damage: 50,
+ radius: 1
+ },
+ consumable: true
+ },
+
+ bible: {
+ id: 'bible',
+ name: '圣经',
+ type: 'special',
+ icon: '📿',
+ baseValue: 0,
+ description: '一本神圣的书籍,可以用来祈祷。',
+ stackable: false,
+ effect: { sanity: 10 }
}
}
+
+/**
+ * 获取物品商店分类
+ * @returns {Object} 分类列表
+ */
+export const ITEM_CATEGORIES = {
+ weapon: { id: 'weapon', name: '武器', icon: '⚔️' },
+ armor: { id: 'armor', name: '防具', icon: '🛡️' },
+ shield: { id: 'shield', name: '盾牌', icon: '🛡️' },
+ accessory: { id: 'accessory', name: '饰品', icon: '💍' },
+ consumable: { id: 'consumable', name: '消耗品', icon: '🧪' },
+ book: { id: 'book', name: '书籍', icon: '📖' },
+ material: { id: 'material', name: '素材', icon: '📦' },
+ key: { id: 'key', name: '钥匙', icon: '🔑' },
+ special: { id: 'special', name: '特殊', icon: '✨' }
+}
diff --git a/config/locations.js b/config/locations.js
index 78eea87..f9ba6d1 100644
--- a/config/locations.js
+++ b/config/locations.js
@@ -8,7 +8,7 @@ export const LOCATION_CONFIG = {
description: '一个临时的幸存者营地,相对安全。',
connections: ['market', 'blackmarket', 'wild1'],
npcs: ['injured_adventurer'],
- activities: ['rest', 'talk', 'trade']
+ activities: ['rest', 'talk', 'trade', 'crafting']
},
market: {
diff --git a/config/skills.js b/config/skills.js
index e927d67..5165f40 100644
--- a/config/skills.js
+++ b/config/skills.js
@@ -49,5 +49,67 @@ export const SKILL_CONFIG = {
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
},
unlockCondition: { location: 'basement' }
+ },
+
+ // ===== 制造技能 =====
+ crafting: {
+ id: 'crafting',
+ name: '制造',
+ type: 'life',
+ category: 'crafting',
+ icon: '🔨',
+ maxLevel: 20,
+ expPerLevel: (level) => level * 80,
+ parentSkill: null,
+ milestones: {
+ 1: { desc: '解锁基础制造配方', effect: {} },
+ 3: { desc: '制造时间-10%', effect: { craftingSpeed: 0.1 } },
+ 5: { desc: '所有制造成功率+5%', effect: { craftingSuccessRate: 5 } },
+ 10: { desc: '制造时间-25%', effect: { craftingSpeed: 0.25 } },
+ 15: { desc: '所有制造成功率+10%', effect: { craftingSuccessRate: 10 } },
+ 20: { desc: '制造品质+10', effect: { craftingQuality: 10 } }
+ },
+ unlockCondition: null
+ },
+
+ blacksmith: {
+ id: 'blacksmith',
+ name: '锻造',
+ type: 'life',
+ category: 'crafting',
+ icon: '⚒️',
+ maxLevel: 15,
+ expPerLevel: (level) => level * 120,
+ parentSkill: 'crafting',
+ milestones: {
+ 1: { desc: '解锁武器锻造', effect: {} },
+ 5: { desc: '武器品质+15', effect: { weaponQuality: 15 } },
+ 10: { desc: '防具品质+15', effect: { armorQuality: 15 } },
+ 15: { desc: '所有锻造成功率+15%', effect: { smithingSuccessRate: 15 } }
+ },
+ unlockCondition: {
+ type: 'skill',
+ skillId: 'crafting',
+ level: 5
+ }
+ },
+
+ herbalism: {
+ id: 'herbalism',
+ name: '草药学',
+ type: 'life',
+ category: 'crafting',
+ icon: '🌿',
+ maxLevel: 15,
+ expPerLevel: (level) => level * 60,
+ parentSkill: null,
+ milestones: {
+ 1: { desc: '解锁药水制作', effect: {} },
+ 3: { desc: '药水效果+20%', effect: { potionEffect: 1.2 } },
+ 5: { desc: '解锁高级药水', effect: {} },
+ 10: { desc: '药水效果+50%', effect: { potionEffect: 1.5 } },
+ 15: { desc: '所有制药成功率+20%', effect: { herbingSuccessRate: 20 } }
+ },
+ unlockCondition: null
}
}
diff --git a/pages/index/index.vue b/pages/index/index.vue
index 09556d7..b24cb3d 100644
--- a/pages/index/index.vue
+++ b/pages/index/index.vue
@@ -43,6 +43,14 @@
>
+
+
+
+
+
@@ -58,6 +66,7 @@ import LogPanel from '@/components/panels/LogPanel.vue'
import InventoryDrawer from '@/components/drawers/InventoryDrawer.vue'
import EventDrawer from '@/components/drawers/EventDrawer.vue'
import ShopDrawer from '@/components/drawers/ShopDrawer.vue'
+import CraftingDrawer from '@/components/drawers/CraftingDrawer.vue'
const game = useGameStore()
const player = usePlayerStore()
diff --git a/store/game.js b/store/game.js
index 2aa736d..7d1bffc 100644
--- a/store/game.js
+++ b/store/game.js
@@ -9,7 +9,8 @@ export const useGameStore = defineStore('game', () => {
const drawerState = ref({
inventory: false,
event: false,
- shop: false
+ shop: false,
+ crafting: false
})
// 日志
diff --git a/uni.scss b/uni.scss
index 90c4383..7867b8a 100644
--- a/uni.scss
+++ b/uni.scss
@@ -92,6 +92,7 @@ $accent: #4ecdc4;
$danger: #ff6b6b;
$warning: #ffe66d;
$success: #4ade80;
+$info: #60a5fa;
/* 品质颜色 */
$quality-trash: #808080;
diff --git a/utils/combatSystem.js b/utils/combatSystem.js
index 6e0843a..bf4cad3 100644
--- a/utils/combatSystem.js
+++ b/utils/combatSystem.js
@@ -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
diff --git a/utils/environmentSystem.js b/utils/environmentSystem.js
index c5917ec..64c512c 100644
--- a/utils/environmentSystem.js
+++ b/utils/environmentSystem.js
@@ -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 }
diff --git a/utils/eventSystem.js b/utils/eventSystem.js
index cffa030..9323a66 100644
--- a/utils/eventSystem.js
+++ b/utils/eventSystem.js
@@ -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: '逃跑失败' }
+ }
+}
diff --git a/utils/gameLoop.js b/utils/gameLoop.js
index bf08c4d..08a491a 100644
--- a/utils/gameLoop.js
+++ b/utils/gameLoop.js
@@ -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')
}
diff --git a/utils/levelingSystem.js b/utils/levelingSystem.js
new file mode 100644
index 0000000..50d8806
--- /dev/null
+++ b/utils/levelingSystem.js
@@ -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
+}
diff --git a/utils/skillSystem.js b/utils/skillSystem.js
index f2ec37c..1ca5b82 100644
--- a/utils/skillSystem.js
+++ b/utils/skillSystem.js
@@ -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
diff --git a/utils/soundSystem.js b/utils/soundSystem.js
new file mode 100644
index 0000000..7e48c0b
--- /dev/null
+++ b/utils/soundSystem.js
@@ -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()
diff --git a/utils/taskSystem.js b/utils/taskSystem.js
index 55e55c0..021b8d3 100644
--- a/utils/taskSystem.js
+++ b/utils/taskSystem.js
@@ -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
}
diff --git a/计划第二步.md b/计划第二步.md
new file mode 100644
index 0000000..2094d38
--- /dev/null
+++ b/计划第二步.md
@@ -0,0 +1,45 @@
+接下来开发计划
+阶段1:修复代码问题(优先)
+# 任务 文件
+1 添加gameStore中shop抽屉状态 store/game.js
+2 修复eventSystem中SKILL_CONFIG导入 utils/eventSystem.js
+3 修复environmentSystem中里程碑奖励调用 utils/environmentSystem.js
+4 统一击杀数存储方式 utils/eventSystem.js, components/panels/MapPanel.vue
+5 InventoryDrawer使用itemSystem函数 components/drawers/InventoryDrawer.vue
+6 任务完成使用addSkillExp utils/taskSystem.js
+阶段2:补充未实现功能
+# 功能 描述
+1 阅读功能 将书籍任务与UI关联,显示阅读进度
+2 训练功能 添加训练任务UI,技能训练
+3 任务UI面板 显示活动中的挂机任务进度
+4 装备属性生效 确保装备属性正确应用到战斗计算
+5 探索功能 添加探索事件触发
+阶段3:UI美化
+# 内容 说明
+1 状态面板优化 添加属性详情展开/折叠
+2 战斗视觉反馈 伤害数字跳动、暴击动画
+3 品质特效 不同品质物品的光效/边框
+4 日志颜色优化 更精细的日志类型着色
+5 技能面板 里程碑奖励可视化显示
+6 过渡动画 页面切换、抽屉弹出动画
+阶段4:数值调整
+# 内容 当前 建议
+1 初始属性 全部10 按角色差异化
+2 敌人伤害 野狗攻击8 对新手可能偏高
+3 经验曲线 线性增长 考虑指数衰减
+4 耐力消耗 战斗2/秒 平衡性测试
+5 物品价格 基础价格10-50 经济平衡
+6 品质概率 随机50-150 高品质概率过低
+阶段5:内容扩展(剧情之前先讨论)
+# 内容 说明
+1 更多武器类型 剑、斧、法杖等
+2 更多消耗品 药水、食物种类
+3 更多敌人 不同区域的怪物
+4 更多被动技能 适应不同环境
+5 成就系统 记录玩家成就
+🎯 建议优先处理顺序
+立即修复:代码逻辑问题(阶段1)
+核心功能补全:阅读、训练、任务UI(阶段2)
+数值平衡测试:实际游玩体验后调整(阶段4)
+UI美化:在功能稳定后进行(阶段3)
+剧情讨论:最后我们一起讨论完善(阶段5)
\ No newline at end of file