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:
@@ -1,10 +1,17 @@
|
||||
<template>
|
||||
<view
|
||||
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"
|
||||
>
|
||||
<text>{{ text }}</text>
|
||||
<text v-if="!loading" class="text-button__text">{{ text }}</text>
|
||||
<view v-else class="text-button__spinner"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -12,13 +19,14 @@
|
||||
const props = defineProps({
|
||||
text: { type: String, required: true },
|
||||
type: { type: String, default: 'default' },
|
||||
disabled: { type: Boolean, default: false }
|
||||
disabled: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
function handleClick() {
|
||||
if (!props.disabled) {
|
||||
if (!props.disabled && !props.loading) {
|
||||
emit('click')
|
||||
}
|
||||
}
|
||||
@@ -26,43 +34,133 @@ function handleClick() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16rpx 32rpx;
|
||||
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 {
|
||||
background-color: $bg-tertiary;
|
||||
color: $text-primary;
|
||||
border: 1rpx solid transparent;
|
||||
|
||||
&:active {
|
||||
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 {
|
||||
background-color: rgba($danger, 0.2);
|
||||
background-color: rgba($danger, 0.15);
|
||||
color: $danger;
|
||||
border: 1rpx solid rgba($danger, 0.3);
|
||||
|
||||
&:active {
|
||||
background-color: rgba($danger, 0.3);
|
||||
background-color: rgba($danger, 0.25);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: rgba($warning, 0.2);
|
||||
background-color: rgba($warning, 0.15);
|
||||
color: $warning;
|
||||
border: 1rpx solid rgba($warning, 0.3);
|
||||
|
||||
&: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 {
|
||||
opacity: 0.5;
|
||||
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>
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
>
|
||||
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
|
||||
<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 }}
|
||||
<text v-if="item.qualityName" class="quality-badge"> [{{ item.qualityName }}]</text>
|
||||
</text>
|
||||
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
|
||||
</view>
|
||||
@@ -73,6 +74,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { usePlayerStore } from '@/store/player'
|
||||
import { useGameStore } from '@/store/game'
|
||||
import { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
|
||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||
import TextButton from '@/components/common/TextButton.vue'
|
||||
|
||||
@@ -128,16 +130,6 @@ function isEquipped(item) {
|
||||
return equipped && (equipped.uniqueId === item.uniqueId || equipped.id === item.id)
|
||||
}
|
||||
|
||||
function qualityColor(quality) {
|
||||
if (!quality) return '#ffffff'
|
||||
if (quality >= 200) return '#f97316' // 传说
|
||||
if (quality >= 160) return '#a855f7' // 史诗
|
||||
if (quality >= 130) return '#60a5fa' // 稀有
|
||||
if (quality >= 100) return '#4ade80' // 优秀
|
||||
if (quality >= 50) return '#ffffff' // 普通
|
||||
return '#808080' // 垃圾
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
@@ -148,30 +140,10 @@ function selectItem(item) {
|
||||
|
||||
function equipItem() {
|
||||
if (!selectedItem.value) return
|
||||
const item = selectedItem.value
|
||||
const slot = slotMap[item.type]
|
||||
|
||||
if (!slot) return
|
||||
|
||||
// 如果该槽位已有装备,先卸下旧装备放回背包
|
||||
const oldEquip = player.equipment[slot]
|
||||
if (oldEquip) {
|
||||
player.inventory.push(oldEquip)
|
||||
const result = equipItemUtil(player, game, selectedItem.value.uniqueId)
|
||||
if (result.success) {
|
||||
selectedItem.value = null
|
||||
}
|
||||
|
||||
// 从背包中移除该物品
|
||||
const index = player.inventory.findIndex(i =>
|
||||
(i.uniqueId === item.uniqueId) || (i.id === item.id && i.uniqueId === undefined)
|
||||
)
|
||||
if (index > -1) {
|
||||
player.inventory.splice(index, 1)
|
||||
}
|
||||
|
||||
// 装备该物品
|
||||
player.equipment[slot] = item
|
||||
|
||||
game.addLog(`装备了 ${item.name}`, 'info')
|
||||
selectedItem.value = null
|
||||
}
|
||||
|
||||
function unequipItem() {
|
||||
@@ -181,57 +153,19 @@ function unequipItem() {
|
||||
|
||||
if (!slot) return
|
||||
|
||||
// 从装备槽移除
|
||||
player.equipment[slot] = null
|
||||
|
||||
// 放回背包
|
||||
player.inventory.push(item)
|
||||
|
||||
game.addLog(`卸下了 ${item.name}`, 'info')
|
||||
selectedItem.value = null
|
||||
const result = unequipItemBySlot(player, game, slot)
|
||||
if (result.success) {
|
||||
selectedItem.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function useItem() {
|
||||
if (!selectedItem.value) return
|
||||
const item = selectedItem.value
|
||||
|
||||
// 检查是否是消耗品
|
||||
if (item.type !== 'consumable') return
|
||||
|
||||
// 应用效果
|
||||
if (item.effect) {
|
||||
if (item.effect.health) {
|
||||
player.currentStats.health = Math.min(
|
||||
player.currentStats.maxHealth,
|
||||
player.currentStats.health + item.effect.health
|
||||
)
|
||||
}
|
||||
if (item.effect.stamina) {
|
||||
player.currentStats.stamina = Math.min(
|
||||
player.currentStats.maxStamina,
|
||||
player.currentStats.stamina + item.effect.stamina
|
||||
)
|
||||
}
|
||||
if (item.effect.sanity) {
|
||||
player.currentStats.sanity = Math.min(
|
||||
player.currentStats.maxSanity,
|
||||
player.currentStats.sanity + item.effect.sanity
|
||||
)
|
||||
}
|
||||
const result = useItemUtil(player, game, selectedItem.value.id, 1)
|
||||
if (result.success) {
|
||||
// 如果阅读任务,可以在这里处理
|
||||
selectedItem.value = null
|
||||
}
|
||||
|
||||
// 从背包中移除(如果是堆叠物品,减少数量)
|
||||
if (item.count > 1) {
|
||||
item.count--
|
||||
} else {
|
||||
const index = player.inventory.findIndex(i => i.id === item.id)
|
||||
if (index > -1) {
|
||||
player.inventory.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
game.addLog(`使用了 ${item.name}`, 'info')
|
||||
selectedItem.value = null
|
||||
}
|
||||
|
||||
function sellItem() {
|
||||
@@ -322,6 +256,13 @@ function sellItem() {
|
||||
font-size: 22rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.quality-badge {
|
||||
font-size: 18rpx;
|
||||
opacity: 0.8;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
&__equipped-badge {
|
||||
width: 32rpx;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<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>
|
||||
<slot></slot>
|
||||
</view>
|
||||
@@ -9,7 +14,8 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
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'])
|
||||
@@ -26,9 +32,20 @@ function handleMaskClick() {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -36,14 +53,70 @@ function handleMaskClick() {
|
||||
bottom: 0;
|
||||
background-color: $bg-secondary;
|
||||
border-left: 1rpx solid $border-color;
|
||||
animation: slideIn 0.3s ease;
|
||||
display: flex;
|
||||
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%); }
|
||||
to { transform: translateX(0); }
|
||||
// 不同的位置动画
|
||||
.drawer--left {
|
||||
.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>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:class="`log-item--${log.type}`"
|
||||
: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__message">{{ log.message }}</text>
|
||||
</view>
|
||||
@@ -45,52 +46,88 @@ watch(lastLogId, (newId) => {
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 获取日志类型图标
|
||||
function getLogIcon(type) {
|
||||
const icons = {
|
||||
combat: '⚔️',
|
||||
system: '🔔',
|
||||
reward: '🎁',
|
||||
info: 'ℹ️',
|
||||
warning: '⚠️',
|
||||
error: '❌',
|
||||
success: '✅',
|
||||
story: '📖'
|
||||
}
|
||||
return icons[type] || '•'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.log-panel {
|
||||
height: 100%;
|
||||
padding: 16rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background-color: $bg-primary;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 8rpx 0;
|
||||
border-bottom: 1rpx solid $bg-tertiary;
|
||||
align-items: flex-start;
|
||||
padding: 8rpx 12rpx;
|
||||
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 {
|
||||
color: $text-muted;
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__message {
|
||||
color: $text-primary;
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
// 不同类型的日志样式
|
||||
&--combat {
|
||||
background-color: rgba($danger, 0.1);
|
||||
border-left-color: $danger;
|
||||
|
||||
.log-item__message {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&--system {
|
||||
background-color: rgba($accent, 0.1);
|
||||
border-left-color: $accent;
|
||||
|
||||
.log-item__message {
|
||||
color: $accent;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&--reward {
|
||||
background-color: rgba($warning, 0.1);
|
||||
border-left-color: $warning;
|
||||
|
||||
.log-item__message {
|
||||
color: $warning;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,25 +138,55 @@ watch(lastLogId, (newId) => {
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: rgba($warning, 0.1);
|
||||
border-left-color: $warning;
|
||||
|
||||
.log-item__message {
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: rgba($danger, 0.15);
|
||||
border-left-color: $danger;
|
||||
|
||||
.log-item__message {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
background-color: rgba($success, 0.1);
|
||||
border-left-color: $success;
|
||||
|
||||
.log-item__message {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&--story {
|
||||
background-color: rgba($info, 0.1);
|
||||
border-left-color: $info;
|
||||
|
||||
.log-item__message {
|
||||
color: $info;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-anchor {
|
||||
height: 1rpx;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,9 +105,10 @@ import { useGameStore } from '@/store/game'
|
||||
import { usePlayerStore } from '@/store/player'
|
||||
import { LOCATION_CONFIG } from '@/config/locations'
|
||||
import { NPC_CONFIG } from '@/config/npcs.js'
|
||||
import { ENEMY_CONFIG } from '@/config/enemies.js'
|
||||
import { ENEMY_CONFIG, getRandomEnemyForLocation } from '@/config/enemies.js'
|
||||
import { getShopConfig } from '@/config/shop.js'
|
||||
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
|
||||
import { triggerExploreEvent, tryFlee } from '@/utils/eventSystem.js'
|
||||
import TextButton from '@/components/common/TextButton.vue'
|
||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||
@@ -151,10 +152,10 @@ const availableLocations = computed(() => {
|
||||
// 检查解锁条件
|
||||
if (loc.unlockCondition) {
|
||||
if (loc.unlockCondition.type === 'kill') {
|
||||
// 检查击杀条件(需要追踪击杀数)
|
||||
const killCount = player.flags[`kill_${loc.unlockCondition.target}`] || 0
|
||||
// 检查击杀条件(使用统一的killCount存储)
|
||||
const killCount = (player.killCount && player.killCount[loc.unlockCondition.target]) || 0
|
||||
locked = killCount < loc.unlockCondition.count
|
||||
lockReason = `需要击杀${loc.unlockCondition.count}只${getEnemyName(loc.unlockCondition.target)}`
|
||||
lockReason = `需要击杀${loc.unlockCondition.count}只${getEnemyName(loc.unlockCondition.target)} (当前: ${killCount})`
|
||||
} else if (loc.unlockCondition.type === 'item') {
|
||||
// 检查物品条件
|
||||
const hasItem = player.inventory.some(i => i.id === loc.unlockCondition.item)
|
||||
@@ -191,7 +192,8 @@ const currentActions = computed(() => {
|
||||
trade: { id: 'trade', label: '交易', type: 'default' },
|
||||
explore: { id: 'explore', label: '探索', type: 'warning' },
|
||||
combat: { id: 'combat', label: '战斗', type: 'danger' },
|
||||
read: { id: 'read', label: '阅读', type: 'default' }
|
||||
read: { id: 'read', label: '阅读', type: 'default' },
|
||||
crafting: { id: 'crafting', label: '制造', type: 'primary' }
|
||||
}
|
||||
|
||||
// 战斗中只显示战斗相关
|
||||
@@ -302,33 +304,37 @@ function doAction(actionId) {
|
||||
break
|
||||
case 'explore':
|
||||
game.addLog('开始探索...', 'info')
|
||||
// 探索逻辑
|
||||
const exploreResult = triggerExploreEvent(game, player)
|
||||
if (!exploreResult.success) {
|
||||
game.addLog(exploreResult.message, 'info')
|
||||
}
|
||||
break
|
||||
case 'combat':
|
||||
startCombat()
|
||||
break
|
||||
case 'flee':
|
||||
game.addLog('尝试逃跑...', 'combat')
|
||||
// 逃跑逻辑
|
||||
const fleeResult = tryFlee(game, player)
|
||||
if (!fleeResult.success) {
|
||||
// 逃跑失败,玩家会多受到一次攻击(在战斗循环中处理)
|
||||
}
|
||||
break
|
||||
case 'read':
|
||||
game.addLog('找个安静的地方阅读...', 'info')
|
||||
break
|
||||
case 'crafting':
|
||||
game.drawerState.crafting = true
|
||||
game.addLog('打开制造界面...', 'info')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function startCombat() {
|
||||
const current = LOCATION_CONFIG[player.currentLocation]
|
||||
if (!current || !current.enemies || current.enemies.length === 0) {
|
||||
game.addLog('这里没有敌人', 'info')
|
||||
return
|
||||
}
|
||||
|
||||
const enemyId = current.enemies[0]
|
||||
const enemyConfig = ENEMY_CONFIG[enemyId]
|
||||
// 使用新的敌人获取函数
|
||||
const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
|
||||
|
||||
if (!enemyConfig) {
|
||||
game.addLog('敌人配置错误', 'error')
|
||||
game.addLog('这里没有敌人', 'info')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -337,8 +343,8 @@ function startCombat() {
|
||||
// 获取环境类型
|
||||
const environment = getEnvironmentType(player.currentLocation)
|
||||
|
||||
// 使用 initCombat 初始化战斗(包含 expReward 和 drops)
|
||||
game.combatState = initCombat(enemyId, enemyConfig, environment)
|
||||
// 使用 initCombat 初始化战斗
|
||||
game.combatState = initCombat(enemyConfig.id, enemyConfig, environment)
|
||||
game.inCombat = true
|
||||
}
|
||||
|
||||
@@ -368,8 +374,8 @@ function toggleAutoCombat() {
|
||||
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
|
||||
// 如果当前不在战斗中且在危险区域,立即开始战斗
|
||||
if (!game.inCombat) {
|
||||
const current = LOCATION_CONFIG[player.currentLocation]
|
||||
if (current && current.enemies && current.enemies.length > 0) {
|
||||
const enemyConfig = getRandomEnemyForLocation(player.currentLocation)
|
||||
if (enemyConfig) {
|
||||
startCombat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,21 +145,137 @@
|
||||
<text class="skill-empty__text">暂无技能</text>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { usePlayerStore } from '@/store/player'
|
||||
import { useGameStore } from '@/store/game'
|
||||
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 StatItem from '@/components/common/StatItem.vue'
|
||||
import Collapse from '@/components/common/Collapse.vue'
|
||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||
import TextButton from '@/components/common/TextButton.vue'
|
||||
|
||||
const player = usePlayerStore()
|
||||
const game = useGameStore()
|
||||
const showMoreStats = ref(false)
|
||||
const currentSkillFilter = ref('all')
|
||||
const showBooks = ref(false)
|
||||
const showTraining = ref(false)
|
||||
const showTasks = ref(true)
|
||||
|
||||
const skillFilterTabs = [
|
||||
{ id: 'all', label: '全部' },
|
||||
@@ -183,15 +299,8 @@ const currencyText = computed(() => {
|
||||
const filteredSkills = computed(() => {
|
||||
const skills = Object.entries(player.skills || {})
|
||||
.filter(([_, skill]) => skill.unlocked)
|
||||
.map(([id, skill]) => ({
|
||||
id,
|
||||
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
|
||||
}))
|
||||
.map(([id, _]) => getSkillDisplayInfo(player, id))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.level - a.level || b.exp - a.exp)
|
||||
|
||||
if (currentSkillFilter.value === 'all') {
|
||||
@@ -201,33 +310,191 @@ const filteredSkills = computed(() => {
|
||||
})
|
||||
|
||||
const finalStats = computed(() => {
|
||||
// 计算最终属性(基础值 + 装备加成)
|
||||
let attack = 0
|
||||
// 计算最终属性(基础值 + 装备加成 + 全局加成)
|
||||
let attack = player.baseStats?.strength || 0
|
||||
let defense = 0
|
||||
let critRate = 5
|
||||
let critRate = (player.baseStats?.dexterity || 0) + (player.globalBonus?.critRate || 0)
|
||||
|
||||
// 武器攻击力
|
||||
if (player.equipment.weapon) {
|
||||
attack += player.equipment.weapon.baseDamage || 0
|
||||
// 装备属性加成
|
||||
if (player.equipmentStats) {
|
||||
attack += player.equipmentStats.attack || 0
|
||||
defense += player.equipmentStats.defense || 0
|
||||
critRate += player.equipmentStats.critRate || 0
|
||||
}
|
||||
|
||||
// 防具防御力
|
||||
if (player.equipment.armor) {
|
||||
defense += player.equipment.armor.baseDefense || 0
|
||||
// 如果没有equipmentStats,直接从装备计算
|
||||
if (!player.equipmentStats) {
|
||||
// 武器攻击力(使用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
|
||||
const ap = player.baseStats.dexterity + player.baseStats.intuition * 0.2
|
||||
const ep = player.baseStats.agility + player.baseStats.intuition * 0.2
|
||||
// 计算AP和EP(包含装备加成)
|
||||
const ap = (player.baseStats?.dexterity || 0) + (player.baseStats?.intuition || 0) * 0.2
|
||||
const ep = (player.baseStats?.agility || 0) + (player.baseStats?.intuition || 0) * 0.2
|
||||
|
||||
return {
|
||||
attack: Math.floor(attack),
|
||||
defense: Math.floor(defense),
|
||||
critRate: critRate,
|
||||
critRate: Math.min(80, Math.floor(critRate)),
|
||||
ap: Math.floor(ap),
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -420,4 +687,180 @@ const finalStats = computed(() => {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user