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 -- >
< 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"
: class = "{'aspect-video': isDesktop, 'min-h-screen': !isDesktop}" >
<!-- 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"
/ >
< 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"
@ openInventory = "showInventory = true"
@ openGodSystem = "showGodSystem = true"
@ openShop = "showShop = true"
@ openAdventure = "showAdventureSelect = true"
/ >
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"
title = "ACHIEVEMENTS"
>
< AchievementsOverlay :achievements ="ACHIEVEMENTS_DATA" / >
< / PixelModal >
<!-- Inventory Overlay -- >
< PixelModal
: isOpen = "showInventory"
@ close = "showInventory = false"
title = "INVENTORY"
>
< InventoryOverlay
: items = "inventory"
@ equip = "handleEquip"
@ unequip = "handleUnequip"
@ use = "handleUseItem"
@ delete = "handleDeleteItem"
/ >
< / PixelModal >
<!-- God System Overlay -- >
< PixelModal
: isOpen = "showGodSystem"
@ close = "showGodSystem = false"
title = "GOD SYSTEM"
>
< GodSystemOverlay
: currentDeity = "currentDeity"
: deities = "deities"
@ switchDeity = "handleSwitchDeity"
@ addFavor = "handleAddFavor"
/ >
< / PixelModal >
<!-- Shop Overlay -- >
< PixelModal
: isOpen = "showShop"
@ close = "showShop = false"
title = "SHOP"
>
< ShopOverlay
: playerGold = "playerStats.gold || 0"
: inventory = "inventory"
: shopItems = "SHOP_ITEMS"
@ buy = "handleBuyItem"
@ sell = "handleSellItem"
/ >
< / PixelModal >
<!-- Adventure Selection Overlay -- >
< PixelModal
: isOpen = "showAdventureSelect"
@ close = "showAdventureSelect = false"
title = "ADVENTURE"
>
< 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"
class = "mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black uppercase tracking-widest"
>
確定
< / button >
< / div >
< / div >
< / div >
2025-11-25 10:04:01 +00:00
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' ;
import {
ItemType ,
Rarity ,
EquipSlot ,
DeityId ,
ItemCategory
} from '~/types/pixel' ;
import type {
EntityStats ,
Achievement ,
Item ,
Deity ,
AdventureLocation
} from '~/types/pixel' ;
// --- SYSTEMS INITIALIZATION ---
const apiService = new ApiService ( { useMock : true } ) ; // Use mock for now
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 ) ;
return {
name : "Pet" ,
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 06:53:44 +00:00
hunger : Math . floor ( s . hunger ) ,
maxHunger : 100 ,
happiness : Math . floor ( s . happiness ) ,
maxHappiness : 100 ,
2025-11-25 10:04:01 +00:00
2025-11-26 06:53:44 +00:00
age : formatAge ( s . ageSeconds ) ,
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 ) ,
spd : Math . floor ( s . speed || 0 )
} ;
} ) ;
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
} ) ) ;
} ) ;
const deities = computed < Record < DeityId , Deity > > ( ( ) => {
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 ;
} ) ;
const currentDeity = computed ( ( ) => systemState . value ? . currentDeityId || DeityId . Mazu ) ;
// 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 [ ] > ( [ ] ) ;
// --- LIFECYCLE ---
2025-11-25 10:04:01 +00:00
2025-11-26 06:53:44 +00:00
onMounted ( async ( ) => {
petSystem . value = new PetSystem ( apiService ) ;
templeSystem . value = new TempleSystem ( petSystem . value , apiService ) ;
2025-11-25 10:04:01 +00:00
2025-11-26 06:53:44 +00:00
await petSystem . value . initialize ( ) ;
await templeSystem . value . initialize ( ) ;
2025-11-25 10:04:01 +00:00
2025-11-26 06:53:44 +00:00
systemState . value = petSystem . value . getState ( ) ;
allDeities . value = templeSystem . value . getDeities ( ) ;
2025-11-25 10:04:01 +00:00
2025-11-26 06:53:44 +00:00
petSystem . value . startTickLoop ( ( newState ) => {
systemState . value = newState ;
} ) ;
initialized . value = true ;
} ) ;
2025-11-25 10:04:01 +00:00
2025-11-26 06:53:44 +00:00
onUnmounted ( ( ) => {
if ( petSystem . value ) {
petSystem . value . stopTickLoop ( ) ;
}
} ) ;
// --- HELPERS ---
const formatAge = ( seconds : number ) => {
if ( ! seconds ) return '0h' ;
const days = Math . floor ( seconds / 86400 ) ;
const hours = Math . floor ( ( seconds % 86400 ) / 3600 ) ;
if ( days > 0 ) return ` ${ days } d ${ hours } h ` ;
return ` ${ hours } h ` ;
} ;
// --- 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 = [ ] ;
} ;
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 ) ;
} ;
const handleSwitchDeity = async ( id : DeityId ) => {
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 ) {
const sellPrice = Math . floor ( item . price / 2 ) ;
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 ( ) ;
}
} ;
// --- MOCK DATA FOR STATIC CONTENT ---
const ADVENTURE _LOCATIONS : AdventureLocation [ ] = [
{ id : '1' , name : '自家後院' , description : '安全的新手探險地,偶爾會有小蟲子。' , costHunger : 5 , costGold : 5 , difficulty : 'Easy' , enemyName : '野蟲' } ,
{ id : '2' , name : '附近的公園' , description : '熱鬧的公園,但也潛藏著流浪動物的威脅。' , costHunger : 15 , costGold : 10 , reqStats : { str : 20 } , difficulty : 'Medium' , enemyName : '流浪貓' } ,
{ id : '3' , name : '神秘森林' , description : '危險的未知區域,只有強者才能生存。' , costHunger : 30 , costGold : 20 , reqStats : { str : 50 , int : 30 } , difficulty : 'Hard' , enemyName : '樹妖' }
] ;
const ACHIEVEMENTS _DATA : Achievement [ ] = [
{ id : '1' , title : 'First Step' , description : 'Pet age reaches 1 hour' , reward : 'STR Growth +5% INT Growth +5%' , progress : 100 , unlocked : true , icon : 'baby' , color : '#ffe762' } ,
{ id : '2' , title : 'One Day Plan' , description : 'Pet age reaches 1 day' , reward : 'STR/INT/DEX Growth +10% LUCK +2' , progress : 100 , unlocked : true , icon : 'calendar' , color : '#ffe762' } ,
] ;
const SHOP _ITEMS : Item [ ] = [
{ id : 's1' , name : 'Fortune Cookie' , type : ItemType . Consumable , category : ItemCategory . Food , price : 10 , rarity : Rarity . Common , description : 'A crisp cookie with a fortune inside.' , statsDescription : 'Happiness +5' , icon : 'cookie' } ,
{ id : 's2' , name : 'Tuna Can' , type : ItemType . Consumable , category : ItemCategory . Food , price : 30 , rarity : Rarity . Common , description : 'High quality tuna. Cats love it.' , statsDescription : 'Hunger -50' , icon : 'fish' } ,
{ id : 's3' , name : 'Premium Food' , type : ItemType . Consumable , category : ItemCategory . Food , price : 50 , rarity : Rarity . Excellent , description : 'Gourmet pet food.' , statsDescription : 'Hunger -100 Happiness +10' , icon : 'star' } ,
{ id : 's4' , name : 'Magic Wand' , type : ItemType . Equipment , category : ItemCategory . Toy , price : 150 , rarity : Rarity . Rare , description : 'A toy wand that sparkles.' , statsDescription : 'Happiness Regen' , slot : EquipSlot . Weapon , icon : 'wand' } ,
{ id : 's5' , name : 'Ball' , type : ItemType . Equipment , category : ItemCategory . Toy , price : 20 , rarity : Rarity . Common , description : 'A bouncy ball.' , statsDescription : 'Play +10' , slot : EquipSlot . Weapon , icon : 'ball' } ,
{ id : 's6' , name : 'Lucky Coin' , type : ItemType . Equipment , category : ItemCategory . Accessory , price : 500 , rarity : Rarity . Epic , description : 'Increases luck significantly.' , statsDescription : 'LCK +10' , slot : EquipSlot . Accessory , icon : 'coin' } ,
{ id : 's7' , name : 'Health Elixir' , type : ItemType . Consumable , category : ItemCategory . Medicine , price : 100 , rarity : Rarity . Rare , description : 'Fully restores health.' , statsDescription : 'HP Full' , icon : 'potion' } ,
] ;
2025-11-25 10:04:01 +00:00
< / script >