fix: query pointer
This commit is contained in:
parent
873bc64cd2
commit
8def70de92
17
app/app.vue
17
app/app.vue
|
|
@ -6,13 +6,26 @@
|
|||
|
||||
<style>
|
||||
/* 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 {
|
||||
margin: 0;
|
||||
background-color: #1b1026;
|
||||
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 */
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
@theme {
|
||||
|
|
@ -15,4 +24,7 @@
|
|||
--color-pixel-panel: #2b193f;
|
||||
--color-pixel-panel-dark: #1b1026;
|
||||
--color-pixel-panel-border: #4a3b5e;
|
||||
|
||||
/* Pixel Font Family - 使用俐方體 11 號 */
|
||||
--font-family-pixel: 'Cubic 11', monospace;
|
||||
}
|
||||
|
|
@ -76,8 +76,10 @@
|
|||
<script setup lang="ts">
|
||||
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 RetroProgressBar from './RetroProgressBar.vue'; // Need to create this one too!
|
||||
import type { Achievement } from '~/types/pixel';
|
||||
import PixelFrame from './PixelFrame.vue';
|
||||
import RetroProgressBar from './RetroProgressBar.vue';
|
||||
|
||||
type Achievement = any;
|
||||
|
||||
interface Props {
|
||||
achievements: Achievement[];
|
||||
|
|
|
|||
|
|
@ -1,45 +1,29 @@
|
|||
<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 -->
|
||||
<div class="absolute inset-0 bg-[#231533] opacity-50 pointer-events-none" />
|
||||
|
||||
<!-- Main Grid Layout -->
|
||||
<div class="flex-grow grid grid-cols-12 gap-2 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">
|
||||
<!-- Main Action Grid -->
|
||||
<div class="flex-grow z-10">
|
||||
<PixelFrame class="h-full bg-[#2b193f]">
|
||||
<div class="grid grid-cols-4 grid-rows-3 gap-2 h-full p-1">
|
||||
<template v-for="(action, index) in gridItems" :key="index">
|
||||
<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 displayActions" :key="index">
|
||||
<button
|
||||
v-if="action"
|
||||
@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]">
|
||||
<component :is="action.icon" :size="20" :color="action.color" class="group-active:text-[#f6b26b]" />
|
||||
<!-- Icon Container -->
|
||||
<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>
|
||||
<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 }}
|
||||
</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 bottom-0 left-0 w-1 h-1 bg-[#4a3b5e] group-hover:bg-[#f6b26b]" />
|
||||
</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>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
|
@ -92,55 +41,111 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import PixelFrame from './PixelFrame.vue';
|
||||
import {
|
||||
Sword, Shield, FlaskConical, Crown, Hand, Footprints, Shirt,
|
||||
Utensils, Gamepad2, Dumbbell, Puzzle, Brush, Pill, Sun, Sparkles, ShoppingBag, Swords
|
||||
} from 'lucide-vue-next';
|
||||
import type { EntityStats } from '~/types/pixel';
|
||||
|
||||
type EntityStats = any;
|
||||
|
||||
interface Props {
|
||||
playerStats?: EntityStats;
|
||||
}
|
||||
|
||||
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 = [
|
||||
{ id: 'feed', icon: Utensils, color: '#9fd75b', label: 'FEED 餵食' },
|
||||
{ id: 'play', icon: Gamepad2, color: '#f6b26b', label: 'PLAY 玩耍' },
|
||||
{ id: 'train', icon: Dumbbell, color: '#d75b5b', label: 'TRAIN 訓練' },
|
||||
{ id: 'puzzle', icon: Puzzle, color: '#2ce8f4', label: 'PUZZLE 益智' },
|
||||
{ id: 'clean', icon: Brush, color: '#8f80a0', label: 'CLEAN 清理' },
|
||||
{ id: 'heal', icon: Pill, color: '#9fd75b', label: 'HEAL 治療' },
|
||||
{ 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: 'shop', icon: ShoppingBag, color: '#ffa500', label: 'SHOP 商店' },
|
||||
const BASE_ACTIONS = [
|
||||
{
|
||||
id: 'feed',
|
||||
label: '餵食',
|
||||
color: '#9fd75b',
|
||||
pixelPath: 'M4 1H6V3H4V1ZM3 3H7V4H3V3ZM2 4H8V8H2V4ZM3 8H7V9H3V8Z',
|
||||
pixelPath2: 'M4 2H5V3H4V2ZM6 2H7V3H6V2Z', color2: '#e0d8f0', opacity2: 0.5
|
||||
},
|
||||
{
|
||||
id: 'play',
|
||||
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(() => {
|
||||
return Array.from({ length: 12 }).map((_, i) => ACTIONS[i] || null);
|
||||
});
|
||||
const displayActions = computed(() => {
|
||||
const actions = [...BASE_ACTIONS];
|
||||
const isSleeping = props.playerStats?.isSleeping;
|
||||
|
||||
const handCards = [
|
||||
{ name: 'Slash', cost: 2, icon: Sword, color: '#d75b5b' },
|
||||
{ name: 'Block', cost: 1, icon: Shield, color: '#f6b26b' },
|
||||
{ name: 'Heal', cost: 3, icon: FlaskConical, color: '#9fd75b' }
|
||||
];
|
||||
const sleepAction = isSleeping
|
||||
? {
|
||||
id: 'wake',
|
||||
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(() => {
|
||||
const s = props.playerStats || { str:0, int:0, dex:0, luck:0, atk:0, def:0, spd:0 };
|
||||
return [
|
||||
{l:'STR', v:s.str}, {l:'ATK', v:s.atk, c: '#d75b5b'},
|
||||
{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},
|
||||
];
|
||||
// Insert sleep action before 'pray' (index 7)
|
||||
actions.splice(7, 0, sleepAction);
|
||||
|
||||
return actions;
|
||||
});
|
||||
|
||||
const handleActionClick = (id: string) => {
|
||||
if (id === 'pray') emit('openGodSystem');
|
||||
else if (id === 'shop') emit('openShop');
|
||||
// Emit specific events for each action type
|
||||
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 === 'sleep' || id === 'wake') emit('toggleSleep');
|
||||
else if (id === 'pray') emit('openGodSystem');
|
||||
else if (id === 'shop') emit('openShop');
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -74,8 +74,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Map, Drumstick, Coins, X } from 'lucide-vue-next';
|
||||
import type { AdventureLocation, EntityStats } from '~/types/pixel';
|
||||
import { Map, Drumstick, Coins, X, Swords } from 'lucide-vue-next';
|
||||
import PixelFrame from './PixelFrame.vue';
|
||||
import PixelButton from './PixelButton.vue';
|
||||
|
||||
type AdventureLocation = any;
|
||||
type EntityStats = any;
|
||||
|
||||
interface Props {
|
||||
locations: AdventureLocation[];
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import PixelAvatar from './PixelAvatar.vue';
|
||||
import type { DeityId } from '~/types/pixel';
|
||||
|
||||
interface Props {
|
||||
currentDeityId?: string;
|
||||
|
|
|
|||
|
|
@ -225,18 +225,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2 } from 'lucide-vue-next';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Heart, Sparkles, Scroll, Repeat, CheckCircle2, Gift } from 'lucide-vue-next';
|
||||
import PixelButton from './PixelButton.vue';
|
||||
import PixelAvatar from './PixelAvatar.vue';
|
||||
import PixelFrame from './PixelFrame.vue';
|
||||
import JiaobeiBlocks from './JiaobeiBlocks.vue';
|
||||
import { DeityId, JiaobeiResult, LotPhase } from '~/types/pixel';
|
||||
import type { Deity } from '~/types/pixel';
|
||||
|
||||
type Deity = any;
|
||||
|
||||
interface Props {
|
||||
currentDeity: DeityId;
|
||||
deities: Record<DeityId, Deity>;
|
||||
currentDeity: string;
|
||||
deities: Record<string, Deity>;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
|
@ -244,8 +244,25 @@ defineEmits(['switchDeity', 'addFavor']);
|
|||
|
||||
const activeTab = ref('PRAY');
|
||||
const isTossing = ref(false);
|
||||
const lastResult = ref<JiaobeiResult | null>(null);
|
||||
const lotPhase = ref<LotPhase>(LotPhase.Idle);
|
||||
const lastResult = ref<string | null>(null);
|
||||
|
||||
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 saintCupCount = ref(0);
|
||||
|
||||
|
|
@ -273,7 +290,7 @@ const LOT_RESULT_DATA = {
|
|||
|
||||
const activeDeity = computed(() => props.deities[props.currentDeity]);
|
||||
|
||||
const calculateToss = (): JiaobeiResult => {
|
||||
const calculateToss = (): string => {
|
||||
const rand = Math.random();
|
||||
if (rand < 0.5) return JiaobeiResult.Saint;
|
||||
if (rand < 0.75) return JiaobeiResult.Smile;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
|
||||
|
||||
<!-- 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="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 -->
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
:current="deity.favor"
|
||||
:max="deity.maxFavor"
|
||||
type="energy"
|
||||
label="Favor (好感度)"
|
||||
label="好感度"
|
||||
:icon="Heart"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -38,15 +38,15 @@
|
|||
<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">
|
||||
<Sparkles :size="14" />
|
||||
<span class="text-xs font-bold uppercase">Blessing</span>
|
||||
<span class="text-xs font-bold uppercase">神恩</span>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-[#e0d8f0] italic leading-relaxed">
|
||||
"{{ deity.description }}"
|
||||
「{{ deity.description }}」
|
||||
</p>
|
||||
|
||||
<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]">
|
||||
{{ currentEffect }}
|
||||
</span>
|
||||
|
|
@ -63,7 +63,8 @@ import { Heart, Sparkles } from 'lucide-vue-next';
|
|||
import PixelFrame from './PixelFrame.vue';
|
||||
import RetroResourceBar from './RetroResourceBar.vue';
|
||||
import PixelAvatar from './PixelAvatar.vue';
|
||||
import type { Deity } from '~/types/pixel';
|
||||
|
||||
type Deity = any;
|
||||
|
||||
interface Props {
|
||||
deity: Deity;
|
||||
|
|
|
|||
|
|
@ -169,11 +169,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 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 {
|
||||
items: Item[];
|
||||
|
|
@ -186,7 +189,33 @@ const selectedItemId = ref<string | null>(null);
|
|||
|
||||
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.Excellent]: '#9fd75b', // Green
|
||||
[Rarity.Rare]: '#2ce8f4', // Blue
|
||||
|
|
@ -194,7 +223,7 @@ const RARITY_COLORS: Record<Rarity, string> = {
|
|||
[Rarity.Legendary]: '#ffa500', // Orange
|
||||
};
|
||||
|
||||
const SLOT_ICONS: Record<EquipSlot, any> = {
|
||||
const SLOT_ICONS: Record<string, any> = {
|
||||
[EquipSlot.Weapon]: Sword,
|
||||
[EquipSlot.Armor]: Shield,
|
||||
[EquipSlot.Hat]: Crown,
|
||||
|
|
@ -203,7 +232,7 @@ const SLOT_ICONS: Record<EquipSlot, any> = {
|
|||
[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);
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { JiaobeiResult } from '~/types/pixel';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
result: JiaobeiResult | null;
|
||||
result: string | null;
|
||||
isTossing: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,25 +1,32 @@
|
|||
<template>
|
||||
<div class="h-full flex flex-col p-4 gap-4 bg-[#1b1026] overflow-y-auto custom-scrollbar">
|
||||
<!-- Pet Avatar -->Portrait & Basic Info -->
|
||||
<PixelFrame class="flex-shrink-0" title="PET INFO">
|
||||
<!-- Pet Avatar -->
|
||||
<PixelFrame class="flex-shrink-0" title="寵物資訊">
|
||||
<!-- 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
|
||||
@click="$emit('openAchievements')"
|
||||
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" />
|
||||
</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 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 -->
|
||||
<div class="absolute inset-0 bg-[#2b193f] opacity-50" />
|
||||
<div class="absolute inset-0 bg-[#2b193f] opacity-50 overflow-hidden" />
|
||||
|
||||
<!-- The Animated Pixel Avatar -->
|
||||
<div class="scale-110 transform translate-y-1">
|
||||
<div class="scale-110 transform translate-y-1 relative z-10">
|
||||
<PixelAvatar
|
||||
skinColor="#ffdbac"
|
||||
hairColor="#e0d8f0"
|
||||
|
|
@ -28,84 +35,413 @@
|
|||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
</PixelFrame>
|
||||
|
||||
<!-- Vitals - Updated to Health, Hunger, Happiness -->
|
||||
<div class="flex flex-col gap-1 px-1">
|
||||
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" label="Health" :icon="Heart" />
|
||||
<RetroResourceBar v-if="stats.hunger !== undefined" :current="stats.hunger" :max="stats.maxHunger || 100" type="energy" label="Hunger" :icon="Drumstick" />
|
||||
<RetroResourceBar v-if="stats.happiness !== undefined" :current="stats.happiness" :max="stats.maxHappiness || 100" type="mana" label="Happy" :icon="Smile" />
|
||||
<div class="flex flex-col gap-2 px-1 mb-2">
|
||||
<!-- Health (Heart) -->
|
||||
<RetroResourceBar :current="stats.hp" :max="stats.maxHp" type="hp" 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="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>
|
||||
|
||||
<!-- Pet Details Grid -->
|
||||
<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 border-r border-[#4a3b5e] pr-1">
|
||||
<span class="text-[#4a3b5e]">Age</span>
|
||||
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.age }}</span>
|
||||
<div class="flex flex-col gap-2 p-1">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-[10px] uppercase text-[#8f80a0]">
|
||||
<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 class="flex flex-col pl-1">
|
||||
<span class="text-[#4a3b5e]">Gen</span>
|
||||
<span class="text-[#e0d8f0] font-mono tracking-wide">{{ stats.generation }}</span>
|
||||
<div class="flex justify-between items-center px-1">
|
||||
<span class="text-[10px] text-[#2ce8f4] font-mono">智力</span>
|
||||
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.int || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 border-t border-[#4a3b5e] pt-1 col-span-2">
|
||||
<Ruler :size="10" />
|
||||
<span class="text-[#e0d8f0]">{{ stats.height }}</span>
|
||||
<span class="text-[#4a3b5e] mx-1">|</span>
|
||||
<Scale :size="10" />
|
||||
<span class="text-[#e0d8f0]">{{ stats.weight }}</span>
|
||||
<div class="flex justify-between items-center px-1">
|
||||
<span class="text-[10px] text-[#99e550] font-mono">敏捷</span>
|
||||
<span class="text-[10px] text-[#e0d8f0] font-mono">{{ Math.floor(stats.dex || 0) }}</span>
|
||||
</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>
|
||||
</PixelFrame>
|
||||
|
||||
<!-- Fate & God Favor -->
|
||||
<div class="flex flex-col gap-2 mt-2 px-1">
|
||||
<!-- Fate -->
|
||||
<div v-if="stats.fate" class="flex items-center gap-2 bg-[#2b193f] p-1 border border-[#4a3b5e] rounded">
|
||||
<Leaf :size="12" color="#99e550" />
|
||||
<div class="flex flex-col leading-none">
|
||||
<span class="text-[8px] text-[#8f80a0] uppercase">Fate</span>
|
||||
<span class="text-[10px] text-[#e0d8f0] tracking-wide">{{ stats.fate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fate Display (Enhanced) -->
|
||||
<div v-if="fateInfo" class="flex flex-col gap-2 mt-2 mb-6 px-1 flex-shrink-0">
|
||||
<PixelFrame class="relative overflow-visible" variant="inset" title="命運">
|
||||
<!-- Tier Badge -->
|
||||
<div class="absolute -top-2 -right-2 z-10">
|
||||
<div
|
||||
class="px-2 py-0.5 text-[8px] font-bold font-mono border-2 shadow-lg"
|
||||
:style="{
|
||||
backgroundColor: fateInfo.color,
|
||||
borderColor: fateInfo.color,
|
||||
color: '#000',
|
||||
boxShadow: `0 0 10px ${fateInfo.color}, 0 0 20px ${fateInfo.color}40`
|
||||
}"
|
||||
>
|
||||
{{ fateInfo.tierName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- God Favor -->
|
||||
<div v-if="stats.godFavor" class="flex flex-col gap-1">
|
||||
<div class="flex justify-between text-[10px] text-[#8f80a0] uppercase">
|
||||
<span>Favor: {{ stats.godFavor.name }}</span>
|
||||
<span>{{ stats.godFavor.current }}/{{ stats.godFavor.max }}</span>
|
||||
</div>
|
||||
<div class="h-2 bg-[#150c1f] border border-[#4a3b5e] rounded-full overflow-hidden">
|
||||
<div :style="{ width: `${(stats.godFavor.current / stats.godFavor.max) * 100}%` }" class="h-full bg-[#f6b26b]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 flex gap-2">
|
||||
<!-- Pixel Icon with Glow -->
|
||||
<div
|
||||
class="w-12 h-12 flex-shrink-0 bg-[#0f0816] border-2 flex items-center justify-center relative overflow-hidden"
|
||||
:style="{
|
||||
borderColor: fateInfo.color,
|
||||
boxShadow: `inset 0 0 10px ${fateInfo.color}40, 0 0 15px ${fateInfo.color}60`
|
||||
}"
|
||||
>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Gold -->
|
||||
<div class="mt-auto px-1 pb-1">
|
||||
<RetroCounter :icon="Coins" :value="stats.gold || 0" color="#ffe762" />
|
||||
<!-- Gold & Inventory -->
|
||||
<div class="mt-auto px-1 pb-4 flex-shrink-0 flex gap-2">
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<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 RetroResourceBar from './RetroResourceBar.vue';
|
||||
import RetroCounter from './RetroCounter.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 {
|
||||
stats: EntityStats;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
defineEmits(['openAchievements']);
|
||||
const props = defineProps<Props>();
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<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">
|
||||
<span v-if="label" class="text-[8px] text-[#8f80a0] uppercase">{{ label }}</span>
|
||||
<span class="font-mono text-sm font-bold text-[#e0d8f0]">{{ value }}</span>
|
||||
|
|
@ -12,7 +16,7 @@
|
|||
import type { Component } from 'vue';
|
||||
|
||||
interface Props {
|
||||
icon: Component;
|
||||
icon?: Component;
|
||||
value: number | string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,47 @@
|
|||
<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 class="flex items-center gap-1 text-[#8f80a0]">
|
||||
<component :is="icon" v-if="icon" :size="10" />
|
||||
<span class="text-[10px] uppercase font-bold leading-none">{{ label }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Pixel Icon -->
|
||||
<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>
|
||||
<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 class="h-3 bg-[#0f0816] border border-[#4a3b5e] p-[1px] relative">
|
||||
<div
|
||||
class="h-full transition-all duration-300 relative"
|
||||
:style="{ width: `${percentage}%`, backgroundColor: barColor }"
|
||||
>
|
||||
<!-- Shine effect -->
|
||||
<div class="absolute top-0 left-0 w-full h-[1px] bg-white opacity-30" />
|
||||
|
||||
<!-- Segmented Bar Container -->
|
||||
<div class="h-4 bg-[#1b1026] border-2 border-[#1b1026] relative p-[2px] shadow-[0_0_0_1px_#4a3b5e]">
|
||||
<!-- Background (Empty segments) -->
|
||||
<div class="absolute inset-[2px] flex gap-[2px]">
|
||||
<div v-for="i in segments" :key="`bg-${i}`" class="flex-1 bg-[#2b193f]"></div>
|
||||
</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>
|
||||
|
|
@ -21,23 +49,34 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
interface Props {
|
||||
current: number;
|
||||
max: number;
|
||||
type: 'hp' | 'energy' | 'mana';
|
||||
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)));
|
||||
|
||||
// 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(() => {
|
||||
if (props.type === 'energy') return '#f6b26b'; // Orange
|
||||
if (props.type === 'mana') return '#2ce8f4'; // Cyan
|
||||
return '#d95763'; // HP Red (default)
|
||||
if (props.type === 'energy') return '#f6b26b'; // Orange (Star)
|
||||
if (props.type === 'mana') return '#99e550'; // Green (Potion) - Changed from Cyan
|
||||
return '#d95763'; // Red (Heart)
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -108,10 +108,13 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 { ItemCategory, Rarity } from '~/types/pixel';
|
||||
import type { Item } from '~/types/pixel';
|
||||
import PixelFrame from './PixelFrame.vue';
|
||||
|
||||
import { ITEM_CATEGORY as DATA_CATEGORIES, ITEM_TYPE } from '../../../../data/items.js';
|
||||
|
||||
type Item = any;
|
||||
|
||||
interface Props {
|
||||
playerGold: number;
|
||||
|
|
@ -125,6 +128,19 @@ defineEmits(['buy', 'sell']);
|
|||
const mode = ref<'BUY' | 'SELL'>('BUY');
|
||||
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 = [
|
||||
{ id: 'ALL', label: '全部 (ALL)', icon: Search },
|
||||
{ id: ItemCategory.Food, label: '食物', icon: Cookie },
|
||||
|
|
@ -139,6 +155,15 @@ const displayedItems = computed(() => {
|
|||
return source.filter(item => {
|
||||
if (mode.value === 'SELL' && item.isEquipped) return false; // Cannot sell equipped items
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<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">
|
||||
<!-- 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}">
|
||||
<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">
|
||||
|
||||
<!-- 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">
|
||||
|
|
@ -10,6 +9,8 @@
|
|||
v-if="initialized"
|
||||
:stats="playerStats"
|
||||
@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">
|
||||
Initializing...
|
||||
|
|
@ -34,10 +35,17 @@
|
|||
<ActionArea
|
||||
v-if="initialized"
|
||||
:playerStats="playerStats"
|
||||
@feed="handleFeed"
|
||||
@play="handlePlay"
|
||||
@train="handleTrain"
|
||||
@puzzle="handlePuzzle"
|
||||
@clean="handleClean"
|
||||
@heal="handleHeal"
|
||||
@openInventory="showInventory = true"
|
||||
@openGodSystem="showGodSystem = true"
|
||||
@openShop="showShop = true"
|
||||
@openAdventure="showAdventureSelect = true"
|
||||
@toggleSleep="handleToggleSleep"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,7 +66,7 @@
|
|||
<PixelModal
|
||||
:isOpen="showAchievements"
|
||||
@close="showAchievements = false"
|
||||
title="ACHIEVEMENTS"
|
||||
title="成就"
|
||||
>
|
||||
<AchievementsOverlay :achievements="ACHIEVEMENTS_DATA" />
|
||||
</PixelModal>
|
||||
|
|
@ -67,7 +75,7 @@
|
|||
<PixelModal
|
||||
:isOpen="showInventory"
|
||||
@close="showInventory = false"
|
||||
title="INVENTORY"
|
||||
title="背包"
|
||||
>
|
||||
<InventoryOverlay
|
||||
:items="inventory"
|
||||
|
|
@ -82,7 +90,7 @@
|
|||
<PixelModal
|
||||
:isOpen="showGodSystem"
|
||||
@close="showGodSystem = false"
|
||||
title="GOD SYSTEM"
|
||||
title="神明系統"
|
||||
>
|
||||
<GodSystemOverlay
|
||||
:currentDeity="currentDeity"
|
||||
|
|
@ -96,7 +104,7 @@
|
|||
<PixelModal
|
||||
:isOpen="showShop"
|
||||
@close="showShop = false"
|
||||
title="SHOP"
|
||||
title="商店"
|
||||
>
|
||||
<ShopOverlay
|
||||
:playerGold="playerStats.gold || 0"
|
||||
|
|
@ -111,7 +119,7 @@
|
|||
<PixelModal
|
||||
:isOpen="showAdventureSelect"
|
||||
@close="showAdventureSelect = false"
|
||||
title="ADVENTURE"
|
||||
title="冒險"
|
||||
>
|
||||
<AdventureOverlay
|
||||
:locations="ADVENTURE_LOCATIONS"
|
||||
|
|
@ -131,7 +139,7 @@
|
|||
<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"
|
||||
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black tracking-widest"
|
||||
>
|
||||
確定
|
||||
</button>
|
||||
|
|
@ -139,6 +147,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Naming Overlay -->
|
||||
<NamingOverlay
|
||||
v-if="showNamingOverlay"
|
||||
@submit="handleNameSubmit"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -160,24 +174,22 @@ 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';
|
||||
// Import from data and core instead of types
|
||||
import { DEITIES } from '../../data/deities.js';
|
||||
import { ITEMS, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../data/items.js';
|
||||
import { ADVENTURES } from '../../data/adventures.js';
|
||||
import { ACHIEVEMENTS } from '../../data/achievements.js';
|
||||
|
||||
// Type definitions (minimal, based on actual data structures)
|
||||
type EntityStats = any;
|
||||
import NamingOverlay from '~/components/pixel/NamingOverlay.vue';
|
||||
type Deity = typeof DEITIES[0];
|
||||
type Item = typeof ITEMS[keyof typeof ITEMS];
|
||||
type AdventureLocation = typeof ADVENTURES[0];
|
||||
|
||||
// --- 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 templeSystem = ref<TempleSystem | null>(null);
|
||||
const initialized = ref(false);
|
||||
|
|
@ -215,8 +227,12 @@ const playerStats = computed<EntityStats>(() => {
|
|||
const s = systemState.value;
|
||||
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 {
|
||||
name: "Pet",
|
||||
name: s.name || "Pet",
|
||||
class: s.stage,
|
||||
hp: Math.floor(s.health),
|
||||
maxHp: 100,
|
||||
|
|
@ -224,12 +240,12 @@ const playerStats = computed<EntityStats>(() => {
|
|||
maxSp: 100,
|
||||
lvl: 1,
|
||||
|
||||
hunger: Math.floor(s.hunger),
|
||||
hunger: Math.floor(s.hunger || 0),
|
||||
maxHunger: 100,
|
||||
happiness: Math.floor(s.happiness),
|
||||
happiness: Math.floor(s.happiness ?? 100),
|
||||
maxHappiness: 100,
|
||||
|
||||
age: formatAge(s.ageSeconds),
|
||||
age: formatAge(currentAge),
|
||||
generation: s.generation || 1,
|
||||
height: `${s.height || 0} cm`,
|
||||
weight: `${Math.floor(s.weight || 0)} g`,
|
||||
|
|
@ -248,7 +264,13 @@ const playerStats = computed<EntityStats>(() => {
|
|||
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)
|
||||
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> = {};
|
||||
allDeities.value.forEach(d => {
|
||||
const favor = systemState.value?.deityFavors?.[d.id] || 0;
|
||||
|
|
@ -270,7 +292,7 @@ const deities = computed<Record<DeityId, Deity>>(() => {
|
|||
return map;
|
||||
});
|
||||
|
||||
const currentDeity = computed(() => systemState.value?.currentDeityId || DeityId.Mazu);
|
||||
const currentDeity = computed(() => systemState.value?.currentDeityId || 'mazu');
|
||||
|
||||
// Modal States
|
||||
const showAchievements = ref(false);
|
||||
|
|
@ -283,40 +305,103 @@ const showBattleResult = ref(false);
|
|||
// Battle State
|
||||
const isFighting = ref(false);
|
||||
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 ---
|
||||
|
||||
const stateSyncInterval = ref<any>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
petSystem.value = new PetSystem(apiService);
|
||||
templeSystem.value = new TempleSystem(petSystem.value, apiService);
|
||||
// Initialize Systems
|
||||
petSystem.value = new PetSystem(apiService);
|
||||
templeSystem.value = new TempleSystem(apiService, petSystem.value);
|
||||
|
||||
await petSystem.value.initialize();
|
||||
await templeSystem.value.initialize();
|
||||
// Load Data
|
||||
console.log("Initializing PetSystem...");
|
||||
const state = await petSystem.value.initialize();
|
||||
console.log("Initial State:", state);
|
||||
systemState.value = state;
|
||||
|
||||
systemState.value = petSystem.value.getState();
|
||||
allDeities.value = templeSystem.value.getDeities();
|
||||
// Check if naming is required
|
||||
if (!state.name) {
|
||||
showNamingOverlay.value = true;
|
||||
}
|
||||
|
||||
petSystem.value.startTickLoop((newState) => {
|
||||
systemState.value = newState;
|
||||
});
|
||||
// Load Deities
|
||||
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(() => {
|
||||
if (petSystem.value) {
|
||||
petSystem.value.stopTickLoop();
|
||||
}
|
||||
// Clear intervals first
|
||||
if (stateSyncInterval.value) {
|
||||
clearInterval(stateSyncInterval.value);
|
||||
stateSyncInterval.value = null;
|
||||
}
|
||||
|
||||
// Stop tick loop
|
||||
if (petSystem.value) {
|
||||
petSystem.value.stopTickLoop();
|
||||
petSystem.value = null;
|
||||
}
|
||||
if (templeSystem.value) {
|
||||
templeSystem.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// --- HELPERS ---
|
||||
|
||||
const formatAge = (seconds: number) => {
|
||||
if (!seconds) return '0h';
|
||||
if (!seconds) return '0s';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
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`;
|
||||
return `${hours}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
if (minutes > 0) return `${minutes}m ${secs}s`;
|
||||
return `${secs}s`;
|
||||
};
|
||||
|
||||
// --- HANDLERS ---
|
||||
|
|
@ -360,6 +445,76 @@ const handleCloseBattleResult = () => {
|
|||
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) => {
|
||||
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);
|
||||
};
|
||||
|
||||
const handleSwitchDeity = async (id: DeityId) => {
|
||||
const handleSwitchDeity = async (id: string) => {
|
||||
if (templeSystem.value) {
|
||||
await templeSystem.value.switchDeity(id);
|
||||
systemState.value = petSystem.value?.getState();
|
||||
|
|
@ -403,7 +558,7 @@ const handleBuyItem = async (item: Item) => {
|
|||
|
||||
const handleSellItem = async (item: Item) => {
|
||||
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 newInventory = systemState.value.inventory.filter((i: any) => i.id !== item.id);
|
||||
await petSystem.value.updateState({ coins: newCoins, inventory: newInventory });
|
||||
|
|
@ -411,25 +566,15 @@ const handleSellItem = async (item: Item) => {
|
|||
}
|
||||
};
|
||||
|
||||
// --- 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: '樹妖' }
|
||||
];
|
||||
// Use imported data instead of mock
|
||||
const ADVENTURE_LOCATIONS = ADVENTURES;
|
||||
const ACHIEVEMENTS_DATA = ACHIEVEMENTS;
|
||||
|
||||
// Convert ITEMS object to array for shop
|
||||
const SHOP_ITEMS = Object.values(ITEMS).filter((item: any) => item.type === 'consumable' || item.type === 'equipment').map((item: any) => ({
|
||||
...item,
|
||||
statsDescription: item.description
|
||||
}));
|
||||
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
375
console-demo.js
375
console-demo.js
|
|
@ -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(' - 所有數值操作都會同步到 API(mock 模式使用 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
|
||||
}
|
||||
|
||||
|
|
@ -204,9 +204,19 @@ export class ApiService {
|
|||
|
||||
getMockPetState() {
|
||||
// 從 localStorage 或預設值讀取
|
||||
if (typeof localStorage === 'undefined') {
|
||||
console.warn('[ApiService] localStorage is not available');
|
||||
return null;
|
||||
}
|
||||
const stored = localStorage.getItem('petState')
|
||||
console.log('[ApiService] getMockPetState:', stored ? 'Found' : 'Not Found');
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,11 +65,13 @@ export class PetSystem {
|
|||
const config = PET_SPECIES[speciesId]
|
||||
const state = {
|
||||
speciesId,
|
||||
name: null, // 寵物名稱,初始為 null
|
||||
stage: 'egg',
|
||||
hunger: 100,
|
||||
happiness: 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,
|
||||
poopCount: 0,
|
||||
str: 10,
|
||||
|
|
@ -351,7 +353,24 @@ export class PetSystem {
|
|||
|
||||
// 獲取當前狀態
|
||||
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
|
||||
|
||||
// 立即執行一次回調,確保 UI 獲得最新狀態
|
||||
if (callback) callback(this.getState())
|
||||
|
||||
this.tickInterval = setInterval(async () => {
|
||||
await this.tick()
|
||||
if (callback) callback(this.getState())
|
||||
|
|
@ -678,6 +700,18 @@ export class PetSystem {
|
|||
|
||||
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) {
|
||||
console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`)
|
||||
|
|
|
|||
|
|
@ -397,3 +397,24 @@ export const EQUIPMENT_SLOTS = {
|
|||
talisman: { 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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ export const PET_SPECIES = {
|
|||
description: '活潑可愛的小貓咪',
|
||||
baseStats: {
|
||||
// 系統更新間隔(毫秒)
|
||||
physiologyTickInterval: 10000, // 生理系統刷新間隔:30秒
|
||||
physiologyTickInterval: 1000, // 生理系統刷新間隔:1秒 (更流暢的視覺效果)
|
||||
eventCheckInterval: 10000, // 事件檢查間隔:10秒
|
||||
|
||||
// 衰減速率 (每 tick 60秒)
|
||||
// 調整為更輕鬆:飢餓 8 小時,快樂 5 小時
|
||||
hungerDecayPerTick: 0.2, // 原 0.28 → 0.2 (更慢)
|
||||
happinessDecayPerTick: 0.33, // 原 0.42 → 0.33 (更慢)
|
||||
// 衰減速率 (每 tick 1秒)
|
||||
// 調整為:飢餓 8 小時,快樂 5 小時 (數值除以 10)
|
||||
hungerDecayPerTick: 0.02, // 原 0.2 (10秒) → 0.02 (1秒)
|
||||
happinessDecayPerTick: 0.033, // 原 0.33 (10秒) → 0.033 (1秒)
|
||||
|
||||
// 便便系統
|
||||
poopChancePerTick: 0.05, // 約 20 分鐘產生一次
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ export default defineNuxtConfig({
|
|||
app: {
|
||||
head: {
|
||||
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: [
|
||||
{ name: 'mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 548 KiB |
|
|
@ -15,6 +15,10 @@ export default <Config>{
|
|||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Cubic 11', 'ui-sans-serif', 'system-ui'],
|
||||
mono: ['Cubic 11', 'ui-monospace', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
// Pixel Dungeon Palette
|
||||
pixel: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue