feat: add backpack
This commit is contained in:
parent
4dddadcaf7
commit
01cf45ceb9
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue