pet_data/app/pages/index.vue

856 lines
28 KiB
Vue
Raw Normal View History

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-27 08:42:55 +00:00
@openDeity="showGodSystem = 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 15:31:46 +00:00
@debugAddItems="handleDebugAddItems"
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
>
2025-11-26 15:31:46 +00:00
<AchievementsOverlay :achievements="achievementsState" />
2025-11-26 06:53:44 +00:00
</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"
2025-11-26 15:31:46 +00:00
@repair="handleRepair"
2025-11-26 06:53:44 +00:00
/>
</PixelModal>
<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"
2025-11-27 08:42:55 +00:00
:deitySystemState="petSystem?.deitySystem?.state"
:petSystemState="systemState"
:dailyPrayerCount="systemState?.dailyPrayerCount || 0"
2025-11-26 06:53:44 +00:00
@switchDeity="handleSwitchDeity"
@addFavor="handleAddFavor"
2025-11-27 08:42:55 +00:00
@onJiaobei="handleJiaobei"
@evolve="handleDeityEvolve"
2025-11-26 06:53:44 +00:00
/>
</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';
2025-11-27 08:42:55 +00:00
import NamingOverlay from '~/components/pixel/NamingOverlay.vue';
2025-11-26 06:53:44 +00:00
import { PetSystem } from '../../core/pet-system.js';
import { TempleSystem } from '../../core/temple-system.js';
2025-11-26 15:31:46 +00:00
import { AchievementSystem } from '../../core/achievement-system.js';
2025-11-26 06:53:44 +00:00
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;
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);
2025-11-26 15:31:46 +00:00
const achievementSystem = ref<AchievementSystem | null>(null);
2025-11-26 06:53:44 +00:00
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);
2025-11-27 08:42:55 +00:00
// Removed: now using DEITIES directly for hot reload support
2025-11-26 15:31:46 +00:00
const achievementsState = ref<any[]>([]);
2025-11-26 06:53:44 +00:00
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;
2025-11-27 08:42:55 +00:00
const currentDeity = DEITIES.find(d => d.id === s.currentDeityId);
2025-11-26 06:53:44 +00:00
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 15:31:46 +00:00
species: s.species || 'cat',
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 15:31:46 +00:00
ageSeconds: Math.floor(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> = {};
2025-11-27 08:42:55 +00:00
DEITIES.forEach(d => {
2025-11-26 06:53:44 +00:00
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 15:31:46 +00:00
const handleNameSubmit = async (payload: { name: string, species: string }) => {
2025-11-26 09:53:03 +00:00
if (petSystem.value) {
2025-11-26 15:31:46 +00:00
const speciesId = payload.species === 'dog' ? 'tinyPuppy' : 'tinyTigerCat';
await petSystem.value.updateState({
name: payload.name,
speciesId: speciesId,
species: payload.species // Keep this for UI if needed, or rely on speciesId
});
// Force reload of species config in system if needed, or re-initialize
// Since updateState might not reload config, we might need a specific method or just handle it in updateState
2025-11-26 09:53:03 +00:00
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();
2025-11-26 15:31:46 +00:00
if (achievementSystem.value) {
await achievementSystem.value.reset();
}
2025-11-26 09:53:03 +00:00
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);
2025-11-27 08:42:55 +00:00
templeSystem.value = new TempleSystem(petSystem.value, apiService);
2025-11-26 15:31:46 +00:00
achievementSystem.value = new AchievementSystem(petSystem.value, null, templeSystem.value, apiService);
// Connect achievementSystem to petSystem (circular reference after creation)
petSystem.value.achievementSystem = achievementSystem.value;
2025-11-26 09:53:03 +00:00
// Load Data
console.log("Initializing PetSystem...");
const state = await petSystem.value.initialize();
console.log("Initial State:", state);
systemState.value = state;
2025-11-26 15:31:46 +00:00
// Initialize other systems
await templeSystem.value.initialize();
await achievementSystem.value.initialize();
2025-11-26 09:53:03 +00:00
// Check if naming is required
if (!state.name) {
showNamingOverlay.value = true;
}
2025-11-27 08:42:55 +00:00
// Deities are now loaded directly from DEITIES import for hot reload support
2025-11-26 09:53:03 +00:00
// 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 };
2025-11-26 15:31:46 +00:00
updateAchievementsState();
2025-11-26 09:53:03 +00:00
}
});
// 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 };
2025-11-26 15:31:46 +00:00
updateAchievementsState();
2025-11-26 09:53:03 +00:00
}
} 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 15:31:46 +00:00
// ... (imports)
// Helper to recalculate stats based on equipped items
const recalculateStats = (inventory: any[]) => {
const newBuffs: { flat: Record<string, number>, percent: Record<string, number> } = { flat: {}, percent: {} };
inventory.forEach(item => {
if (item.isEquipped && !item.isAppearance && item.effects) {
// Apply flat effects
if (item.effects.flat) {
Object.entries(item.effects.flat).forEach(([key, value]) => {
newBuffs.flat[key] = (newBuffs.flat[key] || 0) + (value as number);
});
}
// Apply percent effects
if (item.effects.percent) {
Object.entries(item.effects.percent).forEach(([key, value]) => {
newBuffs.percent[key] = (newBuffs.percent[key] || 0) + (value as number);
});
}
}
});
return newBuffs;
};
2025-11-26 06:53:44 +00:00
const handleEquip = async (itemId: string, asAppearance: boolean) => {
2025-11-26 15:31:46 +00:00
if (!petSystem.value) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex === -1) return;
const item = inventory[itemIndex];
const slot = item.slot;
if (!slot) return; // Should not happen for equipment
// Unequip existing item in the same slot and mode
const existingItemIndex = inventory.findIndex((i: any) =>
i.isEquipped && i.slot === slot && !!i.isAppearance === asAppearance
);
if (existingItemIndex !== -1) {
inventory[existingItemIndex].isEquipped = false;
inventory[existingItemIndex].isAppearance = false;
}
// Equip new item and mark as having been equipped (for durability tracking)
inventory[itemIndex].isEquipped = true;
inventory[itemIndex].isAppearance = asAppearance;
inventory[itemIndex].hasBeenEquipped = true; // Mark that this item has been used
// Recalculate stats
const newEquipmentBuffs = recalculateStats(inventory);
await petSystem.value.updateState({
inventory,
equipmentBuffs: newEquipmentBuffs
});
systemState.value = petSystem.value.getState();
2025-11-26 06:53:44 +00:00
};
2025-11-26 15:31:46 +00:00
const handleUnequip = async (slot: string, asAppearance: boolean) => {
if (!petSystem.value) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) =>
i.isEquipped && i.slot === slot && !!i.isAppearance === asAppearance
);
if (itemIndex !== -1) {
inventory[itemIndex].isEquipped = false;
inventory[itemIndex].isAppearance = false;
// Recalculate stats
const newEquipmentBuffs = recalculateStats(inventory);
await petSystem.value.updateState({
inventory,
equipmentBuffs: newEquipmentBuffs
});
systemState.value = petSystem.value.getState();
}
2025-11-26 06:53:44 +00:00
};
const handleUseItem = async (itemId: string) => {
2025-11-26 15:31:46 +00:00
if (!petSystem.value) return;
const currentState = petSystem.value.getState();
let inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex === -1) return;
const item = inventory[itemIndex];
// Apply effects
if (item.effects) {
const updates: any = {};
// Handle modifyStats (e.g. hunger, happiness, health)
if (item.effects.modifyStats) {
Object.entries(item.effects.modifyStats).forEach(([key, value]) => {
const currentVal = currentState[key] || 0;
// Simple clamp to 0-100 for basic stats, though petSystem might handle it
// For now, let's just add it and let petSystem clamp if needed, or clamp here
let newVal = currentVal + (value as number);
if (['hunger', 'happiness', 'health'].includes(key)) {
newVal = Math.min(100, Math.max(0, newVal));
}
updates[key] = newVal;
});
}
// Handle cureSickness
if (item.effects.cureSickness) {
updates.isSick = false;
}
// Handle addBuff (simplified)
if (item.effects.addBuff) {
// This would require a more complex buff system in petSystem
console.log("Buffs not fully implemented yet", item.effects.addBuff);
}
// Apply updates
if (Object.keys(updates).length > 0) {
await petSystem.value.updateState(updates);
}
}
// Reduce quantity or remove
if (item.stackable && item.quantity > 1) {
inventory[itemIndex].quantity--;
} else {
inventory.splice(itemIndex, 1);
}
await petSystem.value.updateState({ inventory });
systemState.value = petSystem.value.getState();
2025-11-26 06:53:44 +00:00
};
const handleDeleteItem = async (itemId: string) => {
2025-11-26 15:31:46 +00:00
if (!petSystem.value) return;
if (!confirm('確定要丟棄這個物品嗎?')) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex !== -1) {
const item = inventory[itemIndex];
inventory.splice(itemIndex, 1);
// Recalculate stats in case an equipped item was deleted
const newEquipmentBuffs = recalculateStats(inventory);
await petSystem.value.updateState({
inventory,
equipmentBuffs: newEquipmentBuffs
});
systemState.value = petSystem.value.getState();
}
2025-11-26 06:53:44 +00:00
};
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();
}
};
2025-11-27 08:42:55 +00:00
const handleJiaobei = async (payload: { result: string }) => {
if (payload.result === 'saint' && petSystem.value?.deitySystem) {
// Update quest progress for jiaobei type
await petSystem.value.deitySystem.updateQuestProgress('jiaobei', 1);
systemState.value = petSystem.value.getState();
}
};
const handleDeityEvolve = async () => {
if (petSystem.value?.deitySystem) {
const success = petSystem.value.deitySystem.evolve();
if (success) {
systemState.value = petSystem.value.getState();
console.log('神明進化成功!');
} else {
console.log('進化失敗:條件不足');
}
}
};
2025-11-26 06:53:44 +00:00
const handleBuyItem = async (item: Item) => {
2025-11-26 15:31:46 +00:00
const price = (item as any).price || 100;
if (petSystem.value && systemState.value.coins >= price) {
const newCoins = systemState.value.coins - price;
2025-11-26 06:53:44 +00:00
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!");
}
};
2025-11-26 15:31:46 +00:00
const handleDebugAddItems = async () => {
if (!petSystem.value) return;
// Add ALL items from ITEMS definition
const newItems = Object.values(ITEMS).map((itemDef: any) => {
const newItem = { ...itemDef };
newItem.id = `${newItem.id}_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
// For debug purposes, set items at full durability (not randomized)
// Durability will only decrease once the item has been equipped (hasBeenEquipped = true)
newItem.quantity = 1;
newItem.isEquipped = false;
// Don't set hasBeenEquipped - it should be undefined for new items
return newItem;
});
const currentInventory = systemState.value.inventory || [];
const updatedInventory = [...currentInventory, ...newItems];
await petSystem.value.updateState({ inventory: updatedInventory });
systemState.value = petSystem.value.getState();
};
const handleRepair = async (itemId: string) => {
if (!petSystem.value) return;
const currentState = petSystem.value.getState();
const inventory = [...currentState.inventory];
const itemIndex = inventory.findIndex((i: any) => i.id === itemId);
if (itemIndex === -1) return;
const item = inventory[itemIndex];
if (!item.maxDurability || item.maxDurability === Infinity) return;
const missingDurability = item.maxDurability - item.durability;
if (missingDurability <= 0) return;
const cost = Math.ceil(missingDurability * 0.5);
if (currentState.coins >= cost) {
inventory[itemIndex].durability = item.maxDurability;
await petSystem.value.updateState({
coins: currentState.coins - cost,
inventory
});
systemState.value = petSystem.value.getState();
console.log(`🔨 Repaired item ${item.name} for ${cost} gold`);
}
};
2025-11-26 06:53:44 +00:00
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;
2025-11-26 15:31:46 +00:00
const updateAchievementsState = () => {
if (achievementSystem.value) {
const all = achievementSystem.value.achievements;
achievementsState.value = all.map(achievement => {
const progress = achievementSystem.value!.getAchievementProgress(achievement);
const isUnlocked = achievementSystem.value!.unlockedAchievements.includes(achievement.id);
return {
...achievement,
unlocked: isUnlocked,
currentValue: progress.current,
maxValue: progress.target,
progress: Math.floor(progress.progress || 0)
};
});
}
};
2025-11-26 09:53:03 +00:00
// 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>