Files
text-adventure-game/components/drawers/InventoryDrawer.vue
Claude 5d4371ba1f feat: 完善数学模型和品质系统
- 修复品质等级范围重叠问题(优秀/稀有无重叠)
- 统一 formulas.js 与 constants.js 的品质判定
- 修复经验公式与游戏实际逻辑不一致
- 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10%
- 添加数学模型文档和README
- 添加数值验证脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:54:20 +08:00

374 lines
8.4 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: 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>
<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="canRead"
text="阅读"
type="primary"
@click="readItem"
/>
<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 { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
import { startTask } from '@/utils/taskSystem.js'
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 canRead = computed(() => {
if (!selectedItem.value) return false
return selectedItem.value.type === 'book'
})
// 装备槽位映射
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 比较,确保每个装备独立判断
return equipped && equipped.uniqueId === item.uniqueId
}
function close() {
emit('close')
}
function selectItem(item) {
selectedItem.value = item
}
function equipItem() {
if (!selectedItem.value) return
const result = equipItemUtil(player, game, selectedItem.value.uniqueId)
if (result.success) {
selectedItem.value = null
}
}
function unequipItem() {
if (!selectedItem.value) return
const item = selectedItem.value
const slot = slotMap[item.type]
if (!slot) return
const result = unequipItemBySlot(player, game, slot)
if (result.success) {
selectedItem.value = null
}
}
function useItem() {
if (!selectedItem.value) return
const result = useItemUtil(player, game, selectedItem.value.id, 1)
if (result.success) {
selectedItem.value = null
}
}
function readItem() {
if (!selectedItem.value) return
// 检查是否已经在阅读中
const existingTask = game.activeTasks?.find(t => t.type === 'reading')
if (existingTask) {
game.addLog('已经在阅读其他书籍了', 'error')
return
}
const result = startTask(game, player, 'reading', {
itemId: selectedItem.value.id,
duration: selectedItem.value.readingTime || 60
})
if (result.success) {
game.addLog(`开始阅读《${selectedItem.value.name}`, 'info')
selectedItem.value = null
emit('close') // 关闭背包以便看到阅读进度
} else {
game.addLog(result.message, 'error')
}
}
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%;
}
}
.quality-badge {
font-size: 18rpx;
opacity: 0.8;
margin-left: 8rpx;
}
.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>