858 lines
23 KiB
HTML
858 lines
23 KiB
HTML
|
|
<!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>
|