fix: query pointer

This commit is contained in:
王性驊 2025-11-26 17:53:03 +08:00
parent 873bc64cd2
commit 8def70de92
27 changed files with 3196 additions and 1070 deletions

View File

@ -6,13 +6,26 @@
<style> <style>
/* Global Styles for Pixel Art */ /* Global Styles for Pixel Art */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
/* 俐方體 11 號字型 */
@font-face {
font-family: 'Cubic 11';
src: url('/fonts/cubicll.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
body { body {
margin: 0; margin: 0;
background-color: #1b1026; background-color: #1b1026;
color: #e0d8f0; color: #e0d8f0;
font-family: 'Press Start 2P', monospace; font-family: 'Cubic 11', 'DotGothic16', 'Courier New', monospace !important;
}
/* Force all elements to use Cubic 11 */
* {
font-family: 'Cubic 11', 'DotGothic16', 'Courier New', monospace !important;
} }
/* Scrollbar Styling */ /* Scrollbar Styling */

View File

@ -1,3 +1,12 @@
/* 俐方體 11 號字型 */
@font-face {
font-family: 'Cubic 11';
src: url('/fonts/cubicll.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@import "tailwindcss"; @import "tailwindcss";
@theme { @theme {
@ -15,4 +24,7 @@
--color-pixel-panel: #2b193f; --color-pixel-panel: #2b193f;
--color-pixel-panel-dark: #1b1026; --color-pixel-panel-dark: #1b1026;
--color-pixel-panel-border: #4a3b5e; --color-pixel-panel-border: #4a3b5e;
/* Pixel Font Family - 使用俐方體 11 號 */
--font-family-pixel: 'Cubic 11', monospace;
} }

View File

@ -76,8 +76,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { CheckCircle2, Lock, Trophy, Baby, CalendarDays, Egg, Sprout, Cake, Star, Diamond, Milk, Utensils, Gamepad2, Sparkles, BookOpen, Search, Leaf, Dumbbell, Brush, Pill } from 'lucide-vue-next'; import { CheckCircle2, Lock, Trophy, Baby, CalendarDays, Egg, Sprout, Cake, Star, Diamond, Milk, Utensils, Gamepad2, Sparkles, BookOpen, Search, Leaf, Dumbbell, Brush, Pill } from 'lucide-vue-next';
import RetroProgressBar from './RetroProgressBar.vue'; // Need to create this one too! import PixelFrame from './PixelFrame.vue';
import type { Achievement } from '~/types/pixel'; import RetroProgressBar from './RetroProgressBar.vue';
type Achievement = any;
interface Props { interface Props {
achievements: Achievement[]; achievements: Achievement[];

View File

@ -1,45 +1,29 @@
<template> <template>
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar"> <div class="h-full flex flex-col p-4 bg-[#1b1026] overflow-y-auto custom-scrollbar relative">
<!-- Table Background styling --> <!-- Table Background styling -->
<div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" /> <div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" />
<!-- Main Grid Layout --> <!-- Main Action Grid -->
<div class="flex-grow grid grid-cols-12 gap-2 z-10"> <div class="flex-grow z-10">
<!-- Left: Hand (Col 3) -->
<div class="col-span-3 flex flex-col gap-2">
<PixelFrame title="HAND" class="h-full bg-[#1b1026]" variant="inset">
<div class="flex flex-col gap-2 h-full overflow-y-auto pr-1 custom-scrollbar">
<div v-for="(card, i) in handCards" :key="i" class="bg-[#2b193f] border-2 border-[#4a3b5e] p-1.5 flex flex-col hover:-translate-y-1 transition-transform cursor-pointer group shadow-lg">
<div class="flex justify-between items-start mb-1">
<div class="w-5 h-5 bg-[#1b1026] flex items-center justify-center rounded-sm">
<span class="text-[#f6b26b] text-[10px] font-bold">{{ card.cost }}</span>
</div>
<component :is="card.icon" :size="14" :color="card.color" />
</div>
<span class="text-xs text-[#e0d8f0] uppercase tracking-wide group-hover:text-[#f6b26b]">{{ card.name }}</span>
</div>
<!-- Empty Slot -->
<div class="border-2 border-dashed border-[#4a3b5e] rounded h-16 opacity-30 flex items-center justify-center text-xs">EMPTY</div>
</div>
</PixelFrame>
</div>
<!-- Center: Action Grid (Col 6) -->
<div class="col-span-6 flex flex-col relative">
<PixelFrame class="h-full bg-[#2b193f]"> <PixelFrame class="h-full bg-[#2b193f]">
<div class="grid grid-cols-4 grid-rows-3 gap-2 h-full p-1"> <div class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-2 p-2 h-full content-start overflow-y-auto custom-scrollbar">
<template v-for="(action, index) in gridItems" :key="index"> <template v-for="(action, index) in displayActions" :key="index">
<button <button
v-if="action"
@click="handleActionClick(action.id)" @click="handleActionClick(action.id)"
class="relative bg-[#1b1026] border-2 border-[#4a3b5e] hover:border-[#f6b26b] hover:bg-[#231533] active:bg-[#f6b26b] active:border-[#f6b26b] group flex flex-col items-center justify-center p-1 transition-colors" class="aspect-square relative bg-[#1b1026] border-2 border-[#4a3b5e] hover:border-[#f6b26b] hover:bg-[#231533] active:bg-[#f6b26b] active:border-[#f6b26b] group flex flex-col items-center justify-center p-2 transition-colors"
> >
<div class="mb-1 p-1 rounded-sm bg-[#231533] group-active:bg-[#1b1026]"> <!-- Icon Container -->
<component :is="action.icon" :size="20" :color="action.color" class="group-active:text-[#f6b26b]" /> <div class="mb-1 p-1 rounded-sm bg-[#231533] group-active:bg-[#1b1026] w-8 h-8 flex items-center justify-center flex-shrink-0">
<!-- Pixel Icon SVG -->
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path :d="action.pixelPath" :fill="action.color" class="group-active:fill-[#f6b26b]"/>
<path v-if="action.pixelPath2" :d="action.pixelPath2" :fill="action.color2 || action.color" :fill-opacity="action.opacity2 || 1" />
</svg>
</div> </div>
<span class="text-[9px] md:text-[10px] text-[#8f80a0] uppercase tracking-wider group-hover:text-white group-active:text-[#1b1026] font-bold text-center leading-tight">
<!-- Text Label -->
<span class="text-[10px] text-[#8f80a0] group-hover:text-white group-active:text-[#1b1026] font-bold text-center leading-tight font-mono">
{{ action.label }} {{ action.label }}
</span> </span>
@ -47,44 +31,9 @@
<div class="absolute top-0 right-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" /> <div class="absolute top-0 right-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
<div class="absolute bottom-0 left-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" /> <div class="absolute bottom-0 left-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
</button> </button>
<div v-else class="bg-[#150c1f] border-2 border-[#2b193f] flex items-center justify-center opacity-50 cursor-not-allowed">
<div class="w-2 h-2 bg-[#2b193f] rounded-full"></div>
</div>
</template> </template>
</div> </div>
</PixelFrame> </PixelFrame>
</div>
<!-- Right: Stats & EQ (Col 3) -->
<div class="col-span-3 flex flex-col gap-2">
<!-- Stats Table -->
<PixelFrame title="STATS" class="bg-[#1b1026] h-2/3" variant="inset">
<div class="grid grid-cols-2 gap-x-2 content-start h-full p-1 overflow-y-auto custom-scrollbar">
<div v-for="(stat, i) in statsList" :key="i" class="flex justify-between items-center border-b border-[#2b193f] pb-0.5 mb-0.5">
<span class="text-[#8f80a0] text-[9px]">{{ stat.l }}</span>
<span class="font-mono text-[10px]" :style="{ color: stat.c || '#e0d8f0' }">{{ stat.v }}</span>
</div>
</div>
</PixelFrame>
<!-- Equipment Grid -->
<PixelFrame title="EQ" class="bg-[#1b1026] h-1/3" variant="inset">
<div
class="grid grid-cols-4 gap-1 h-full content-center cursor-pointer relative group"
@click="$emit('openInventory')"
title="Open Backpack"
>
<div v-for="(Icon, idx) in [Crown, Shirt, Hand, Footprints]" :key="idx" class="aspect-square bg-[#2b193f] border border-[#4a3b5e] flex items-center justify-center group-hover:border-[#f6b26b] transition-colors">
<component :is="Icon" :size="12" class="text-[#5a4b6e] group-hover:text-[#e0d8f0]" />
</div>
<!-- Hover Hint -->
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-[#f6b26b] font-bold pointer-events-none">
OPEN
</div>
</div>
</PixelFrame>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -92,55 +41,111 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import {
Sword, Shield, FlaskConical, Crown, Hand, Footprints, Shirt, type EntityStats = any;
Utensils, Gamepad2, Dumbbell, Puzzle, Brush, Pill, Sun, Sparkles, ShoppingBag, Swords
} from 'lucide-vue-next';
import type { EntityStats } from '~/types/pixel';
interface Props { interface Props {
playerStats?: EntityStats; playerStats?: EntityStats;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits(['openInventory', 'openGodSystem', 'openShop', 'openAdventure']); const emit = defineEmits(['feed', 'play', 'train', 'puzzle', 'clean', 'heal', 'openInventory', 'openGodSystem', 'openShop', 'openAdventure', 'toggleSleep']);
const ACTIONS = [ const BASE_ACTIONS = [
{ id: 'feed', icon: Utensils, color: '#9fd75b', label: 'FEED 餵食' }, {
{ id: 'play', icon: Gamepad2, color: '#f6b26b', label: 'PLAY 玩耍' }, id: 'feed',
{ id: 'train', icon: Dumbbell, color: '#d75b5b', label: 'TRAIN 訓練' }, label: '餵食',
{ id: 'puzzle', icon: Puzzle, color: '#2ce8f4', label: 'PUZZLE 益智' }, color: '#9fd75b',
{ id: 'clean', icon: Brush, color: '#8f80a0', label: 'CLEAN 清理' }, pixelPath: 'M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z',
{ id: 'heal', icon: Pill, color: '#9fd75b', label: 'HEAL 治療' }, pixelPath2: 'M4 2H5V3H4V2ZM6 2H7V3H6V2Z', color2: '#e0d8f0', opacity2: 0.5
{ id: 'fight', icon: Swords, color: '#d95763', label: 'FIGHT 戰鬥' }, },
{ id: 'wake', icon: Sun, color: '#ffe762', label: 'WAKE 起床' }, {
{ id: 'pray', icon: Sparkles, color: '#e0d8f0', label: 'PRAY 祈福' }, id: 'play',
{ id: 'shop', icon: ShoppingBag, color: '#ffa500', label: 'SHOP 商店' }, label: '玩耍',
color: '#f6b26b',
pixelPath: 'M2 3H8V7H2V3ZM3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 5H6V6H4V5Z'
},
{
id: 'train',
label: '訓練',
color: '#d75b5b',
pixelPath: 'M2 2H4V3H2V2ZM7 2H9V3H7V2ZM3 3H8V4H3V3ZM4 4H7V5H4V4ZM3 5H8V6H3V5ZM2 6H4V7H2V6ZM7 6H9V7H7V6Z'
},
{
id: 'puzzle',
label: '益智',
color: '#2ce8f4',
pixelPath: 'M4 1H6V3H4V1ZM2 3H4V4H2V3ZM6 3H8V4H6V3ZM1 4H3V6H1V4ZM7 4H9V6H7V4ZM4 6H6V7H4V6ZM4 7H6V9H4V7Z'
},
{
id: 'clean',
label: '清理',
color: '#8f80a0',
pixelPath: 'M7 1H9V3H7V1ZM6 3H8V4H6V3ZM5 4H7V5H5V4ZM4 5H6V6H4V5ZM3 6H5V7H3V6ZM2 7H4V9H2V7Z'
},
{
id: 'heal',
label: '治療',
color: '#9fd75b',
pixelPath: 'M2 4H4V5H2V4ZM6 4H8V5H6V4ZM2 5H8V6H2V5ZM3 6H7V7H3V6ZM4 2H6V3H4V2ZM4 7H6V8H4V7Z',
pixelPath2: 'M4 3H6V7H4V3ZM3 4H7V6H3V4Z', color2: '#e0d8f0'
},
{
id: 'fight',
label: '戰鬥',
color: '#d95763',
pixelPath: 'M2 2H3V3H2V2ZM3 3H4V4H3V3ZM4 4H5V5H4V4ZM5 5H6V6H5V5ZM6 6H7V7H6V6ZM7 7H8V8H7V7ZM7 2H8V3H7V2ZM6 3H7V4H6V3ZM3 6H4V7H3V6ZM2 7H3V8H2V7Z'
},
// Sleep/Wake will be inserted here dynamically
{
id: 'pray',
label: '祈福',
color: '#e0d8f0',
pixelPath: 'M4 1H6V3H4V1ZM2 3H8V4H2V3ZM1 4H9V5H1V4ZM2 5H8V9H2V5ZM4 6H6V8H4V6Z'
},
{
id: 'shop',
label: '商店',
color: '#ffa500',
pixelPath: 'M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7ZM4 4H6V5H4V4Z'
}
]; ];
const gridItems = computed(() => { const displayActions = computed(() => {
return Array.from({ length: 12 }).map((_, i) => ACTIONS[i] || null); const actions = [...BASE_ACTIONS];
}); const isSleeping = props.playerStats?.isSleeping;
const handCards = [ const sleepAction = isSleeping
{ name: 'Slash', cost: 2, icon: Sword, color: '#d75b5b' }, ? {
{ name: 'Block', cost: 1, icon: Shield, color: '#f6b26b' }, id: 'wake',
{ name: 'Heal', cost: 3, icon: FlaskConical, color: '#9fd75b' } label: '起床',
]; color: '#ffe762',
pixelPath: 'M4 1H6V2H4V1ZM2 2H3V3H2V2ZM7 2H8V3H7V2ZM1 4H2V6H1V4ZM8 4H9V6H8V4ZM2 7H3V8H2V7ZM7 7H8V8H7V7ZM4 8H6V9H4V8ZM4 3H6V7H4V3ZM3 4H7V6H3V4Z' // Sun
}
: {
id: 'sleep',
label: '睡覺',
color: '#2ce8f4',
pixelPath: 'M3 2H6V3H3V2ZM2 3H5V4H2V3ZM2 4H4V6H2V4ZM3 6H6V7H3V6ZM5 7H8V8H5V7ZM6 3H8V6H6V3ZM6 2H8V3H6V2Z' // Moon/Zzz
};
const statsList = computed(() => { // Insert sleep action before 'pray' (index 7)
const s = props.playerStats || { str:0, int:0, dex:0, luck:0, atk:0, def:0, spd:0 }; actions.splice(7, 0, sleepAction);
return [
{l:'STR', v:s.str}, {l:'ATK', v:s.atk, c: '#d75b5b'}, return actions;
{l:'INT', v:s.int}, {l:'DEF', v:s.def, c: '#f6b26b'},
{l:'DEX', v:s.dex}, {l:'SPD', v:s.spd},
{l:'LCK', v:s.luck},
];
}); });
const handleActionClick = (id: string) => { const handleActionClick = (id: string) => {
if (id === 'pray') emit('openGodSystem'); // Emit specific events for each action type
else if (id === 'shop') emit('openShop'); if (id === 'feed') emit('feed');
else if (id === 'play') emit('play');
else if (id === 'train') emit('train');
else if (id === 'puzzle') emit('puzzle');
else if (id === 'clean') emit('clean');
else if (id === 'heal') emit('heal');
else if (id === 'fight') emit('openAdventure'); else if (id === 'fight') emit('openAdventure');
else if (id === 'sleep' || id === 'wake') emit('toggleSleep');
else if (id === 'pray') emit('openGodSystem');
else if (id === 'shop') emit('openShop');
}; };
</script> </script>

View File

@ -74,8 +74,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Map, Drumstick, Coins, X } from 'lucide-vue-next'; import { Map, Drumstick, Coins, X, Swords } from 'lucide-vue-next';
import type { AdventureLocation, EntityStats } from '~/types/pixel'; import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
type AdventureLocation = any;
type EntityStats = any;
interface Props { interface Props {
locations: AdventureLocation[]; locations: AdventureLocation[];

View File

@ -75,7 +75,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick } from 'vue'; import { ref, watch, nextTick } from 'vue';
import PixelAvatar from './PixelAvatar.vue'; import PixelAvatar from './PixelAvatar.vue';
import type { DeityId } from '~/types/pixel';
interface Props { interface Props {
currentDeityId?: string; currentDeityId?: string;

View File

@ -225,18 +225,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2 } from 'lucide-vue-next'; import { Heart, Sparkles, Scroll, Repeat, CheckCircle2, Gift } from 'lucide-vue-next';
import PixelButton from './PixelButton.vue'; import PixelButton from './PixelButton.vue';
import PixelAvatar from './PixelAvatar.vue'; import PixelAvatar from './PixelAvatar.vue';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import JiaobeiBlocks from './JiaobeiBlocks.vue'; import JiaobeiBlocks from './JiaobeiBlocks.vue';
import { DeityId, JiaobeiResult, LotPhase } from '~/types/pixel';
import type { Deity } from '~/types/pixel'; type Deity = any;
interface Props { interface Props {
currentDeity: DeityId; currentDeity: string;
deities: Record<DeityId, Deity>; deities: Record<string, Deity>;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
@ -244,8 +244,25 @@ defineEmits(['switchDeity', 'addFavor']);
const activeTab = ref('PRAY'); const activeTab = ref('PRAY');
const isTossing = ref(false); const isTossing = ref(false);
const lastResult = ref<JiaobeiResult | null>(null); const lastResult = ref<string | null>(null);
const lotPhase = ref<LotPhase>(LotPhase.Idle);
const LotPhase = {
Idle: 'idle',
Drawing: 'drawing',
Verifying: 'verifying',
Result: 'result',
PendingVerify: 'pending_verify',
Success: 'success',
Failed: 'failed'
};
const JiaobeiResult = {
Saint: 'Saint',
Smile: 'Smile',
Cry: 'Cry'
};
const lotPhase = ref<string>(LotPhase.Idle);
const drawnLotNumber = ref<number | null>(null); const drawnLotNumber = ref<number | null>(null);
const saintCupCount = ref(0); const saintCupCount = ref(0);
@ -273,7 +290,7 @@ const LOT_RESULT_DATA = {
const activeDeity = computed(() => props.deities[props.currentDeity]); const activeDeity = computed(() => props.deities[props.currentDeity]);
const calculateToss = (): JiaobeiResult => { const calculateToss = (): string => {
const rand = Math.random(); const rand = Math.random();
if (rand < 0.5) return JiaobeiResult.Saint; if (rand < 0.5) return JiaobeiResult.Saint;
if (rand < 0.75) return JiaobeiResult.Smile; if (rand < 0.75) return JiaobeiResult.Smile;

View File

@ -2,7 +2,7 @@
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar"> <div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
<!-- Deity Portrait & Header --> <!-- Deity Portrait & Header -->
<PixelFrame class="flex-shrink-0" title="WORSHIP" highlight> <PixelFrame class="flex-shrink-0" title="信仰" highlight>
<div class="flex flex-col items-center p-1"> <div class="flex flex-col items-center p-1">
<div class="w-24 h-24 bg-[#1b1026] border-4 border-[#d75b5b] mb-2 p-1 relative flex items-center justify-center overflow-hidden"> <div class="w-24 h-24 bg-[#1b1026] border-4 border-[#d75b5b] mb-2 p-1 relative flex items-center justify-center overflow-hidden">
<!-- Background for portrait --> <!-- Background for portrait -->
@ -28,7 +28,7 @@
:current="deity.favor" :current="deity.favor"
:max="deity.maxFavor" :max="deity.maxFavor"
type="energy" type="energy"
label="Favor (好感度)" label="好感度"
:icon="Heart" :icon="Heart"
/> />
</div> </div>
@ -38,15 +38,15 @@
<div class="flex flex-col gap-2 h-full p-1"> <div class="flex flex-col gap-2 h-full p-1">
<div class="flex items-center gap-2 text-[#9fd75b] border-b border-[#4a3b5e] pb-1 sticky top-0 bg-[#150c1f] z-10"> <div class="flex items-center gap-2 text-[#9fd75b] border-b border-[#4a3b5e] pb-1 sticky top-0 bg-[#150c1f] z-10">
<Sparkles :size="14" /> <Sparkles :size="14" />
<span class="text-xs font-bold uppercase">Blessing</span> <span class="text-xs font-bold uppercase">神恩</span>
</div> </div>
<p class="text-xs text-[#e0d8f0] italic leading-relaxed"> <p class="text-xs text-[#e0d8f0] italic leading-relaxed">
"{{ deity.description }}" {{ deity.description }}
</p> </p>
<div class="mt-auto p-2 bg-[#231533] border border-[#4a3b5e]"> <div class="mt-auto p-2 bg-[#231533] border border-[#4a3b5e]">
<span class="text-[10px] text-[#8f80a0] uppercase block mb-1">Current Effect:</span> <span class="text-[10px] text-[#8f80a0] uppercase block mb-1">當前效果</span>
<span class="text-xs text-[#2ce8f4]"> <span class="text-xs text-[#2ce8f4]">
{{ currentEffect }} {{ currentEffect }}
</span> </span>
@ -63,7 +63,8 @@ import { Heart, Sparkles } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import RetroResourceBar from './RetroResourceBar.vue'; import RetroResourceBar from './RetroResourceBar.vue';
import PixelAvatar from './PixelAvatar.vue'; import PixelAvatar from './PixelAvatar.vue';
import type { Deity } from '~/types/pixel';
type Deity = any;
interface Props { interface Props {
deity: Deity; deity: Deity;

View File

@ -169,11 +169,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { Sword, Shield, Crown, Gem, Sparkles, Star, Shirt, HelpCircle, Trash2, Zap, Heart } from 'lucide-vue-next'; import { Sword, Shield, Crown, Gem, Sparkles, Star, Shirt, HelpCircle, Trash2, Zap, Heart, Package, X } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue'; import PixelButton from './PixelButton.vue';
import { ItemType, EquipSlot, Rarity } from '~/types/pixel';
import type { Item } from '~/types/pixel'; import { ITEM_TYPE, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../../../data/items.js';
// Use any types since we're using data directly
type Item = any;
interface Props { interface Props {
items: Item[]; items: Item[];
@ -186,7 +189,33 @@ const selectedItemId = ref<string | null>(null);
const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value)); const selectedItem = computed(() => props.items.find(i => i.id === selectedItemId.value));
const RARITY_COLORS: Record<Rarity, string> = { // Map data constants to local helpers for template compatibility
const ItemType = {
Equipment: ITEM_TYPE.EQUIPMENT,
Consumable: ITEM_TYPE.CONSUMABLE,
Talisman: ITEM_TYPE.TALISMAN,
Special: ITEM_TYPE.SPECIAL,
Appearance: ITEM_TYPE.APPEARANCE
};
const Rarity = {
Common: 'common',
Excellent: 'uncommon',
Rare: 'rare',
Epic: 'epic',
Legendary: 'legendary'
};
const EquipSlot = {
Weapon: 'weapon',
Armor: 'armor',
Hat: 'hat',
Accessory: 'accessory',
Charm: 'talisman',
Special: 'special'
};
const RARITY_COLORS: Record<string, string> = {
[Rarity.Common]: '#9ca3af', // Gray [Rarity.Common]: '#9ca3af', // Gray
[Rarity.Excellent]: '#9fd75b', // Green [Rarity.Excellent]: '#9fd75b', // Green
[Rarity.Rare]: '#2ce8f4', // Blue [Rarity.Rare]: '#2ce8f4', // Blue
@ -194,7 +223,7 @@ const RARITY_COLORS: Record<Rarity, string> = {
[Rarity.Legendary]: '#ffa500', // Orange [Rarity.Legendary]: '#ffa500', // Orange
}; };
const SLOT_ICONS: Record<EquipSlot, any> = { const SLOT_ICONS: Record<string, any> = {
[EquipSlot.Weapon]: Sword, [EquipSlot.Weapon]: Sword,
[EquipSlot.Armor]: Shield, [EquipSlot.Armor]: Shield,
[EquipSlot.Hat]: Crown, [EquipSlot.Hat]: Crown,
@ -203,7 +232,7 @@ const SLOT_ICONS: Record<EquipSlot, any> = {
[EquipSlot.Special]: Star, [EquipSlot.Special]: Star,
}; };
const getEquippedItem = (slot: EquipSlot, isAppearance: boolean) => { const getEquippedItem = (slot: string, isAppearance: boolean) => {
return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance); return props.items.find(i => i.isEquipped && i.slot === slot && !!i.isAppearance === isAppearance);
}; };
</script> </script>

View File

@ -43,10 +43,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { JiaobeiResult } from '~/types/pixel'; import { ref, computed, watch } from 'vue';
interface Props { interface Props {
result: JiaobeiResult | null; result: string | null;
isTossing: boolean; isTossing: boolean;
} }

View File

@ -0,0 +1,54 @@
<template>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<PixelFrame class="w-full max-w-md bg-[#1b1026] p-6 flex flex-col items-center gap-6" title="NEW ADVENTURE">
<div class="text-center">
<h2 class="text-[#99e550] text-xl font-bold mb-2 tracking-widest">歡迎來到電子雞世界</h2>
<p class="text-[#8f80a0] text-sm">請為您的新夥伴取個名字</p>
</div>
<div class="w-full">
<input
v-model="name"
type="text"
placeholder="輸入名字..."
class="w-full bg-[#0f0816] border-2 border-[#4a3b5e] text-[#e0d8f0] p-3 text-center focus:border-[#9fd75b] focus:outline-none transition-colors placeholder-[#4a3b5e]"
@keyup.enter="submit"
maxlength="12"
/>
<div class="text-right mt-1">
<span class="text-[10px]" :class="name.length > 10 ? 'text-[#d95763]' : 'text-[#4a3b5e]'">
{{ name.length }}/12
</span>
</div>
</div>
<PixelButton
class="w-full py-3 text-lg"
:disabled="!isValid"
@click="submit"
>
開始冒險 (START)
</PixelButton>
</PixelFrame>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import PixelFrame from './PixelFrame.vue';
import PixelButton from './PixelButton.vue';
const emit = defineEmits(['submit']);
const name = ref('');
const isValid = computed(() => name.value.trim().length > 0 && name.value.length <= 12);
const submit = () => {
if (isValid.value) {
emit('submit', name.value.trim());
}
};
</script>

View File

@ -1,25 +1,32 @@
<template> <template>
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar"> <div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
<!-- Pet Avatar -->Portrait & Basic Info --> <!-- Pet Avatar -->
<PixelFrame class="flex-shrink-0" title="PET INFO"> <PixelFrame class="flex-shrink-0" title="寵物資訊">
<!-- Helper Buttons Overlay --> <!-- Helper Buttons Overlay -->
<div class="absolute top-1 right-1 z-30"> <div class="absolute top-1 right-1 z-30 flex gap-1">
<button <button
@click="$emit('openAchievements')" @click="$emit('openAchievements')"
class="p-1 bg-[#2b193f] border border-[#f6b26b] hover:bg-[#3d2459] active:translate-y-0.5 group" class="p-1 bg-[#2b193f] border border-[#f6b26b] hover:bg-[#3d2459] active:translate-y-0.5 group"
title="Achievements" title="成就"
> >
<Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" /> <Trophy :size="14" class="text-[#f6b26b] group-hover:text-white" />
</button> </button>
<button
@click="$emit('deletePet')"
class="p-1 bg-[#2b193f] border border-[#d95763] hover:bg-[#3d2459] active:translate-y-0.5 group"
title="刪除寵物"
>
<Trash2 :size="14" class="text-[#d95763] group-hover:text-white" />
</button>
</div> </div>
<div class="flex flex-col items-center p-1 relative"> <div class="flex flex-col items-center p-1 relative">
<div class="w-20 h-20 bg-[#1b1026] border-4 border-[#4a3b5e] mb-2 relative overflow-hidden group shadow-inner flex items-center justify-center"> <div class="w-20 h-20 bg-[#1b1026] border-4 border-[#4a3b5e] mb-2 relative overflow-visible group shadow-inner flex items-center justify-center">
<!-- Background for portrait --> <!-- Background for portrait -->
<div class="absolute inset-0 bg-[#2b193f] opacity-50" /> <div class="absolute inset-0 bg-[#2b193f] opacity-50 overflow-hidden" />
<!-- The Animated Pixel Avatar --> <!-- The Animated Pixel Avatar -->
<div class="scale-110 transform translate-y-1"> <div class="scale-110 transform translate-y-1 relative z-10">
<PixelAvatar <PixelAvatar
skinColor="#ffdbac" skinColor="#ffdbac"
hairColor="#e0d8f0" hairColor="#e0d8f0"
@ -28,84 +35,413 @@
</div> </div>
<!-- Scanline on portrait --> <!-- Scanline on portrait -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.2)_50%)] bg-[length:100%_4px] pointer-events-none z-20" /> <div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.2)_50%)] bg-[length:100%_4px] pointer-events-none z-20 overflow-hidden" />
<!-- Mood Bubble (Shows on Hover) -->
<div class="absolute -top-6 -right-6 z-30 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
<div class="relative bg-[#e0d8f0] text-[#1b1026] p-2 border-2 border-[#1b1026] shadow-[4px_4px_0_rgba(0,0,0,0.2)]">
<!-- Pixel Tail -->
<div class="absolute bottom-[-6px] left-2 w-0 h-0 border-l-[6px] border-l-transparent border-t-[6px] border-t-[#1b1026] border-r-[6px] border-r-transparent"></div>
<div class="absolute bottom-[-3px] left-2 w-0 h-0 border-l-[4px] border-l-transparent border-t-[4px] border-t-[#e0d8f0] border-r-[4px] border-r-transparent z-10"></div>
<div class="flex flex-col items-center gap-1 min-w-[40px]">
<!-- Pixel Mood Icon -->
<svg width="24" height="24" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="image-pixelated">
<!-- Happy -->
<g v-if="mood.type === 'happy'">
<path d="M2 3H3V5H2V3ZM7 3H8V5H7V3Z" fill="#1b1026"/>
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6ZM3 7H7V8H3V7Z" fill="#1b1026"/>
</g>
<!-- Calm -->
<g v-else-if="mood.type === 'calm'">
<path d="M2 4H3V5H2V4ZM7 4H8V5H7V4Z" fill="#1b1026"/>
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
</g>
<!-- Bored -->
<g v-else-if="mood.type === 'bored'">
<path d="M2 4H4V5H2V4ZM6 4H8V5H6V4Z" fill="#1b1026"/>
<path d="M3 7H7V8H3V7Z" fill="#1b1026"/>
</g>
<!-- Sad -->
<g v-else>
<path d="M2 4H3V6H2V4ZM7 4H8V6H7V4Z" fill="#1b1026"/>
<path d="M3 7H4V8H3V7ZM6 7H7V8H6V7ZM4 6H6V7H4V6Z" fill="#1b1026"/>
<path d="M2 6H3V7H2V6ZM7 6H8V7H7V6Z" fill="#2ce8f4" fill-opacity="0.5"/>
</g>
</svg>
<span class="text-[10px] font-bold font-mono leading-none">{{ mood.text }}</span>
</div>
</div>
</div>
</div>
<h2 class="text-xl text-[#f6b26b] tracking-[0.2em] font-bold border-b-2 border-[#f6b26b] mb-1 leading-none pb-1 font-mono">{{ stats.name }}</h2>
<span class="text-xs text-[#8f80a0] uppercase tracking-wide font-mono mb-2">{{ translateStage(stats.class || stats.stage) }}</span>
<!-- Status Indicators -->
<div class="flex gap-2 items-center justify-center w-full mt-1 flex-wrap">
<!-- Sleeping Status -->
<div v-if="stats.isSleeping" class="px-2 py-0.5 bg-[#2b193f] border border-[#2ce8f4] text-[10px] text-[#2ce8f4] font-mono animate-pulse flex items-center gap-1" title="睡覺中">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H7V3H2V2ZM2 3H3V4H2V3ZM3 4H4V5H3V4ZM4 5H5V6H4V5ZM5 6H6V7H5V6ZM6 7H7V8H6V7ZM2 7H7V8H2V7Z" fill="#2ce8f4"/>
</svg>
睡眠
</div>
<!-- Poop Status -->
<div v-if="stats.poopCount > 0" class="px-2 py-0.5 bg-[#2b193f] border border-[#d95763] text-[10px] text-[#d95763] font-mono animate-pulse flex items-center gap-1" title="需要清理便便">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2H6V3H4V2ZM3 3H7V4H3V3ZM2 4H8V7H2V4ZM3 7H7V8H3V7Z" fill="#d95763"/>
<path d="M4 3H5V4H4V3ZM6 4H7V5H6V4Z" fill="#ff8f9c" fill-opacity="0.5"/>
</svg>
{{ stats.poopCount }}
</div>
<!-- Sick Status -->
<div v-if="stats.isSick" class="px-2 py-0.5 bg-[#2b193f] border border-[#99e550] text-[10px] text-[#99e550] font-mono animate-pulse flex items-center gap-1" title="生病了">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 2H7V3H3V2ZM2 3H8V8H2V3ZM3 8H7V9H3V8Z" fill="#99e550"/>
<path d="M3 4H4V5H3V4ZM6 4H7V5H6V4ZM4 6H6V7H4V6Z" fill="#1b1026"/>
</svg>
生病
</div>
<!-- Dying Status -->
<div v-if="stats.dyingSeconds > 0" class="px-2 py-0.5 bg-[#2b193f] border border-[#d95763] text-[10px] text-[#d95763] font-mono animate-pulse font-bold flex items-center gap-1" title="瀕死狀態!快急救!">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H8V4H2V2ZM2 4H3V6H2V4ZM7 4H8V6H7V4ZM3 6H7V7H3V6ZM4 7H6V9H4V7Z" fill="#d95763"/>
<path d="M3 3H4V4H3V3ZM6 3H7V4H6V3Z" fill="#1b1026"/>
</svg>
瀕死
</div>
</div> </div>
<h2 class="text-xl text-[#f6b26b] tracking-[0.2em] font-bold border-b-2 border-[#f6b26b] mb-1 leading-none pb-1">{{ stats.name }}</h2>
<span class="text-xs text-[#8f80a0] uppercase tracking-wide">{{ stats.class }}</span>
</div> </div>
</PixelFrame> </PixelFrame>
<!-- Vitals - Updated to Health, Hunger, Happiness --> <!-- Vitals - Updated to Health, Hunger, Happiness -->
<div class="flex flex-col gap-1 px-1"> <div class="flex flex-col gap-2 px-1 mb-2">
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="Health" :icon="Heart" /> <!-- Health (Heart) -->
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="Hunger" :icon="Drumstick" /> <RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="生命" :segments="10">
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="Happy" :icon="Smile" /> <template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H4V3H2V2ZM6 2H8V3H6V2ZM1 3H2V5H1V3ZM8 3H9V5H8V3ZM2 5H3V6H2V5ZM7 5H8V6H7V5ZM3 6H4V7H3V6ZM6 6H7V7H6V6ZM4 7H6V8H4V7Z" fill="#d95763"/>
<path d="M2 3H4V4H2V3ZM6 3H8V4H6V3ZM2 4H8V5H2V4ZM3 5H7V6H3V5ZM4 6H6V7H4V6Z" fill="#ff8f9c" fill-opacity="0.5"/>
</svg>
</template>
</RetroResourceBar>
<!-- Hunger (Star) -->
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="飢餓" :segments="10">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1H6V3H4V1ZM1 4H3V6H1V4ZM7 4H9V6H7V4ZM4 7H6V9H4V7Z" fill="#f6b26b"/>
<path d="M4 3H6V7H4V3ZM3 4H7V6H3V4Z" fill="#ffe762"/>
</svg>
</template>
</RetroResourceBar>
<!-- Happiness (Potion) -->
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="快樂" :segments="10">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z" fill="#99e550"/>
<path d="M4 2H5V3H4V2ZM3 5H5V6H3V5ZM6 6H7V7H6V6Z" fill="#e0d8f0" fill-opacity="0.8"/>
</svg>
</template>
</RetroResourceBar>
</div> </div>
<!-- Pet Details Grid --> <!-- Pet Details Grid -->
<PixelFrame class="flex-shrink-0 mt-1" variant="inset"> <PixelFrame class="flex-shrink-0 mt-1" variant="inset">
<div class="grid grid-cols-2 gap-x-2 gap-y-2 text-[10px] uppercase text-[#8f80a0]"> <div class="flex flex-col gap-2 p-1">
<div class="flex flex-col border-r border-[#4a3b5e] pr-1"> <!-- Basic Info -->
<span class="text-[#4a3b5e]">Age</span> <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px] uppercase text-[#8f80a0]">
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span> <div class="flex flex-col border-r border-[#4a3b5e] pr-2">
<span class="text-[#4a3b5e] font-mono">等級</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">Lv {{ stats.lvl }}</span>
</div>
<div class="flex flex-col pl-2">
<span class="text-[#4a3b5e] font-mono">世代</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span>
</div>
<div class="flex flex-col border-r border-[#4a3b5e] border-t border-t-[#4a3b5e] pr-2 pt-2">
<span class="text-[#4a3b5e] font-mono">年齡</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span>
</div>
<!-- Height (Text Label) -->
<div class="flex flex-col border-t border-[#4a3b5e] pt-2 pl-2">
<span class="text-[#4a3b5e] font-mono">身高</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.height }}</span>
</div>
<!-- Weight (Text Label) -->
<div class="flex flex-col border-t border-[#4a3b5e] pt-2 col-span-2">
<span class="text-[#4a3b5e] font-mono">體重</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.weight }}</span>
</div>
</div>
<!-- Core Stats -->
<div class="grid grid-cols-2 gap-x-4 gap-y-2 pt-2 border-t border-[#4a3b5e]">
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#f6b26b] font-mono">力量</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.str || 0) }}</span>
</div> </div>
<div class="flex flex-col pl-1"> <div class="flex justify-between items-center px-1">
<span class="text-[#4a3b5e]">Gen</span> <span class="text-[10px] text-[#2ce8f4] font-mono">智力</span>
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span> <span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.int || 0) }}</span>
</div> </div>
<div class="flex items-center gap-1 border-t border-[#4a3b5e] pt-1 col-span-2"> <div class="flex justify-between items-center px-1">
<Ruler :size="10" /> <span class="text-[10px] text-[#99e550] font-mono">敏捷</span>
<span class="text-[#e0d8f0]">{{ stats.height }}</span> <span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.dex || 0) }}</span>
<span class="text-[#4a3b5e] mx-1">|</span>
<Scale :size="10" />
<span class="text-[#e0d8f0]">{{ stats.weight }}</span>
</div> </div>
<div class="flex justify-between items-center px-1">
<span class="text-[10px] text-[#d95763] font-mono">運氣</span>
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.luck || 0) }}</span>
</div>
</div>
<!-- Combat Stats -->
<div class="grid grid-cols-3 gap-x-2 gap-y-2 pt-2 border-t border-[#4a3b5e]">
<div class="flex flex-col items-center">
<span class="text-[8px] text-[#d95763] font-mono mb-0.5">攻擊</span>
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.atk || 0) }}</span>
</div>
<div class="flex flex-col items-center">
<span class="text-[8px] text-[#f6b26b] font-mono mb-0.5">防禦</span>
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.def || 0) }}</span>
</div>
<div class="flex flex-col items-center">
<span class="text-[8px] text-[#2ce8f4] font-mono mb-0.5">速度</span>
<span class="text-[10px] text-[#e0d8f0] font-mono font-bold">{{ Math.floor(stats.spd || 0) }}</span>
</div>
</div>
</div> </div>
</PixelFrame> </PixelFrame>
<!-- Fate & God Favor --> <!-- Fate Display (Enhanced) -->
<div class="flex flex-col gap-2 mt-2 px-1"> <div v-if="fateInfo" class="flex flex-col gap-2 mt-2 mb-6 px-1 flex-shrink-0">
<!-- Fate --> <PixelFrame class="relative overflow-visible" variant="inset" title="命運">
<div v-if="stats.fate" class="flex items-center gap-2 bg-[#2b193f] p-1 border border-[#4a3b5e] rounded"> <!-- Tier Badge -->
<Leaf :size="12" color="#99e550" /> <div class="absolute -top-2 -right-2 z-10">
<div class="flex flex-col leading-none"> <div
<span class="text-[8px] text-[#8f80a0] uppercase">Fate</span> class="px-2 py-0.5 text-[8px] font-bold font-mono border-2 shadow-lg"
<span class="text-[10px] text-[#e0d8f0] tracking-wide">{{ stats.fate }}</span> :style="{
</div> backgroundColor: fateInfo.color,
</div> borderColor: fateInfo.color,
color: '#000',
boxShadow: `0 0 10px ${fateInfo.color}, 0 0 20px ${fateInfo.color}40`
}"
>
{{ fateInfo.tierName }}
</div>
</div>
<!-- God Favor --> <div class="p-2 flex gap-2">
<div v-if="stats.godFavor" class="flex flex-col gap-1"> <!-- Pixel Icon with Glow -->
<div class="flex justify-between text-[10px] text-[#8f80a0] uppercase"> <div
<span>Favor: {{ stats.godFavor.name }}</span> class="w-12 h-12 flex-shrink-0 bg-[#0f0816] border-2 flex items-center justify-center relative overflow-hidden"
<span>{{ stats.godFavor.current }}/{{ stats.godFavor.max }}</span> :style="{
</div> borderColor: fateInfo.color,
<div class="h-2 bg-[#150c1f] border border-[#4a3b5e] rounded-full overflow-hidden"> boxShadow: `inset 0 0 10px ${fateInfo.color}40, 0 0 15px ${fateInfo.color}60`
<div :style="{ width: `${(stats.godFavor.current / stats.godFavor.max) * 100}%` }" class="h-full bg-[#f6b26b]" /> }"
</div> >
</div> <!-- Pixel art background pattern -->
<div class="absolute inset-0 opacity-20" style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.1) 2px, rgba(255,255,255,0.1) 4px)"></div>
<!-- Icon based on tier -->
<div class="relative text-2xl animate-pulse" :style="{ filter: `drop-shadow(0 0 4px ${fateInfo.color})` }">
<Sparkles v-if="fateInfo.tier === 'SSR'" :size="32" :color="fateInfo.color" />
<Star v-else-if="fateInfo.tier === 'SR'" :size="28" :color="fateInfo.color" />
<Gem v-else-if="fateInfo.tier === 'R'" :size="24" :color="fateInfo.color" />
<Circle v-else-if="fateInfo.tier === 'N'" :size="20" :color="fateInfo.color" />
<Leaf v-else :size="16" :color="fateInfo.color" />
</div>
<!-- Scanlines -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0)_50%,rgba(0,0,0,0.3)_50%)] bg-[length:100%_4px] pointer-events-none"></div>
</div>
<!-- Fate Info -->
<div class="flex-1 min-w-0">
<!-- Name with glow effect -->
<div
class="text-sm font-bold font-mono mb-1 truncate"
:style="{
color: fateInfo.color,
textShadow: `0 0 8px ${fateInfo.color}, 0 0 12px ${fateInfo.color}80`
}"
>
{{ stats.fate }}
</div>
<!-- Description -->
<p class="text-[9px] text-[#8f80a0] leading-tight mb-2 line-clamp-2 font-mono">
{{ fateInfo.description }}
</p>
<!-- Buffs -->
<div v-if="fateInfo.buffsList && fateInfo.buffsList.length > 0" class="flex flex-wrap gap-1">
<div
v-for="(buff, index) in fateInfo.buffsList"
:key="index"
class="text-[8px] px-1 py-0.5 border bg-[#0f0816] font-mono"
:style="{ borderColor: fateInfo.color + '60', color: fateInfo.color }"
:title="buff"
>
{{ buff }}
</div>
</div>
</div>
</div>
</PixelFrame>
</div> </div>
<!-- Gold --> <!-- Gold & Inventory -->
<div class="mt-auto px-1 pb-1"> <div class="mt-auto px-1 pb-4 flex-shrink-0 flex gap-2">
<RetroCounter :icon="Coins" :value="stats.gold || 0" color="#ffe762" /> <!-- Gold Display -->
<div class="flex-1">
<RetroCounter :value="stats.gold || 0" color="#ffe762">
<template #icon>
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 2H7V3H3V2ZM2 3H8V7H2V3ZM3 7H7V8H3V7Z" fill="#ffe762"/>
<path d="M4 3H5V6H4V3ZM6 3H7V6H6V3Z" fill="#b48b38"/>
</svg>
</template>
</RetroCounter>
</div>
<!-- Inventory Button (Styled like Action Buttons) -->
<button
@click="$emit('openInventory')"
class="w-10 bg-[#1b1026] border border-[#4a3b5e] hover:bg-[#2b193f] active:translate-y-0.5 group flex flex-col items-center justify-center gap-1 py-1 transition-all relative overflow-hidden"
title="背包"
>
<!-- Pixel Icon -->
<div class="w-4 h-4 relative z-10">
<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 1H7V3H3V1ZM1 3H9V9H1V3ZM4 4H6V5H4V4Z" fill="#8f80a0" class="group-hover:fill-[#e0d8f0] transition-colors"/>
</svg>
</div>
<!-- Corner Accents -->
<div class="absolute top-0 left-0 w-1 h-1 border-t border-l border-[#4a3b5e] opacity-50"></div>
<div class="absolute top-0 right-0 w-1 h-1 border-t border-r border-[#4a3b5e] opacity-50"></div>
<div class="absolute bottom-0 left-0 w-1 h-1 border-b border-l border-[#4a3b5e] opacity-50"></div>
<div class="absolute bottom-0 right-0 w-1 h-1 border-b border-r border-[#4a3b5e] opacity-50"></div>
</button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Ruler, Scale, Heart, Smile, Drumstick, Coins, Leaf, Trophy } from 'lucide-vue-next'; import { computed } from 'vue';
import { Trophy, Trash2, Sparkles, Star, Gem, Circle, Leaf } from 'lucide-vue-next';
import PixelFrame from './PixelFrame.vue'; import PixelFrame from './PixelFrame.vue';
import RetroResourceBar from './RetroResourceBar.vue'; import RetroResourceBar from './RetroResourceBar.vue';
import RetroCounter from './RetroCounter.vue'; import RetroCounter from './RetroCounter.vue';
import PixelAvatar from './PixelAvatar.vue'; import PixelAvatar from './PixelAvatar.vue';
import type { EntityStats } from '~/types/pixel'; import { FATE_TIERS, FATES } from '../../../data/fates.js';
type EntityStats = any;
interface Props { interface Props {
stats: EntityStats; stats: EntityStats;
} }
defineProps<Props>(); const props = defineProps<Props>();
defineEmits(['openAchievements']); defineEmits(['openAchievements', 'deletePet', 'openInventory']);
// Determine mood based on happiness
const mood = computed(() => {
const happiness = props.stats.happiness || 0;
if (happiness >= 80) return { icon: '😄', text: '開心', type: 'happy' };
if (happiness >= 50) return { icon: '🙂', text: '平靜', type: 'calm' };
if (happiness >= 20) return { icon: '😐', text: '無聊', type: 'bored' };
return { icon: '😭', text: '難過', type: 'sad' };
});
// Helper to translate stage
function translateStage(stage: string): string {
if (!stage) return '';
const map: Record<string, string> = {
'egg': '蛋',
'baby': '幼年期',
'child': '成長期',
'adult': '成熟期',
'mythic': '神話期',
'EGG': '蛋',
'BABY': '幼年期',
'CHILD': '成長期',
'ADULT': '成熟期',
'MYTHIC': '神話期'
};
return map[stage] || stage;
}
// Process fate information
const fateInfo = computed(() => {
if (!props.stats.fate) return null;
// Find fate data
const fateData = FATES.find(f => f.name === props.stats.fate);
if (!fateData) return null;
const tierData = FATE_TIERS[fateData.tier as keyof typeof FATE_TIERS];
// Format buffs into readable list
const buffsList: string[] = [];
if (fateData.buffs) {
for (const [key, value] of Object.entries(fateData.buffs)) {
let displayValue = value as number;
let prefix = '+';
// Handle percentage buffs
if (typeof value === 'number') {
if (value < 0) {
prefix = '';
}
if (Math.abs(value) < 1 && value !== 0) {
displayValue = Math.round(value * 100);
buffsList.push(`${prefix}${displayValue}% ${formatBuffKey(key)}`);
} else {
buffsList.push(`${prefix}${displayValue} ${formatBuffKey(key)}`);
}
}
}
}
return {
tier: fateData.tier,
tierName: tierData.name,
color: tierData.color,
description: fateData.description,
buffsList
};
});
// Helper to format buff keys
function formatBuffKey(key: string): string {
const keyMap: Record<string, string> = {
luck: '運氣',
attack: '攻擊',
defense: '防禦',
speed: '速度',
strGain: '力量成長',
intGain: '智力成長',
dexGain: '敏捷成長',
healthRegen: '健康恢復',
happinessRecovery: '快樂恢復',
hungerDecay: '飢餓速度',
sicknessReduction: '生病機率↓',
badEventReduction: '壞事機率↓',
resourceGain: '資源獲得',
dropRate: '掉寶率',
gameSuccessRate: '遊戲成功率',
miniGameBonus: '小遊戲獎勵',
breedingSuccess: '繁殖成功率'
};
return keyMap[key] || key;
}
</script> </script>

View File

@ -1,6 +1,10 @@
<template> <template>
<div class="flex items-center gap-2 bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded-sm"> <div class="flex items-center gap-2 bg-[#0f0816] border border-[#4a3b5e] px-2 py-1 rounded-sm">
<component :is="icon" :size="14" :style="{ color: color }" /> <div class="w-4 h-4 flex items-center justify-center">
<slot name="icon">
<component :is="icon" v-if="icon" :size="14" :style="{ color: color }" />
</slot>
</div>
<div class="flex flex-col leading-none"> <div class="flex flex-col leading-none">
<span v-if="label" class="text-[8px] text-[#8f80a0] uppercase">{{ label }}</span> <span v-if="label" class="text-[8px] text-[#8f80a0] uppercase">{{ label }}</span>
<span class="font-mono text-sm font-bold text-[#e0d8f0]">{{ value }}</span> <span class="font-mono text-sm font-bold text-[#e0d8f0]">{{ value }}</span>
@ -12,7 +16,7 @@
import type { Component } from 'vue'; import type { Component } from 'vue';
interface Props { interface Props {
icon: Component; icon?: Component;
value: number | string; value: number | string;
label?: string; label?: string;
color?: string; color?: string;

View File

@ -1,19 +1,47 @@
<template> <template>
<div class="flex flex-col gap-0.5 w-full"> <div class="flex flex-col gap-1 w-full">
<div v-if="label" class="flex justify-between items-end px-0.5"> <div v-if="label" class="flex justify-between items-end px-0.5">
<div class="flex items-center gap-1 text-[#8f80a0]"> <div class="flex items-center gap-2">
<component :is="icon" v-if="icon" :size="10" /> <!-- Pixel Icon -->
<span class="text-[10px] uppercase font-bold leading-none">{{ label }}</span> <div class="w-4 h-4 flex items-center justify-center">
<slot name="icon"></slot>
</div>
<span class="text-[10px] font-mono font-bold leading-none text-[#8f80a0]">{{ label }}</span>
</div> </div>
<span class="text-[10px] font-mono text-[#e0d8f0] leading-none">{{ current }}/{{ max }}</span> <span class="text-[10px] font-mono text-[#e0d8f0] leading-none">{{ Math.floor(current) }}/{{ max }}</span>
</div> </div>
<div class="h-3 bg-[#0f0816] border border-[#4a3b5e] p-[1px] relative">
<div <!-- Segmented Bar Container -->
class="h-full transition-all duration-300 relative" <div class="h-4 bg-[#1b1026] border-2 border-[#1b1026] relative p-[2px] shadow-[0_0_0_1px_#4a3b5e]">
:style="{ width: `${percentage}%`, backgroundColor: barColor }" <!-- Background (Empty segments) -->
> <div class="absolute inset-[2px] flex gap-[2px]">
<!-- Shine effect --> <div v-for="i in segments" :key="`bg-${i}`" class="flex-1 bg-[#2b193f]"></div>
<div class="absolute top-0 left-0 w-full h-[1px] bg-white opacity-30" /> </div>
<!-- Foreground (Filled segments) -->
<div class="absolute inset-[2px] flex gap-[2px] overflow-hidden">
<div
v-for="i in segments"
:key="`fill-${i}`"
class="flex-1 transition-all duration-300 relative"
:class="[
i <= filledSegments ? 'opacity-100' : 'opacity-0',
i === filledSegments + 1 && partialFill > 0 ? 'opacity-100' : ''
]"
>
<!-- The colored block -->
<div
class="w-full h-full relative"
:style="{
backgroundColor: barColor,
width: i === filledSegments + 1 ? `${partialFill}%` : '100%'
}"
>
<!-- Shine/Highlight for 3D effect -->
<div class="absolute top-0 left-0 right-0 h-[40%] bg-white opacity-20"></div>
<div class="absolute bottom-0 left-0 right-0 h-[20%] bg-black opacity-10"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -21,23 +49,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import type { Component } from 'vue';
interface Props { interface Props {
current: number; current: number;
max: number; max: number;
type: 'hp' | 'energy' | 'mana'; type: 'hp' | 'energy' | 'mana';
label?: string; label?: string;
icon?: Component; segments?: number; // Number of blocks, default 10
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
segments: 10
});
const percentage = computed(() => Math.min(100, Math.max(0, (props.current / props.max) * 100))); const percentage = computed(() => Math.min(100, Math.max(0, (props.current / props.max) * 100)));
// Calculate how many segments are fully filled
const filledSegments = computed(() => Math.floor((percentage.value / 100) * props.segments));
// Calculate the percentage of the partially filled segment
const partialFill = computed(() => {
const segmentSize = 100 / props.segments;
const remainder = percentage.value % segmentSize;
return (remainder / segmentSize) * 100;
});
const barColor = computed(() => { const barColor = computed(() => {
if (props.type === 'energy') return '#f6b26b'; // Orange if (props.type === 'energy') return '#f6b26b'; // Orange (Star)
if (props.type === 'mana') return '#2ce8f4'; // Cyan if (props.type === 'mana') return '#99e550'; // Green (Potion) - Changed from Cyan
return '#d95763'; // HP Red (default) return '#d95763'; // Red (Heart)
}); });
</script> </script>

View File

@ -108,10 +108,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { ShoppingBag, Coins, Filter, Cookie, Pill, Sword, Gamepad2, Gem, Search, RefreshCw } from 'lucide-vue-next'; import { ShoppingBag, Coins, Filter, Cookie, Pill, Sword, Gamepad2, Gem, Search, RefreshCw, X } from 'lucide-vue-next';
import PixelButton from './PixelButton.vue'; import PixelButton from './PixelButton.vue';
import { ItemCategory, Rarity } from '~/types/pixel'; import PixelFrame from './PixelFrame.vue';
import type { Item } from '~/types/pixel';
import { ITEM_CATEGORY as DATA_CATEGORIES, ITEM_TYPE } from '../../../../data/items.js';
type Item = any;
interface Props { interface Props {
playerGold: number; playerGold: number;
@ -125,6 +128,19 @@ defineEmits(['buy', 'sell']);
const mode = ref<'BUY' | 'SELL'>('BUY'); const mode = ref<'BUY' | 'SELL'>('BUY');
const filter = ref<string>('ALL'); const filter = ref<string>('ALL');
// Map for UI filters
const ItemCategory = {
Food: DATA_CATEGORIES.FOOD,
Medicine: DATA_CATEGORIES.MEDICINE,
Equipment: 'equipment_filter',
Toy: DATA_CATEGORIES.TOY,
Accessory: 'accessory_filter'
};
const Rarity = {
Legendary: 'legendary'
};
const CATEGORY_FILTERS = [ const CATEGORY_FILTERS = [
{ id: 'ALL', label: '全部 (ALL)', icon: Search }, { id: 'ALL', label: '全部 (ALL)', icon: Search },
{ id: ItemCategory.Food, label: '食物', icon: Cookie }, { id: ItemCategory.Food, label: '食物', icon: Cookie },
@ -139,6 +155,15 @@ const displayedItems = computed(() => {
return source.filter(item => { return source.filter(item => {
if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items
if (filter.value === 'ALL') return true; if (filter.value === 'ALL') return true;
// Custom filter logic
if (filter.value === ItemCategory.Equipment) {
return item.type === ITEM_TYPE.EQUIPMENT;
}
if (filter.value === ItemCategory.Accessory) {
return item.category === DATA_CATEGORIES.ACCESSORY || item.category === DATA_CATEGORIES.TALISMAN;
}
return item.category === filter.value; return item.category === filter.value;
}); });
}); });

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="w-full min-h-screen bg-[#1b1026] flex items-center justify-center p-2 md:p-4 lg:p-8 font-sans"> <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 --> <!-- 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" <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">
:class="{'aspect-video': isDesktop, 'min-h-screen': !isDesktop}">
<!-- Left Column: Player Panel --> <!-- 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"> <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">
@ -10,6 +9,8 @@
v-if="initialized" v-if="initialized"
:stats="playerStats" :stats="playerStats"
@openAchievements="showAchievements = true" @openAchievements="showAchievements = true"
@deletePet="handleDeletePet"
@openInventory="showInventory = true"
/> />
<div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs"> <div v-else class="flex items-center justify-center h-32 md:h-full text-[#8f80a0] text-xs">
Initializing... Initializing...
@ -34,10 +35,17 @@
<ActionArea <ActionArea
v-if="initialized" v-if="initialized"
:playerStats="playerStats" :playerStats="playerStats"
@feed="handleFeed"
@play="handlePlay"
@train="handleTrain"
@puzzle="handlePuzzle"
@clean="handleClean"
@heal="handleHeal"
@openInventory="showInventory = true" @openInventory="showInventory = true"
@openGodSystem="showGodSystem = true" @openGodSystem="showGodSystem = true"
@openShop="showShop = true" @openShop="showShop = true"
@openAdventure="showAdventureSelect = true" @openAdventure="showAdventureSelect = true"
@toggleSleep="handleToggleSleep"
/> />
</div> </div>
</div> </div>
@ -58,7 +66,7 @@
<PixelModal <PixelModal
:isOpen="showAchievements" :isOpen="showAchievements"
@close="showAchievements = false" @close="showAchievements = false"
title="ACHIEVEMENTS" title="成就"
> >
<AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" /> <AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" />
</PixelModal> </PixelModal>
@ -67,7 +75,7 @@
<PixelModal <PixelModal
:isOpen="showInventory" :isOpen="showInventory"
@close="showInventory = false" @close="showInventory = false"
title="INVENTORY" title="背包"
> >
<InventoryOverlay <InventoryOverlay
:items="inventory" :items="inventory"
@ -82,7 +90,7 @@
<PixelModal <PixelModal
:isOpen="showGodSystem" :isOpen="showGodSystem"
@close="showGodSystem = false" @close="showGodSystem = false"
title="GOD SYSTEM" title="神明系統"
> >
<GodSystemOverlay <GodSystemOverlay
:currentDeity="currentDeity" :currentDeity="currentDeity"
@ -96,7 +104,7 @@
<PixelModal <PixelModal
:isOpen="showShop" :isOpen="showShop"
@close="showShop = false" @close="showShop = false"
title="SHOP" title="商店"
> >
<ShopOverlay <ShopOverlay
:playerGold="playerStats.gold || 0" :playerGold="playerStats.gold || 0"
@ -111,7 +119,7 @@
<PixelModal <PixelModal
:isOpen="showAdventureSelect" :isOpen="showAdventureSelect"
@close="showAdventureSelect = false" @close="showAdventureSelect = false"
title="ADVENTURE" title="冒險"
> >
<AdventureOverlay <AdventureOverlay
:locations="ADVENTURE_LOCATIONS" :locations="ADVENTURE_LOCATIONS"
@ -131,7 +139,7 @@
<p class="text-gray-400 text-sm">這次沒有獲得任何獎勵...</p> <p class="text-gray-400 text-sm">這次沒有獲得任何獎勵...</p>
<button <button
@click="handleCloseBattleResult" @click="handleCloseBattleResult"
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black uppercase tracking-widest" class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black tracking-widest"
> >
確定 確定
</button> </button>
@ -139,6 +147,12 @@
</div> </div>
</div> </div>
<!-- Naming Overlay -->
<NamingOverlay
v-if="showNamingOverlay"
@submit="handleNameSubmit"
/>
</div> </div>
</template> </template>
@ -160,24 +174,22 @@ import { PetSystem } from '../../core/pet-system.js';
import { TempleSystem } from '../../core/temple-system.js'; import { TempleSystem } from '../../core/temple-system.js';
import { ApiService } from '../../core/api-service.js'; import { ApiService } from '../../core/api-service.js';
import { // Import from data and core instead of types
ItemType, import { DEITIES } from '../../data/deities.js';
Rarity, import { ITEMS, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../data/items.js';
EquipSlot, import { ADVENTURES } from '../../data/adventures.js';
DeityId, import { ACHIEVEMENTS } from '../../data/achievements.js';
ItemCategory
} from '~/types/pixel'; // Type definitions (minimal, based on actual data structures)
import type { type EntityStats = any;
EntityStats, import NamingOverlay from '~/components/pixel/NamingOverlay.vue';
Achievement, type Deity = typeof DEITIES[0];
Item, type Item = typeof ITEMS[keyof typeof ITEMS];
Deity, type AdventureLocation = typeof ADVENTURES[0];
AdventureLocation
} from '~/types/pixel';
// --- SYSTEMS INITIALIZATION --- // --- SYSTEMS INITIALIZATION ---
const apiService = new ApiService({ useMock: true }); // Use mock for now const apiService = new ApiService({ useMock: true }); // Use localStorage mock for now
const petSystem = ref<PetSystem | null>(null); const petSystem = ref<PetSystem | null>(null);
const templeSystem = ref<TempleSystem | null>(null); const templeSystem = ref<TempleSystem | null>(null);
const initialized = ref(false); const initialized = ref(false);
@ -215,8 +227,12 @@ const playerStats = computed<EntityStats>(() => {
const s = systemState.value; const s = systemState.value;
const currentDeity = allDeities.value.find(d => d.id === s.currentDeityId); const currentDeity = allDeities.value.find(d => d.id === s.currentDeityId);
// 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;
return { return {
name: "Pet", name: s.name || "Pet",
class: s.stage, class: s.stage,
hp: Math.floor(s.health), hp: Math.floor(s.health),
maxHp: 100, maxHp: 100,
@ -224,12 +240,12 @@ const playerStats = computed<EntityStats>(() => {
maxSp: 100, maxSp: 100,
lvl: 1, lvl: 1,
hunger: Math.floor(s.hunger), hunger: Math.floor(s.hunger || 0),
maxHunger: 100, maxHunger: 100,
happiness: Math.floor(s.happiness), happiness: Math.floor(s.happiness ?? 100),
maxHappiness: 100, maxHappiness: 100,
age: formatAge(s.ageSeconds), age: formatAge(currentAge),
generation: s.generation || 1, generation: s.generation || 1,
height: `${s.height || 0} cm`, height: `${s.height || 0} cm`,
weight: `${Math.floor(s.weight || 0)} g`, weight: `${Math.floor(s.weight || 0)} g`,
@ -248,7 +264,13 @@ const playerStats = computed<EntityStats>(() => {
luck: Math.floor(s.effectiveLuck || s.luck), luck: Math.floor(s.effectiveLuck || s.luck),
atk: Math.floor(s.attack || 0), atk: Math.floor(s.attack || 0),
def: Math.floor(s.defense || 0), def: Math.floor(s.defense || 0),
spd: Math.floor(s.speed || 0) spd: Math.floor(s.speed || 0),
// Status flags
poopCount: s.poopCount || 0,
isSick: s.isSick || false,
isSleeping: s.isSleeping || false,
dyingSeconds: s.dyingSeconds || 0
}; };
}); });
@ -261,7 +283,7 @@ const inventory = computed<Item[]>(() => {
})); }));
}); });
const deities = computed<Record<DeityId, Deity>>(() => { const deities = computed(() => {
const map: Record<string, Deity> = {}; const map: Record<string, Deity> = {};
allDeities.value.forEach(d => { allDeities.value.forEach(d => {
const favor = systemState.value?.deityFavors?.[d.id] || 0; const favor = systemState.value?.deityFavors?.[d.id] || 0;
@ -270,7 +292,7 @@ const deities = computed<Record<DeityId, Deity>>(() => {
return map; return map;
}); });
const currentDeity = computed(() => systemState.value?.currentDeityId || DeityId.Mazu); const currentDeity = computed(() => systemState.value?.currentDeityId || 'mazu');
// Modal States // Modal States
const showAchievements = ref(false); const showAchievements = ref(false);
@ -283,40 +305,103 @@ const showBattleResult = ref(false);
// Battle State // Battle State
const isFighting = ref(false); const isFighting = ref(false);
const battleLogs = ref<string[]>([]); const battleLogs = ref<string[]>([]);
const showNamingOverlay = ref(false);
const handleNameSubmit = async (name: string) => {
if (petSystem.value) {
await petSystem.value.updateState({ name });
systemState.value = petSystem.value.getState();
showNamingOverlay.value = false;
}
};
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
}
}
};
// --- LIFECYCLE --- // --- LIFECYCLE ---
const stateSyncInterval = ref<any>(null);
onMounted(async () => { onMounted(async () => {
petSystem.value = new PetSystem(apiService); // Initialize Systems
templeSystem.value = new TempleSystem(petSystem.value, apiService); petSystem.value = new PetSystem(apiService);
templeSystem.value = new TempleSystem(apiService, petSystem.value);
await petSystem.value.initialize(); // Load Data
await templeSystem.value.initialize(); console.log("Initializing PetSystem...");
const state = await petSystem.value.initialize();
console.log("Initial State:", state);
systemState.value = state;
systemState.value = petSystem.value.getState(); // Check if naming is required
allDeities.value = templeSystem.value.getDeities(); if (!state.name) {
showNamingOverlay.value = true;
}
petSystem.value.startTickLoop((newState) => { // Load Deities
systemState.value = newState; allDeities.value = await templeSystem.value.getDeities();
});
initialized.value = true; // 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;
}); });
onUnmounted(() => { onUnmounted(() => {
if (petSystem.value) { // Clear intervals first
petSystem.value.stopTickLoop(); 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;
}
}); });
// --- HELPERS --- // --- HELPERS ---
const formatAge = (seconds: number) => { const formatAge = (seconds: number) => {
if (!seconds) return '0h'; if (!seconds) return '0s';
const days = Math.floor(seconds / 86400); const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600); const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (days > 0) return `${days}d ${hours}h`; if (days > 0) return `${days}d ${hours}h`;
return `${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
}; };
// --- HANDLERS --- // --- HANDLERS ---
@ -360,6 +445,76 @@ const handleCloseBattleResult = () => {
battleLogs.value = []; battleLogs.value = [];
}; };
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);
}
}
};
const handleEquip = async (itemId: string, asAppearance: boolean) => { const handleEquip = async (itemId: string, asAppearance: boolean) => {
console.log("Equip not fully implemented in core yet", itemId); console.log("Equip not fully implemented in core yet", itemId);
}; };
@ -376,7 +531,7 @@ const handleDeleteItem = async (itemId: string) => {
console.log("Delete item not fully implemented in core yet", itemId); console.log("Delete item not fully implemented in core yet", itemId);
}; };
const handleSwitchDeity = async (id: DeityId) => { const handleSwitchDeity = async (id: string) => {
if (templeSystem.value) { if (templeSystem.value) {
await templeSystem.value.switchDeity(id); await templeSystem.value.switchDeity(id);
systemState.value = petSystem.value?.getState(); systemState.value = petSystem.value?.getState();
@ -403,7 +558,7 @@ const handleBuyItem = async (item: Item) => {
const handleSellItem = async (item: Item) => { const handleSellItem = async (item: Item) => {
if (petSystem.value) { if (petSystem.value) {
const sellPrice = Math.floor(item.price / 2); const sellPrice = Math.floor((item as any).price / 2);
const newCoins = systemState.value.coins + sellPrice; const newCoins = systemState.value.coins + sellPrice;
const newInventory = systemState.value.inventory.filter((i: any) => i.id !== item.id); const newInventory = systemState.value.inventory.filter((i: any) => i.id !== item.id);
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory }); await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
@ -411,25 +566,15 @@ const handleSellItem = async (item: Item) => {
} }
}; };
// --- MOCK DATA FOR STATIC CONTENT --- // Use imported data instead of mock
const ADVENTURE_LOCATIONS: AdventureLocation[] = [ const ADVENTURE_LOCATIONS = ADVENTURES;
{ id: '1', name: '自家後院', description: '安全的新手探險地,偶爾會有小蟲子。', costHunger: 5, costGold: 5, difficulty: 'Easy', enemyName: '野蟲' }, const ACHIEVEMENTS_DATA = ACHIEVEMENTS;
{ 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: '樹妖' } // 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
}));
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' },
];
</script> </script>

View File

@ -1,180 +0,0 @@
export enum StatType {
HP = 'HP',
SP = 'SP',
ATK = 'ATK',
DEF = 'DEF',
SPD = 'SPD',
LUCK = 'LCK'
}
export interface EntityStats {
hp: number;
maxHp: number;
sp: number; // Re-purposed for 'Hunger' or general resource if needed
maxSp: number;
lvl: number;
name: string;
class: string;
// New Pet Fields
hunger?: number;
maxHunger?: number;
happiness?: number;
maxHappiness?: number;
age?: string;
generation?: number;
height?: string;
weight?: string;
gold?: number;
fate?: string; // e.g. "Resource Recycling Grandma"
godFavor?: {
name: string;
current: number;
max: number;
};
// Detailed Stats
str?: number;
int?: number;
dex?: number;
luck?: number;
atk?: number;
def?: number;
spd?: number;
}
export interface ActionItem {
id: string;
name: string;
iconName: string; // Mapping to Lucide icon
cooldown: number;
cost: number;
}
export interface EquipmentSlot {
slot: string;
item: string;
}
export interface Achievement {
id: string;
title: string;
description: string;
reward?: string;
progress: number; // 0 to 100 percentage
currentValue?: number;
maxValue?: number;
unlocked: boolean;
icon: string; // Helper to map to Lucide icon in component
color?: string;
}
// --- Inventory System Types ---
export enum Rarity {
Common = 'Common',
Excellent = 'Excellent',
Rare = 'Rare',
Epic = 'Epic',
Legendary = 'Legendary'
}
export enum ItemType {
Equipment = 'Equipment',
Consumable = 'Consumable'
}
export enum ItemCategory {
Food = 'Food',
Medicine = 'Medicine',
Equipment = 'Equipment',
Toy = 'Toy',
Accessory = 'Accessory',
Misc = 'Misc'
}
export enum EquipSlot {
Weapon = 'Weapon',
Armor = 'Armor',
Hat = 'Hat',
Accessory = 'Accessory',
Charm = 'Charm',
Special = 'Special'
}
export interface Item {
id: string;
name: string;
type: ItemType;
category?: ItemCategory; // New field for Shop filters
price: number; // New field for Shop
slot?: EquipSlot; // Only for Equipment
rarity: Rarity;
description: string;
statsDescription?: string; // e.g. "DEF +8 MaxHP +10"
effects?: string[];
icon: string;
quantity?: number; // For consumables
// State helpers
isEquipped?: boolean;
isAppearance?: boolean; // If true, it's in the appearance slot
}
// --- God System Types ---
export enum DeityId {
Mazu = 'Mazu', // 媽祖
EarthGod = 'EarthGod', // 土地公
Matchmaker = 'Matchmaker', // 月老
Wenchang = 'Wenchang' // 文昌
}
export enum JiaobeiResult {
Saint = 'Saint', // 聖杯 (One up, one down) - YES
Smile = 'Smile', // 笑杯 (Two flat faces up) - LAUGH/MAYBE
Cry = 'Cry', // 陰杯 (Two round faces up) - NO
None = 'None'
}
export enum LotPhase {
Idle = 'Idle',
Drawing = 'Drawing', // Shaking the cylinder
PendingVerify = 'PendingVerify', // Lot drawn, needs 3 saint cups
Verifying = 'Verifying', // Tossing blocks
Success = 'Success', // Got 3 saint cups
Failed = 'Failed' // Failed mid-way
}
export interface Deity {
id: DeityId;
name: string;
title: string;
description: string;
favor: number;
maxFavor: number;
colors: {
skin: string;
hair: string;
outfit: string;
accessory: string;
};
}
// --- Adventure System Types ---
export interface AdventureLocation {
id: string;
name: string;
description: string;
costHunger: number;
costGold: number;
reqStats?: {
str?: number;
int?: number;
};
difficulty: 'Easy' | 'Medium' | 'Hard';
enemyName: string;
}

View File

@ -1,375 +0,0 @@
// Console 互動版本 - 可點擊操作所有功能
// 使用方式:在瀏覽器 console 或 Node.js 環境執行
import { PetSystem } from './core/pet-system.js'
import { EventSystem } from './core/event-system.js'
import { TempleSystem } from './core/temple-system.js'
import { ApiService } from './core/api-service.js'
// 創建 API 服務(可切換 mock/real
const apiService = new ApiService({
useMock: true, // 設為 false 可切換到真實 API
baseUrl: 'http://localhost:3000/api',
mockDelay: 100
})
// 全局系統實例
let petSystem, eventSystem, templeSystem
let isRunning = false
// 初始化系統
async function init() {
console.log('=== 虛擬寵物系統初始化 ===\n')
// 創建系統實例
petSystem = new PetSystem(apiService)
eventSystem = new EventSystem(petSystem, apiService)
templeSystem = new TempleSystem(petSystem, apiService)
// 初始化
await petSystem.initialize()
await eventSystem.initialize()
await templeSystem.initialize()
console.log('✅ 系統初始化完成!')
console.log('📝 輸入 help() 查看所有可用命令\n')
// 顯示初始狀態
showStatus()
return { petSystem, eventSystem, templeSystem }
}
// 顯示狀態
function showStatus() {
const state = petSystem.getState()
const buffs = eventSystem.getBuffManager().getActiveBuffs()
const currentDeity = templeSystem.getCurrentDeity()
const favorStars = templeSystem.getFavorStars(state.currentDeityId)
console.log('\n' + '='.repeat(50))
console.log('🐾 寵物狀態')
console.log('='.repeat(50))
console.log(`種類: ${state.speciesId}`)
console.log(`階段: ${state.stage}`)
console.log(`年齡: ${Math.floor(state.ageSeconds)}`)
console.log(`\n📊 基礎數值:`)
console.log(` 飢餓: ${state.hunger.toFixed(1)}/100`)
console.log(` 快樂: ${state.happiness.toFixed(1)}/100`)
console.log(` 健康: ${state.health.toFixed(1)}/100`)
console.log(` 體重: ${state.weight.toFixed(1)}`)
console.log(`\n💪 屬性:`)
console.log(` 力量: ${state.str.toFixed(1)}`)
console.log(` 智力: ${state.int.toFixed(1)}`)
console.log(` 敏捷: ${state.dex.toFixed(1)}`)
console.log(` 運勢: ${state.luck.toFixed(1)}`)
console.log(`\n🎭 狀態:`)
console.log(` 睡覺: ${state.isSleeping ? '是' : '否'}`)
console.log(` 生病: ${state.isSick ? '是' : '否'}`)
console.log(` 死亡: ${state.isDead ? '是' : '否'}`)
console.log(` 便便: ${state.poopCount}/4`)
console.log(`\n🙏 神明:`)
console.log(` 當前: ${currentDeity.name}`)
console.log(` 好感: ${favorStars} (${state.deityFavors[state.currentDeityId]}/100)`)
console.log(` 今日祈福: ${state.dailyPrayerCount}/3`)
console.log(`\n✨ 當前 Buff:`)
if (buffs.length === 0) {
console.log(' (無)')
} else {
buffs.forEach(b => {
const duration = b.durationTicks === Infinity ? '永久' : `${b.currentTicks} ticks`
console.log(` - ${b.name} (${duration})`)
})
}
console.log('='.repeat(50) + '\n')
}
// 啟動遊戲循環
function start() {
if (isRunning) {
console.log('⚠️ 遊戲循環已在運行中')
return
}
isRunning = true
petSystem.startTickLoop((state) => {
console.log(`\n⏰ Tick: ${new Date().toLocaleTimeString()}`)
showStatus()
})
eventSystem.startEventCheck()
console.log('✅ 遊戲循環已啟動(每 3 秒 tick每 10 秒檢查事件)')
console.log('💡 輸入 stop() 停止循環\n')
}
// 停止遊戲循環
function stop() {
if (!isRunning) {
console.log('⚠️ 遊戲循環未運行')
return
}
petSystem.stopTickLoop()
eventSystem.stopEventCheck()
isRunning = false
console.log('⏹️ 遊戲循環已停止')
}
// ========== 互動命令 ==========
// 餵食
async function feed(amount = 20) {
const result = await petSystem.feed(amount)
if (result.success) {
console.log(`✅ 餵食成功!飢餓 +${amount},體重 +${(amount * 0.5).toFixed(1)}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 玩耍
async function play(amount = 15) {
const result = await petSystem.play(amount)
if (result.success) {
console.log(`✅ 玩耍成功!快樂 +${amount},敏捷 +0.5`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 清理便便
async function clean() {
const result = await petSystem.cleanPoop()
if (result.success) {
console.log('✅ 清理成功!快樂 +10')
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 治療
async function heal(amount = 20) {
const result = await petSystem.heal(amount)
if (result.success) {
console.log(`✅ 治療成功!健康 +${amount}${result.cured ? ',疾病已治癒' : ''}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 睡覺/起床
async function sleep() {
const result = await petSystem.toggleSleep()
if (result.success) {
console.log(`${result.isSleeping ? '寵物已入睡' : '寵物已醒來'}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 觸發事件(測試用)
async function triggerEvent(eventId) {
const result = await eventSystem.triggerEvent(eventId)
if (result) {
console.log(`✅ 事件 ${eventId} 觸發成功`)
} else {
console.log(`❌ 事件 ${eventId} 觸發失敗或條件不滿足`)
}
showStatus()
}
// 查看事件列表
async function listEvents() {
const events = await apiService.getEvents()
console.log('\n📋 可用事件列表:')
console.log('='.repeat(50))
events.forEach(e => {
console.log(`\n${e.id} (${e.type})`)
console.log(` 權重: ${e.weight}`)
console.log(` 效果數: ${e.effects.length}`)
})
console.log('='.repeat(50) + '\n')
}
// 查看事件歷史
function history() {
const history = eventSystem.getHistory()
console.log('\n📜 事件歷史:')
console.log('='.repeat(50))
if (history.length === 0) {
console.log(' (無)')
} else {
history.forEach((h, i) => {
const time = new Date(h.timestamp).toLocaleTimeString()
console.log(`${i + 1}. [${time}] ${h.eventId} (${h.eventType})`)
})
}
console.log('='.repeat(50) + '\n')
}
// 祈福
async function pray() {
const result = await templeSystem.pray()
if (result.success) {
console.log(`✅ 祈福成功!`)
console.log(` 好感度 +${result.favorIncrease}${result.newFavor}`)
console.log(` ${result.dialogue}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 抽籤
async function drawFortune() {
// 使用 drawLot 方法
const result = await templeSystem.drawLot()
if (result.success) {
console.log('\n🎴 抽籤結果:')
console.log('='.repeat(50))
console.log(`等級: ${result.lot.grade}`)
console.log(`籤詩: ${result.lot.poem1}`)
if (result.lot.poem2) {
console.log(` ${result.lot.poem2}`)
}
console.log(`解釋: ${result.lot.meaning}`)
if (result.needVerification) {
console.log(`\n⚠️ 需要三聖筊驗證才能解籤`)
console.log(`目前: ${result.verificationCount}/${result.requiredHoly} 聖筊`)
}
console.log('='.repeat(50) + '\n')
} else {
console.log(`${result.message}`)
}
}
// 切換神明
async function switchDeity(deityId) {
const result = await templeSystem.switchDeity(deityId)
if (result.success) {
console.log(`✅ 已切換到 ${result.deity.name}`)
} else {
console.log(`${result.message}`)
}
showStatus()
}
// 查看神明列表
function listDeities() {
const deities = templeSystem.getDeities()
console.log('\n🙏 神明列表:')
console.log('='.repeat(50))
deities.forEach(d => {
const state = petSystem.getState()
const favor = state.deityFavors[d.id] || 0
const stars = templeSystem.getFavorStars(d.id)
console.log(`\n${d.id}: ${d.name}`)
console.log(` 個性: ${d.personality}`)
console.log(` 好感: ${stars} (${favor}/100)`)
console.log(` 加成: ${d.buffDescriptions.join(', ')}`)
})
console.log('='.repeat(50) + '\n')
}
// 應用 Buff每 tick 自動執行,也可手動)
async function applyBuffs() {
await eventSystem.applyBuffs()
eventSystem.getBuffManager().tick()
console.log('✅ Buff 已應用並更新')
showStatus()
}
// 幫助
function help() {
console.log('\n' + '='.repeat(50))
console.log('📖 可用命令列表')
console.log('='.repeat(50))
console.log('\n🎮 遊戲控制:')
console.log(' start() - 啟動遊戲循環')
console.log(' stop() - 停止遊戲循環')
console.log(' showStatus() - 顯示當前狀態')
console.log('\n🐾 寵物互動:')
console.log(' feed(amount) - 餵食(預設 +20')
console.log(' play(amount) - 玩耍(預設 +15')
console.log(' clean() - 清理便便')
console.log(' heal(amount) - 治療(預設 +20')
console.log(' sleep() - 睡覺/起床')
console.log('\n🎲 事件系統:')
console.log(' triggerEvent(id) - 手動觸發事件')
console.log(' listEvents() - 查看所有事件')
console.log(' history() - 查看事件歷史')
console.log(' applyBuffs() - 手動應用 Buff')
console.log('\n🙏 神明系統:')
console.log(' pray() - 祈福(每日 3 次)')
console.log(' drawFortune() - 抽籤')
console.log(' switchDeity(id) - 切換神明')
console.log(' listDeities() - 查看神明列表')
console.log('\n💡 提示:')
console.log(' - 所有數值操作都會同步到 APImock 模式使用 localStorage')
console.log(' - 事件每 10 秒自動檢查10% 機率觸發)')
console.log(' - 遊戲循環每 3 秒執行一次 tick')
console.log(' - 輸入 help() 再次查看此列表')
console.log('='.repeat(50) + '\n')
}
// 匯出到全局(瀏覽器環境)
if (typeof window !== 'undefined') {
window.petSystem = petSystem
window.eventSystem = eventSystem
window.templeSystem = templeSystem
window.showStatus = showStatus
window.start = start
window.stop = stop
window.feed = feed
window.play = play
window.clean = clean
window.heal = heal
window.sleep = sleep
window.triggerEvent = triggerEvent
window.listEvents = listEvents
window.history = history
window.pray = pray
window.drawFortune = drawFortune
window.switchDeity = switchDeity
window.listDeities = listDeities
window.applyBuffs = applyBuffs
window.help = help
window.init = init
}
// Node.js 環境自動初始化
if (typeof window === 'undefined') {
init().then(() => {
console.log('\n💡 提示:在瀏覽器環境中,這些函數會自動掛載到 window 物件')
console.log(' 在 Node.js 環境中,請使用 await 呼叫這些函數\n')
})
}
export {
init,
showStatus,
start,
stop,
feed,
play,
clean,
heal,
sleep,
triggerEvent,
listEvents,
history,
pray,
drawFortune,
switchDeity,
listDeities,
applyBuffs,
help
}

View File

@ -204,9 +204,19 @@ export class ApiService {
getMockPetState() { getMockPetState() {
// 從 localStorage 或預設值讀取 // 從 localStorage 或預設值讀取
if (typeof localStorage === 'undefined') {
console.warn('[ApiService] localStorage is not available');
return null;
}
const stored = localStorage.getItem('petState') const stored = localStorage.getItem('petState')
console.log('[ApiService] getMockPetState:', stored ? 'Found' : 'Not Found');
if (stored) { if (stored) {
return JSON.parse(stored) try {
return JSON.parse(stored)
} catch (e) {
console.error('[ApiService] Failed to parse stored state:', e);
return null;
}
} }
return null return null
} }

View File

@ -65,11 +65,13 @@ export class PetSystem {
const config = PET_SPECIES[speciesId] const config = PET_SPECIES[speciesId]
const state = { const state = {
speciesId, speciesId,
name: null, // 寵物名稱,初始為 null
stage: 'egg', stage: 'egg',
hunger: 100, hunger: 100,
happiness: 100, happiness: 100,
health: 100, health: 100,
weight: 500, height: config.lifecycle[0]?.height || config.baseStats.defaultHeight || 10, // 從第一階段獲取身高
weight: config.lifecycle[0]?.baseWeight || config.baseStats.defaultWeight || 500, // 從第一階段獲取體重
ageSeconds: 0, ageSeconds: 0,
poopCount: 0, poopCount: 0,
str: 10, str: 10,
@ -351,7 +353,24 @@ export class PetSystem {
// 獲取當前狀態 // 獲取當前狀態
getState() { getState() {
return { ...this.state } const state = { ...this.state };
// Ensure critical values are valid numbers
if (typeof state.happiness !== 'number' || isNaN(state.happiness)) {
console.warn('[PetSystem] Invalid happiness value, resetting to 100');
state.happiness = 100;
this.state.happiness = 100;
}
if (typeof state.hunger !== 'number' || isNaN(state.hunger)) {
console.warn('[PetSystem] Invalid hunger value, resetting to 100');
state.hunger = 100;
this.state.hunger = 100;
}
if (typeof state.health !== 'number' || isNaN(state.health)) {
console.warn('[PetSystem] Invalid health value, resetting to 100');
state.health = 100;
this.state.health = 100;
}
return state;
} }
// 刪除寵物 // 刪除寵物
@ -384,6 +403,9 @@ export class PetSystem {
// 从配置读取间隔时间 // 从配置读取间隔时间
const interval = this.speciesConfig?.baseStats?.physiologyTickInterval || 60000 const interval = this.speciesConfig?.baseStats?.physiologyTickInterval || 60000
// 立即執行一次回調,確保 UI 獲得最新狀態
if (callback) callback(this.getState())
this.tickInterval = setInterval(async () => { this.tickInterval = setInterval(async () => {
await this.tick() await this.tick()
if (callback) callback(this.getState()) if (callback) callback(this.getState())
@ -678,6 +700,18 @@ export class PetSystem {
const updates = { stage: targetStage } const updates = { stage: targetStage }
// 更新身高和體重(從新階段配置中獲取)
const newStageIndex = config.lifecycle.findIndex(s => s.stage === targetStage)
if (newStageIndex >= 0) {
const newStageConfig = config.lifecycle[newStageIndex]
if (newStageConfig.height) {
updates.height = newStageConfig.height
}
if (newStageConfig.baseWeight) {
updates.weight = newStageConfig.baseWeight
}
}
// 應用進化分支效果 // 應用進化分支效果
if (evolutionBranch) { if (evolutionBranch) {
console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`) console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`)

View File

@ -397,3 +397,24 @@ export const EQUIPMENT_SLOTS = {
talisman: { name: '護身符', icon: '🔮' }, talisman: { name: '護身符', icon: '🔮' },
special: { name: '特殊', icon: '⭐' } special: { name: '特殊', icon: '⭐' }
} }
// 道具類型定義
export const ITEM_TYPE = {
EQUIPMENT: 'equipment',
CONSUMABLE: 'consumable',
TALISMAN: 'talisman',
SPECIAL: 'special',
APPEARANCE: 'appearance'
}
// 道具類別定義
export const ITEM_CATEGORY = {
FOOD: 'food',
MEDICINE: 'potion',
WEAPON: 'weapon',
ARMOR: 'armor',
TOY: 'toy',
ACCESSORY: 'accessory',
TALISMAN: 'talisman',
BOOK: 'book'
}

View File

@ -6,13 +6,13 @@ export const PET_SPECIES = {
description: '活潑可愛的小貓咪', description: '活潑可愛的小貓咪',
baseStats: { baseStats: {
// 系統更新間隔(毫秒) // 系統更新間隔(毫秒)
physiologyTickInterval: 10000, // 生理系統刷新間隔30秒 physiologyTickInterval: 1000, // 生理系統刷新間隔1秒 (更流暢的視覺效果)
eventCheckInterval: 10000, // 事件檢查間隔10秒 eventCheckInterval: 10000, // 事件檢查間隔10秒
// 衰減速率 (每 tick 60秒) // 衰減速率 (每 tick 1秒)
// 調整為更輕鬆:飢餓 8 小時,快樂 5 小時 // 調整為:飢餓 8 小時,快樂 5 小時 (數值除以 10)
hungerDecayPerTick: 0.2, // 原 0.28 → 0.2 (更慢) hungerDecayPerTick: 0.02, // 原 0.2 (10秒) → 0.02 (1秒)
happinessDecayPerTick: 0.33, // 原 0.42 → 0.33 (更慢) happinessDecayPerTick: 0.033, // 原 0.33 (10秒) → 0.033 (1秒)
// 便便系統 // 便便系統
poopChancePerTick: 0.05, // 約 20 分鐘產生一次 poopChancePerTick: 0.05, // 約 20 分鐘產生一次

View File

@ -26,6 +26,11 @@ export default defineNuxtConfig({
app: { app: {
head: { head: {
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover', viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover',
link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=DotGothic16&display=swap' }
],
meta: [ meta: [
{ name: 'mobile-web-app-capable', content: 'yes' }, { name: 'mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' }, { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },

View File

@ -1,230 +0,0 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虛擬寵物系統 - Console 互動版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #4ec9b0;
margin-bottom: 10px;
border-bottom: 2px solid #4ec9b0;
padding-bottom: 10px;
}
.info {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #007acc;
}
.info h2 {
color: #4ec9b0;
margin-bottom: 10px;
}
.console-area {
background: #1e1e1e;
border: 2px solid #3c3c3c;
border-radius: 5px;
padding: 15px;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.command-list {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.command-list h3 {
color: #4ec9b0;
margin-bottom: 10px;
}
.command-list code {
color: #ce9178;
background: #1e1e1e;
padding: 2px 6px;
border-radius: 3px;
}
.command-list ul {
list-style: none;
padding-left: 0;
}
.command-list li {
margin: 8px 0;
padding-left: 20px;
}
.status-display {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
border-left: 4px solid #4ec9b0;
}
.warning {
color: #f48771;
background: #3c1e1e;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.success {
color: #4ec9b0;
}
</style>
</head>
<body>
<div class="container">
<h1>🐾 虛擬寵物系統 - Console 互動版</h1>
<div class="info">
<h2>📖 使用說明</h2>
<p>1. 打開瀏覽器的開發者工具F12 或 Cmd+Option+I</p>
<p>2. 切換到 <strong>Console</strong> 標籤</p>
<p>3. 系統會自動初始化,然後你可以在 console 中輸入命令</p>
<p>4. 輸入 <code>help()</code> 查看所有可用命令</p>
<p>5. 輸入 <code>start()</code> 開始遊戲循環</p>
</div>
<div class="warning">
⚠️ <strong>注意:</strong>此版本使用 Mock API資料儲存在 localStorage未來可切換到真實 API。
</div>
<div class="status-display">
<h3>📊 系統狀態</h3>
<p id="system-status">正在載入...</p>
</div>
<div class="command-list">
<h3>🎮 快速命令參考</h3>
<ul>
<li><code>init()</code> - 初始化系統</li>
<li><code>showStatus()</code> - 顯示寵物狀態</li>
<li><code>start()</code> - 啟動遊戲循環</li>
<li><code>stop()</code> - 停止遊戲循環</li>
<li><code>feed(20)</code> - 餵食</li>
<li><code>play(15)</code> - 玩耍</li>
<li><code>clean()</code> - 清理便便</li>
<li><code>heal(20)</code> - 治療</li>
<li><code>pray()</code> - 祈福</li>
<li><code>drawFortune()</code> - 抽籤</li>
<li><code>help()</code> - 查看完整幫助</li>
</ul>
</div>
<div class="console-area" id="console-output">
<div class="success">✅ 系統載入中...</div>
</div>
</div>
<script type="module">
// 動態載入模組(使用絕對路徑,從專案根目錄)
const basePath = window.location.origin
const modulePath = basePath + '/console-demo.js'
let init, showStatus, start, stop, help
// 載入模組
import(modulePath).then(module => {
init = module.init
showStatus = module.showStatus
start = module.start
stop = module.stop
help = module.help
// 掛載到 window 供 console 使用
window.init = init
window.showStatus = showStatus
window.start = start
window.stop = stop
window.help = help
// 初始化系統
initializeSystem()
}).catch(error => {
console.error('載入模組失敗:', error)
updateStatus('❌ 載入模組失敗,請確認路徑正確')
})
// 更新狀態顯示
function updateStatus(message) {
const statusEl = document.getElementById('system-status')
const outputEl = document.getElementById('console-output')
if (statusEl) statusEl.textContent = message
if (outputEl) {
const div = document.createElement('div')
div.className = 'success'
div.textContent = message
outputEl.appendChild(div)
outputEl.scrollTop = outputEl.scrollHeight
}
}
// 初始化系統
async function initializeSystem() {
try {
updateStatus('正在初始化系統...')
await init()
updateStatus('✅ 系統初始化完成!輸入 help() 查看所有命令')
// 覆蓋 console.log 以顯示在頁面上
const originalLog = console.log
console.log = function(...args) {
originalLog.apply(console, args)
const outputEl = document.getElementById('console-output')
if (outputEl) {
const div = document.createElement('div')
div.textContent = args.join(' ')
outputEl.appendChild(div)
outputEl.scrollTop = outputEl.scrollHeight
}
}
} catch (error) {
updateStatus('❌ 初始化失敗: ' + error.message)
console.error(error)
}
}
// 等待 DOM 載入
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
// 模組載入完成後會自動初始化
})
}
</script>
</body>
</html>

2152
public/fonts/cubicll.ttf Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 KiB

View File

@ -15,6 +15,10 @@ export default <Config>{
], ],
theme: { theme: {
extend: { extend: {
fontFamily: {
sans: ['Cubic 11', 'ui-sans-serif', 'system-ui'],
mono: ['Cubic 11', 'ui-monospace', 'monospace'],
},
colors: { colors: {
// Pixel Dungeon Palette // Pixel Dungeon Palette
pixel: { pixel: {