2025-11-25 10:04:01 +00:00
|
|
|
<template>
|
2025-11-26 06:53:44 +00:00
|
|
|
<div class="w-full min-h-screen bg-[#1b1026] flex items-center justify-center p-2 md:p-4 lg:p-8 font-sans">
|
|
|
|
|
<!-- Main Container -->
|
2025-11-26 09:53:03 +00:00
|
|
|
<div class="w-full max-w-7xl bg-[#0f0816] border-4 md:border-6 border-[#2b193f] relative shadow-2xl flex flex-col md:flex-row overflow-hidden rounded-lg min-h-screen md:min-h-0 md:aspect-video">
|
2025-11-26 06:53:44 +00:00
|
|
|
|
|
|
|
|
<!-- Left Column: Player Panel -->
|
|
|
|
|
<div class="w-full md:w-1/3 lg:w-1/4 h-auto md:h-full border-b-4 md:border-b-0 md:border-r-4 border-[#2b193f] bg-[#1b1026] z-20">
|
|
|
|
|
<PlayerPanel
|
|
|
|
|
v-if="initialized"
|
|
|
|
|
:stats="playerStats"
|
|
|
|
|
@openAchievements="showAchievements = true"
|
2025-11-26 09:53:03 +00:00
|
|
|
@deletePet="handleDeletePet"
|
|
|
|
|
@openInventory="showInventory = true"
|
2025-11-26 06:53:44 +00:00
|
|
|
/>
|
|
|
|
|
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
|
|
|
|
|
Initializing...
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
<!-- Middle Column: Room + Actions -->
|
|
|
|
|
<div class="w-full md:w-1/3 lg:w-1/2 h-auto md:h-full flex flex-col relative z-10">
|
|
|
|
|
|
|
|
|
|
<!-- Top: Battle/Room Area -->
|
|
|
|
|
<div class="h-64 md:h-[55%] border-b-4 border-[#2b193f] relative bg-[#0f0816]">
|
|
|
|
|
<BattleArea
|
|
|
|
|
v-if="initialized"
|
|
|
|
|
:currentDeityId="currentDeity"
|
|
|
|
|
:isFighting="isFighting"
|
|
|
|
|
:battleLogs="battleLogs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
<!-- Bottom: Action Area -->
|
|
|
|
|
<div class="h-auto md:h-[45%] bg-[#1b1026]">
|
|
|
|
|
<ActionArea
|
|
|
|
|
v-if="initialized"
|
|
|
|
|
:playerStats="playerStats"
|
2025-11-26 09:53:03 +00:00
|
|
|
@feed="handleFeed"
|
|
|
|
|
@play="handlePlay"
|
|
|
|
|
@train="handleTrain"
|
|
|
|
|
@puzzle="handlePuzzle"
|
|
|
|
|
@clean="handleClean"
|
|
|
|
|
@heal="handleHeal"
|
2025-11-26 06:53:44 +00:00
|
|
|
@openInventory="showInventory = true"
|
|
|
|
|
@openGodSystem="showGodSystem = true"
|
|
|
|
|
@openShop="showShop = true"
|
|
|
|
|
@openAdventure="showAdventureSelect = true"
|
2025-11-26 09:53:03 +00:00
|
|
|
@toggleSleep="handleToggleSleep"
|
2025-11-26 06:53:44 +00:00
|
|
|
/>
|
2025-11-25 10:04:01 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
<!-- Right Column: Deity Panel (Info Panel) -->
|
|
|
|
|
<div class="w-full md:w-1/3 lg:w-1/4 h-auto md:h-full border-t-4 md:border-t-0 md:border-l-4 border-[#2b193f] bg-[#1b1026] z-20">
|
|
|
|
|
<InfoPanel v-if="deities[currentDeity]" :deity="deities[currentDeity]" />
|
|
|
|
|
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
|
|
|
|
|
Loading...
|
|
|
|
|
</div>
|
2025-11-25 10:04:01 +00:00
|
|
|
</div>
|
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
</div>
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
<!-- --- MODALS --- -->
|
|
|
|
|
|
|
|
|
|
<!-- Achievements Overlay -->
|
|
|
|
|
<PixelModal
|
|
|
|
|
:isOpen="showAchievements"
|
|
|
|
|
@close="showAchievements = false"
|
2025-11-26 09:53:03 +00:00
|
|
|
title="成就"
|
2025-11-26 06:53:44 +00:00
|
|
|
>
|
|
|
|
|
<AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" />
|
|
|
|
|
</PixelModal>
|
|
|
|
|
|
|
|
|
|
<!-- Inventory Overlay -->
|
|
|
|
|
<PixelModal
|
|
|
|
|
:isOpen="showInventory"
|
|
|
|
|
@close="showInventory = false"
|
2025-11-26 09:53:03 +00:00
|
|
|
title="背包"
|
2025-11-26 06:53:44 +00:00
|
|
|
>
|
|
|
|
|
<InventoryOverlay
|
|
|
|
|
:items="inventory"
|
|
|
|
|
@equip="handleEquip"
|
|
|
|
|
@unequip="handleUnequip"
|
|
|
|
|
@use="handleUseItem"
|
|
|
|
|
@delete="handleDeleteItem"
|
|
|
|
|
/>
|
|
|
|
|
</PixelModal>
|
|
|
|
|
|
|
|
|
|
<!-- God System Overlay -->
|
|
|
|
|
<PixelModal
|
|
|
|
|
:isOpen="showGodSystem"
|
|
|
|
|
@close="showGodSystem = false"
|
2025-11-26 09:53:03 +00:00
|
|
|
title="神明系統"
|
2025-11-26 06:53:44 +00:00
|
|
|
>
|
|
|
|
|
<GodSystemOverlay
|
|
|
|
|
:currentDeity="currentDeity"
|
|
|
|
|
:deities="deities"
|
|
|
|
|
@switchDeity="handleSwitchDeity"
|
|
|
|
|
@addFavor="handleAddFavor"
|
|
|
|
|
/>
|
|
|
|
|
</PixelModal>
|
|
|
|
|
|
|
|
|
|
<!-- Shop Overlay -->
|
|
|
|
|
<PixelModal
|
|
|
|
|
:isOpen="showShop"
|
|
|
|
|
@close="showShop = false"
|
2025-11-26 09:53:03 +00:00
|
|
|
title="商店"
|
2025-11-26 06:53:44 +00:00
|
|
|
>
|
|
|
|
|
<ShopOverlay
|
|
|
|
|
:playerGold="playerStats.gold || 0"
|
|
|
|
|
:inventory="inventory"
|
|
|
|
|
:shopItems="SHOP_ITEMS"
|
|
|
|
|
@buy="handleBuyItem"
|
|
|
|
|
@sell="handleSellItem"
|
|
|
|
|
/>
|
|
|
|
|
</PixelModal>
|
|
|
|
|
|
|
|
|
|
<!-- Adventure Selection Overlay -->
|
|
|
|
|
<PixelModal
|
|
|
|
|
:isOpen="showAdventureSelect"
|
|
|
|
|
@close="showAdventureSelect = false"
|
2025-11-26 09:53:03 +00:00
|
|
|
title="冒險"
|
2025-11-26 06:53:44 +00:00
|
|
|
>
|
|
|
|
|
<AdventureOverlay
|
|
|
|
|
:locations="ADVENTURE_LOCATIONS"
|
|
|
|
|
:playerStats="playerStats"
|
|
|
|
|
@selectLocation="handleStartAdventure"
|
|
|
|
|
@close="showAdventureSelect = false"
|
|
|
|
|
/>
|
|
|
|
|
</PixelModal>
|
|
|
|
|
|
|
|
|
|
<!-- Battle Result Modal (Custom Styling Modal) -->
|
|
|
|
|
<div v-if="showBattleResult" class="fixed inset-0 z-[110] flex items-center justify-center bg-black/80">
|
|
|
|
|
<div class="w-[500px] border-4 border-[#2ce8f4] bg-black p-1 shadow-[0_0_50px_#2ce8f4]">
|
|
|
|
|
<div class="border-2 border-[#2ce8f4] p-8 flex flex-col items-center gap-4">
|
|
|
|
|
<PartyPopper :size="48" class="text-[#99e550] animate-bounce" />
|
|
|
|
|
<h2 class="text-2xl text-[#99e550] font-bold tracking-widest">冒險完成 !</h2>
|
|
|
|
|
<div class="w-full border-t border-gray-700 my-2"></div>
|
|
|
|
|
<p class="text-gray-400 text-sm">這次沒有獲得任何獎勵...</p>
|
|
|
|
|
<button
|
|
|
|
|
@click="handleCloseBattleResult"
|
2025-11-26 09:53:03 +00:00
|
|
|
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black tracking-widest"
|
2025-11-26 06:53:44 +00:00
|
|
|
>
|
|
|
|
|
確定
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
<!-- Naming Overlay -->
|
|
|
|
|
<NamingOverlay
|
|
|
|
|
v-if="showNamingOverlay"
|
|
|
|
|
@submit="handleNameSubmit"
|
|
|
|
|
/>
|
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
</div>
|
|
|
|
|
</template>
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
|
|
|
import { PartyPopper } from 'lucide-vue-next';
|
|
|
|
|
import PlayerPanel from '~/components/pixel/PlayerPanel.vue';
|
|
|
|
|
import BattleArea from '~/components/pixel/BattleArea.vue';
|
|
|
|
|
import ActionArea from '~/components/pixel/ActionArea.vue';
|
|
|
|
|
import InfoPanel from '~/components/pixel/InfoPanel.vue';
|
|
|
|
|
import PixelModal from '~/components/pixel/PixelModal.vue';
|
|
|
|
|
import AchievementsOverlay from '~/components/pixel/AchievementsOverlay.vue';
|
|
|
|
|
import InventoryOverlay from '~/components/pixel/InventoryOverlay.vue';
|
|
|
|
|
import GodSystemOverlay from '~/components/pixel/GodSystemOverlay.vue';
|
|
|
|
|
import ShopOverlay from '~/components/pixel/ShopOverlay.vue';
|
|
|
|
|
import AdventureOverlay from '~/components/pixel/AdventureOverlay.vue';
|
|
|
|
|
|
|
|
|
|
import { PetSystem } from '../../core/pet-system.js';
|
|
|
|
|
import { TempleSystem } from '../../core/temple-system.js';
|
|
|
|
|
import { ApiService } from '../../core/api-service.js';
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
// Import from data and core instead of types
|
|
|
|
|
import { DEITIES } from '../../data/deities.js';
|
|
|
|
|
import { ITEMS, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../data/items.js';
|
|
|
|
|
import { ADVENTURES } from '../../data/adventures.js';
|
|
|
|
|
import { ACHIEVEMENTS } from '../../data/achievements.js';
|
|
|
|
|
|
|
|
|
|
// Type definitions (minimal, based on actual data structures)
|
|
|
|
|
type EntityStats = any;
|
|
|
|
|
import NamingOverlay from '~/components/pixel/NamingOverlay.vue';
|
|
|
|
|
type Deity = typeof DEITIES[0];
|
|
|
|
|
type Item = typeof ITEMS[keyof typeof ITEMS];
|
|
|
|
|
type AdventureLocation = typeof ADVENTURES[0];
|
2025-11-26 06:53:44 +00:00
|
|
|
|
|
|
|
|
// --- SYSTEMS INITIALIZATION ---
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const apiService = new ApiService({ useMock: true }); // Use localStorage mock for now
|
2025-11-26 06:53:44 +00:00
|
|
|
const petSystem = ref<PetSystem | null>(null);
|
|
|
|
|
const templeSystem = ref<TempleSystem | null>(null);
|
|
|
|
|
const initialized = ref(false);
|
|
|
|
|
|
|
|
|
|
// --- RESPONSIVE ---
|
|
|
|
|
const isDesktop = ref(true);
|
|
|
|
|
|
|
|
|
|
// Detect screen size
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
const updateScreenSize = () => {
|
|
|
|
|
isDesktop.value = window.innerWidth >= 768;
|
|
|
|
|
};
|
|
|
|
|
updateScreenSize();
|
|
|
|
|
window.addEventListener('resize', updateScreenSize);
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', updateScreenSize);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- STATE ---
|
|
|
|
|
|
|
|
|
|
// Reactive state mapped from PetSystem
|
|
|
|
|
const systemState = ref<any>(null);
|
|
|
|
|
const allDeities = ref<Deity[]>([]);
|
|
|
|
|
|
|
|
|
|
const playerStats = computed<EntityStats>(() => {
|
|
|
|
|
if (!systemState.value) return {
|
|
|
|
|
name: "Loading...", class: "Egg", hp: 100, maxHp: 100, sp: 0, maxSp: 0, lvl: 1,
|
|
|
|
|
hunger: 100, maxHunger: 100, happiness: 100, maxHappiness: 100,
|
|
|
|
|
age: "0d 0h", generation: 1, height: "0 cm", weight: "0 g", gold: 0, fate: "Unknown",
|
|
|
|
|
godFavor: { name: "None", current: 0, max: 100 },
|
|
|
|
|
str: 0, int: 0, dex: 0, luck: 0, atk: 0, def: 0, spd: 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const s = systemState.value;
|
|
|
|
|
const currentDeity = allDeities.value.find(d => d.id === s.currentDeityId);
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
// Calculate real-time age (stored ageSeconds + time since last tick)
|
|
|
|
|
const timeSinceLastTick = s.lastTickTime ? (Date.now() - s.lastTickTime) / 1000 : 0;
|
|
|
|
|
const currentAge = (s.ageSeconds || 0) + timeSinceLastTick;
|
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
return {
|
2025-11-26 09:53:03 +00:00
|
|
|
name: s.name || "Pet",
|
2025-11-26 06:53:44 +00:00
|
|
|
class: s.stage,
|
|
|
|
|
hp: Math.floor(s.health),
|
|
|
|
|
maxHp: 100,
|
|
|
|
|
sp: 0,
|
|
|
|
|
maxSp: 100,
|
|
|
|
|
lvl: 1,
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
hunger: Math.floor(s.hunger || 0),
|
2025-11-26 06:53:44 +00:00
|
|
|
maxHunger: 100,
|
2025-11-26 09:53:03 +00:00
|
|
|
happiness: Math.floor(s.happiness ?? 100),
|
2025-11-26 06:53:44 +00:00
|
|
|
maxHappiness: 100,
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
age: formatAge(currentAge),
|
2025-11-26 06:53:44 +00:00
|
|
|
generation: s.generation || 1,
|
|
|
|
|
height: `${s.height || 0} cm`,
|
|
|
|
|
weight: `${Math.floor(s.weight || 0)} g`,
|
|
|
|
|
gold: s.coins || 0,
|
|
|
|
|
fate: s.destiny?.name || "None",
|
|
|
|
|
|
|
|
|
|
godFavor: {
|
|
|
|
|
name: currentDeity?.name || "None",
|
|
|
|
|
current: s.deityFavors?.[s.currentDeityId] || 0,
|
|
|
|
|
max: 100
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
str: Math.floor(s.effectiveStr || s.str),
|
|
|
|
|
int: Math.floor(s.effectiveInt || s.int),
|
|
|
|
|
dex: Math.floor(s.effectiveDex || s.dex),
|
|
|
|
|
luck: Math.floor(s.effectiveLuck || s.luck),
|
|
|
|
|
atk: Math.floor(s.attack || 0),
|
|
|
|
|
def: Math.floor(s.defense || 0),
|
2025-11-26 09:53:03 +00:00
|
|
|
spd: Math.floor(s.speed || 0),
|
|
|
|
|
|
|
|
|
|
// Status flags
|
|
|
|
|
poopCount: s.poopCount || 0,
|
|
|
|
|
isSick: s.isSick || false,
|
|
|
|
|
isSleeping: s.isSleeping || false,
|
|
|
|
|
dyingSeconds: s.dyingSeconds || 0
|
2025-11-26 06:53:44 +00:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const inventory = computed<Item[]>(() => {
|
|
|
|
|
if (!systemState.value || !systemState.value.inventory) return [];
|
|
|
|
|
return systemState.value.inventory.map((i: any) => ({
|
|
|
|
|
...i,
|
|
|
|
|
icon: i.icon || 'circle',
|
|
|
|
|
statsDescription: i.description
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const deities = computed(() => {
|
2025-11-26 06:53:44 +00:00
|
|
|
const map: Record<string, Deity> = {};
|
|
|
|
|
allDeities.value.forEach(d => {
|
|
|
|
|
const favor = systemState.value?.deityFavors?.[d.id] || 0;
|
|
|
|
|
map[d.id] = { ...d, favor, maxFavor: 100 };
|
|
|
|
|
});
|
|
|
|
|
return map;
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const currentDeity = computed(() => systemState.value?.currentDeityId || 'mazu');
|
2025-11-26 06:53:44 +00:00
|
|
|
|
|
|
|
|
// Modal States
|
|
|
|
|
const showAchievements = ref(false);
|
|
|
|
|
const showInventory = ref(false);
|
|
|
|
|
const showGodSystem = ref(false);
|
|
|
|
|
const showShop = ref(false);
|
|
|
|
|
const showAdventureSelect = ref(false);
|
|
|
|
|
const showBattleResult = ref(false);
|
|
|
|
|
|
|
|
|
|
// Battle State
|
|
|
|
|
const isFighting = ref(false);
|
|
|
|
|
const battleLogs = ref<string[]>([]);
|
2025-11-26 09:53:03 +00:00
|
|
|
const showNamingOverlay = ref(false);
|
2025-11-26 06:53:44 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const handleNameSubmit = async (name: string) => {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
await petSystem.value.updateState({ name });
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
showNamingOverlay.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const handleDeletePet = async () => {
|
|
|
|
|
if (confirm('確定要刪除寵物嗎?此操作無法撤銷!(Are you sure you want to delete your pet?)')) {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
await petSystem.value.deletePet();
|
|
|
|
|
location.reload(); // Reload to reset state and trigger new game flow
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
// --- LIFECYCLE ---
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const stateSyncInterval = ref<any>(null);
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
onMounted(async () => {
|
|
|
|
|
// Initialize Systems
|
|
|
|
|
petSystem.value = new PetSystem(apiService);
|
|
|
|
|
templeSystem.value = new TempleSystem(apiService, petSystem.value);
|
|
|
|
|
|
|
|
|
|
// Load Data
|
|
|
|
|
console.log("Initializing PetSystem...");
|
|
|
|
|
const state = await petSystem.value.initialize();
|
|
|
|
|
console.log("Initial State:", state);
|
|
|
|
|
systemState.value = state;
|
|
|
|
|
|
|
|
|
|
// Check if naming is required
|
|
|
|
|
if (!state.name) {
|
|
|
|
|
showNamingOverlay.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load Deities
|
|
|
|
|
allDeities.value = await templeSystem.value.getDeities();
|
|
|
|
|
|
|
|
|
|
// Start Game Loop with callback to update UI
|
|
|
|
|
petSystem.value.startTickLoop((newState: any) => {
|
|
|
|
|
// Use nextTick to ensure safe state updates
|
|
|
|
|
if (newState && petSystem.value) {
|
|
|
|
|
systemState.value = { ...newState };
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Polling for frequent UI updates (every 1s)
|
|
|
|
|
stateSyncInterval.value = setInterval(() => {
|
|
|
|
|
if (petSystem.value && !document.hidden) {
|
|
|
|
|
try {
|
|
|
|
|
const currentState = petSystem.value.getState();
|
|
|
|
|
if (currentState) {
|
|
|
|
|
systemState.value = { ...currentState };
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error updating state:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
initialized.value = true;
|
2025-11-26 06:53:44 +00:00
|
|
|
});
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
onUnmounted(() => {
|
2025-11-26 09:53:03 +00:00
|
|
|
// Clear intervals first
|
|
|
|
|
if (stateSyncInterval.value) {
|
|
|
|
|
clearInterval(stateSyncInterval.value);
|
|
|
|
|
stateSyncInterval.value = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop tick loop
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
petSystem.value.stopTickLoop();
|
|
|
|
|
petSystem.value = null;
|
|
|
|
|
}
|
|
|
|
|
if (templeSystem.value) {
|
|
|
|
|
templeSystem.value = null;
|
|
|
|
|
}
|
2025-11-26 06:53:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --- HELPERS ---
|
|
|
|
|
|
|
|
|
|
const formatAge = (seconds: number) => {
|
2025-11-26 09:53:03 +00:00
|
|
|
if (!seconds) return '0s';
|
2025-11-26 06:53:44 +00:00
|
|
|
const days = Math.floor(seconds / 86400);
|
|
|
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
2025-11-26 09:53:03 +00:00
|
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
if (days > 0) return `${days}d ${hours}h`;
|
2025-11-26 09:53:03 +00:00
|
|
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
|
|
|
if (minutes > 0) return `${minutes}m ${secs}s`;
|
|
|
|
|
return `${secs}s`;
|
2025-11-26 06:53:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- HANDLERS ---
|
|
|
|
|
|
|
|
|
|
const handleStartAdventure = (location: AdventureLocation) => {
|
|
|
|
|
showAdventureSelect.value = false;
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
petSystem.value.updateState({
|
|
|
|
|
hunger: Math.max(0, systemState.value.hunger - location.costHunger),
|
|
|
|
|
coins: Math.max(0, systemState.value.coins - location.costGold)
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
isFighting.value = true;
|
|
|
|
|
battleLogs.value = [`Entered ${location.name}...`, `Encountered ${location.enemyName}!`];
|
|
|
|
|
|
|
|
|
|
let turn = 1;
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
if (turn > 5) {
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
battleLogs.value.push("Victory!", "Obtained 10 EXP!");
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isFighting.value = false;
|
|
|
|
|
showBattleResult.value = true;
|
|
|
|
|
}, 1500);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isPlayerTurn = turn % 2 !== 0;
|
|
|
|
|
if (isPlayerTurn) {
|
|
|
|
|
battleLogs.value.push(`You used Attack! Dealt ${Math.floor(Math.random() * 20) + 10} damage.`);
|
|
|
|
|
} else {
|
|
|
|
|
battleLogs.value.push(`${location.enemyName} attacked! You took ${Math.floor(Math.random() * 10)} damage.`);
|
|
|
|
|
}
|
|
|
|
|
turn++;
|
|
|
|
|
}, 1000);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCloseBattleResult = () => {
|
|
|
|
|
showBattleResult.value = false;
|
|
|
|
|
battleLogs.value = [];
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const handleFeed = async () => {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
const result = await petSystem.value.feed();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
console.log('🍎 餵食成功');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('餵食失敗:', result.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePlay = async (gameType = 'normal') => {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
const result = await petSystem.value.play({ gameType });
|
|
|
|
|
if (result.success) {
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
console.log('🎮 玩耍成功');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('玩耍失敗:', result.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTrain = async () => {
|
|
|
|
|
// 訓練 = 訓練類型的玩耍
|
|
|
|
|
handlePlay('training');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePuzzle = async () => {
|
|
|
|
|
// 益智 = 益智類型的玩耍
|
|
|
|
|
handlePlay('puzzle');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClean = async () => {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
const result = await petSystem.value.cleanPoop();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
console.log('🧹 清理成功');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('清理失敗:', result.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleHeal = async () => {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
const result = await petSystem.value.heal();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
console.log('💊 治療成功');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('治療失敗:', result.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggleSleep = async () => {
|
|
|
|
|
if (petSystem.value) {
|
|
|
|
|
const result = await petSystem.value.toggleSleep();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
console.log(result.isSleeping ? '😴 寵物睡著了' : '⏰ 寵物醒來了');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('切換睡眠失敗:', result.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
const handleEquip = async (itemId: string, asAppearance: boolean) => {
|
|
|
|
|
console.log("Equip not fully implemented in core yet", itemId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUnequip = async (slot: EquipSlot, asAppearance: boolean) => {
|
|
|
|
|
console.log("Unequip not fully implemented in core yet", slot);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUseItem = async (itemId: string) => {
|
|
|
|
|
console.log("Use item not fully implemented in core yet", itemId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDeleteItem = async (itemId: string) => {
|
|
|
|
|
console.log("Delete item not fully implemented in core yet", itemId);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
const handleSwitchDeity = async (id: string) => {
|
2025-11-26 06:53:44 +00:00
|
|
|
if (templeSystem.value) {
|
|
|
|
|
await templeSystem.value.switchDeity(id);
|
|
|
|
|
systemState.value = petSystem.value?.getState();
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-25 10:04:01 +00:00
|
|
|
|
2025-11-26 06:53:44 +00:00
|
|
|
const handleAddFavor = async (amount: number) => {
|
|
|
|
|
if (templeSystem.value) {
|
|
|
|
|
await templeSystem.value.pray();
|
|
|
|
|
systemState.value = petSystem.value?.getState();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBuyItem = async (item: Item) => {
|
|
|
|
|
if (petSystem.value && systemState.value.coins >= item.price) {
|
|
|
|
|
const newCoins = systemState.value.coins - item.price;
|
|
|
|
|
const newInventory = [...(systemState.value.inventory || []), { ...item, id: `buy-${Date.now()}` }];
|
|
|
|
|
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
} else {
|
|
|
|
|
alert("Not enough gold!");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSellItem = async (item: Item) => {
|
|
|
|
|
if (petSystem.value) {
|
2025-11-26 09:53:03 +00:00
|
|
|
const sellPrice = Math.floor((item as any).price / 2);
|
2025-11-26 06:53:44 +00:00
|
|
|
const newCoins = systemState.value.coins + sellPrice;
|
|
|
|
|
const newInventory = systemState.value.inventory.filter((i: any) => i.id !== item.id);
|
|
|
|
|
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
|
|
|
|
|
systemState.value = petSystem.value.getState();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
// Use imported data instead of mock
|
|
|
|
|
const ADVENTURE_LOCATIONS = ADVENTURES;
|
|
|
|
|
const ACHIEVEMENTS_DATA = ACHIEVEMENTS;
|
|
|
|
|
|
|
|
|
|
// Convert ITEMS object to array for shop
|
|
|
|
|
const SHOP_ITEMS = Object.values(ITEMS).filter((item: any) => item.type === 'consumable' || item.type === 'equipment').map((item: any) => ({
|
|
|
|
|
...item,
|
|
|
|
|
statsDescription: item.description
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
2025-11-25 10:04:01 +00:00
|
|
|
</script>
|