feat: add backpack

This commit is contained in:
王性驊 2025-11-21 22:26:10 +08:00
parent 4dddadcaf7
commit 01cf45ceb9
3 changed files with 443 additions and 13 deletions

View File

@ -3,7 +3,7 @@
<button class="icon-btn icon-clean" @click="$emit('clean')" :disabled="disabled || poopCount === 0" title="清理"></button>
<button class="icon-btn icon-medicine" @click="$emit('medicine')" :disabled="disabled || !isSick" title="治療"></button>
<button class="icon-btn icon-training" @click="$emit('training')" :disabled="disabled" title="祈禱"></button>
<button class="icon-btn icon-info" @click="$emit('info')" :disabled="disabled" title="資訊"></button>
<button class="icon-btn icon-backpack" @click="$emit('inventory')" :disabled="disabled" title="背包"></button>
</div>
</template>
@ -27,7 +27,7 @@ const props = defineProps({
}
});
defineEmits(['clean', 'medicine', 'training', 'info']);
defineEmits(['clean', 'medicine', 'training', 'inventory']);
</script>
<style scoped>
@ -116,19 +116,28 @@ defineEmits(['clean', 'medicine', 'training', 'info']);
-6px 0px 0 #ffcc00, 6px 0px 0 #ffcc00;
}
/* Info Icon (i) */
.icon-info::before {
/* Backpack Icon */
.icon-backpack::before {
content: '';
position: absolute;
width: 2px;
height: 2px;
background: #4444ff;
background: #8d6e63;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow:
0px -6px 0 #4444ff,
0px -2px 0 #4444ff, 0px 0px 0 #4444ff,
0px 2px 0 #4444ff, 0px 4px 0 #4444ff, 0px 6px 0 #4444ff;
/* Top flap */
-2px -6px 0 #8d6e63, 0px -6px 0 #8d6e63, 2px -6px 0 #8d6e63,
-4px -4px 0 #8d6e63, -2px -4px 0 #a1887f, 0px -4px 0 #a1887f, 2px -4px 0 #a1887f, 4px -4px 0 #8d6e63,
/* Body */
-4px -2px 0 #8d6e63, -2px -2px 0 #5d4037, 0px -2px 0 #5d4037, 2px -2px 0 #5d4037, 4px -2px 0 #8d6e63,
-4px 0px 0 #8d6e63, -2px 0px 0 #5d4037, 0px 0px 0 #5d4037, 2px 0px 0 #5d4037, 4px 0px 0 #8d6e63,
-4px 2px 0 #8d6e63, -2px 2px 0 #5d4037, 0px 2px 0 #5d4037, 2px 2px 0 #5d4037, 4px 2px 0 #8d6e63,
-4px 4px 0 #8d6e63, -2px 4px 0 #5d4037, 0px 4px 0 #5d4037, 2px 4px 0 #5d4037, 4px 4px 0 #8d6e63,
/* Bottom */
-2px 6px 0 #8d6e63, 0px 6px 0 #8d6e63, 2px 6px 0 #8d6e63;
}
</style>

View File

@ -0,0 +1,385 @@
<template>
<div class="inventory-screen" @click.self="$emit('close')">
<div class="inventory-container" @click="$emit('close')">
<div class="screen-title">背包</div>
<div class="inventory-grid">
<div
v-for="(item, index) in items"
:key="index"
class="inventory-slot"
:class="{ 'selected': selectedIndex === index, 'empty': !item }"
:draggable="!!item"
@dragstart="handleDragStart(index, $event)"
@dragover.prevent
@drop="handleDrop(index)"
@click.stop="selectItem(index)"
@mouseenter="item ? handleMouseEnter(item, $event) : null"
@mouseleave="handleMouseLeave"
@dblclick="item ? useItem() : null"
>
<template v-if="item">
<div class="item-icon" :class="item.iconClass"></div>
<div class="item-count" v-if="item.count > 1">x{{ item.count }}</div>
</template>
</div>
</div>
</div>
<!-- Floating Tooltip -->
<div
v-if="hoveredItem"
class="floating-tooltip"
:style="tooltipStyle"
@mouseenter="cancelHideTooltip"
@mouseleave="handleMouseLeave"
>
<div class="tooltip-name">{{ hoveredItem.name }}</div>
<div class="tooltip-desc">{{ hoveredItem.description }}</div>
<div class="tooltip-footer">
<div class="tooltip-hint">雙擊使用</div>
<button class="tooltip-delete-btn" @click.stop="handleDeleteItem(hoveredItem)">
<div class="trash-icon-small"></div>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
inventory: {
type: Array,
default: () => [
{ id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' },
{ id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' },
{ id: 'amulet', name: '平安符', description: '保佑寵物平安', count: 1, iconClass: 'icon-amulet' }
]
}
});
const emit = defineEmits(['close', 'use-item', 'update:inventory']);
const selectedIndex = ref(-1);
const draggedIndex = ref(null);
const isDragging = ref(false);
const items = computed(() => props.inventory);
const selectedItem = computed(() => {
if (selectedIndex.value >= 0 && selectedIndex.value < items.value.length) {
return items.value[selectedIndex.value];
}
return null;
});
function selectItem(index) {
if (items.value[index]) {
selectedIndex.value = index;
} else {
selectedIndex.value = -1;
}
}
// Drag and Drop Logic
function handleDragStart(index, event) {
if (!items.value[index]) return;
draggedIndex.value = index;
isDragging.value = true;
// Set drag image or effect if needed
event.dataTransfer.effectAllowed = 'move';
}
function handleDrop(targetIndex) {
isDragging.value = false;
if (draggedIndex.value === null) return;
const newInventory = [...items.value];
const draggedItem = newInventory[draggedIndex.value];
const targetItem = newInventory[targetIndex];
// Swap items
newInventory[draggedIndex.value] = targetItem;
newInventory[targetIndex] = draggedItem;
emit('update:inventory', newInventory);
draggedIndex.value = null;
}
function handleDeleteItem(item) {
const index = items.value.indexOf(item);
if (index !== -1) {
const newInventory = [...items.value];
newInventory[index] = null;
emit('update:inventory', newInventory);
// Clear hover if deleted
if (hoveredItem.value === item) {
hoveredItem.value = null;
}
}
}
function useItem() {
// Use selected item if available, otherwise use hovered item (for double click)
const itemToUse = selectedItem.value || hoveredItem.value;
if (itemToUse) {
emit('use-item', itemToUse);
}
}
// Tooltip Logic
const hoveredItem = ref(null);
const tooltipStyle = ref({ top: '0px', left: '0px' });
let hideTooltipTimeout = null;
function handleMouseEnter(item, event) {
cancelHideTooltip();
hoveredItem.value = item;
updateTooltipPosition(event);
}
function handleMouseLeave() {
// Delay hiding to allow moving to tooltip
hideTooltipTimeout = setTimeout(() => {
hoveredItem.value = null;
}, 200);
}
function cancelHideTooltip() {
if (hideTooltipTimeout) {
clearTimeout(hideTooltipTimeout);
hideTooltipTimeout = null;
}
}
function updateTooltipPosition(event) {
const rect = event.target.getBoundingClientRect();
const tooltipWidth = 150; // Estimated width
const tooltipHeight = 80; // Estimated height
let top = rect.top - 10;
let left = rect.left + rect.width / 2;
let transform = 'translate(-50%, -100%)';
// Check top boundary
if (top < tooltipHeight) {
// Show below if not enough space above
top = rect.bottom + 10;
transform = 'translate(-50%, 0)';
}
// Check left/right boundary
const viewportWidth = window.innerWidth;
if (left - tooltipWidth / 2 < 0) {
left = tooltipWidth / 2 + 10;
} else if (left + tooltipWidth / 2 > viewportWidth) {
left = viewportWidth - tooltipWidth / 2 - 10;
}
tooltipStyle.value = {
top: top + 'px',
left: left + 'px',
transform: transform
};
}
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
.inventory-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
font-family: 'DotGothic16', monospace;
}
.inventory-container {
width: 100%;
height: 100%;
background: #f0d09c;
border: none;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
}
.screen-title {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #3d2f1f;
border-bottom: 2px dashed #8b4513;
padding-bottom: 8px;
}
.inventory-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
background: #e0b070;
padding: 4px;
border-radius: 4px;
border: 2px solid #c49454;
}
.inventory-slot {
aspect-ratio: 1;
background: #f8e8c8;
border: 2px solid #c49454;
border-radius: 4px;
position: relative;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s;
}
.inventory-slot:hover {
background: #fff;
transform: translateY(-1px);
}
.inventory-slot.selected {
border-color: #d32f2f;
background: #fff;
box-shadow: 0 0 0 2px #d32f2f;
}
.inventory-slot.empty {
background: rgba(0,0,0,0.05);
border-color: rgba(0,0,0,0.1);
cursor: default;
}
.inventory-slot.empty:hover {
background: rgba(0,0,0,0.05);
transform: none;
}
.item-count {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 10px;
color: #333;
background: rgba(255,255,255,0.8);
padding: 0 2px;
border-radius: 2px;
}
/* Item Icons (CSS Shapes) */
.icon-cookie::before {
content: '';
width: 16px;
height: 16px;
background: #d4a373;
border-radius: 50%;
display: block;
box-shadow: inset -2px -2px 0 #8b4513;
}
.icon-water::before {
content: '';
width: 12px;
height: 16px;
background: #4fc3f7;
border-radius: 40% 40% 40% 40% / 60% 60% 40% 40%;
display: block;
box-shadow: inset -2px -2px 0 #0288d1;
}
.icon-amulet::before {
content: '';
width: 14px;
height: 18px;
background: #ffeb3b;
border: 1px solid #fbc02d;
display: block;
box-shadow: 0 2px 0 rgba(0,0,0,0.2);
}
/* Floating Tooltip */
.floating-tooltip {
position: fixed; /* Use fixed to position relative to viewport */
background: rgba(0, 0, 0, 0.9);
border: 1px solid #fff;
padding: 8px;
border-radius: 4px;
/* pointer-events: none; <-- Removed to allow clicking button */
z-index: 200; /* Above everything */
min-width: 120px;
max-width: 200px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.tooltip-name {
color: #ffcc00;
font-weight: bold;
font-size: 12px;
margin-bottom: 2px;
}
.tooltip-desc {
color: #fff;
font-size: 10px;
line-height: 1.3;
margin-bottom: 4px;
}
.tooltip-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed #555;
}
.tooltip-hint {
color: #aaa;
font-size: 9px;
font-style: italic;
}
.tooltip-delete-btn {
background: #d32f2f;
border: 1px solid #b71c1c;
border-radius: 2px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
}
.tooltip-delete-btn:hover {
background: #f44336;
}
.trash-icon-small {
width: 10px;
height: 10px;
background: #fff;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E") no-repeat center;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'/%3E%3C/svg%3E") no-repeat center;
}
</style>

View File

@ -179,6 +179,15 @@
@close="showPetInfo = false"
/>
<!-- Inventory Screen -->
<InventoryScreen
v-if="showInventory"
:inventory="inventory"
@close="showInventory = false"
@use-item="handleUseItem"
@update:inventory="handleInventoryUpdate"
/>
<!-- Action Menu (Bottom) -->
<ActionMenu
:disabled="stage === 'egg'"
@ -188,7 +197,7 @@
@clean="$emit('action', 'clean')"
@medicine="$emit('action', 'medicine')"
@training="showPrayerMenu = true"
@info="showPetInfo = !showPetInfo"
@inventory="showInventory = !showInventory"
/>
</div>
@ -206,6 +215,7 @@ import JiaobeiAnimation from './JiaobeiAnimation.vue';
import FortuneStickAnimation from './FortuneStickAnimation.vue';
import FortuneResult from './FortuneResult.vue';
import PetInfoScreen from './PetInfoScreen.vue';
import InventoryScreen from './InventoryScreen.vue';
import guanyinLots from '../assets/guanyin_100_lots.json';
const props = defineProps({
@ -240,19 +250,24 @@ const showJiaobeiAnimation = ref(false);
const showFortuneStick = ref(false);
const showFortuneResult = ref(false);
const currentLotData = ref(null);
const currentLotNumber = ref(null);
const consecutiveSaintCount = ref(0);
const showPetInfo = ref(false);
const showInventory = ref(false);
const inventory = ref(new Array(16).fill(null));
// Initialize some items
inventory.value[0] = { id: 'cookie', name: '幸運餅乾', description: '增加一點快樂值', count: 5, iconClass: 'icon-cookie' };
inventory.value[1] = { id: 'water', name: '神水', description: '恢復健康', count: 2, iconClass: 'icon-water' };
inventory.value[2] = { id: 'amulet', name: '平安符', description: '保佑寵物平安', count: 1, iconClass: 'icon-amulet' };
const infoPage = ref(0);
const handlePrayerSelect = (mode) => {
fortuneMode.value = mode;
showPrayerMenu.value = false;
if (mode === 'jiaobei') {
fortuneMode.value = 'normal';
showJiaobeiAnimation.value = true;
} else if (mode === 'lots') {
showFortuneStick.value = true;
} else if (type === 'fortune') {
} else if (mode === 'fortune') {
//
fortuneMode.value = 'fortune';
showFortuneStick.value = true;
@ -322,6 +337,27 @@ function handleCloseResult() {
fortuneMode.value = 'normal';
}
function handleUseItem(item) {
console.log('Used item:', item.name);
// TODO: Implement item effects
// Decrease count or remove item
const index = inventory.value.findIndex(i => i === item);
if (index !== -1) {
if (inventory.value[index].count > 1) {
inventory.value[index].count--;
} else {
inventory.value[index] = null;
}
}
showInventory.value = false;
}
function handleInventoryUpdate(newInventory) {
inventory.value = newInventory;
}
// Stats visibility toggle (removed local ref, using prop instead)
// Poop position calculator (all on left side, strictly in game area)