feat: init
This commit is contained in:
commit
d0e1bb91de
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>temp-vue-app</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,858 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-Hant">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>電子寵物 - 頭後方 ZZZ / 💀 + 墓碑</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
background: radial-gradient(circle at top, #ffe8c7, #f5c7c7);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
/* 外殼 */
|
||||
.device {
|
||||
width: 280px;
|
||||
height: 340px;
|
||||
border-radius: 60% 60% 55% 55%;
|
||||
background: radial-gradient(circle at 30% 0%, #ffe7ff, #ffc6f1);
|
||||
box-shadow:
|
||||
0 18px 40px rgba(0, 0, 0, 0.35),
|
||||
inset 0 4px 10px rgba(255, 255, 255, 0.8);
|
||||
padding-top: 38px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.device-title {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.screen-wrapper {
|
||||
width: 210px;
|
||||
height: 160px;
|
||||
border-radius: 18px;
|
||||
background: #888;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
background: #c3e4aa;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 0 0 2px rgba(0, 0, 0, 0.25),
|
||||
inset 0 0 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* LCD 格線 */
|
||||
.screen::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
|
||||
background-size: 3px 3px;
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 20%, #fff8e7, #f3a3af);
|
||||
box-shadow:
|
||||
0 4px 0 #c26c7a,
|
||||
0 4px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* 狀態切換按鈕(Demo 用) */
|
||||
.state-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.state-controls button {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #ffffffaa;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.state-controls button.active {
|
||||
background: #ffb3c6;
|
||||
}
|
||||
|
||||
/* ---------------- 通用寵物 sprite ---------------- */
|
||||
|
||||
.pet-root {
|
||||
position: absolute;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.pet-inner {
|
||||
position: relative;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.pet-inner.face-right {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.pet-inner.face-left {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.pet-pixel {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* ===== 狀態:Idle / Sleep / Sick / Dead(控制小雞) ===== */
|
||||
|
||||
.state-idle .pet-root {
|
||||
animation:
|
||||
pet-breathe-idle 2s ease-in-out infinite,
|
||||
pet-head-tilt 6s ease-in-out infinite;
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.state-sleep .pet-root {
|
||||
animation: pet-breathe-sleep 4s ease-in-out infinite;
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.state-sick .pet-root {
|
||||
animation:
|
||||
pet-breathe-idle 2s ease-in-out infinite,
|
||||
pet-sick-shake 0.6s ease-in-out infinite;
|
||||
filter: brightness(0.8) saturate(0.9);
|
||||
opacity: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Dead:小雞整隻消失,由墓碑取代 */
|
||||
.state-dead .pet-root {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes pet-breathe-idle {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pet-breathe-sleep {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.015);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pet-head-tilt {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(-2deg) scale(1.02);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(2deg) scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pet-sick-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 尾巴 ===== */
|
||||
|
||||
.tail-pixel {
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.state-idle .tail-pixel {
|
||||
animation: tail-wag-idle 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sleep .tail-pixel {
|
||||
animation: tail-wag-sleep 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sick .tail-pixel {
|
||||
animation: tail-wag-sick 0.7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-dead .tail-pixel {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes tail-wag-idle {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tail-wag-sleep {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(0.5px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tail-wag-sick {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(0.8px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 腳 ===== */
|
||||
|
||||
.leg-front,
|
||||
.leg-back {
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.state-idle .leg-front {
|
||||
animation: leg-front-step 0.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-idle .leg-back {
|
||||
animation: leg-back-step 0.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sick .leg-front,
|
||||
.state-sick .leg-back {
|
||||
animation: leg-sick-step 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sleep .leg-front,
|
||||
.state-sleep .leg-back,
|
||||
.state-dead .leg-front,
|
||||
.state-dead .leg-back {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes leg-front-step {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leg-back-step {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leg-sick-step {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 耳朵 ===== */
|
||||
|
||||
.ear-pixel {
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.state-idle .ear-pixel {
|
||||
animation: ear-twitch 3.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sick .ear-pixel {
|
||||
animation: ear-twitch 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sleep .ear-pixel,
|
||||
.state-dead .ear-pixel {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes ear-twitch {
|
||||
|
||||
0%,
|
||||
90%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
92% {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
96% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 腮紅 ===== */
|
||||
|
||||
.blush-dot {
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.state-idle .blush-dot,
|
||||
.state-sick .blush-dot {
|
||||
animation: blush-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sleep .blush-dot {
|
||||
animation: blush-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-dead .blush-dot {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes blush-pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Sleep ZZZ(跟著頭後方跑) ===== */
|
||||
|
||||
.sleep-zzz {
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
color: #5a7a4a;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sleep-zzz span {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
opacity: 0;
|
||||
animation: zzz-float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sleep-zzz .z1 {
|
||||
left: 0;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.sleep-zzz .z2 {
|
||||
left: 8px;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
.sleep-zzz .z3 {
|
||||
left: 15px;
|
||||
animation-delay: 1.6s;
|
||||
}
|
||||
|
||||
@keyframes zzz-float {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(0) scale(0.7);
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translateY(-4px) scale(0.9);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 1;
|
||||
transform: translateY(-16px) scale(1.05);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-24px) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.state-sleep .sleep-zzz {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.state-dead .sleep-zzz {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ===== Sick 骷髏頭(跟著頭後方跑) ===== */
|
||||
|
||||
.sick-icon {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.state-sick .sick-icon {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
animation: sick-icon-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.state-sleep .sick-icon,
|
||||
.state-dead .sick-icon {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes sick-icon-pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 墓碑 ===== */
|
||||
|
||||
.tombstone {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 52%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 48px;
|
||||
height: 56px;
|
||||
border-radius: 16px 16px 6px 6px;
|
||||
background: #9ba7a0;
|
||||
border: 2px solid #5e6861;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(0, 0, 0, 0.2),
|
||||
0 4px 4px rgba(0, 0, 0, 0.35);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tombstone::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
top: 16px;
|
||||
height: 2px;
|
||||
background: #5e6861;
|
||||
}
|
||||
|
||||
.tombstone::after {
|
||||
content: "RIP";
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 8px;
|
||||
letter-spacing: 1px;
|
||||
color: #39413c;
|
||||
}
|
||||
|
||||
.state-dead .tombstone {
|
||||
display: block;
|
||||
animation: tomb-float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes tomb-float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-50%, -50%) translateY(-4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="device">
|
||||
<div class="device-title">PET EGG</div>
|
||||
|
||||
<div class="screen-wrapper">
|
||||
<div class="screen" id="screen">
|
||||
<!-- 睡覺 ZZZ(位置由 JS 根據頭+朝向計算) -->
|
||||
<div class="sleep-zzz" id="sleepIcon">
|
||||
<span class="z1">Z</span>
|
||||
<span class="z2">Z</span>
|
||||
<span class="z3">Z</span>
|
||||
</div>
|
||||
<!-- 生病骷髏頭(位置由 JS 根據頭+朝向計算) -->
|
||||
<div class="sick-icon" id="sickIcon">💀</div>
|
||||
<!-- 死亡墓碑 -->
|
||||
<div class="tombstone"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="btn"></div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo 狀態切換 -->
|
||||
<div class="state-controls">
|
||||
<button data-state="idle" class="active">Idle</button>
|
||||
<button data-state="sleep">Sleep</button>
|
||||
<button data-state="sick">Sick</button>
|
||||
<button data-state="dead">Dead</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* =======================
|
||||
1. PRESET
|
||||
======================= */
|
||||
|
||||
const SPRITE_PRESETS = {
|
||||
tigerChick: {
|
||||
name: 'tigerChick',
|
||||
pixelSize: 3,
|
||||
sprite: [
|
||||
'1100000111000000',
|
||||
'1241111331000000',
|
||||
'1005102301000000',
|
||||
'1054103320000000',
|
||||
'1241143320100000',
|
||||
'1230432311100110',
|
||||
'1245321330100421',
|
||||
'1240001311100111',
|
||||
'1020103030111421',
|
||||
'0100000331245210',
|
||||
'0001111111240210',
|
||||
'0015022221345210',
|
||||
'0004022235350010',
|
||||
'0011400023203210',
|
||||
'0005011112115210',
|
||||
'0001100001100110',
|
||||
],
|
||||
palette: {
|
||||
'1': '#2b2825',
|
||||
'2': '#d0974b',
|
||||
'3': '#e09037',
|
||||
'4': '#4a2b0d',
|
||||
'5': '#724e22',
|
||||
},
|
||||
tailPixels: [
|
||||
[15, 8], [14, 8],
|
||||
[15, 9], [14, 9],
|
||||
[15, 10], [14, 10],
|
||||
[15, 11], [14, 11],
|
||||
],
|
||||
legFrontPixels: [
|
||||
[6, 13], [7, 13],
|
||||
[6, 14], [7, 14],
|
||||
],
|
||||
legBackPixels: [
|
||||
[9, 13], [10, 13],
|
||||
[9, 14], [10, 14],
|
||||
],
|
||||
earPixels: [
|
||||
[2, 0], [3, 0], [4, 0],
|
||||
[11, 0], [12, 0], [13, 0],
|
||||
],
|
||||
blushPixels: [
|
||||
[4, 7], [5, 7],
|
||||
[10, 7], [11, 7],
|
||||
],
|
||||
// 頭後方標記點(依朝向不同用不同座標)
|
||||
iconBackLeft: { x: 3, y: 2 }, // 面向右時:在左邊這個點附近(後腦勺)
|
||||
iconBackRight: { x: 12, y: 2 }, // 面向左時:在右邊這個點附近(後腦勺)
|
||||
},
|
||||
};
|
||||
|
||||
const CURRENT_PRESET = SPRITE_PRESETS.tigerChick;
|
||||
let currentState = 'idle';
|
||||
|
||||
/* =======================
|
||||
2. 建立 sprite
|
||||
======================= */
|
||||
|
||||
function createPetSprite(preset) {
|
||||
const {
|
||||
sprite, palette, pixelSize,
|
||||
tailPixels = [],
|
||||
legFrontPixels = [],
|
||||
legBackPixels = [],
|
||||
earPixels = [],
|
||||
blushPixels = [],
|
||||
} = preset;
|
||||
|
||||
const rows = sprite.length;
|
||||
const cols = sprite[0].length;
|
||||
const width = cols * pixelSize;
|
||||
const height = rows * pixelSize;
|
||||
|
||||
const rootEl = document.createElement('div');
|
||||
rootEl.className = 'pet-root';
|
||||
rootEl.style.width = width + 'px';
|
||||
rootEl.style.height = height + 'px';
|
||||
|
||||
const innerEl = document.createElement('div');
|
||||
innerEl.className = 'pet-inner face-right';
|
||||
rootEl.appendChild(innerEl);
|
||||
|
||||
sprite.forEach((row, y) => {
|
||||
[...row].forEach((ch, x) => {
|
||||
if (ch === '0') return;
|
||||
|
||||
const dot = document.createElement('div');
|
||||
|
||||
const isTail = tailPixels.some(([tx, ty]) => tx === x && ty === y);
|
||||
const isLegFront = legFrontPixels.some(([lx, ly]) => lx === x && ly === y);
|
||||
const isLegBack = legBackPixels.some(([lx, ly]) => lx === x && ly === y);
|
||||
const isEar = earPixels.some(([ex, ey]) => ex === x && ey === y);
|
||||
const isBlush = blushPixels.some(([bx, by]) => bx === x && by === y);
|
||||
|
||||
let className = 'pet-pixel';
|
||||
if (isTail) className += ' tail-pixel';
|
||||
if (isLegFront) className += ' leg-front';
|
||||
if (isLegBack) className += ' leg-back';
|
||||
if (isEar) className += ' ear-pixel';
|
||||
if (isBlush) className += ' blush-dot';
|
||||
|
||||
dot.className = className;
|
||||
dot.style.width = pixelSize + 'px';
|
||||
dot.style.height = pixelSize + 'px';
|
||||
dot.style.left = (x * pixelSize) + 'px';
|
||||
dot.style.top = (y * pixelSize) + 'px';
|
||||
dot.style.background = palette[ch] || '#000';
|
||||
|
||||
innerEl.appendChild(dot);
|
||||
});
|
||||
});
|
||||
|
||||
return { rootEl, innerEl, width, height };
|
||||
}
|
||||
|
||||
/* =======================
|
||||
3. 初始化 + 移動 + 頭後方 ICON 定位
|
||||
======================= */
|
||||
|
||||
const screenEl = document.getElementById('screen');
|
||||
const sleepIconEl = document.getElementById('sleepIcon');
|
||||
const sickIconEl = document.getElementById('sickIcon');
|
||||
|
||||
const { rootEl: petRoot, innerEl: petInner } = (function initPetOnScreen(preset, screenEl) {
|
||||
const { rootEl, innerEl, width, height } = createPetSprite(preset);
|
||||
screenEl.appendChild(rootEl);
|
||||
|
||||
const margin = 8;
|
||||
|
||||
let x = Math.floor(screenEl.clientWidth / 2 - width / 2);
|
||||
let y = Math.floor(screenEl.clientHeight / 2 - height / 2);
|
||||
rootEl.style.left = x + 'px';
|
||||
rootEl.style.top = y + 'px';
|
||||
|
||||
function updateHeadIconsPosition() {
|
||||
const { pixelSize, iconBackLeft, iconBackRight } = CURRENT_PRESET;
|
||||
const isFacingRight = innerEl.classList.contains('face-right');
|
||||
|
||||
// 面向右 → icon 用左邊的標記點;面向左 → icon 用右邊的標記點
|
||||
const marker = isFacingRight ? iconBackLeft : iconBackRight;
|
||||
|
||||
const baseX = rootEl.offsetLeft + marker.x * pixelSize;
|
||||
const baseY = rootEl.offsetTop + marker.y * pixelSize;
|
||||
|
||||
// ZZZ 稍微比頭高一點
|
||||
sleepIconEl.style.left = (baseX - 2) + 'px';
|
||||
sleepIconEl.style.top = (baseY - 10) + 'px';
|
||||
|
||||
// 骷髏頭稍微貼近頭後側一點
|
||||
sickIconEl.style.left = (baseX - 2) + 'px';
|
||||
sickIconEl.style.top = (baseY - 6) + 'px';
|
||||
}
|
||||
|
||||
function moveRandomly() {
|
||||
if (currentState === 'sleep' || currentState === 'dead') {
|
||||
updateHeadIconsPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
const step = preset.pixelSize * 2;
|
||||
const dir = Math.floor(Math.random() * 4);
|
||||
|
||||
const oldX = rootEl.offsetLeft;
|
||||
const oldY = rootEl.offsetTop;
|
||||
|
||||
let newX = oldX;
|
||||
let newY = oldY;
|
||||
|
||||
if (dir === 0) newY -= step;
|
||||
if (dir === 1) newY += step;
|
||||
if (dir === 2) newX -= step;
|
||||
if (dir === 3) newX += step;
|
||||
|
||||
const maxX = screenEl.clientWidth - width - margin;
|
||||
const maxY = screenEl.clientHeight - height - margin;
|
||||
const minX = margin;
|
||||
const minY = margin;
|
||||
|
||||
newX = Math.max(minX, Math.min(maxX, newX));
|
||||
newY = Math.max(minY, Math.min(maxY, newY));
|
||||
|
||||
rootEl.style.left = newX + 'px';
|
||||
rootEl.style.top = newY + 'px';
|
||||
|
||||
const dx = newX - oldX;
|
||||
if (dx > 0) {
|
||||
innerEl.classList.remove('face-right');
|
||||
innerEl.classList.add('face-left');
|
||||
} else if (dx < 0) {
|
||||
innerEl.classList.remove('face-left');
|
||||
innerEl.classList.add('face-right');
|
||||
}
|
||||
|
||||
updateHeadIconsPosition();
|
||||
}
|
||||
|
||||
// 初始定位一次
|
||||
updateHeadIconsPosition();
|
||||
// 每 600ms 移動/更新一次
|
||||
setInterval(moveRandomly, 600);
|
||||
|
||||
// 視窗尺寸變化時也重新對頭定位一次
|
||||
window.addEventListener('resize', updateHeadIconsPosition);
|
||||
|
||||
return { rootEl, innerEl };
|
||||
})(CURRENT_PRESET, screenEl);
|
||||
|
||||
/* =======================
|
||||
4. 狀態切換
|
||||
======================= */
|
||||
|
||||
function setState(newState) {
|
||||
currentState = newState;
|
||||
|
||||
screenEl.classList.remove('state-idle', 'state-sleep', 'state-sick', 'state-dead');
|
||||
screenEl.classList.add('state-' + newState);
|
||||
|
||||
document.querySelectorAll('.state-controls button').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.state === newState);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.state-controls button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
setState(btn.dataset.state);
|
||||
});
|
||||
});
|
||||
|
||||
setState('idle');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "temp-vue-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,129 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import DeviceShell from './components/DeviceShell.vue';
|
||||
import DeviceScreen from './components/DeviceScreen.vue';
|
||||
import PetGame from './components/PetGame.vue';
|
||||
import Menu from './components/Menu.vue';
|
||||
|
||||
const currentScreen = ref('game');
|
||||
const currentState = ref('idle');
|
||||
const currentStage = ref('adult'); // 'egg' or 'adult'
|
||||
const petGameRef = ref(null);
|
||||
|
||||
// State Management
|
||||
function setPetState(state) {
|
||||
currentState.value = state;
|
||||
}
|
||||
|
||||
function handleStateUpdate(newState) {
|
||||
currentState.value = newState;
|
||||
}
|
||||
|
||||
function toggleStage() {
|
||||
currentStage.value = currentStage.value === 'egg' ? 'adult' : 'egg';
|
||||
}
|
||||
|
||||
// Feeding Logic
|
||||
const stateBeforeFeeding = ref('idle');
|
||||
|
||||
function startFeeding() {
|
||||
if (currentState.value === 'sleep' || currentState.value === 'dead') return;
|
||||
|
||||
if (currentState.value === 'sick') {
|
||||
if (petGameRef.value && petGameRef.value.shakeHead) {
|
||||
petGameRef.value.shakeHead();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState.value === 'idle') {
|
||||
stateBeforeFeeding.value = currentState.value;
|
||||
currentState.value = 'eating';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DeviceShell>
|
||||
<DeviceScreen>
|
||||
<!-- Dynamic Component Switching -->
|
||||
<PetGame
|
||||
ref="petGameRef"
|
||||
:state="currentState"
|
||||
:stage="currentStage"
|
||||
@update:state="handleStateUpdate"
|
||||
/>
|
||||
</DeviceScreen>
|
||||
</DeviceShell>
|
||||
|
||||
<!-- Controls (Outside Machine) -->
|
||||
<div class="controls">
|
||||
<div class="btn-group">
|
||||
<button @click="setPetState('idle')" :disabled="currentStage === 'egg'">Idle</button>
|
||||
<button @click="setPetState('sleep')" :disabled="currentStage === 'egg'">Sleep</button>
|
||||
<button @click="setPetState('sick')" :disabled="currentStage === 'egg'">Sick</button>
|
||||
<button @click="setPetState('dead')" :disabled="currentStage === 'egg'">Dead</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button @click="startFeeding" :disabled="currentStage === 'egg'">Feed</button>
|
||||
<button @click="toggleStage">{{ currentStage === 'egg' ? 'Hatch' : 'Reset to Egg' }}</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button @click="currentScreen = 'game'" :disabled="currentStage === 'egg'">Game</button>
|
||||
<button @click="currentScreen = 'menu'" :disabled="currentStage === 'egg'">Menu</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #ffffffaa;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button.active {
|
||||
background: #ffb3c6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="screen">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
background: #c3e4aa;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 0 0 2px rgba(0, 0, 0, 0.25),
|
||||
inset 0 0 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* LCD 格線 */
|
||||
.screen::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
|
||||
background-size: 3px 3px;
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
z-index: 10; /* Ensure grid is above content but below UI overlays if any */
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="device">
|
||||
<div class="device-title">PET EGG</div>
|
||||
|
||||
<div class="screen-wrapper">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="btn"></div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 外殼 */
|
||||
.device {
|
||||
width: 280px;
|
||||
height: 340px;
|
||||
border-radius: 60% 60% 55% 55%;
|
||||
background: radial-gradient(circle at 30% 0%, #ffe7ff, #ffc6f1);
|
||||
box-shadow:
|
||||
0 18px 40px rgba(0, 0, 0, 0.35),
|
||||
inset 0 4px 10px rgba(255, 255, 255, 0.8);
|
||||
padding-top: 38px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.device-title {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.screen-wrapper {
|
||||
width: 210px;
|
||||
height: 160px;
|
||||
border-radius: 18px;
|
||||
background: #888;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 20%, #fff8e7, #f3a3af);
|
||||
box-shadow:
|
||||
0 4px 0 #c26c7a,
|
||||
0 4px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div class="menu">
|
||||
<div class="menu-title">MENU</div>
|
||||
<div class="menu-list">
|
||||
<div class="menu-item selected"> > STATUS</div>
|
||||
<div class="menu-item"> FEED</div>
|
||||
<div class="menu-item"> PLAY</div>
|
||||
<div class="menu-item"> CLEAN</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.menu {
|
||||
padding: 12px;
|
||||
color: #333;
|
||||
font-family: "Press Start 2P", monospace; /* Fallback to monospace if font not loaded */
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menu-item.selected {
|
||||
/* Blinking effect for selected item */
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,666 @@
|
|||
<template>
|
||||
<div class="pet-game-container" ref="containerRef">
|
||||
<!-- 寵物本體 -->
|
||||
<div
|
||||
class="pet-root"
|
||||
ref="petRef"
|
||||
:style="{
|
||||
left: petX + 'px',
|
||||
top: petY + 'px',
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
display: state === 'dead' ? 'none' : 'block'
|
||||
}"
|
||||
:class="['state-' + state, { 'shaking-head': isShakingHead }]"
|
||||
>
|
||||
<div class="pet-inner" :class="isFacingRight ? 'face-right' : 'face-left'">
|
||||
<!-- 根據是否張嘴選擇顯示的像素 -->
|
||||
<div
|
||||
v-for="(pixel, index) in currentPixels"
|
||||
:key="index"
|
||||
:class="['pet-pixel', pixel.className]"
|
||||
:style="{
|
||||
width: pixelSize + 'px',
|
||||
height: pixelSize + 'px',
|
||||
left: pixel.x * pixelSize + 'px',
|
||||
top: pixel.y * pixelSize + 'px',
|
||||
background: pixel.color
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 食物 -->
|
||||
<div
|
||||
v-if="state === 'eating' && foodVisible"
|
||||
class="food-item"
|
||||
:style="{
|
||||
left: foodX + 'px',
|
||||
top: foodY + 'px',
|
||||
width: (10 * pixelSize) + 'px',
|
||||
height: (10 * pixelSize) + 'px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(pixel, index) in currentFoodPixels"
|
||||
:key="'food-'+index"
|
||||
class="pet-pixel"
|
||||
:style="{
|
||||
width: pixelSize + 'px',
|
||||
height: pixelSize + 'px',
|
||||
left: pixel.x * pixelSize + 'px',
|
||||
top: pixel.y * pixelSize + 'px',
|
||||
background: pixel.color
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 睡覺 ZZZ -->
|
||||
<div class="sleep-zzz" :style="iconStyle" v-show="state === 'sleep'">
|
||||
<span class="z1">Z</span>
|
||||
<span class="z2">Z</span>
|
||||
<span class="z3">Z</span>
|
||||
</div>
|
||||
|
||||
<!-- 生病骷髏頭 -->
|
||||
<div class="sick-icon" :style="iconStyle" v-show="state === 'sick'">💀</div>
|
||||
|
||||
<!-- 死亡墓碑 -->
|
||||
<div class="tombstone" v-show="state === 'dead'"></div>
|
||||
|
||||
<!-- Debug Overlay -->
|
||||
<div class="debug-overlay">
|
||||
{{ containerWidth }}x{{ containerHeight }} | {{ Math.round(petX) }},{{ Math.round(petY) }} | {{ state }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import { SPRITE_PRESETS } from '../data/petPresets.js';
|
||||
import { FOOD_OPTIONS } from '../data/foodOptions.js';
|
||||
|
||||
const props = defineProps({
|
||||
state: {
|
||||
type: String,
|
||||
default: 'idle'
|
||||
},
|
||||
stage: {
|
||||
type: String,
|
||||
default: 'adult' // 'egg' or 'adult'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:state']);
|
||||
|
||||
// Default food choice
|
||||
const currentFood = 'banana'; // Can be: 'apple', 'banana', 'strawberry'
|
||||
const FOOD_SPRITES = FOOD_OPTIONS[currentFood].sprites;
|
||||
const FOOD_PALETTE = FOOD_OPTIONS[currentFood].palette;
|
||||
|
||||
|
||||
|
||||
const CURRENT_PRESET = SPRITE_PRESETS.tinyTigerCatB;
|
||||
const pixelSize = CURRENT_PRESET.pixelSize;
|
||||
|
||||
// Define dimensions
|
||||
const rows = CURRENT_PRESET.sprite.length;
|
||||
const cols = CURRENT_PRESET.sprite[0].length;
|
||||
const width = cols * pixelSize;
|
||||
const height = rows * pixelSize;
|
||||
|
||||
|
||||
// 2. Generate Pixels Helper
|
||||
function generatePixels(spriteMap, paletteOverride = null) {
|
||||
const pxs = [];
|
||||
const palette = paletteOverride || CURRENT_PRESET.palette;
|
||||
|
||||
spriteMap.forEach((rowStr, y) => {
|
||||
[...rowStr].forEach((ch, x) => {
|
||||
if (ch === '0') return;
|
||||
|
||||
// Only apply body part classes if NOT an egg
|
||||
let className = '';
|
||||
if (props.stage !== 'egg') {
|
||||
const isTail = CURRENT_PRESET.tailPixels?.some(([tx, ty]) => tx === x && ty === y);
|
||||
const isLegFront = CURRENT_PRESET.legFrontPixels?.some(([lx, ly]) => lx === x && ly === y);
|
||||
const isLegBack = CURRENT_PRESET.legBackPixels?.some(([lx, ly]) => lx === x && ly === y);
|
||||
const isEar = CURRENT_PRESET.earPixels?.some(([ex, ey]) => ex === x && ey === y);
|
||||
const isBlush = CURRENT_PRESET.blushPixels?.some(([bx, by]) => bx === x && by === y);
|
||||
|
||||
if (isTail) className += ' tail-pixel';
|
||||
if (isLegFront) className += ' leg-front';
|
||||
if (isLegBack) className += ' leg-back';
|
||||
if (isEar) className += ' ear-pixel';
|
||||
if (isBlush) className += ' blush-dot';
|
||||
}
|
||||
|
||||
pxs.push({
|
||||
x, y,
|
||||
color: palette[ch] || '#000',
|
||||
className: className.trim()
|
||||
});
|
||||
});
|
||||
});
|
||||
return pxs;
|
||||
}
|
||||
|
||||
// 3. State & Movement
|
||||
const containerRef = ref(null);
|
||||
const petX = ref(0);
|
||||
const petY = ref(0);
|
||||
const isFacingRight = ref(true);
|
||||
const iconX = ref(0);
|
||||
const iconY = ref(0);
|
||||
const isShakingHead = ref(false); // 控制搖頭時停止其他動畫
|
||||
|
||||
// Debug Refs
|
||||
const containerWidth = ref(0);
|
||||
const containerHeight = ref(0);
|
||||
|
||||
// Feeding State
|
||||
const isMouthOpen = ref(false);
|
||||
const foodX = ref(0);
|
||||
const foodY = ref(0);
|
||||
const foodStage = ref(0); // 0, 1, 2
|
||||
const foodVisible = ref(false);
|
||||
|
||||
const currentPixels = computed(() => {
|
||||
if (props.stage === 'egg') {
|
||||
return generatePixels(CURRENT_PRESET.eggSprite, CURRENT_PRESET.eggPalette);
|
||||
}
|
||||
return isMouthOpen.value
|
||||
? generatePixels(CURRENT_PRESET.spriteMouthOpen)
|
||||
: generatePixels(CURRENT_PRESET.sprite);
|
||||
});
|
||||
|
||||
const currentFoodPixels = computed(() => {
|
||||
const sprite = FOOD_SPRITES[foodStage.value];
|
||||
const pxs = [];
|
||||
if(!sprite) return pxs;
|
||||
sprite.forEach((row, y) => {
|
||||
[...row].forEach((ch, x) => {
|
||||
if (ch === '0') return;
|
||||
pxs.push({
|
||||
x, y,
|
||||
color: FOOD_PALETTE[ch] || '#d00' // Use food palette
|
||||
});
|
||||
});
|
||||
});
|
||||
return pxs;
|
||||
});
|
||||
|
||||
const iconStyle = computed(() => ({
|
||||
left: iconX.value + 'px',
|
||||
top: iconY.value + 'px'
|
||||
}));
|
||||
|
||||
function updateHeadIconsPosition() {
|
||||
const { iconBackLeft, iconBackRight } = CURRENT_PRESET;
|
||||
const marker = isFacingRight.value ? iconBackLeft : iconBackRight;
|
||||
const baseX = petX.value + marker.x * pixelSize;
|
||||
const baseY = petY.value + marker.y * pixelSize;
|
||||
|
||||
if (props.state === 'sleep') {
|
||||
iconX.value = baseX - 2;
|
||||
iconY.value = baseY - 10;
|
||||
} else if (props.state === 'sick') {
|
||||
iconX.value = baseX - 2;
|
||||
iconY.value = baseY - 28; // Moved higher to avoid overlap with pet body
|
||||
}
|
||||
}
|
||||
|
||||
function moveRandomly() {
|
||||
// Egg does not move
|
||||
if (props.stage === 'egg') {
|
||||
// Force center position just in case
|
||||
if (containerRef.value) {
|
||||
const cw = containerRef.value.clientWidth;
|
||||
const ch = containerRef.value.clientHeight;
|
||||
petX.value = Math.floor(cw / 2 - width / 2);
|
||||
petY.value = Math.floor(ch / 2 - height / 2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('moveRandomly called. State:', props.state);
|
||||
if (props.state === 'sleep' || props.state === 'dead' || props.state === 'eating') {
|
||||
updateHeadIconsPosition();
|
||||
return;
|
||||
}
|
||||
if (!containerRef.value) {
|
||||
console.warn('containerRef is null');
|
||||
return;
|
||||
}
|
||||
|
||||
const cw = containerRef.value.clientWidth;
|
||||
const ch = containerRef.value.clientHeight;
|
||||
containerWidth.value = cw;
|
||||
containerHeight.value = ch;
|
||||
console.log('Container size:', cw, ch);
|
||||
|
||||
// Safety check: if container has no size, don't run logic to avoid sticking to (8,8)
|
||||
if (cw === 0 || ch === 0) return;
|
||||
|
||||
const margin = 10;
|
||||
const step = 4;
|
||||
|
||||
// 0: up, 1: down, 2: left, 3: right
|
||||
const dir = Math.floor(Math.random() * 4);
|
||||
let newX = petX.value;
|
||||
let newY = petY.value;
|
||||
|
||||
if (dir === 0) newY -= step;
|
||||
if (dir === 1) newY += step;
|
||||
if (dir === 2) newX -= step;
|
||||
if (dir === 3) newX += step;
|
||||
|
||||
const maxX = cw - width - margin;
|
||||
const maxY = ch - height - margin;
|
||||
const minX = margin;
|
||||
const minY = margin;
|
||||
|
||||
newX = Math.max(minX, Math.min(maxX, newX));
|
||||
newY = Math.max(minY, Math.min(maxY, newY));
|
||||
|
||||
const dx = newX - petX.value;
|
||||
if (dx > 0) isFacingRight.value = false;
|
||||
else if (dx < 0) isFacingRight.value = true;
|
||||
|
||||
petX.value = newX;
|
||||
petY.value = newY;
|
||||
|
||||
updateHeadIconsPosition();
|
||||
}
|
||||
|
||||
// Feeding Logic
|
||||
async function startFeeding() {
|
||||
// Reset food
|
||||
foodStage.value = 0;
|
||||
foodVisible.value = true;
|
||||
|
||||
// Calculate food position: in front of pet, at mouth height
|
||||
// Food drops in front (not directly at mouth)
|
||||
const foodSize = 10 * pixelSize;
|
||||
const frontOffsetX = isFacingRight.value ? -foodSize - 5 : width + 5; // In front of pet
|
||||
const mouthY = 8.5; // Mouth is at row 8-9
|
||||
|
||||
// Set horizontal position (in front of pet)
|
||||
foodX.value = petX.value + frontOffsetX;
|
||||
foodY.value = 0; // Start from top of screen
|
||||
|
||||
// Calculate target Y (at mouth level)
|
||||
const targetY = petY.value + (mouthY * pixelSize) - (foodSize / 2);
|
||||
|
||||
// Animate falling to front of pet
|
||||
const duration = 800;
|
||||
const startTime = performance.now();
|
||||
|
||||
await new Promise(resolve => {
|
||||
function animateFall(time) {
|
||||
const elapsed = time - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// Ease out for smoother landing
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
foodY.value = eased * targetY;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animateFall);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animateFall);
|
||||
});
|
||||
|
||||
// Food is now at mouth level in front of pet
|
||||
// Pet takes 3 bites
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Wait a moment before opening mouth
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
// Open mouth (bite!)
|
||||
isMouthOpen.value = true;
|
||||
await new Promise(r => setTimeout(r, 250));
|
||||
|
||||
// Close mouth and reduce food
|
||||
isMouthOpen.value = false;
|
||||
foodStage.value = i + 1; // 0->1 (2/3), 1->2 (1/3), 2->3 (Gone)
|
||||
|
||||
// If last bite, hide food
|
||||
if (foodStage.value >= 3) {
|
||||
foodVisible.value = false;
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// Finish eating
|
||||
foodVisible.value = false;
|
||||
emit('update:state', 'idle');
|
||||
}
|
||||
|
||||
// Initialize position
|
||||
let resizeObserver;
|
||||
|
||||
function initPosition() {
|
||||
if (containerRef.value) {
|
||||
const cw = containerRef.value.clientWidth;
|
||||
const ch = containerRef.value.clientHeight;
|
||||
containerWidth.value = cw;
|
||||
containerHeight.value = ch;
|
||||
|
||||
if (cw > 0 && ch > 0) {
|
||||
petX.value = Math.floor(cw / 2 - width / 2);
|
||||
petY.value = Math.floor(ch / 2 - height / 2);
|
||||
updateHeadIconsPosition();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick(); // Wait for DOM
|
||||
|
||||
// Polling init if 0 size
|
||||
if (!initPosition()) {
|
||||
const initInterval = setInterval(() => {
|
||||
if (initPosition()) {
|
||||
clearInterval(initInterval);
|
||||
}
|
||||
}, 100);
|
||||
// Stop polling after 5 seconds
|
||||
setTimeout(() => clearInterval(initInterval), 5000);
|
||||
}
|
||||
|
||||
if (containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
initPosition(); // Re-check size on resize
|
||||
});
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
|
||||
intervalId = setInterval(moveRandomly, 600);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
});
|
||||
|
||||
// Watch state changes to update icon position immediately
|
||||
watch(() => props.state, (newState) => {
|
||||
updateHeadIconsPosition();
|
||||
if (newState === 'eating') {
|
||||
startFeeding();
|
||||
} else {
|
||||
// Reset feeding state if interrupted
|
||||
isMouthOpen.value = false;
|
||||
foodVisible.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Shake head function (refuse to eat)
|
||||
async function shakeHead() {
|
||||
const originalDirection = isFacingRight.value;
|
||||
|
||||
// 開始搖頭,暫停其他動畫
|
||||
isShakingHead.value = true;
|
||||
|
||||
// 搖頭三次(慢慢地,像生病一樣虛弱)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Turn opposite (slowly)
|
||||
isFacingRight.value = !isFacingRight.value;
|
||||
await new Promise(r => setTimeout(r, 400)); // 放慢速度
|
||||
|
||||
// Turn back (slowly)
|
||||
isFacingRight.value = !isFacingRight.value;
|
||||
await new Promise(r => setTimeout(r, 400)); // 放慢速度
|
||||
}
|
||||
|
||||
// Restore original direction
|
||||
isFacingRight.value = originalDirection;
|
||||
|
||||
// 搖頭結束,恢復動畫
|
||||
isShakingHead.value = false;
|
||||
}
|
||||
|
||||
// Expose shakeHead function to parent component
|
||||
defineExpose({
|
||||
shakeHead
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pet-game-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.debug-overlay {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
font-size: 8px;
|
||||
color: rgba(0,0,0,0.5);
|
||||
pointer-events: none;
|
||||
background: rgba(255,255,255,0.5);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.pet-root {
|
||||
position: absolute;
|
||||
transform-origin: center bottom;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.pet-inner {
|
||||
position: relative;
|
||||
transform-origin: center bottom;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pet-inner.face-right {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.pet-inner.face-left {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.pet-pixel {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.food-item {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.pet-root.state-idle {
|
||||
animation: pet-breathe-idle 2s ease-in-out infinite, pet-head-tilt 6s ease-in-out infinite;
|
||||
}
|
||||
.pet-root.state-eating {
|
||||
animation: none; /* No body movement when eating, only mouth opens/closes */
|
||||
}
|
||||
|
||||
/* Stop all animations when shaking head */
|
||||
.pet-root.shaking-head {
|
||||
animation: none !important;
|
||||
}
|
||||
.pet-root.shaking-head .tail-pixel,
|
||||
.pet-root.shaking-head .leg-front,
|
||||
.pet-root.shaking-head .leg-back,
|
||||
.pet-root.shaking-head .ear-pixel,
|
||||
.pet-root.shaking-head .blush-dot {
|
||||
animation: none !important;
|
||||
}
|
||||
.pet-root.state-sleep {
|
||||
animation: pet-breathe-sleep 4s ease-in-out infinite;
|
||||
}
|
||||
.pet-root.state-sick {
|
||||
animation: pet-breathe-idle 2s ease-in-out infinite, pet-sick-shake 0.6s ease-in-out infinite;
|
||||
filter: brightness(0.8) saturate(0.9);
|
||||
}
|
||||
|
||||
@keyframes pet-breathe-idle {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.03); }
|
||||
}
|
||||
@keyframes pet-breathe-sleep {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.015); }
|
||||
}
|
||||
@keyframes pet-head-tilt {
|
||||
0%, 100% { transform: rotate(0deg) scale(1); }
|
||||
25% { transform: rotate(-2deg) scale(1.02); }
|
||||
75% { transform: rotate(2deg) scale(1.02); }
|
||||
}
|
||||
@keyframes pet-sick-shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-2px); }
|
||||
75% { transform: translateX(2px); }
|
||||
}
|
||||
|
||||
/* Tail */
|
||||
.tail-pixel { transform-origin: center center; }
|
||||
.pet-root.state-idle .tail-pixel { animation: tail-wag-idle 0.5s ease-in-out infinite; }
|
||||
.pet-root.state-sleep .tail-pixel { animation: tail-wag-sleep 1.6s ease-in-out infinite; }
|
||||
.pet-root.state-sick .tail-pixel { animation: tail-wag-sick 0.7s ease-in-out infinite; }
|
||||
|
||||
@keyframes tail-wag-idle {
|
||||
0% { transform: translateX(0px); }
|
||||
50% { transform: translateX(1px); }
|
||||
100% { transform: translateX(0px); }
|
||||
}
|
||||
@keyframes tail-wag-sleep {
|
||||
0% { transform: translateX(0px); }
|
||||
50% { transform: translateX(0.5px); }
|
||||
100% { transform: translateX(0px); }
|
||||
}
|
||||
@keyframes tail-wag-sick {
|
||||
0% { transform: translateX(0px); }
|
||||
50% { transform: translateX(0.8px); }
|
||||
100% { transform: translateX(0px); }
|
||||
}
|
||||
|
||||
/* Legs */
|
||||
.leg-front, .leg-back { transform-origin: center center; }
|
||||
.pet-root.state-idle .leg-front { animation: leg-front-step 0.6s ease-in-out infinite; }
|
||||
.pet-root.state-idle .leg-back { animation: leg-back-step 0.6s ease-in-out infinite; }
|
||||
.pet-root.state-sick .leg-front, .pet-root.state-sick .leg-back { animation: leg-sick-step 0.8s ease-in-out infinite; }
|
||||
|
||||
@keyframes leg-front-step {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-1px); }
|
||||
}
|
||||
@keyframes leg-back-step {
|
||||
0%, 100% { transform: translateY(-1px); }
|
||||
50% { transform: translateY(0); }
|
||||
}
|
||||
@keyframes leg-sick-step {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-0.5px); }
|
||||
}
|
||||
|
||||
/* Ears */
|
||||
.ear-pixel { transform-origin: center center; }
|
||||
.pet-root.state-idle .ear-pixel { animation: ear-twitch 3.2s ease-in-out infinite; }
|
||||
.pet-root.state-sick .ear-pixel { animation: ear-twitch 4s ease-in-out infinite; }
|
||||
|
||||
@keyframes ear-twitch {
|
||||
0%, 90%, 100% { transform: translateY(0); }
|
||||
92% { transform: translateY(-1px); }
|
||||
96% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Blush */
|
||||
.blush-dot { transform-origin: center center; }
|
||||
.pet-root.state-idle .blush-dot, .pet-root.state-sick .blush-dot { animation: blush-pulse 2s ease-in-out infinite; }
|
||||
.pet-root.state-sleep .blush-dot { animation: blush-pulse 3s ease-in-out infinite; }
|
||||
|
||||
@keyframes blush-pulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Sleep ZZZ */
|
||||
.sleep-zzz {
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
color: #5a7a4a;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.sleep-zzz span {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
opacity: 0;
|
||||
animation: zzz-float 3s ease-in-out infinite;
|
||||
}
|
||||
.sleep-zzz .z1 { left: 0; animation-delay: 0s; }
|
||||
.sleep-zzz .z2 { left: 8px; animation-delay: 0.8s; }
|
||||
.sleep-zzz .z3 { left: 15px; animation-delay: 1.6s; }
|
||||
|
||||
@keyframes zzz-float {
|
||||
0% { opacity: 0; transform: translateY(0) scale(0.7); }
|
||||
20% { opacity: 1; transform: translateY(-4px) scale(0.9); }
|
||||
80% { opacity: 1; transform: translateY(-16px) scale(1.05); }
|
||||
100% { opacity: 0; transform: translateY(-24px) scale(1.1); }
|
||||
}
|
||||
|
||||
/* Sick Icon */
|
||||
.sick-icon {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
animation: sick-icon-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes sick-icon-pulse {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
/* Tombstone */
|
||||
.tombstone {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 52%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 48px;
|
||||
height: 56px;
|
||||
border-radius: 16px 16px 6px 6px;
|
||||
background: #9ba7a0;
|
||||
border: 2px solid #5e6861;
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2), 0 4px 4px rgba(0, 0, 0, 0.35);
|
||||
z-index: 10;
|
||||
animation: tomb-float 3s ease-in-out infinite;
|
||||
}
|
||||
.tombstone::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px; right: 8px; top: 16px;
|
||||
height: 2px;
|
||||
background: #5e6861;
|
||||
}
|
||||
.tombstone::after {
|
||||
content: "RIP";
|
||||
position: absolute;
|
||||
top: 22px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 8px;
|
||||
letter-spacing: 1px;
|
||||
color: #39413c;
|
||||
}
|
||||
|
||||
@keyframes tomb-float {
|
||||
0%, 100% { transform: translate(-50%, -50%) translateY(0); }
|
||||
50% { transform: translate(-50%, -50%) translateY(-4px); }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// Food Sprite Options
|
||||
// 定義各種食物的像素藝術數據
|
||||
|
||||
export const FOOD_OPTIONS = {
|
||||
apple: {
|
||||
name: '蘋果',
|
||||
sprites: [
|
||||
// Stage 0: Full apple
|
||||
[
|
||||
'0000660000',
|
||||
'0006116000',
|
||||
'0061111600',
|
||||
'0622222260',
|
||||
'6222222226',
|
||||
'6222222226',
|
||||
'6222222226',
|
||||
'0622222260',
|
||||
'0062222600',
|
||||
'0006666000',
|
||||
],
|
||||
// Stage 1: 2/3 eaten
|
||||
[
|
||||
'0000660000',
|
||||
'0006116000',
|
||||
'0061111600',
|
||||
'0622222260',
|
||||
'6222222000',
|
||||
'6222220000',
|
||||
'6222200000',
|
||||
'0622200000',
|
||||
'0062000000',
|
||||
'0006000000',
|
||||
],
|
||||
// Stage 2: 1/3 eaten
|
||||
[
|
||||
'0000660000',
|
||||
'0006116000',
|
||||
'0061111000',
|
||||
'0622222000',
|
||||
'6222200000',
|
||||
'6222000000',
|
||||
'6220000000',
|
||||
'0620000000',
|
||||
'0060000000',
|
||||
'0000000000',
|
||||
]
|
||||
],
|
||||
palette: {
|
||||
'1': '#7a5436', // Brown stem
|
||||
'2': '#d32e2e', // Red apple
|
||||
'6': '#8b1a1a', // Dark red shadow
|
||||
}
|
||||
},
|
||||
banana: {
|
||||
name: '香蕉',
|
||||
sprites: [
|
||||
// Stage 0: Full banana(完整香蕉)
|
||||
[
|
||||
'0000000440',
|
||||
'0000004440',
|
||||
'0000043221',
|
||||
'0000433221',
|
||||
'0004333221',
|
||||
'0043332221',
|
||||
'0433322200',
|
||||
'4333221000',
|
||||
'0432221000',
|
||||
'0044400000',
|
||||
],
|
||||
// Stage 1: 2/3 eaten(吃掉約 1/3)
|
||||
[
|
||||
'0000000440',
|
||||
'0000004440',
|
||||
'0000043220',
|
||||
'0000433200',
|
||||
'0004333200',
|
||||
'0043332000',
|
||||
'0433320000',
|
||||
'4333200000',
|
||||
'0432200000',
|
||||
'0044400000',
|
||||
],
|
||||
// Stage 2: 1/3 eaten(只剩前端)
|
||||
[
|
||||
'0000000440',
|
||||
'0000004440',
|
||||
'0000043200',
|
||||
'0000432000',
|
||||
'0000430000',
|
||||
'0004330000',
|
||||
'0043300000',
|
||||
'0433000000',
|
||||
'0430000000',
|
||||
'0044000000',
|
||||
]
|
||||
],
|
||||
palette: {
|
||||
// 外框+陰影(深咖),會讓香蕉邊緣比較立體
|
||||
'1': '#b0862d',
|
||||
// 主體黃色
|
||||
'2': '#f6e15a',
|
||||
// 高光比較亮的黃色
|
||||
'3': '#fff6a0',
|
||||
// 果蒂 & 最深陰影(咖啡色)
|
||||
'4': '#8b5a2b',
|
||||
}
|
||||
},
|
||||
strawberry: {
|
||||
name: '草莓',
|
||||
sprites: [
|
||||
// Stage 0: Full strawberry
|
||||
[
|
||||
'0001111000',
|
||||
'0011111100',
|
||||
'0022222200',
|
||||
'0222222220',
|
||||
'2222222222',
|
||||
'2222222222',
|
||||
'0222222220',
|
||||
'0022222200',
|
||||
'0002222000',
|
||||
'0000220000',
|
||||
],
|
||||
// Stage 1: 2/3 eaten
|
||||
[
|
||||
'0001111000',
|
||||
'0011111100',
|
||||
'0022222200',
|
||||
'0222222220',
|
||||
'2222222000',
|
||||
'2222220000',
|
||||
'0222220000',
|
||||
'0022220000',
|
||||
'0002220000',
|
||||
'0000220000',
|
||||
],
|
||||
// Stage 2: 1/3 eaten
|
||||
[
|
||||
'0001111000',
|
||||
'0011111100',
|
||||
'0022222000',
|
||||
'0222222000',
|
||||
'2222200000',
|
||||
'2222000000',
|
||||
'0222000000',
|
||||
'0022000000',
|
||||
'0002000000',
|
||||
'0000000000',
|
||||
]
|
||||
],
|
||||
palette: {
|
||||
'1': '#4a7c3e', // Green leaves
|
||||
'2': '#e63946', // Red strawberry
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
// Pet Sprite Presets
|
||||
// 定義各種寵物的像素藝術數據
|
||||
|
||||
export const SPRITE_PRESETS = {
|
||||
tigerChick: {
|
||||
name: 'tigerChick',
|
||||
pixelSize: 3,
|
||||
sprite: [
|
||||
'1100000111000000',
|
||||
'1241111331000000',
|
||||
'1005102301000000',
|
||||
'1054103320000000',
|
||||
'1241143320100000',
|
||||
'1230432311100110',
|
||||
'1245321330100421',
|
||||
'1240001311100111',
|
||||
'1020103030111421',
|
||||
'0100000331245210',
|
||||
'0001111111240210',
|
||||
'0015022221345210',
|
||||
'0004022235350010',
|
||||
'0011400023203210',
|
||||
'0005011112115210',
|
||||
'0001100001100110',
|
||||
],
|
||||
// 張嘴版:修改嘴巴部分的像素(row 8-9 是嘴巴區域)
|
||||
spriteMouthOpen: [
|
||||
'1100000111000000', // Row 0
|
||||
'1241111331000000', // Row 1
|
||||
'1005102301000000', // Row 2
|
||||
'1054103320000000', // Row 3
|
||||
'1241143320100000', // Row 4
|
||||
'1230432311100110', // Row 5
|
||||
'1245321330100421', // Row 6 - 保持不變
|
||||
'1240001311100111', // Row 7 - 保持不變
|
||||
'1000000030111421', // Row 8 - 嘴巴張開(更往前,移除位置2-7)
|
||||
'0000000331245210', // Row 9 - 嘴巴張開(更往前,移除位置1-7)
|
||||
'0001111111240210', // Row 10
|
||||
'0015022221345210', // Row 11
|
||||
'0004022235350010', // Row 12
|
||||
'0011400023203210', // Row 13
|
||||
'0005011112115210', // Row 14
|
||||
'0001100001100110', // Row 15
|
||||
],
|
||||
palette: {
|
||||
'1': '#2b2825',
|
||||
'2': '#d0974b',
|
||||
'3': '#e09037',
|
||||
'4': '#4a2b0d',
|
||||
'5': '#724e22',
|
||||
},
|
||||
tailPixels: [
|
||||
[15, 8], [14, 8],
|
||||
[15, 9], [14, 9],
|
||||
[15, 10], [14, 10],
|
||||
[15, 11], [14, 11],
|
||||
],
|
||||
legFrontPixels: [
|
||||
[6, 13], [7, 13],
|
||||
[6, 14], [7, 14],
|
||||
],
|
||||
legBackPixels: [
|
||||
[9, 13], [10, 13],
|
||||
[9, 14], [10, 14],
|
||||
],
|
||||
earPixels: [
|
||||
[2, 0], [3, 0], [4, 0],
|
||||
[11, 0], [12, 0], [13, 0],
|
||||
],
|
||||
blushPixels: [
|
||||
[4, 7], [5, 7],
|
||||
[10, 7], [11, 7],
|
||||
],
|
||||
iconBackLeft: { x: 3, y: 2 },
|
||||
iconBackRight: { x: 12, y: 2 },
|
||||
},
|
||||
tinyTigerCat: {
|
||||
name: '小虎斑貓',
|
||||
pixelSize: 3,
|
||||
sprite: [
|
||||
'0000000000000000',
|
||||
'0011000000110000', // row 1 - Ears
|
||||
'0122111111221000', // row 2
|
||||
'0122222222221000', // row 3
|
||||
'0122322223221000', // row 4 - Stripes
|
||||
'0122222222221000', // row 5
|
||||
'0120022220021000', // row 6 - Eyes
|
||||
'0122223322221000', // row 7 - Nose/Mouth
|
||||
'0122222222221000', // row 8
|
||||
'0011222222110000', // row 9 - Body
|
||||
'0001222222121000', // row 10 - Body + Tail
|
||||
'0001222222121000', // row 11
|
||||
'0001100110110000', // row 12 - Legs
|
||||
'0000000000000000', // row 13
|
||||
'0000000000000000', // row 14
|
||||
'0000000000000000', // row 15
|
||||
],
|
||||
spriteMouthOpen: [
|
||||
'0000000000000000',
|
||||
'0011000000110000',
|
||||
'0122111111221000',
|
||||
'0122222222221000',
|
||||
'0122322223221000',
|
||||
'0122222222221000',
|
||||
'0120022220021000',
|
||||
'0122223322221000',
|
||||
'0122200002221000', // Mouth Open
|
||||
'0011222222110000',
|
||||
'0001222222121000',
|
||||
'0001222222121000',
|
||||
'0001100110110000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
],
|
||||
palette: {
|
||||
'0': '#000000', // Black eyes/outline
|
||||
'1': '#2b1d12', // Dark brown outline
|
||||
'2': '#ffb347', // Orange fur
|
||||
'3': '#cd853f', // Darker stripes/nose
|
||||
},
|
||||
tailPixels: [
|
||||
[11, 10], [12, 10],
|
||||
[11, 11], [12, 11],
|
||||
],
|
||||
earPixels: [
|
||||
[2, 1], [3, 1],
|
||||
[10, 1], [11, 1],
|
||||
],
|
||||
legFrontPixels: [
|
||||
[4, 12], [5, 12],
|
||||
],
|
||||
legBackPixels: [
|
||||
[8, 12], [9, 12],
|
||||
],
|
||||
blushPixels: [
|
||||
[3, 7], [10, 7]
|
||||
],
|
||||
iconBackLeft: { x: 2, y: 2 },
|
||||
iconBackRight: { x: 11, y: 2 }
|
||||
},
|
||||
tinyTigerCatB: {
|
||||
name: '小虎斑貓',
|
||||
pixelSize: 3,
|
||||
sprite: [
|
||||
'0000000000000000',
|
||||
'0011000000110000', // row 1 - Ears
|
||||
'0124444111442100', // row 2 粉紅耳朵內側
|
||||
'0123222323221000', // row 3 三條虎紋
|
||||
'0122322223221000', // row 4 - Stripes
|
||||
'0122522222522100', // row 5 眼睛反光
|
||||
'0125052225052100', // row 6 大圓眼+黑瞳孔+白反光
|
||||
'0112223322221100', // row 7 鼻子+左右鬍鬚
|
||||
'0122220222221000', // row 8 小微笑
|
||||
'0011222222110000', // row 9 - Body
|
||||
'0001222222121000', // row 10 - Body + Tail
|
||||
'0001222222121000', // row 11
|
||||
'0001100110110000', // row 12 - Legs
|
||||
'0000000000000000', // row 13
|
||||
'0000000000000000', // row 14
|
||||
'0000000000000000', // row 15
|
||||
],
|
||||
spriteMouthOpen: [
|
||||
'0000000000000000',
|
||||
'0011000000110000',
|
||||
'0124444111442100',
|
||||
'0123222323221000',
|
||||
'0122322223221000',
|
||||
'0122522222522100',
|
||||
'0125052225052100',
|
||||
'0112223322221100',
|
||||
'0122204002221000', // Mouth Open 粉紅舌頭
|
||||
'0011222222110000',
|
||||
'0001222222121000',
|
||||
'0001222222121000',
|
||||
'0001100110110000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
],
|
||||
palette: {
|
||||
'0': '#000000', // Black eyes/outline
|
||||
'1': '#2b1d12', // Dark brown outline
|
||||
'2': '#ffb347', // Orange fur
|
||||
'3': '#cd853f', // Darker stripes/nose
|
||||
'4': '#ffb6c1', // Pink (ears, blush, tongue)
|
||||
'5': '#ffffff' // White eye highlight
|
||||
},
|
||||
tailPixels: [
|
||||
[11, 10], [12, 10],
|
||||
[11, 11], [12, 11],
|
||||
],
|
||||
earPixels: [
|
||||
[2, 1], [3, 1],
|
||||
[10, 1], [11, 1],
|
||||
],
|
||||
legFrontPixels: [
|
||||
[4, 12], [5, 12],
|
||||
],
|
||||
legBackPixels: [
|
||||
[8, 12], [9, 12],
|
||||
],
|
||||
blushPixels: [
|
||||
[3, 7], [10, 7]
|
||||
],
|
||||
iconBackLeft: { x: 2, y: 2 },
|
||||
iconBackRight: { x: 13, y: 2 },
|
||||
|
||||
// Growth Stages
|
||||
eggSprite: [
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
'0000000111000000', // Top (Narrow)
|
||||
'0000001222100000',
|
||||
'0000012232210000', // Small stripe
|
||||
'0000122333221000',
|
||||
'0000122232221000',
|
||||
'0001222222222100', // Widest part
|
||||
'0001233322332100', // Side stripes
|
||||
'0001223222232100',
|
||||
'0000122222221000',
|
||||
'0000122222221000',
|
||||
'0000011222110000', // Bottom
|
||||
'0000000111000000',
|
||||
'0000000000000000',
|
||||
'0000000000000000',
|
||||
],
|
||||
eggPalette: {
|
||||
'1': '#5d4037', // Dark brown outline
|
||||
'2': '#fff8e1', // Creamy white shell
|
||||
'3': '#ffb74d', // Orange tiger stripes
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/* Global Reset & Body Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
background: radial-gradient(circle at top, #ffe8c7, #f5c7c7);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Loading…
Reference in New Issue