Files
text-adventure-game/components/panels/StatusPanel.vue
Claude 16223c89a5 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>
2026-01-23 16:20:10 +08:00

867 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="status-panel">
<!-- 资源进度条 -->
<view class="status-panel__resources">
<view class="resource-row">
<text class="resource-label">HP</text>
<view class="resource-bar">
<ProgressBar
:value="player.currentStats.health"
:max="player.currentStats.maxHealth"
color="#ff6b6b"
:showText="true"
height="24rpx"
/>
</view>
</view>
<view class="resource-row">
<text class="resource-label">耐力</text>
<view class="resource-bar">
<ProgressBar
:value="player.currentStats.stamina"
:max="player.currentStats.maxStamina"
color="#ffe66d"
:showText="true"
height="24rpx"
/>
</view>
</view>
<view class="resource-row">
<text class="resource-label">精神</text>
<view class="resource-bar">
<ProgressBar
:value="player.currentStats.sanity"
:max="player.currentStats.maxSanity"
color="#4ecdc4"
:showText="true"
height="24rpx"
/>
</view>
</view>
</view>
<!-- 主属性摘要 -->
<view class="status-panel__stats">
<StatItem label="力量" :value="player.baseStats.strength" icon="💪" />
<StatItem label="敏捷" :value="player.baseStats.agility" icon="🏃" />
<StatItem label="灵巧" :value="player.baseStats.dexterity" icon="🎯" />
<StatItem label="智力" :value="player.baseStats.intuition" icon="🧠" />
<StatItem label="体质" :value="player.baseStats.vitality" icon="❤️" />
</view>
<!-- 等级信息 -->
<view class="status-panel__level">
<text class="level-text">Lv.{{ player.level.current }}</text>
<ProgressBar
:value="player.level.exp"
:max="player.level.maxExp"
color="#a855f7"
:showText="true"
height="16rpx"
/>
</view>
<!-- 货币 -->
<view class="status-panel__currency">
<text class="currency-icon">💰</text>
<text class="currency-text">{{ currencyText }}</text>
</view>
<!-- 装备栏 -->
<view class="status-panel__equipment">
<text class="section-title">🎽 装备</text>
<view class="equipment-slots">
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
<text class="equipment-slot__label">武器</text>
<text v-if="player.equipment.weapon" class="equipment-slot__item">
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
</text>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.armor }">
<text class="equipment-slot__label">防具</text>
<text v-if="player.equipment.armor" class="equipment-slot__item">
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
</text>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.shield }">
<text class="equipment-slot__label">盾牌</text>
<text v-if="player.equipment.shield" class="equipment-slot__item">
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
</text>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.accessory }">
<text class="equipment-slot__label">饰品</text>
<text v-if="player.equipment.accessory" class="equipment-slot__item">
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
</text>
<text v-else class="equipment-slot__empty"></text>
</view>
</view>
</view>
<!-- 更多属性折叠 -->
<Collapse
title="更多属性"
:expanded="showMoreStats"
@toggle="showMoreStats = $event"
>
<view class="status-panel__more-stats">
<StatItem label="攻击力" :value="finalStats.attack" />
<StatItem label="防御力" :value="finalStats.defense" />
<StatItem label="暴击率" :value="finalStats.critRate + '%'" />
<StatItem label="AP" :value="finalStats.ap" />
<StatItem label="EP" :value="finalStats.ep" />
</view>
</Collapse>
<!-- 技能筛选 -->
<FilterTabs
:tabs="skillFilterTabs"
:modelValue="currentSkillFilter"
@update:modelValue="currentSkillFilter = $event"
/>
<!-- 技能列表 -->
<scroll-view class="status-panel__skills" scroll-y>
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
<view class="skill-item__info">
<text class="skill-item__name">{{ skill.name }}</text>
<text class="skill-item__level">Lv.{{ skill.level }}</text>
</view>
<view class="skill-item__bar">
<ProgressBar
:value="skill.exp"
:max="skill.maxExp"
height="8rpx"
:showText="false"
/>
</view>
</view>
<view v-if="filteredSkills.length === 0" class="skill-empty">
<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: '全部' },
{ id: 'combat', label: '战斗' },
{ id: 'life', label: '生活' },
{ id: 'passive', label: '被动' }
]
const currencyText = computed(() => {
const c = player.currency.copper
const gold = Math.floor(c / 10000)
const silver = Math.floor((c % 10000) / 100)
const copper = c % 100
const parts = []
if (gold > 0) parts.push(`${gold}`)
if (silver > 0 || gold > 0) parts.push(`${silver}`)
parts.push(`${copper}`)
return parts.join(' ')
})
const filteredSkills = computed(() => {
const skills = Object.entries(player.skills || {})
.filter(([_, skill]) => skill.unlocked)
.map(([id, _]) => getSkillDisplayInfo(player, id))
.filter(Boolean)
.sort((a, b) => b.level - a.level || b.exp - a.exp)
if (currentSkillFilter.value === 'all') {
return skills
}
return skills.filter(s => s.type === currentSkillFilter.value)
})
const finalStats = computed(() => {
// 计算最终属性(基础值 + 装备加成 + 全局加成)
let attack = player.baseStats?.strength || 0
let defense = 0
let critRate = (player.baseStats?.dexterity || 0) + (player.globalBonus?.critRate || 0)
// 装备属性加成
if (player.equipmentStats) {
attack += player.equipmentStats.attack || 0
defense += player.equipmentStats.defense || 0
critRate += player.equipmentStats.critRate || 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 || 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: 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>
.status-panel {
display: flex;
flex-direction: column;
height: 100%;
padding: 16rpx;
background-color: $bg-primary;
&__resources {
display: flex;
flex-direction: column;
gap: 12rpx;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__stats {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__level {
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__currency {
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__equipment {
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__more-stats {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
&__skills {
flex: 1;
margin-top: 16rpx;
}
}
.resource-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.resource-bar {
flex: 1;
min-width: 0;
}
.resource-label {
min-width: 80rpx;
color: $text-secondary;
font-size: 24rpx;
}
.level-text {
display: block;
color: $accent;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.currency-icon {
font-size: 32rpx;
}
.currency-text {
color: $warning;
font-size: 28rpx;
font-weight: bold;
}
.skill-item {
display: flex;
align-items: center;
gap: 12rpx;
padding: 12rpx;
background-color: $bg-secondary;
border-radius: 8rpx;
margin-bottom: 8rpx;
&__icon {
font-size: 32rpx;
width: 48rpx;
text-align: center;
}
&__info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
&__name {
color: $text-primary;
font-size: 26rpx;
}
&__level {
color: $accent;
font-size: 24rpx;
}
&__bar {
flex: 1;
min-width: 0;
}
}
.skill-empty {
padding: 48rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 28rpx;
}
}
.section-title {
display: block;
color: $text-primary;
font-size: 24rpx;
font-weight: bold;
margin-bottom: 12rpx;
}
.equipment-slots {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8rpx;
}
.equipment-slot {
display: flex;
flex-direction: column;
align-items: center;
padding: 12rpx;
background-color: $bg-primary;
border-radius: 8rpx;
border: 1rpx solid $border-color;
&--empty {
opacity: 0.6;
}
&__label {
color: $text-secondary;
font-size: 20rpx;
margin-bottom: 4rpx;
}
&__item {
color: $text-primary;
font-size: 22rpx;
}
&__empty {
color: $text-muted;
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>