Initial commit: Text Adventure Game

Features:
- Combat system with AP/EP hit calculation and three-layer defense
- Auto-combat/farming mode
- Item system with stacking support
- Skill system with levels, milestones, and parent skill sync
- Shop system with dynamic pricing
- Inventory management with bulk selling
- Event system
- Game loop with offline earnings
- Save/Load system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-21 17:13:51 +08:00
commit cb412544e9
90 changed files with 17149 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
<template>
<scroll-view
class="log-panel"
scroll-y
:scroll-into-view="scrollToId"
:scroll-with-animation="true"
:scroll-top="scrollTop"
>
<view
v-for="log in logs"
:key="log.id"
class="log-item"
:class="`log-item--${log.type}`"
:id="'log-' + log.id"
>
<text class="log-item__time">[{{ log.time }}]</text>
<text class="log-item__message">{{ log.message }}</text>
</view>
<view :id="'log-anchor-' + lastLogId" class="log-anchor"></view>
</scroll-view>
</template>
<script setup>
import { computed, watch, nextTick, ref } from 'vue'
import { useGameStore } from '@/store/game'
const game = useGameStore()
const scrollToId = ref('')
const scrollTop = ref(0)
const logs = computed(() => game.logs)
const lastLogId = computed(() => {
return logs.value.length > 0 ? logs.value[logs.value.length - 1].id : 0
})
watch(lastLogId, (newId) => {
if (newId) {
nextTick(() => {
scrollToId.value = 'log-anchor-' + newId
// 重置scrollToId以允许下次滚动
setTimeout(() => {
scrollToId.value = ''
}, 100)
})
}
}, { immediate: true })
</script>
<style lang="scss" scoped>
.log-panel {
height: 100%;
padding: 16rpx;
background-color: $bg-primary;
}
.log-item {
display: flex;
flex-wrap: wrap;
padding: 8rpx 0;
border-bottom: 1rpx solid $bg-tertiary;
&__time {
color: $text-muted;
font-size: 22rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
&__message {
color: $text-primary;
font-size: 26rpx;
line-height: 1.5;
flex: 1;
}
// 不同类型的日志样式
&--combat {
.log-item__message {
color: $danger;
}
}
&--system {
.log-item__message {
color: $accent;
font-weight: bold;
}
}
&--reward {
.log-item__message {
color: $warning;
}
}
&--info {
.log-item__message {
color: $text-secondary;
}
}
&--warning {
.log-item__message {
color: $warning;
}
}
&--error {
.log-item__message {
color: $danger;
}
}
&--success {
.log-item__message {
color: $success;
}
}
}
.log-anchor {
height: 1rpx;
}
</style>

View File

@@ -0,0 +1,606 @@
<template>
<view class="map-panel">
<!-- 顶部状态 -->
<view class="map-panel__header">
<view class="location-info">
<text class="location-icon">📍</text>
<text class="location-name">{{ currentLocation.name }}</text>
</view>
<TextButton text="📦 背包" type="default" @click="openInventory" />
</view>
<!-- 当前位置描述 -->
<view class="map-panel__description">
<text class="description-text">{{ currentLocation.description }}</text>
</view>
<!-- 战斗状态 -->
<view v-if="game.inCombat" class="combat-status">
<text class="combat-status__title"> 战斗中</text>
<text class="combat-status__enemy">{{ combatState.enemyName }}</text>
<ProgressBar
:value="combatState.enemyHp"
:max="combatState.enemyMaxHp"
color="#ff6b6b"
:showText="true"
height="20rpx"
/>
<FilterTabs
:tabs="stanceTabs"
:modelValue="combatState.stance"
@update:modelValue="changeStance"
/>
</view>
<!-- 区域列表 -->
<view class="map-panel__locations-header">
<text class="locations-title">可前往的区域</text>
</view>
<scroll-view class="map-panel__locations" scroll-y>
<view
v-for="loc in availableLocations"
:key="loc.id"
class="location-item"
:class="{ 'location-item--locked': loc.locked, 'location-item--current': loc.id === player.currentLocation }"
@click="tryTravel(loc)"
>
<view class="location-item__main">
<text class="location-item__name">{{ loc.name }}</text>
<text class="location-item__type">[{{ loc.typeText }}]</text>
</view>
<text v-if="loc.locked" class="location-item__lock">🔒</text>
<text v-else-if="loc.id === player.currentLocation" class="location-item__current">当前</text>
<text v-else class="location-item__arrow"></text>
</view>
</scroll-view>
<!-- 活动按钮 -->
<view class="map-panel__actions">
<text class="actions-title">可用活动</text>
<view class="actions-list">
<TextButton
v-for="action in currentActions"
:key="action.id"
:text="action.label"
:type="action.type || 'default'"
:disabled="action.disabled"
@click="doAction(action.id)"
/>
</view>
</view>
<!-- 自动战斗开关 -->
<view class="map-panel__auto-combat">
<text class="auto-combat-label">自动战斗</text>
<view
class="auto-combat-toggle"
:class="{ 'auto-combat-toggle--active': game.autoCombat }"
@click="toggleAutoCombat"
>
<text class="auto-combat-toggle__slider">
{{ game.autoCombat ? 'ON' : 'OFF' }}
</text>
</view>
</view>
<!-- NPC 列表 -->
<view v-if="availableNPCs.length > 0" class="map-panel__npcs">
<text class="npcs-title">可交互的角色</text>
<view class="npcs-list">
<TextButton
v-for="npc in availableNPCs"
:key="npc.id"
:text="`${npc.icon} ${npc.name}`"
type="primary"
@click="talkToNPC(npc)"
/>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
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 { getShopConfig } from '@/config/shop.js'
import { initCombat, getEnvironmentType } from '@/utils/combatSystem.js'
import TextButton from '@/components/common/TextButton.vue'
import ProgressBar from '@/components/common/ProgressBar.vue'
import FilterTabs from '@/components/common/FilterTabs.vue'
const game = useGameStore()
const player = usePlayerStore()
const stanceTabs = [
{ id: 'attack', label: '攻击' },
{ id: 'defense', label: '防御' },
{ id: 'balance', label: '平衡' }
]
const currentLocation = computed(() => {
return LOCATION_CONFIG[player.currentLocation] || { name: '未知', description: '', type: 'unknown' }
})
const combatState = computed(() => {
if (!game.combatState) {
return { enemyName: '', enemyHp: 0, enemyMaxHp: 0, stance: 'balance' }
}
return {
enemyName: game.combatState.enemy?.name || '',
enemyHp: game.combatState.enemy?.hp || 0,
enemyMaxHp: game.combatState.enemy?.maxHp || 0,
stance: game.combatState.stance || 'balance'
}
})
const availableLocations = computed(() => {
const current = LOCATION_CONFIG[player.currentLocation]
if (!current || !current.connections) return []
return current.connections.map(locId => {
const loc = LOCATION_CONFIG[locId]
if (!loc) return null
let locked = false
let lockReason = ''
// 检查解锁条件
if (loc.unlockCondition) {
if (loc.unlockCondition.type === 'kill') {
// 检查击杀条件(需要追踪击杀数)
const killCount = player.flags[`kill_${loc.unlockCondition.target}`] || 0
locked = killCount < loc.unlockCondition.count
lockReason = `需要击杀${loc.unlockCondition.count}${getEnemyName(loc.unlockCondition.target)}`
} else if (loc.unlockCondition.type === 'item') {
// 检查物品条件
const hasItem = player.inventory.some(i => i.id === loc.unlockCondition.item)
locked = !hasItem
lockReason = '需要特定钥匙'
}
}
const typeMap = {
safe: '安全',
danger: '危险',
dungeon: '副本'
}
return {
id: loc.id,
name: loc.name,
type: loc.type,
typeText: typeMap[loc.type] || '未知',
locked,
lockReason,
description: loc.description
}
}).filter(Boolean)
})
const currentActions = computed(() => {
const current = LOCATION_CONFIG[player.currentLocation]
if (!current || !current.activities) return []
const actionMap = {
rest: { id: 'rest', label: '休息', type: 'default' },
talk: { id: 'talk', label: '对话', type: 'default' },
trade: { id: 'trade', label: '交易', type: 'default' },
explore: { id: 'explore', label: '探索', type: 'warning' },
combat: { id: 'combat', label: '战斗', type: 'danger' },
read: { id: 'read', label: '阅读', type: 'default' }
}
// 战斗中只显示战斗相关
if (game.inCombat) {
return [
{ id: 'flee', label: '逃跑', type: 'warning' }
]
}
return (current.activities || []).map(act => actionMap[act]).filter(Boolean)
})
// 获取当前可用的 NPC
const availableNPCs = computed(() => {
const currentLocationId = player.currentLocation
return Object.entries(NPC_CONFIG)
.filter(([_, npc]) => {
// NPC 必须在当前位置
if (npc.location !== currentLocationId) return false
// 检查解锁条件
if (npc.unlockCondition) {
if (npc.unlockCondition.type === 'quest' && !player.flags[npc.unlockCondition.quest]) {
return false
}
}
return true
})
.map(([id, npc]) => ({
id,
name: npc.name,
icon: npc.icon || '👤',
dialogue: npc.dialogue
}))
})
function talkToNPC(npc) {
game.addLog(`${npc.name} 开始对话...`, 'info')
// 打开 NPC 对话
if (npc.dialogue && npc.dialogue.first) {
game.drawerState.event = true
game.currentEvent = {
type: 'npc_dialogue',
npcId: npc.id,
dialogue: npc.dialogue.first,
choices: npc.dialogue.first.choices || []
}
}
}
function openInventory() {
game.drawerState.inventory = true
}
function tryTravel(location) {
if (location.locked) {
game.addLog(location.lockReason, 'info')
return
}
if (location.id === player.currentLocation) {
game.addLog('你已经在当前区域了', 'info')
return
}
if (game.inCombat) {
game.addLog('战斗中无法移动!', 'combat')
return
}
// 移动到新区域
player.currentLocation = location.id
game.addLog(`前往了 ${location.name}`, 'system')
// 检查区域事件
checkLocationEvent(location.id)
}
function doAction(actionId) {
switch (actionId) {
case 'rest':
game.addLog('开始休息...', 'info')
// 休息逻辑
player.currentStats.stamina = Math.min(
player.currentStats.maxStamina,
player.currentStats.stamina + 20
)
player.currentStats.health = Math.min(
player.currentStats.maxHealth,
player.currentStats.health + 10
)
game.addLog('恢复了耐力和生命值', 'reward')
break
case 'talk':
game.addLog('寻找可以对话的人...', 'info')
// 对话逻辑
break
case 'trade':
// 检查当前位置是否有商店
const shop = getShopConfig(player.currentLocation)
if (shop) {
game.drawerState.shop = true
game.addLog(`打开了 ${shop.name}`, 'info')
} else {
game.addLog('这里没有商店', 'info')
}
break
case 'explore':
game.addLog('开始探索...', 'info')
// 探索逻辑
break
case 'combat':
startCombat()
break
case 'flee':
game.addLog('尝试逃跑...', 'combat')
// 逃跑逻辑
break
case 'read':
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]
if (!enemyConfig) {
game.addLog('敌人配置错误', 'error')
return
}
game.addLog(`遇到了 ${enemyConfig.name}!`, 'combat')
// 获取环境类型
const environment = getEnvironmentType(player.currentLocation)
// 使用 initCombat 初始化战斗(包含 expReward 和 drops
game.combatState = initCombat(enemyId, enemyConfig, environment)
game.inCombat = true
}
function changeStance(stance) {
if (game.combatState) {
game.combatState.stance = stance
game.addLog(`切换到${stanceTabs.find(t => t.id === stance)?.label}姿态`, 'combat')
}
}
function checkLocationEvent(locationId) {
// 检查位置相关事件
// 这里可以触发事件抽屉等
}
function getEnemyName(enemyId) {
const enemyNames = {
wild_dog: '野狗',
test_boss: '测试Boss'
}
return enemyNames[enemyId] || enemyId
}
function toggleAutoCombat() {
game.autoCombat = !game.autoCombat
if (game.autoCombat) {
game.addLog('自动战斗已开启 - 战斗结束后自动寻找新敌人', 'info')
// 如果当前不在战斗中且在危险区域,立即开始战斗
if (!game.inCombat) {
const current = LOCATION_CONFIG[player.currentLocation]
if (current && current.enemies && current.enemies.length > 0) {
startCombat()
}
}
} else {
game.addLog('自动战斗已关闭', 'info')
}
}
</script>
<style lang="scss" scoped>
.map-panel {
display: flex;
flex-direction: column;
height: 100%;
padding: 16rpx;
background-color: $bg-primary;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__description {
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__locations-header {
padding: 12rpx 16rpx 8rpx;
}
&__locations {
flex: 1;
margin-bottom: 16rpx;
}
&__actions {
background-color: $bg-secondary;
border-radius: 12rpx;
padding: 16rpx;
}
}
.location-info {
display: flex;
align-items: center;
gap: 8rpx;
}
.location-icon {
font-size: 28rpx;
}
.location-name {
color: $text-primary;
font-size: 28rpx;
font-weight: bold;
}
.description-text {
color: $text-secondary;
font-size: 24rpx;
line-height: 1.6;
}
.combat-status {
padding: 16rpx;
background-color: rgba($danger, 0.1);
border: 1rpx solid $danger;
border-radius: 12rpx;
margin-bottom: 16rpx;
&__title {
display: block;
color: $danger;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
&__enemy {
display: block;
color: $text-primary;
font-size: 26rpx;
margin-bottom: 12rpx;
}
}
.locations-title {
color: $text-secondary;
font-size: 24rpx;
}
.location-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 8rpx;
margin-bottom: 8rpx;
transition: background-color 0.2s;
&:active {
background-color: $bg-tertiary;
}
&--locked {
opacity: 0.6;
}
&--current {
opacity: 0.8;
border: 1rpx solid $accent;
}
&__main {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
&__name {
color: $text-primary;
font-size: 28rpx;
}
&__type {
color: $text-secondary;
font-size: 22rpx;
}
&__lock {
font-size: 24rpx;
}
&__current {
color: $accent;
font-size: 22rpx;
}
&__arrow {
color: $text-muted;
font-size: 24rpx;
}
}
.actions-title {
display: block;
color: $text-secondary;
font-size: 24rpx;
margin-bottom: 12rpx;
}
.actions-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.npcs-title {
display: block;
color: $text-secondary;
font-size: 24rpx;
margin-bottom: 12rpx;
}
.npcs-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
// 自动战斗开关样式
.map-panel__auto-combat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.auto-combat-label {
color: $text-primary;
font-size: 26rpx;
}
.auto-combat-toggle {
position: relative;
width: 100rpx;
height: 48rpx;
background-color: $bg-tertiary;
border-radius: 24rpx;
cursor: pointer;
transition: background-color 0.3s;
&--active {
background-color: $accent;
}
&__slider {
position: absolute;
top: 4rpx;
left: 4rpx;
width: 88rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: $bg-primary;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: bold;
transition: transform 0.3s;
color: $text-secondary;
}
&--active &__slider {
background-color: rgba(255, 255, 255, 0.9);
color: $accent;
}
}
</style>

View File

@@ -0,0 +1,423 @@
<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>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { usePlayerStore } from '@/store/player'
import { SKILL_CONFIG } from '@/config/skills'
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'
const player = usePlayerStore()
const showMoreStats = ref(false)
const currentSkillFilter = ref('all')
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, 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
}))
.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 = 0
let defense = 0
let critRate = 5
// 武器攻击力
if (player.equipment.weapon) {
attack += player.equipment.weapon.baseDamage || 0
}
// 防具防御力
if (player.equipment.armor) {
defense += player.equipment.armor.baseDefense || 0
}
// 计算AP和EP
const ap = player.baseStats.dexterity + player.baseStats.intuition * 0.2
const ep = player.baseStats.agility + player.baseStats.intuition * 0.2
return {
attack: Math.floor(attack),
defense: Math.floor(defense),
critRate: critRate,
ap: Math.floor(ap),
ep: Math.floor(ep)
}
})
</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;
}
}
</style>