Files
text-adventure-game/components/drawers/InventoryDrawer.vue
Claude cb412544e9 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>
2026-01-21 17:13:51 +08:00

399 lines
9.0 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="inventory-drawer">
<view class="drawer-header">
<text class="drawer-title">📦 背包</text>
<text class="drawer-close" @click="close">×</text>
</view>
<FilterTabs
:tabs="filterTabs"
:modelValue="currentFilter"
@update:modelValue="currentFilter = $event"
/>
<scroll-view class="inventory-list" scroll-y>
<view
v-for="item in filteredItems"
:key="item.uniqueId || item.id"
class="inventory-item"
:class="{ 'inventory-item--equipped': isEquipped(item) }"
@click="selectItem(item)"
>
<text class="inventory-item__icon">{{ item.icon || '📦' }}</text>
<view class="inventory-item__info">
<text class="inventory-item__name" :style="{ color: qualityColor(item.quality) }">
{{ item.name }}
</text>
<text v-if="item.count > 1" class="inventory-item__count">x{{ item.count }}</text>
</view>
<text v-if="isEquipped(item)" class="inventory-item__equipped-badge">E</text>
</view>
<view v-if="filteredItems.length === 0" class="inventory-empty">
<text class="inventory-empty__text">背包空空如也</text>
</view>
</scroll-view>
<!-- 物品详情 -->
<view v-if="selectedItem" class="item-detail">
<view class="item-detail__header">
<text class="item-detail__name">{{ selectedItem.name }}</text>
<text class="item-detail__close" @click="selectedItem = null">×</text>
</view>
<text class="item-detail__desc">{{ selectedItem.description }}</text>
<text v-if="isEquipped(selectedItem)" class="item-detail__equipped"> 已装备</text>
<view class="item-detail__actions">
<TextButton
v-if="canEquip && !isEquipped(selectedItem)"
text="装备"
type="primary"
@click="equipItem"
/>
<TextButton
v-if="canEquip && isEquipped(selectedItem)"
text="卸下"
type="warning"
@click="unequipItem"
/>
<TextButton
v-if="canUse"
text="使用"
@click="useItem"
/>
<TextButton
text="出售"
type="default"
@click="sellItem"
/>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
import FilterTabs from '@/components/common/FilterTabs.vue'
import TextButton from '@/components/common/TextButton.vue'
const player = usePlayerStore()
const game = useGameStore()
const currentFilter = ref('all')
const selectedItem = ref(null)
const emit = defineEmits(['close'])
const filterTabs = [
{ id: 'all', label: '全部' },
{ id: 'weapon', label: '武器' },
{ id: 'armor', label: '防具' },
{ id: 'consumable', label: '消耗品' },
{ id: 'material', label: '素材' }
]
const filteredItems = computed(() => {
const items = player.inventory || []
if (currentFilter.value === 'all') {
return items
}
return items.filter(item => item.type === currentFilter.value)
})
const canEquip = computed(() => {
if (!selectedItem.value) return false
return ['weapon', 'armor', 'shield', 'accessory'].includes(selectedItem.value.type)
})
const canUse = computed(() => {
if (!selectedItem.value) return false
return selectedItem.value.type === 'consumable'
})
// 装备槽位映射
const slotMap = {
weapon: 'weapon',
armor: 'armor',
shield: 'shield',
accessory: 'accessory'
}
// 检查物品是否已装备
function isEquipped(item) {
if (!item || !item.type) return false
const slot = slotMap[item.type]
if (!slot) return false
const equipped = player.equipment[slot]
// 通过 uniqueId 或 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() {
emit('close')
}
function selectItem(item) {
selectedItem.value = 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 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() {
if (!selectedItem.value) return
const item = selectedItem.value
const slot = slotMap[item.type]
if (!slot) return
// 从装备槽移除
player.equipment[slot] = null
// 放回背包
player.inventory.push(item)
game.addLog(`卸下了 ${item.name}`, 'info')
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
)
}
}
// 从背包中移除(如果是堆叠物品,减少数量)
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() {
if (!selectedItem.value) return
// 已装备的物品不能出售
if (isEquipped(selectedItem.value)) {
game.addLog('已装备的物品需要先卸下才能出售', 'error')
return
}
game.addLog(`出售了 ${selectedItem.value.name}`, 'info')
// 出售逻辑待完善
selectedItem.value = null
}
</script>
<style lang="scss" scoped>
.inventory-drawer {
height: 100%;
display: flex;
flex-direction: column;
background-color: $bg-secondary;
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid $border-color;
}
.drawer-title {
color: $text-primary;
font-size: 32rpx;
font-weight: bold;
}
.drawer-close {
color: $text-secondary;
font-size: 48rpx;
padding: 0 16rpx;
&:active {
color: $text-primary;
}
}
.inventory-list {
flex: 1;
padding: 16rpx;
}
.inventory-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx;
background-color: $bg-primary;
border-radius: 8rpx;
margin-bottom: 8rpx;
&:active {
background-color: $bg-tertiary;
}
&--equipped {
background-color: rgba($accent, 0.15);
border: 1rpx solid $accent;
}
&__icon {
font-size: 40rpx;
}
&__info {
flex: 1;
}
&__name {
display: block;
color: $text-primary;
font-size: 28rpx;
}
&__count {
display: block;
color: $text-secondary;
font-size: 22rpx;
margin-top: 4rpx;
}
&__equipped-badge {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: $accent;
color: $bg-primary;
font-size: 18rpx;
font-weight: bold;
border-radius: 50%;
}
}
.inventory-empty {
padding: 80rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 28rpx;
}
}
.item-detail {
padding: 24rpx;
background-color: $bg-primary;
border-top: 1rpx solid $border-color;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
&__name {
color: $text-primary;
font-size: 30rpx;
font-weight: bold;
}
&__close {
color: $text-secondary;
font-size: 40rpx;
padding: 0 16rpx;
}
&__desc {
display: block;
color: $text-secondary;
font-size: 24rpx;
line-height: 1.6;
margin-bottom: 16rpx;
}
&__equipped {
display: inline-block;
padding: 4rpx 12rpx;
background-color: rgba($accent, 0.2);
color: $accent;
font-size: 22rpx;
border-radius: 4rpx;
margin-bottom: 12rpx;
}
&__actions {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
}
}
</style>