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:
@@ -12,7 +12,8 @@
|
|||||||
"Bash(git config:*)",
|
"Bash(git config:*)",
|
||||||
"Bash(git remote add:*)",
|
"Bash(git remote add:*)",
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)"
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
181
App.vue
181
App.vue
@@ -243,4 +243,185 @@ page {
|
|||||||
background-color: $bg-tertiary;
|
background-color: $bg-tertiary;
|
||||||
border-radius: 4rpx;
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<view
|
<view
|
||||||
class="text-button"
|
class="text-button"
|
||||||
:class="[`text-button--${type}`, { 'text-button--disabled': disabled }]"
|
:class="[
|
||||||
|
`text-button--${type}`,
|
||||||
|
{
|
||||||
|
'text-button--disabled': disabled,
|
||||||
|
'text-button--loading': loading
|
||||||
|
}
|
||||||
|
]"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<text>{{ text }}</text>
|
<text v-if="!loading" class="text-button__text">{{ text }}</text>
|
||||||
|
<view v-else class="text-button__spinner"></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -12,13 +19,14 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
type: { type: String, default: 'default' },
|
type: { type: String, default: 'default' },
|
||||||
disabled: { type: Boolean, default: false }
|
disabled: { type: Boolean, default: false },
|
||||||
|
loading: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['click'])
|
const emit = defineEmits(['click'])
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (!props.disabled) {
|
if (!props.disabled && !props.loading) {
|
||||||
emit('click')
|
emit('click')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,43 +34,133 @@ function handleClick() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.text-button {
|
.text-button {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 16rpx 32rpx;
|
padding: 16rpx 32rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
transition: all 0.2s;
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
// 涟漪效果
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: width 0.3s, height 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active::after {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&--default {
|
&--default {
|
||||||
background-color: $bg-tertiary;
|
background-color: $bg-tertiary;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
|
border: 1rpx solid transparent;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: $bg-secondary;
|
background-color: $bg-secondary;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
background-color: $accent;
|
||||||
|
color: #fff;
|
||||||
|
border: 1rpx solid $accent;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba($accent, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: darken($accent, 10%);
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 6rpx rgba($accent, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--danger {
|
&--danger {
|
||||||
background-color: rgba($danger, 0.2);
|
background-color: rgba($danger, 0.15);
|
||||||
color: $danger;
|
color: $danger;
|
||||||
|
border: 1rpx solid rgba($danger, 0.3);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: rgba($danger, 0.3);
|
background-color: rgba($danger, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
background-color: rgba($warning, 0.2);
|
background-color: rgba($warning, 0.15);
|
||||||
color: $warning;
|
color: $warning;
|
||||||
|
border: 1rpx solid rgba($warning, 0.3);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: rgba($warning, 0.3);
|
background-color: rgba($warning, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background-color: rgba($success, 0.15);
|
||||||
|
color: $success;
|
||||||
|
border: 1rpx solid rgba($success, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba($success, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
background-color: rgba($info, 0.15);
|
||||||
|
color: $info;
|
||||||
|
border: 1rpx solid rgba($info, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba($info, 0.25);
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--disabled {
|
&--disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
filter: grayscale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,8 +21,9 @@
|
|||||||
>
|
>
|
||||||
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
|
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
|
||||||
<view class="inventory-item__info">
|
<view class="inventory-item__info">
|
||||||
<text class="inventory-item__name" :style="{ color: qualityColor(item.quality) }">
|
<text class="inventory-item__name" :style="{ color: item.qualityColor || '#fff' }">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
|
<text v-if="item.qualityName" class="quality-badge"> [{{ item.qualityName }}]</text>
|
||||||
</text>
|
</text>
|
||||||
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { useGameStore } from '@/store/game'
|
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 FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
import TextButton from '@/components/common/TextButton.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)
|
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() {
|
function close() {
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
@@ -148,30 +140,10 @@ function selectItem(item) {
|
|||||||
|
|
||||||
function equipItem() {
|
function equipItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
const item = selectedItem.value
|
const result = equipItemUtil(player, game, selectedItem.value.uniqueId)
|
||||||
const slot = slotMap[item.type]
|
if (result.success) {
|
||||||
|
selectedItem.value = null
|
||||||
if (!slot) return
|
|
||||||
|
|
||||||
// 如果该槽位已有装备,先卸下旧装备放回背包
|
|
||||||
const oldEquip = player.equipment[slot]
|
|
||||||
if (oldEquip) {
|
|
||||||
player.inventory.push(oldEquip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从背包中移除该物品
|
|
||||||
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() {
|
function unequipItem() {
|
||||||
@@ -181,57 +153,19 @@ function unequipItem() {
|
|||||||
|
|
||||||
if (!slot) return
|
if (!slot) return
|
||||||
|
|
||||||
// 从装备槽移除
|
const result = unequipItemBySlot(player, game, slot)
|
||||||
player.equipment[slot] = null
|
if (result.success) {
|
||||||
|
selectedItem.value = null
|
||||||
// 放回背包
|
}
|
||||||
player.inventory.push(item)
|
|
||||||
|
|
||||||
game.addLog(`卸下了 ${item.name}`, 'info')
|
|
||||||
selectedItem.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useItem() {
|
function useItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
const item = selectedItem.value
|
const result = useItemUtil(player, game, selectedItem.value.id, 1)
|
||||||
|
if (result.success) {
|
||||||
// 检查是否是消耗品
|
// 如果阅读任务,可以在这里处理
|
||||||
if (item.type !== 'consumable') return
|
selectedItem.value = null
|
||||||
|
|
||||||
// 应用效果
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从背包中移除(如果是堆叠物品,减少数量)
|
|
||||||
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() {
|
function sellItem() {
|
||||||
@@ -322,6 +256,13 @@ function sellItem() {
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
margin-top: 4rpx;
|
margin-top: 4rpx;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-badge {
|
||||||
|
font-size: 18rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
&__equipped-badge {
|
&__equipped-badge {
|
||||||
width: 32rpx;
|
width: 32rpx;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<view v-if="visible" class="drawer" @click="handleMaskClick">
|
<view
|
||||||
|
v-if="visible"
|
||||||
|
class="drawer"
|
||||||
|
:class="{ 'drawer--visible': visible }"
|
||||||
|
@click="handleMaskClick"
|
||||||
|
>
|
||||||
<view class="drawer__content" :style="{ width }" @click.stop>
|
<view class="drawer__content" :style="{ width }" @click.stop>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</view>
|
</view>
|
||||||
@@ -9,7 +14,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
width: { type: String, default: '600rpx' }
|
width: { type: String, default: '600rpx' },
|
||||||
|
position: { type: String, default: 'right' } // right, left, top, bottom
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
@@ -26,9 +32,20 @@ function handleMaskClick() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0,0,0,0.6);
|
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -36,14 +53,70 @@ function handleMaskClick() {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: $bg-secondary;
|
background-color: $bg-secondary;
|
||||||
border-left: 1rpx solid $border-color;
|
border-left: 1rpx solid $border-color;
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-shadow: -4rpx 0 20rpx rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideInRight 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
// 不同的位置动画
|
||||||
from { transform: translateX(100%); }
|
.drawer--left {
|
||||||
to { transform: translateX(0); }
|
.drawer__content {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: 1rpx solid $border-color;
|
||||||
|
animation: slideInLeft 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
box-shadow: 4rpx 0 20rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭动画 (需要配合Vue transition组件实现更复杂的动画)
|
||||||
|
.drawer.drawer--closing {
|
||||||
|
&::before {
|
||||||
|
animation: fadeOut 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer__content {
|
||||||
|
animation: slideOutRight 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:class="`log-item--${log.type}`"
|
:class="`log-item--${log.type}`"
|
||||||
:id="'log-' + log.id"
|
:id="'log-' + log.id"
|
||||||
>
|
>
|
||||||
|
<text class="log-item__icon">{{ getLogIcon(log.type) }}</text>
|
||||||
<text class="log-item__time">[{{ log.time }}]</text>
|
<text class="log-item__time">[{{ log.time }}]</text>
|
||||||
<text class="log-item__message">{{ log.message }}</text>
|
<text class="log-item__message">{{ log.message }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -45,52 +46,88 @@ watch(lastLogId, (newId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 获取日志类型图标
|
||||||
|
function getLogIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
combat: '⚔️',
|
||||||
|
system: '🔔',
|
||||||
|
reward: '🎁',
|
||||||
|
info: 'ℹ️',
|
||||||
|
warning: '⚠️',
|
||||||
|
error: '❌',
|
||||||
|
success: '✅',
|
||||||
|
story: '📖'
|
||||||
|
}
|
||||||
|
return icons[type] || '•'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.log-panel {
|
.log-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 16rpx;
|
padding: 12rpx 16rpx;
|
||||||
background-color: $bg-primary;
|
background-color: $bg-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-item {
|
.log-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: flex-start;
|
||||||
padding: 8rpx 0;
|
padding: 8rpx 12rpx;
|
||||||
border-bottom: 1rpx solid $bg-tertiary;
|
margin-bottom: 4rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: $bg-secondary;
|
||||||
|
border-left: 3rpx solid transparent;
|
||||||
|
animation: slideIn 0.2s ease;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__time {
|
&__time {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
font-size: 22rpx;
|
font-size: 20rpx;
|
||||||
margin-right: 8rpx;
|
margin-right: 8rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不同类型的日志样式
|
// 不同类型的日志样式
|
||||||
&--combat {
|
&--combat {
|
||||||
|
background-color: rgba($danger, 0.1);
|
||||||
|
border-left-color: $danger;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--system {
|
&--system {
|
||||||
|
background-color: rgba($accent, 0.1);
|
||||||
|
border-left-color: $accent;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--reward {
|
&--reward {
|
||||||
|
background-color: rgba($warning, 0.1);
|
||||||
|
border-left-color: $warning;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $warning;
|
color: $warning;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,25 +138,55 @@ watch(lastLogId, (newId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
|
background-color: rgba($warning, 0.1);
|
||||||
|
border-left-color: $warning;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $warning;
|
color: $warning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
|
background-color: rgba($danger, 0.15);
|
||||||
|
border-left-color: $danger;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $danger;
|
color: $danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--success {
|
&--success {
|
||||||
|
background-color: rgba($success, 0.1);
|
||||||
|
border-left-color: $success;
|
||||||
|
|
||||||
.log-item__message {
|
.log-item__message {
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--story {
|
||||||
|
background-color: rgba($info, 0.1);
|
||||||
|
border-left-color: $info;
|
||||||
|
|
||||||
|
.log-item__message {
|
||||||
|
color: $info;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-anchor {
|
.log-anchor {
|
||||||
height: 1rpx;
|
height: 1rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -105,9 +105,10 @@ import { useGameStore } from '@/store/game'
|
|||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { LOCATION_CONFIG } from '@/config/locations'
|
import { LOCATION_CONFIG } from '@/config/locations'
|
||||||
import { NPC_CONFIG } from '@/config/npcs.js'
|
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 { getShopConfig } from '@/config/shop.js'
|
||||||
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
||||||
|
import { triggerExploreEvent, tryFlee } from '@/utils/eventSystem.js'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
@@ -151,10 +152,10 @@ const availableLocations = computed(() => {
|
|||||||
// 检查解锁条件
|
// 检查解锁条件
|
||||||
if (loc.unlockCondition) {
|
if (loc.unlockCondition) {
|
||||||
if (loc.unlockCondition.type === 'kill') {
|
if (loc.unlockCondition.type === 'kill') {
|
||||||
// 检查击杀条件(需要追踪击杀数)
|
// 检查击杀条件(使用统一的killCount存储)
|
||||||
const killCount = player.flags[`kill_${loc.unlockCondition.target}`] || 0
|
const killCount = (player.killCount && player.killCount[loc.unlockCondition.target]) || 0
|
||||||
locked = killCount < loc.unlockCondition.count
|
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') {
|
} else if (loc.unlockCondition.type === 'item') {
|
||||||
// 检查物品条件
|
// 检查物品条件
|
||||||
const hasItem = player.inventory.some(i => i.id === loc.unlockCondition.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' },
|
trade: { id: 'trade', label: '交易', type: 'default' },
|
||||||
explore: { id: 'explore', label: '探索', type: 'warning' },
|
explore: { id: 'explore', label: '探索', type: 'warning' },
|
||||||
combat: { id: 'combat', label: '战斗', type: 'danger' },
|
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
|
break
|
||||||
case 'explore':
|
case 'explore':
|
||||||
game.addLog('开始探索...', 'info')
|
game.addLog('开始探索...', 'info')
|
||||||
// 探索逻辑
|
const exploreResult = triggerExploreEvent(game, player)
|
||||||
|
if (!exploreResult.success) {
|
||||||
|
game.addLog(exploreResult.message, 'info')
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'combat':
|
case 'combat':
|
||||||
startCombat()
|
startCombat()
|
||||||
break
|
break
|
||||||
case 'flee':
|
case 'flee':
|
||||||
game.addLog('尝试逃跑...', 'combat')
|
game.addLog('尝试逃跑...', 'combat')
|
||||||
// 逃跑逻辑
|
const fleeResult = tryFlee(game, player)
|
||||||
|
if (!fleeResult.success) {
|
||||||
|
// 逃跑失败,玩家会多受到一次攻击(在战斗循环中处理)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'read':
|
case 'read':
|
||||||
game.addLog('找个安静的地方阅读...', 'info')
|
game.addLog('找个安静的地方阅读...', 'info')
|
||||||
break
|
break
|
||||||
|
case 'crafting':
|
||||||
|
game.drawerState.crafting = true
|
||||||
|
game.addLog('打开制造界面...', 'info')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCombat() {
|
function startCombat() {
|
||||||
const current = LOCATION_CONFIG[player.currentLocation]
|
// 使用新的敌人获取函数
|
||||||
if (!current || !current.enemies || current.enemies.length === 0) {
|
const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
|
||||||
game.addLog('这里没有敌人', 'info')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const enemyId = current.enemies[0]
|
|
||||||
const enemyConfig = ENEMY_CONFIG[enemyId]
|
|
||||||
|
|
||||||
if (!enemyConfig) {
|
if (!enemyConfig) {
|
||||||
game.addLog('敌人配置错误', 'error')
|
game.addLog('这里没有敌人', 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,8 +343,8 @@ function startCombat() {
|
|||||||
// 获取环境类型
|
// 获取环境类型
|
||||||
const environment = getEnvironmentType(player.currentLocation)
|
const environment = getEnvironmentType(player.currentLocation)
|
||||||
|
|
||||||
// 使用 initCombat 初始化战斗(包含 expReward 和 drops)
|
// 使用 initCombat 初始化战斗
|
||||||
game.combatState = initCombat(enemyId, enemyConfig, environment)
|
game.combatState = initCombat(enemyConfig.id, enemyConfig, environment)
|
||||||
game.inCombat = true
|
game.inCombat = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,8 +374,8 @@ function toggleAutoCombat() {
|
|||||||
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
|
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
|
||||||
// 如果当前不在战斗中且在危险区域,立即开始战斗
|
// 如果当前不在战斗中且在危险区域,立即开始战斗
|
||||||
if (!game.inCombat) {
|
if (!game.inCombat) {
|
||||||
const current = LOCATION_CONFIG[player.currentLocation]
|
const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
|
||||||
if (current && current.enemies && current.enemies.length > 0) {
|
if (enemyConfig) {
|
||||||
startCombat()
|
startCombat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,21 +145,137 @@
|
|||||||
<text class="skill-empty__text">暂无技能</text>
|
<text class="skill-empty__text">暂无技能</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 书籍区域 -->
|
||||||
|
<Collapse title="📚 书籍" :expanded="showBooks" @toggle="showBooks = $event">
|
||||||
|
<!-- 活动中的阅读任务 -->
|
||||||
|
<view v-if="readingTask" class="reading-task">
|
||||||
|
<text class="reading-task__title">正在阅读:{{ readingTask.bookName }}</text>
|
||||||
|
<ProgressBar
|
||||||
|
:value="readingTask.progress"
|
||||||
|
:max="readingTask.totalTime"
|
||||||
|
color="#4ecdc4"
|
||||||
|
:showText="true"
|
||||||
|
height="16rpx"
|
||||||
|
/>
|
||||||
|
<text class="reading-task__time">{{ readingTimeText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 可阅读的书籍列表 -->
|
||||||
|
<view v-if="!readingTask && ownedBooks.length > 0" class="books-list">
|
||||||
|
<view
|
||||||
|
v-for="book in ownedBooks"
|
||||||
|
:key="book.id"
|
||||||
|
class="book-item"
|
||||||
|
@click="startReading(book)"
|
||||||
|
>
|
||||||
|
<text class="book-item__icon">{{ book.icon || '📖' }}</text>
|
||||||
|
<view class="book-item__info">
|
||||||
|
<text class="book-item__name">{{ book.name }}</text>
|
||||||
|
<text class="book-item__desc">阅读需 {{ book.readingTime }}秒</text>
|
||||||
|
</view>
|
||||||
|
<TextButton text="阅读" type="primary" size="small" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!readingTask && ownedBooks.length === 0" class="books-empty">
|
||||||
|
<text class="books-empty__text">暂无书籍,去商店看看吧</text>
|
||||||
|
</view>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<!-- 训练区域 -->
|
||||||
|
<Collapse title="⚔️ 训练" :expanded="showTraining" @toggle="showTraining = $event">
|
||||||
|
<!-- 活动中的训练任务 -->
|
||||||
|
<view v-if="trainingTask" class="training-task">
|
||||||
|
<text class="training-task__title">正在训练:{{ trainingTask.skillName }}</text>
|
||||||
|
<ProgressBar
|
||||||
|
:value="trainingTask.progress"
|
||||||
|
:max="trainingTask.totalTime || 300"
|
||||||
|
color="#ff6b6b"
|
||||||
|
:showText="true"
|
||||||
|
height="16rpx"
|
||||||
|
/>
|
||||||
|
<TextButton text="停止训练" type="warning" size="small" @click="stopTraining" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 可训练的技能列表 -->
|
||||||
|
<view v-if="!trainingTask && trainableSkills.length > 0" class="trainable-list">
|
||||||
|
<view
|
||||||
|
v-for="skill in trainableSkills"
|
||||||
|
:key="skill.id"
|
||||||
|
class="trainable-item"
|
||||||
|
>
|
||||||
|
<text class="trainable-item__icon">{{ skill.icon || '⚔️' }}</text>
|
||||||
|
<view class="trainable-item__info">
|
||||||
|
<text class="trainable-item__name">{{ skill.name }}</text>
|
||||||
|
<text class="trainable-item__level">Lv.{{ skill.level }}</text>
|
||||||
|
</view>
|
||||||
|
<TextButton
|
||||||
|
text="训练"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="player.currentStats.stamina < 10"
|
||||||
|
@click="startTraining(skill)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!trainingTask && trainableSkills.length === 0" class="training-empty">
|
||||||
|
<text class="training-empty__text">暂无可训练技能</text>
|
||||||
|
</view>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<!-- 活动任务面板 -->
|
||||||
|
<Collapse title="📋 活动任务" :expanded="showTasks" @toggle="showTasks = $event">
|
||||||
|
<view v-if="activeTasksList.length > 0" class="active-tasks-list">
|
||||||
|
<view v-for="task in activeTasksList" :key="task.id" class="active-task-item">
|
||||||
|
<view class="active-task-item__header">
|
||||||
|
<text class="active-task-item__name">{{ task.name }}</text>
|
||||||
|
<TextButton
|
||||||
|
text="取消"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="cancelActiveTask(task)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<ProgressBar
|
||||||
|
:value="task.progress"
|
||||||
|
:max="task.total || 100"
|
||||||
|
color="#4ecdc4"
|
||||||
|
:showText="true"
|
||||||
|
height="12rpx"
|
||||||
|
/>
|
||||||
|
<text class="active-task-item__time">{{ task.timeText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="tasks-empty">
|
||||||
|
<text class="tasks-empty__text">暂无活动任务</text>
|
||||||
|
</view>
|
||||||
|
</Collapse>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
import { useGameStore } from '@/store/game'
|
||||||
import { SKILL_CONFIG } from '@/config/skills'
|
import { SKILL_CONFIG } from '@/config/skills'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items'
|
||||||
|
import { startTask } from '@/utils/taskSystem'
|
||||||
|
import { getSkillDisplayInfo } from '@/utils/skillSystem'
|
||||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||||
import StatItem from '@/components/common/StatItem.vue'
|
import StatItem from '@/components/common/StatItem.vue'
|
||||||
import Collapse from '@/components/common/Collapse.vue'
|
import Collapse from '@/components/common/Collapse.vue'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
|
|
||||||
const player = usePlayerStore()
|
const player = usePlayerStore()
|
||||||
|
const game = useGameStore()
|
||||||
const showMoreStats = ref(false)
|
const showMoreStats = ref(false)
|
||||||
const currentSkillFilter = ref('all')
|
const currentSkillFilter = ref('all')
|
||||||
|
const showBooks = ref(false)
|
||||||
|
const showTraining = ref(false)
|
||||||
|
const showTasks = ref(true)
|
||||||
|
|
||||||
const skillFilterTabs = [
|
const skillFilterTabs = [
|
||||||
{ id: 'all', label: '全部' },
|
{ id: 'all', label: '全部' },
|
||||||
@@ -183,15 +299,8 @@ const currencyText = computed(() => {
|
|||||||
const filteredSkills = computed(() => {
|
const filteredSkills = computed(() => {
|
||||||
const skills = Object.entries(player.skills || {})
|
const skills = Object.entries(player.skills || {})
|
||||||
.filter(([_, skill]) => skill.unlocked)
|
.filter(([_, skill]) => skill.unlocked)
|
||||||
.map(([id, skill]) => ({
|
.map(([id, _]) => getSkillDisplayInfo(player, id))
|
||||||
id,
|
.filter(Boolean)
|
||||||
name: SKILL_CONFIG[id]?.name || id,
|
|
||||||
type: SKILL_CONFIG[id]?.type || 'passive',
|
|
||||||
icon: SKILL_CONFIG[id]?.icon || '',
|
|
||||||
level: skill.level,
|
|
||||||
exp: skill.exp,
|
|
||||||
maxExp: SKILL_CONFIG[id]?.expPerLevel(skill.level + 1) || 100
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.level - a.level || b.exp - a.exp)
|
.sort((a, b) => b.level - a.level || b.exp - a.exp)
|
||||||
|
|
||||||
if (currentSkillFilter.value === 'all') {
|
if (currentSkillFilter.value === 'all') {
|
||||||
@@ -201,33 +310,191 @@ const filteredSkills = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const finalStats = computed(() => {
|
const finalStats = computed(() => {
|
||||||
// 计算最终属性(基础值 + 装备加成)
|
// 计算最终属性(基础值 + 装备加成 + 全局加成)
|
||||||
let attack = 0
|
let attack = player.baseStats?.strength || 0
|
||||||
let defense = 0
|
let defense = 0
|
||||||
let critRate = 5
|
let critRate = (player.baseStats?.dexterity || 0) + (player.globalBonus?.critRate || 0)
|
||||||
|
|
||||||
// 武器攻击力
|
// 装备属性加成
|
||||||
if (player.equipment.weapon) {
|
if (player.equipmentStats) {
|
||||||
attack += player.equipment.weapon.baseDamage || 0
|
attack += player.equipmentStats.attack || 0
|
||||||
|
defense += player.equipmentStats.defense || 0
|
||||||
|
critRate += player.equipmentStats.critRate || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防具防御力
|
// 如果没有equipmentStats,直接从装备计算
|
||||||
if (player.equipment.armor) {
|
if (!player.equipmentStats) {
|
||||||
defense += player.equipment.armor.baseDefense || 0
|
// 武器攻击力(使用finalDamage如果存在)
|
||||||
|
if (player.equipment.weapon) {
|
||||||
|
const weapon = player.equipment.weapon
|
||||||
|
attack += weapon.finalDamage || weapon.baseDamage || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防具防御力(使用finalDefense如果存在)
|
||||||
|
if (player.equipment.armor) {
|
||||||
|
const armor = player.equipment.armor
|
||||||
|
defense += armor.finalDefense || armor.baseDefense || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 盾牌格挡
|
||||||
|
if (player.equipment.shield) {
|
||||||
|
const shield = player.equipment.shield
|
||||||
|
defense += shield.finalDefense || shield.baseDefense || 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算AP和EP
|
// 计算AP和EP(包含装备加成)
|
||||||
const ap = player.baseStats.dexterity + player.baseStats.intuition * 0.2
|
const ap = (player.baseStats?.dexterity || 0) + (player.baseStats?.intuition || 0) * 0.2
|
||||||
const ep = player.baseStats.agility + player.baseStats.intuition * 0.2
|
const ep = (player.baseStats?.agility || 0) + (player.baseStats?.intuition || 0) * 0.2
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attack: Math.floor(attack),
|
attack: Math.floor(attack),
|
||||||
defense: Math.floor(defense),
|
defense: Math.floor(defense),
|
||||||
critRate: critRate,
|
critRate: Math.min(80, Math.floor(critRate)),
|
||||||
ap: Math.floor(ap),
|
ap: Math.floor(ap),
|
||||||
ep: Math.floor(ep)
|
ep: Math.floor(ep)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 书籍相关
|
||||||
|
const ownedBooks = computed(() => {
|
||||||
|
return (player.inventory || []).filter(item => item.type === 'book')
|
||||||
|
})
|
||||||
|
|
||||||
|
const readingTask = computed(() => {
|
||||||
|
const task = (game.activeTasks || []).find(t => t.type === 'reading')
|
||||||
|
if (!task) return null
|
||||||
|
|
||||||
|
const bookConfig = ITEM_CONFIG[task.data.itemId]
|
||||||
|
return {
|
||||||
|
bookName: bookConfig?.name || '未知书籍',
|
||||||
|
progress: task.progress,
|
||||||
|
totalTime: bookConfig?.readingTime || 60
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const readingTimeText = computed(() => {
|
||||||
|
if (!readingTask.value) return ''
|
||||||
|
const remaining = Math.max(0, readingTask.value.totalTime - readingTask.value.progress)
|
||||||
|
const minutes = Math.floor(remaining / 60)
|
||||||
|
const seconds = Math.floor(remaining % 60)
|
||||||
|
return `剩余 ${minutes}:${String(seconds).padStart(2, '0')}`
|
||||||
|
})
|
||||||
|
|
||||||
|
function startReading(book) {
|
||||||
|
const result = startTask(game, player, 'reading', {
|
||||||
|
itemId: book.id,
|
||||||
|
duration: book.readingTime || 60
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(`开始阅读《${book.name}》`, 'info')
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 训练相关
|
||||||
|
const trainableSkills = computed(() => {
|
||||||
|
return Object.entries(player.skills || {})
|
||||||
|
.filter(([_, skill]) => skill.unlocked && SKILL_CONFIG[_]?.type === 'combat')
|
||||||
|
.map(([id, _]) => {
|
||||||
|
const info = getSkillDisplayInfo(player, id)
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: info?.name || id,
|
||||||
|
icon: info?.icon || '⚔️',
|
||||||
|
level: info?.level || 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
|
const trainingTask = computed(() => {
|
||||||
|
const task = (game.activeTasks || []).find(t => t.type === 'training')
|
||||||
|
if (!task) return null
|
||||||
|
|
||||||
|
const info = getSkillDisplayInfo(player, task.data.skillId)
|
||||||
|
return {
|
||||||
|
skillName: info?.name || '未知技能',
|
||||||
|
progress: task.progress,
|
||||||
|
totalTime: task.data.duration || 300,
|
||||||
|
taskId: task.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function startTraining(skill) {
|
||||||
|
const result = startTask(game, player, 'training', {
|
||||||
|
skillId: skill.id,
|
||||||
|
duration: 300 // 5分钟
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(`开始训练 ${skill.name}`, 'info')
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTraining() {
|
||||||
|
if (trainingTask.value) {
|
||||||
|
const { endTask } = require('@/utils/taskSystem')
|
||||||
|
endTask(game, player, trainingTask.value.taskId, false)
|
||||||
|
game.addLog('停止了训练', 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 活动任务相关
|
||||||
|
const activeTasksList = computed(() => {
|
||||||
|
const tasks = (game.activeTasks || []).map(task => {
|
||||||
|
let name = '未知任务'
|
||||||
|
let total = 100
|
||||||
|
|
||||||
|
if (task.type === 'reading') {
|
||||||
|
const book = ITEM_CONFIG[task.data.itemId]
|
||||||
|
name = `阅读《${book?.name || '未知书籍'}》`
|
||||||
|
total = book?.readingTime || 60
|
||||||
|
} else if (task.type === 'training') {
|
||||||
|
const skill = SKILL_CONFIG[task.data.skillId]
|
||||||
|
name = `训练 ${skill?.name || '技能'}`
|
||||||
|
total = task.data.duration || 300
|
||||||
|
} else if (task.type === 'resting') {
|
||||||
|
name = '休息'
|
||||||
|
total = task.data.duration || 60
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = Math.min(task.progress, total)
|
||||||
|
const percentage = Math.floor((progress / total) * 100)
|
||||||
|
|
||||||
|
// 计算剩余时间
|
||||||
|
const remaining = Math.max(0, total - progress)
|
||||||
|
const minutes = Math.floor(remaining / 60)
|
||||||
|
const seconds = Math.floor(remaining % 60)
|
||||||
|
let timeText = ''
|
||||||
|
if (minutes > 0) {
|
||||||
|
timeText = `剩余 ${minutes}分${seconds}秒`
|
||||||
|
} else {
|
||||||
|
timeText = `剩余 ${seconds}秒`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
name,
|
||||||
|
progress,
|
||||||
|
total,
|
||||||
|
percentage,
|
||||||
|
timeText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return tasks.sort((a, b) => b.progress - a.progress)
|
||||||
|
})
|
||||||
|
|
||||||
|
function cancelActiveTask(task) {
|
||||||
|
const { endTask } = require('@/utils/taskSystem')
|
||||||
|
endTask(game, player, task.id, false)
|
||||||
|
game.addLog(`取消了 ${task.name}`, 'info')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -420,4 +687,180 @@ const finalStats = computed(() => {
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 书籍相关样式
|
||||||
|
.reading-task {
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-primary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 22rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-empty {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 训练相关样式
|
||||||
|
.training-task {
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainable-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trainable-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-primary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__level {
|
||||||
|
display: block;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 22rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-empty {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 活动任务样式
|
||||||
|
.active-tasks-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-task-item {
|
||||||
|
padding: 12rpx;
|
||||||
|
background-color: $bg-primary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-empty {
|
||||||
|
padding: 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,45 +1,246 @@
|
|||||||
|
/**
|
||||||
|
* 敌人配置
|
||||||
|
* Phase 4 数值调整 - 战斗数值平衡
|
||||||
|
*/
|
||||||
|
|
||||||
// 敌人配置
|
// 敌人配置
|
||||||
export const ENEMY_CONFIG = {
|
export const ENEMY_CONFIG = {
|
||||||
|
// ===== Lv.1 敌人 =====
|
||||||
wild_dog: {
|
wild_dog: {
|
||||||
id: 'wild_dog',
|
id: 'wild_dog',
|
||||||
name: '野狗',
|
name: '野狗',
|
||||||
level: 1,
|
level: 1,
|
||||||
baseStats: {
|
baseStats: {
|
||||||
health: 30,
|
health: 35,
|
||||||
attack: 8,
|
attack: 8,
|
||||||
defense: 2,
|
defense: 2,
|
||||||
speed: 1.0
|
speed: 1.2
|
||||||
},
|
},
|
||||||
derivedStats: {
|
derivedStats: {
|
||||||
ap: 10, // 攻击点数
|
ap: 8,
|
||||||
ep: 8 // 闪避点数
|
ep: 10
|
||||||
},
|
},
|
||||||
expReward: 15,
|
expReward: 20,
|
||||||
skillExpReward: 10,
|
skillExpReward: 12,
|
||||||
drops: [
|
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: {
|
rat: {
|
||||||
id: 'test_boss',
|
id: 'rat',
|
||||||
name: '测试Boss',
|
name: '老鼠',
|
||||||
level: 5,
|
level: 1,
|
||||||
baseStats: {
|
baseStats: {
|
||||||
health: 200,
|
health: 20,
|
||||||
attack: 25,
|
attack: 5,
|
||||||
defense: 10,
|
defense: 1,
|
||||||
speed: 0.8
|
speed: 1.5
|
||||||
},
|
},
|
||||||
derivedStats: {
|
derivedStats: {
|
||||||
ap: 25,
|
ap: 6,
|
||||||
ep: 15
|
ep: 12
|
||||||
},
|
},
|
||||||
expReward: 150,
|
expReward: 10,
|
||||||
skillExpReward: 50,
|
skillExpReward: 6,
|
||||||
drops: [
|
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
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* Phase 6 核心系统实现
|
* Phase 6 核心系统实现
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LOCATION_CONFIG } from './locations.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件配置
|
* 事件配置
|
||||||
* 每个事件包含:
|
* 每个事件包含:
|
||||||
@@ -236,9 +238,56 @@ export const EVENT_CONFIG = {
|
|||||||
title: '新区域',
|
title: '新区域',
|
||||||
textTemplate: '解锁了新区域:{areaName}\n\n{description}',
|
textTemplate: '解锁了新区域:{areaName}\n\n{description}',
|
||||||
choices: [
|
choices: [
|
||||||
{ text: '前往探索', next: null, action: 'teleport', actionData: { locationId: '{areaId}' } },
|
|
||||||
{ text: '稍后再说', next: null }
|
{ 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' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
451
config/items.js
451
config/items.js
@@ -1,21 +1,172 @@
|
|||||||
// 物品配置
|
// 物品配置
|
||||||
|
// Phase 5 内容扩展 - 更多物品
|
||||||
export const ITEM_CONFIG = {
|
export const ITEM_CONFIG = {
|
||||||
// 武器
|
// ===== 武器 =====
|
||||||
wooden_stick: {
|
wooden_stick: {
|
||||||
id: 'wooden_stick',
|
id: 'wooden_stick',
|
||||||
name: '木棍',
|
name: '木棍',
|
||||||
type: 'weapon',
|
type: 'weapon',
|
||||||
subtype: 'one_handed',
|
subtype: 'one_handed',
|
||||||
icon: '🪵',
|
icon: '🪵',
|
||||||
baseValue: 10, // 基础价值(铜币)
|
baseValue: 10,
|
||||||
baseDamage: 5,
|
baseDamage: 5,
|
||||||
attackSpeed: 1.0,
|
attackSpeed: 1.0,
|
||||||
quality: 100, // 默认品质
|
quality: 100,
|
||||||
unlockSkill: 'stick_mastery',
|
unlockSkill: 'stick_mastery',
|
||||||
description: '一根粗糙的木棍,至少比空手强。'
|
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: {
|
bread: {
|
||||||
id: 'bread',
|
id: 'bread',
|
||||||
name: '面包',
|
name: '面包',
|
||||||
@@ -24,13 +175,63 @@ export const ITEM_CONFIG = {
|
|||||||
icon: '🍞',
|
icon: '🍞',
|
||||||
baseValue: 10,
|
baseValue: 10,
|
||||||
effect: {
|
effect: {
|
||||||
stamina: 20
|
stamina: 20,
|
||||||
|
health: 5
|
||||||
},
|
},
|
||||||
description: '普通的面包,可以恢复耐力。',
|
description: '普通的面包,可以恢复耐力和少量生命。',
|
||||||
stackable: true,
|
stackable: true,
|
||||||
maxStack: 99
|
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: {
|
healing_herb: {
|
||||||
id: 'healing_herb',
|
id: 'healing_herb',
|
||||||
name: '草药',
|
name: '草药',
|
||||||
@@ -39,30 +240,123 @@ export const ITEM_CONFIG = {
|
|||||||
icon: '🌿',
|
icon: '🌿',
|
||||||
baseValue: 15,
|
baseValue: 15,
|
||||||
effect: {
|
effect: {
|
||||||
health: 15
|
health: 20
|
||||||
},
|
},
|
||||||
description: '常见的治疗草药,可以恢复生命值。',
|
description: '常见的治疗草药,可以恢复生命值。',
|
||||||
stackable: true,
|
stackable: true,
|
||||||
maxStack: 99
|
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: {
|
old_book: {
|
||||||
id: 'old_book',
|
id: 'old_book',
|
||||||
name: '破旧书籍',
|
name: '破旧书籍',
|
||||||
type: 'book',
|
type: 'book',
|
||||||
icon: '📖',
|
icon: '📖',
|
||||||
baseValue: 50,
|
baseValue: 50,
|
||||||
readingTime: 60, // 秒
|
readingTime: 60,
|
||||||
expReward: {
|
expReward: {
|
||||||
reading: 10
|
reading: 10
|
||||||
},
|
},
|
||||||
completionBonus: null,
|
completionBonus: null,
|
||||||
description: '一本破旧的书籍,记录着一些基础知识。',
|
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: {
|
dog_skin: {
|
||||||
id: 'dog_skin',
|
id: 'dog_skin',
|
||||||
name: '狗皮',
|
name: '狗皮',
|
||||||
@@ -74,7 +368,74 @@ export const ITEM_CONFIG = {
|
|||||||
maxStack: 99
|
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: {
|
basement_key: {
|
||||||
id: 'basement_key',
|
id: 'basement_key',
|
||||||
name: '地下室钥匙',
|
name: '地下室钥匙',
|
||||||
@@ -83,5 +444,71 @@ export const ITEM_CONFIG = {
|
|||||||
baseValue: 0,
|
baseValue: 0,
|
||||||
description: '一把生锈的钥匙,上面刻着「B」字母。',
|
description: '一把生锈的钥匙,上面刻着「B」字母。',
|
||||||
stackable: false
|
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: '✨' }
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const LOCATION_CONFIG = {
|
|||||||
description: '一个临时的幸存者营地,相对安全。',
|
description: '一个临时的幸存者营地,相对安全。',
|
||||||
connections: ['market', 'blackmarket', 'wild1'],
|
connections: ['market', 'blackmarket', 'wild1'],
|
||||||
npcs: ['injured_adventurer'],
|
npcs: ['injured_adventurer'],
|
||||||
activities: ['rest', 'talk', 'trade']
|
activities: ['rest', 'talk', 'trade', 'crafting']
|
||||||
},
|
},
|
||||||
|
|
||||||
market: {
|
market: {
|
||||||
|
|||||||
@@ -49,5 +49,67 @@ export const SKILL_CONFIG = {
|
|||||||
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
|
10: { desc: '黑暗惩罚-25%', effect: { darkPenaltyReduce: 25 } }
|
||||||
},
|
},
|
||||||
unlockCondition: { location: 'basement' }
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,14 @@
|
|||||||
>
|
>
|
||||||
<ShopDrawer @close="game.drawerState.shop = false" />
|
<ShopDrawer @close="game.drawerState.shop = false" />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<!-- 制造抽屉 -->
|
||||||
|
<Drawer
|
||||||
|
:visible="game.drawerState.crafting"
|
||||||
|
@close="game.drawerState.crafting = false"
|
||||||
|
>
|
||||||
|
<CraftingDrawer @close="game.drawerState.crafting = false" />
|
||||||
|
</Drawer>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -58,6 +66,7 @@ import LogPanel from '@/components/panels/LogPanel.vue'
|
|||||||
import InventoryDrawer from '@/components/drawers/InventoryDrawer.vue'
|
import InventoryDrawer from '@/components/drawers/InventoryDrawer.vue'
|
||||||
import EventDrawer from '@/components/drawers/EventDrawer.vue'
|
import EventDrawer from '@/components/drawers/EventDrawer.vue'
|
||||||
import ShopDrawer from '@/components/drawers/ShopDrawer.vue'
|
import ShopDrawer from '@/components/drawers/ShopDrawer.vue'
|
||||||
|
import CraftingDrawer from '@/components/drawers/CraftingDrawer.vue'
|
||||||
|
|
||||||
const game = useGameStore()
|
const game = useGameStore()
|
||||||
const player = usePlayerStore()
|
const player = usePlayerStore()
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
const drawerState = ref({
|
const drawerState = ref({
|
||||||
inventory: false,
|
inventory: false,
|
||||||
event: false,
|
event: false,
|
||||||
shop: false
|
shop: false,
|
||||||
|
crafting: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 日志
|
// 日志
|
||||||
|
|||||||
1
uni.scss
1
uni.scss
@@ -92,6 +92,7 @@ $accent: #4ecdc4;
|
|||||||
$danger: #ff6b6b;
|
$danger: #ff6b6b;
|
||||||
$warning: #ffe66d;
|
$warning: #ffe66d;
|
||||||
$success: #4ade80;
|
$success: #4ade80;
|
||||||
|
$info: #60a5fa;
|
||||||
|
|
||||||
/* 品质颜色 */
|
/* 品质颜色 */
|
||||||
$quality-trash: #808080;
|
$quality-trash: #808080;
|
||||||
|
|||||||
@@ -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 - 姿态
|
* @param {String} stance - 姿态
|
||||||
@@ -572,6 +555,23 @@ export function getStanceDisplayName(stance) {
|
|||||||
return names[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
|
* @param {String} locationId - 位置ID
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { LOCATION_CONFIG } from '@/config/locations.js'
|
import { LOCATION_CONFIG } from '@/config/locations.js'
|
||||||
import { SKILL_CONFIG } from '@/config/skills.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 }
|
return { [adaptSkillId]: finalExp }
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
import { NPC_CONFIG } from '@/config/npcs.js'
|
import { NPC_CONFIG } from '@/config/npcs.js'
|
||||||
import { EVENT_CONFIG } from '@/config/events.js'
|
import { EVENT_CONFIG } from '@/config/events.js'
|
||||||
import { LOCATION_CONFIG } from '@/config/locations.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 { unlockSkill } from './skillSystem.js'
|
||||||
import { addItemToInventory } from './itemSystem.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) {
|
if (actionData.enemyId) {
|
||||||
gameStore.drawerState.event = false
|
gameStore.drawerState.event = false
|
||||||
// TODO: 启动战斗
|
// 战斗已在探索事件中初始化,这里只是关闭事件抽屉
|
||||||
return { success: true, message: '开始战斗', closeEvent: true }
|
return { success: true, message: '开始战斗', closeEvent: true }
|
||||||
}
|
}
|
||||||
return { success: false, message: '敌人参数错误', closeEvent: false }
|
return { success: false, message: '敌人参数错误', closeEvent: false }
|
||||||
|
|
||||||
|
case 'flee':
|
||||||
|
// 逃跑
|
||||||
|
gameStore.drawerState.event = false
|
||||||
|
return tryFlee(gameStore, playerStore)
|
||||||
|
|
||||||
case 'close':
|
case 'close':
|
||||||
// 直接关闭
|
// 直接关闭
|
||||||
return { success: true, message: '', closeEvent: true }
|
return { success: true, message: '', closeEvent: true }
|
||||||
@@ -547,3 +555,108 @@ export function checkScheduledEvents(gameStore, playerStore) {
|
|||||||
export function clearScheduledEvents(gameStore) {
|
export function clearScheduledEvents(gameStore) {
|
||||||
gameStore.scheduledEvents = []
|
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: '逃跑失败' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { processEnvironmentExp, processEnvironmentEffects } from './environmentS
|
|||||||
import { checkScheduledEvents } from './eventSystem.js'
|
import { checkScheduledEvents } from './eventSystem.js'
|
||||||
import { addSkillExp } from './skillSystem.js'
|
import { addSkillExp } from './skillSystem.js'
|
||||||
import { addItemToInventory } from './itemSystem.js'
|
import { addItemToInventory } from './itemSystem.js'
|
||||||
|
import { addExp, calculateCombatExp } from './levelingSystem.js'
|
||||||
|
|
||||||
// 游戏循环定时器
|
// 游戏循环定时器
|
||||||
let gameLoopInterval = null
|
let gameLoopInterval = null
|
||||||
@@ -196,24 +197,14 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
// 添加日志
|
// 添加日志
|
||||||
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
|
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
|
||||||
|
|
||||||
// 给予经验值
|
// 给予经验值 (使用新的等级系统)
|
||||||
if (enemy.expReward) {
|
if (enemy.expReward) {
|
||||||
playerStore.level.exp += enemy.expReward
|
const adjustedExp = calculateCombatExp(
|
||||||
|
enemy.level || 1,
|
||||||
// 检查升级
|
playerStore.level.current,
|
||||||
const maxExp = playerStore.level.maxExp
|
enemy.expReward
|
||||||
if (playerStore.level.exp >= maxExp) {
|
)
|
||||||
playerStore.level.current++
|
addExp(playerStore, gameStore, adjustedExp)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给予武器技能经验奖励
|
// 给予武器技能经验奖励
|
||||||
@@ -374,6 +365,24 @@ function processDrops(gameStore, playerStore, drops) {
|
|||||||
* @param {Object} rewards - 奖励
|
* @param {Object} rewards - 奖励
|
||||||
*/
|
*/
|
||||||
function handleTaskCompletion(gameStore, playerStore, task, 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) {
|
if (rewards.currency) {
|
||||||
playerStore.currency.copper += 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)) {
|
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)
|
addSkillExp(playerStore, skillId, exp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +401,8 @@ function handleTaskCompletion(gameStore, playerStore, task, rewards) {
|
|||||||
resting: '休息',
|
resting: '休息',
|
||||||
training: '训练',
|
training: '训练',
|
||||||
working: '工作',
|
working: '工作',
|
||||||
praying: '祈祷'
|
praying: '祈祷',
|
||||||
|
crafting: '制造'
|
||||||
}
|
}
|
||||||
gameStore.addLog(`${taskNames[task.type]}任务完成`, 'reward')
|
gameStore.addLog(`${taskNames[task.type]}任务完成`, 'reward')
|
||||||
}
|
}
|
||||||
|
|||||||
287
utils/levelingSystem.js
Normal file
287
utils/levelingSystem.js
Normal 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
|
||||||
|
}
|
||||||
@@ -265,96 +265,6 @@ export function unlockSkill(playerStore, skillId) {
|
|||||||
return true
|
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
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
|||||||
334
utils/soundSystem.js
Normal file
334
utils/soundSystem.js
Normal 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()
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { SKILL_CONFIG } from '@/config/skills.js'
|
import { SKILL_CONFIG } from '@/config/skills.js'
|
||||||
import { ITEM_CONFIG } from '@/config/items.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', // 工作
|
WORKING: 'working', // 工作
|
||||||
COMBAT: 'combat', // 战斗
|
COMBAT: 'combat', // 战斗
|
||||||
EXPLORE: 'explore', // 探索
|
EXPLORE: 'explore', // 探索
|
||||||
PRAYING: 'praying' // 祈祷
|
PRAYING: 'praying', // 祈祷
|
||||||
|
CRAFTING: 'crafting' // 制造
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,15 +30,19 @@ const MUTEX_RULES = {
|
|||||||
full: [
|
full: [
|
||||||
[TASK_TYPES.COMBAT, TASK_TYPES.TRAINING], // 战斗 vs 训练
|
[TASK_TYPES.COMBAT, TASK_TYPES.TRAINING], // 战斗 vs 训练
|
||||||
[TASK_TYPES.COMBAT, TASK_TYPES.WORKING], // 战斗 vs 工作
|
[TASK_TYPES.COMBAT, TASK_TYPES.WORKING], // 战斗 vs 工作
|
||||||
|
[TASK_TYPES.COMBAT, TASK_TYPES.CRAFTING], // 战斗 vs 制造
|
||||||
[TASK_TYPES.TRAINING, TASK_TYPES.WORKING], // 训练 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: [
|
partial: [
|
||||||
[TASK_TYPES.READING, TASK_TYPES.COMBAT], // 阅读 vs 战斗
|
[TASK_TYPES.READING, TASK_TYPES.COMBAT], // 阅读 vs 战斗
|
||||||
[TASK_TYPES.READING, TASK_TYPES.TRAINING], // 阅读 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: '需要圣经' }
|
return { canStart: false, reason: '需要圣经' }
|
||||||
}
|
}
|
||||||
break
|
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: '' }
|
return { canStart: true, reason: '' }
|
||||||
@@ -275,6 +293,9 @@ function processSingleTask(gameStore, playerStore, task, deltaTime) {
|
|||||||
case TASK_TYPES.PRAYING:
|
case TASK_TYPES.PRAYING:
|
||||||
return processPrayingTask(gameStore, playerStore, task, elapsedSeconds)
|
return processPrayingTask(gameStore, playerStore, task, elapsedSeconds)
|
||||||
|
|
||||||
|
case TASK_TYPES.CRAFTING:
|
||||||
|
return processCraftingTask(gameStore, playerStore, task, elapsedSeconds)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { completed: false }
|
return { completed: false }
|
||||||
}
|
}
|
||||||
@@ -491,6 +512,134 @@ function processPrayingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
return { completed: false }
|
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
|
continue
|
||||||
}
|
}
|
||||||
if (skillId === 'faith') {
|
if (skillId === 'faith') {
|
||||||
// 信仰技能经验
|
// 信仰技能经验 - 使用addSkillExp处理
|
||||||
if (!playerStore.skills.faith) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通技能经验
|
// 普通技能经验 - 使用addSkillExp处理升级和里程碑
|
||||||
if (playerStore.skills[skillId]) {
|
if (playerStore.skills[skillId]) {
|
||||||
playerStore.skills[skillId].exp += exp
|
addSkillExp(playerStore, skillId, exp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,7 +696,8 @@ function getTaskTypeName(taskType) {
|
|||||||
working: '工作',
|
working: '工作',
|
||||||
combat: '战斗',
|
combat: '战斗',
|
||||||
explore: '探索',
|
explore: '探索',
|
||||||
praying: '祈祷'
|
praying: '祈祷',
|
||||||
|
crafting: '制造'
|
||||||
}
|
}
|
||||||
return names[taskType] || taskType
|
return names[taskType] || taskType
|
||||||
}
|
}
|
||||||
|
|||||||
45
计划第二步.md
Normal file
45
计划第二步.md
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user