oneko 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -2
- package/SKIN-CREDITS.md +61 -0
- package/dist/adapter.d.ts +1 -0
- package/dist/adapter.js +11 -0
- package/dist/cat-sounds/Cat_baby_ambient1.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_ambient2.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_ambient3.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_ambient4.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_ambient5.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_ambient6.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_ambient7.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_death.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_hurt1.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_hurt2.ogg +0 -0
- package/dist/cat-sounds/Cat_baby_hurt3.ogg +0 -0
- package/dist/cat-sounds/Cat_beg1.ogg +0 -0
- package/dist/cat-sounds/Cat_beg2.ogg +0 -0
- package/dist/cat-sounds/Cat_beg3.ogg +0 -0
- package/dist/cat-sounds/Cat_eat1.ogg +0 -0
- package/dist/cat-sounds/Cat_eat2.ogg +0 -0
- package/dist/cat-sounds/Cat_hiss1.ogg +0 -0
- package/dist/cat-sounds/Cat_hiss2.ogg +0 -0
- package/dist/cat-sounds/Cat_hiss3.ogg +0 -0
- package/dist/cat-sounds/Cat_hurt1.ogg +0 -0
- package/dist/cat-sounds/Cat_hurt2.ogg +0 -0
- package/dist/cat-sounds/Cat_hurt3.ogg +0 -0
- package/dist/cat-sounds/Cat_idle1.ogg +0 -0
- package/dist/cat-sounds/Cat_idle2.ogg +0 -0
- package/dist/cat-sounds/Cat_idle3.ogg +0 -0
- package/dist/cat-sounds/Cat_idle4.ogg +0 -0
- package/dist/cat-sounds/Cat_purr1.ogg +0 -0
- package/dist/cat-sounds/Cat_purr2.ogg +0 -0
- package/dist/cat-sounds/Cat_purr3.ogg +0 -0
- package/dist/cat-sounds/Cat_purreow1.ogg +0 -0
- package/dist/cat-sounds/Cat_purreow2.ogg +0 -0
- package/dist/cat-sounds/Cat_royal_ambient1.ogg +0 -0
- package/dist/cat-sounds/Cat_royal_ambient2.ogg +0 -0
- package/dist/cat-sounds/Cat_royal_ambient3.ogg +0 -0
- package/dist/cat-sounds/Cat_royal_ambient4.ogg +0 -0
- package/dist/cat-sounds/Cat_royal_ambient5.ogg +1 -0
- package/dist/cat-sounds/Cat_royal_ambient6.ogg +1 -0
- package/dist/cat-sounds/Cat_royal_death.ogg +1 -0
- package/dist/cat-sounds/Cat_royal_hurt1.ogg +1 -0
- package/dist/cat-sounds/Cat_royal_hurt2.ogg +1 -0
- package/dist/cat-sounds/Cat_royal_hurt3.ogg +1 -0
- package/dist/cat-sounds/Stray_cat_idle1.ogg +1 -0
- package/dist/cat-sounds/Stray_cat_idle2.ogg +1 -0
- package/dist/cat-sounds/Stray_cat_idle3.ogg +1 -0
- package/dist/cat-sounds/Stray_cat_idle4.ogg +1 -0
- package/dist/chunks/skin-sheets-CG0yKeZb.js +71 -0
- package/dist/chunks/vanilla-CUrawvWx.js +2993 -0
- package/dist/index.js +286 -0
- package/dist/solid.js +17 -0
- package/dist/svelte/Oneko.svelte +17 -0
- package/dist/svelte/Oneko.svelte.d.ts +4 -0
- package/dist/types/components/laser-cursor.d.ts +3 -0
- package/dist/types/components/oneko.d.ts +11 -0
- package/dist/types/hooks/use-cat-animation.d.ts +23 -0
- package/dist/types/hooks/use-motion-allowed.d.ts +3 -0
- package/dist/types/hooks/use-oneko-props-sync.d.ts +14 -0
- package/dist/types/hooks/use-skin-source.d.ts +3 -0
- package/dist/types/hooks/use-sprite-appearance.d.ts +6 -0
- package/dist/types/lib/oneko/animation/activity.d.ts +5 -0
- package/dist/types/lib/oneko/animation/attention.d.ts +13 -0
- package/dist/types/lib/oneko/animation/bubbles.d.ts +5 -0
- package/dist/types/lib/oneko/animation/debug-render.d.ts +5 -0
- package/dist/types/lib/oneko/animation/deps.d.ts +21 -0
- package/dist/types/lib/oneko/animation/frame/interrupt-idle.d.ts +2 -0
- package/dist/types/lib/oneko/animation/frame/tick-laser.d.ts +2 -0
- package/dist/types/lib/oneko/animation/frame/tick-live-state.d.ts +3 -0
- package/dist/types/lib/oneko/animation/frame/tick-movement.d.ts +2 -0
- package/dist/types/lib/oneko/animation/frame/tick-prelude.d.ts +2 -0
- package/dist/types/lib/oneko/animation/frame/tick-sounds.d.ts +3 -0
- package/dist/types/lib/oneko/animation/frame.d.ts +2 -0
- package/dist/types/lib/oneko/animation/grid.d.ts +3 -0
- package/dist/types/lib/oneko/animation/idle.d.ts +4 -0
- package/dist/types/lib/oneko/animation/mouse.d.ts +2 -0
- package/dist/types/lib/oneko/animation/movement.d.ts +3 -0
- package/dist/types/lib/oneko/animation/sprites.d.ts +5 -0
- package/dist/types/lib/oneko/animation-loop.d.ts +5 -0
- package/dist/types/lib/oneko/constants.d.ts +118 -0
- package/dist/types/lib/oneko/debug-controls/bubble.d.ts +4 -0
- package/dist/types/lib/oneko/debug-controls/force-state.d.ts +4 -0
- package/dist/types/lib/oneko/debug-controls/index.d.ts +14 -0
- package/dist/types/lib/oneko/debug-controls/playback.d.ts +4 -0
- package/dist/types/lib/oneko/debug-controls/position.d.ts +4 -0
- package/dist/types/lib/oneko/debug-controls/speed.d.ts +4 -0
- package/dist/types/lib/oneko/debug-controls/ui.d.ts +5 -0
- package/dist/types/lib/oneko/defaults.d.ts +39 -0
- package/dist/types/lib/oneko/dom.d.ts +10 -0
- package/dist/types/lib/oneko/initial-state.d.ts +9 -0
- package/dist/types/lib/oneko/konami.d.ts +4 -0
- package/dist/types/lib/oneko/laser-cursor-dom.d.ts +1 -0
- package/dist/types/lib/oneko/obstacle-filters.d.ts +12 -0
- package/dist/types/lib/oneko/pack.d.ts +12 -0
- package/dist/types/lib/oneko/pathfinding.d.ts +6 -0
- package/dist/types/lib/oneko/persistence.d.ts +18 -0
- package/dist/types/lib/oneko/runtime-config.d.ts +28 -0
- package/dist/types/lib/oneko/skin-catalog.d.ts +98 -0
- package/dist/types/lib/oneko/skin-classic.json.d.ts +4 -0
- package/dist/types/lib/oneko/skin-sheets.json.d.ts +27 -0
- package/dist/types/lib/oneko/skins.d.ts +14 -0
- package/dist/types/lib/oneko/start-cat-animation.d.ts +24 -0
- package/dist/types/lib/oneko/teardown.d.ts +17 -0
- package/dist/types/lib/oneko/types.d.ts +183 -0
- package/dist/types/lib/oneko/vanilla.d.ts +13 -0
- package/dist/types/lib/oneko/zone-runtime.d.ts +20 -0
- package/dist/types/lib/oneko/zones.d.ts +26 -0
- package/dist/types/packages/oneko/src/adapter.d.ts +3 -0
- package/dist/types/packages/oneko/src/index.d.ts +3 -0
- package/dist/types/packages/oneko/src/solid.d.ts +3 -0
- package/dist/types/packages/oneko/src/vanilla.d.ts +7 -0
- package/dist/types/packages/oneko/src/vue.d.ts +173 -0
- package/dist/vanilla.d.ts +1 -0
- package/dist/vanilla.js +9 -0
- package/dist/vue.js +71 -0
- package/package.json +82 -4
|
@@ -0,0 +1,2993 @@
|
|
|
1
|
+
const DOT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 7" shape-rendering="crispEdges" aria-hidden="true" focusable="false" style="display:block;width:100%;height:100%"><rect x="3" y="0" width="1" height="1" fill="var(--destructive)"/><rect x="2" y="1" width="3" height="1" fill="var(--destructive)"/><rect x="1" y="2" width="5" height="1" fill="var(--destructive)"/><rect x="0" y="3" width="7" height="1" fill="var(--destructive)"/><rect x="1" y="4" width="5" height="1" fill="var(--destructive)"/><rect x="2" y="5" width="3" height="1" fill="var(--destructive)"/><rect x="3" y="6" width="1" height="1" fill="var(--destructive)"/><rect x="2" y="2" width="1" height="1" fill="var(--accent)"/></svg>`;
|
|
2
|
+
const LASER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 5" shape-rendering="crispEdges" aria-hidden="true" focusable="false" style="display:block;width:100%;height:100%"><rect x="2" y="0" width="16" height="1" fill="var(--destructive)"/><rect x="1" y="1" width="18" height="1" fill="var(--destructive)"/><rect x="0" y="2" width="20" height="1" fill="var(--destructive)"/><rect x="1" y="3" width="18" height="1" fill="var(--destructive)"/><rect x="2" y="4" width="16" height="1" fill="var(--destructive)"/><rect x="11" y="1" width="1" height="1" fill="var(--primary-foreground)"/><rect x="13" y="1" width="1" height="1" fill="var(--primary-foreground)"/><rect x="15" y="1" width="1" height="1" fill="var(--primary-foreground)"/><rect x="11" y="2" width="1" height="1" fill="var(--primary-foreground)"/><rect x="13" y="2" width="1" height="1" fill="var(--primary-foreground)"/><rect x="15" y="2" width="1" height="1" fill="var(--primary-foreground)"/><rect x="11" y="3" width="1" height="1" fill="var(--primary-foreground)"/><rect x="13" y="3" width="1" height="1" fill="var(--primary-foreground)"/><rect x="15" y="3" width="1" height="1" fill="var(--primary-foreground)"/><rect x="4" y="0" width="3" height="1" fill="var(--primary)"/><rect x="4" y="1" width="3" height="1" fill="var(--primary)"/><rect x="18" y="1" width="3" height="1" fill="var(--accent)"/><rect x="18" y="2" width="3" height="1" fill="var(--accent)"/><rect x="18" y="3" width="3" height="1" fill="var(--accent)"/></svg>`;
|
|
3
|
+
const DOT_SIZE = 9;
|
|
4
|
+
const LASER_W = 88;
|
|
5
|
+
const LASER_H = 20;
|
|
6
|
+
const ANCHOR_INSET = 10;
|
|
7
|
+
const TRAIL_LEN = 22;
|
|
8
|
+
function makeDot(zIndex, size, opacity, glow) {
|
|
9
|
+
const d = document.createElement("div");
|
|
10
|
+
d.setAttribute("aria-hidden", "true");
|
|
11
|
+
d.style.cssText = [
|
|
12
|
+
"position:fixed",
|
|
13
|
+
`width:${size}px`,
|
|
14
|
+
`height:${size}px`,
|
|
15
|
+
"pointer-events:none",
|
|
16
|
+
`z-index:${zIndex}`,
|
|
17
|
+
"left:0",
|
|
18
|
+
"top:0",
|
|
19
|
+
"image-rendering:pixelated",
|
|
20
|
+
`opacity:${opacity}`,
|
|
21
|
+
"transform:translate(-50%,-50%) translate(-100px,-100px)",
|
|
22
|
+
glow ? "filter:drop-shadow(0 0 4px var(--destructive)) drop-shadow(0 0 12px color-mix(in srgb, var(--destructive) 55%, transparent))" : ""
|
|
23
|
+
].join(";");
|
|
24
|
+
d.innerHTML = DOT_SVG;
|
|
25
|
+
return d;
|
|
26
|
+
}
|
|
27
|
+
function createTrail(zIndex) {
|
|
28
|
+
const trail = [];
|
|
29
|
+
for (let i = 1; i <= TRAIL_LEN; i++) {
|
|
30
|
+
const t = i / (TRAIL_LEN + 1);
|
|
31
|
+
const size = Math.max(2, DOT_SIZE * (1 - t * 0.85));
|
|
32
|
+
const opacity = 0.55 * (1 - t);
|
|
33
|
+
trail.push(makeDot(zIndex, size, opacity, false));
|
|
34
|
+
}
|
|
35
|
+
return trail;
|
|
36
|
+
}
|
|
37
|
+
function createLaserBody(zIndex) {
|
|
38
|
+
const laser = document.createElement("div");
|
|
39
|
+
laser.setAttribute("aria-hidden", "true");
|
|
40
|
+
laser.style.cssText = [
|
|
41
|
+
"position:fixed",
|
|
42
|
+
`width:${LASER_W}px`,
|
|
43
|
+
`height:${LASER_H}px`,
|
|
44
|
+
"pointer-events:none",
|
|
45
|
+
`z-index:${zIndex}`,
|
|
46
|
+
"left:0",
|
|
47
|
+
"top:0",
|
|
48
|
+
"image-rendering:pixelated",
|
|
49
|
+
"transform-origin:100% 50%",
|
|
50
|
+
"filter:drop-shadow(0 0 6px color-mix(in srgb, var(--destructive) 45%, transparent))",
|
|
51
|
+
"will-change:transform"
|
|
52
|
+
].join(";");
|
|
53
|
+
laser.innerHTML = LASER_SVG;
|
|
54
|
+
return laser;
|
|
55
|
+
}
|
|
56
|
+
function mountLaserCursor(zIndex) {
|
|
57
|
+
const dot = makeDot(zIndex, DOT_SIZE, 1, true);
|
|
58
|
+
const trail = createTrail(zIndex);
|
|
59
|
+
const laser = createLaserBody(zIndex);
|
|
60
|
+
document.body.appendChild(laser);
|
|
61
|
+
for (const t of trail) {
|
|
62
|
+
document.body.appendChild(t);
|
|
63
|
+
}
|
|
64
|
+
document.body.appendChild(dot);
|
|
65
|
+
const previousCursor = document.body.style.getPropertyValue("cursor");
|
|
66
|
+
const previousPriority = document.body.style.getPropertyPriority("cursor");
|
|
67
|
+
document.body.style.setProperty("cursor", "none");
|
|
68
|
+
let pointerX = window.innerWidth / 2;
|
|
69
|
+
let pointerY = window.innerHeight / 2;
|
|
70
|
+
const history = [];
|
|
71
|
+
for (let i = 0; i <= TRAIL_LEN; i++) {
|
|
72
|
+
history.push({ x: pointerX, y: pointerY });
|
|
73
|
+
}
|
|
74
|
+
const positionLaser = () => {
|
|
75
|
+
const anchorX = ANCHOR_INSET + LASER_W;
|
|
76
|
+
const anchorY = ANCHOR_INSET + LASER_H / 2;
|
|
77
|
+
const dx = pointerX - anchorX;
|
|
78
|
+
const dy = pointerY - anchorY;
|
|
79
|
+
const angle = Math.atan2(dy, dx) * 180 / Math.PI + 180;
|
|
80
|
+
laser.style.transform = `translate(${ANCHOR_INSET}px, ${ANCHOR_INSET}px) rotate(${angle}deg)`;
|
|
81
|
+
};
|
|
82
|
+
let rafId = 0;
|
|
83
|
+
const tick = () => {
|
|
84
|
+
history.unshift({ x: pointerX, y: pointerY });
|
|
85
|
+
if (history.length > TRAIL_LEN + 1) {
|
|
86
|
+
history.pop();
|
|
87
|
+
}
|
|
88
|
+
const d0 = history[0];
|
|
89
|
+
dot.style.transform = `translate(-50%,-50%) translate(${d0.x}px, ${d0.y}px)`;
|
|
90
|
+
for (let i = 0; i < trail.length; i++) {
|
|
91
|
+
const p = history[i + 1] ?? history[history.length - 1];
|
|
92
|
+
trail[i].style.transform = `translate(-50%,-50%) translate(${p.x}px, ${p.y}px)`;
|
|
93
|
+
}
|
|
94
|
+
rafId = window.requestAnimationFrame(tick);
|
|
95
|
+
};
|
|
96
|
+
const onMove = (e) => {
|
|
97
|
+
pointerX = e.clientX;
|
|
98
|
+
pointerY = e.clientY;
|
|
99
|
+
positionLaser();
|
|
100
|
+
};
|
|
101
|
+
positionLaser();
|
|
102
|
+
rafId = window.requestAnimationFrame(tick);
|
|
103
|
+
document.addEventListener("mousemove", onMove);
|
|
104
|
+
window.addEventListener("resize", positionLaser);
|
|
105
|
+
window.addEventListener("scroll", positionLaser, { passive: true });
|
|
106
|
+
return () => {
|
|
107
|
+
window.cancelAnimationFrame(rafId);
|
|
108
|
+
document.removeEventListener("mousemove", onMove);
|
|
109
|
+
window.removeEventListener("resize", positionLaser);
|
|
110
|
+
window.removeEventListener("scroll", positionLaser);
|
|
111
|
+
if (dot.parentNode) {
|
|
112
|
+
dot.parentNode.removeChild(dot);
|
|
113
|
+
}
|
|
114
|
+
for (const t of trail) {
|
|
115
|
+
if (t.parentNode) {
|
|
116
|
+
t.parentNode.removeChild(t);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (laser.parentNode) {
|
|
120
|
+
laser.parentNode.removeChild(laser);
|
|
121
|
+
}
|
|
122
|
+
if (previousCursor) {
|
|
123
|
+
document.body.style.setProperty("cursor", previousCursor, previousPriority);
|
|
124
|
+
} else {
|
|
125
|
+
document.body.style.removeProperty("cursor");
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const TILE = 32;
|
|
130
|
+
const DEFAULT_Z_INDEX = 2147483646;
|
|
131
|
+
const STEER_LERP = 0.3;
|
|
132
|
+
const MAX_VEL_FACTOR = 1.5;
|
|
133
|
+
const OBSTACLE_INTERVAL = 30;
|
|
134
|
+
const MIN_OBSTACLE_AREA = 500;
|
|
135
|
+
const OBSTACLE_SELECTOR = "h1,h2,h3,h4,h5,h6,p,blockquote,a,button,img,svg,picture,video,nav,header,label,[role='button'],[data-oneko-obstacle]";
|
|
136
|
+
const CELL_SIZE = 16;
|
|
137
|
+
const SPRITE_RADIUS = 8;
|
|
138
|
+
const PATH_RECALC_INTERVAL = 10;
|
|
139
|
+
const PATH_RECALC_MOUSE_CELLS = 2;
|
|
140
|
+
const WAYPOINT_REACH_DIST = CELL_SIZE;
|
|
141
|
+
const BUBBLE_DISPLAY_FRAMES = 180;
|
|
142
|
+
const BUBBLE_COOLDOWN_FRAMES = 120;
|
|
143
|
+
const MOUSE_LOOP_MIN_STEP = 5;
|
|
144
|
+
const MOUSE_LOOP_WINDING_TRIGGER = 6.2;
|
|
145
|
+
const MOUSE_LOOP_WINDING_CAP = 13;
|
|
146
|
+
const MOUSE_LOOP_WINDING_DECAY = 0.96;
|
|
147
|
+
const IDLE_MESSAGES = [
|
|
148
|
+
"I am a curious cat Myra",
|
|
149
|
+
"waiting is an art form",
|
|
150
|
+
"pixels are just tiny warm squares",
|
|
151
|
+
"I see the cursor. I simply do not care",
|
|
152
|
+
"404: motivation not found",
|
|
153
|
+
"contemplating the void...",
|
|
154
|
+
"this spot is mine now",
|
|
155
|
+
"do not disturb. cat at work",
|
|
156
|
+
"I could move. but why",
|
|
157
|
+
"every pixel is a potential nap spot",
|
|
158
|
+
"hooman scroll too much",
|
|
159
|
+
"loading personality... 12%",
|
|
160
|
+
"technically I'm working",
|
|
161
|
+
"I'm not staring. I'm supervising",
|
|
162
|
+
"existential crisis in 3... 2...",
|
|
163
|
+
"local cat does nothing, is perfect",
|
|
164
|
+
"this is my emotional support pixel",
|
|
165
|
+
"I have opinions about your CSS",
|
|
166
|
+
"the internet is my territory",
|
|
167
|
+
"your scroll speed is offensive",
|
|
168
|
+
"have you tried turning it off and on again",
|
|
169
|
+
"I don't do mornings. or afternoons",
|
|
170
|
+
"this viewport could use more cat",
|
|
171
|
+
"meow means meow",
|
|
172
|
+
"I live here now. deal with it",
|
|
173
|
+
"my attention span is... oh look a pixel",
|
|
174
|
+
"human. hooman. same thing. irrelevant.",
|
|
175
|
+
"running on 1% battery and pure spite",
|
|
176
|
+
"I accept tribute in the form of attention"
|
|
177
|
+
];
|
|
178
|
+
const MOVING_MESSAGES = [
|
|
179
|
+
"stalking the cursor like it's prey",
|
|
180
|
+
"tiny hunter, big ambitions",
|
|
181
|
+
"tail up, confidence higher",
|
|
182
|
+
"pat pat pat — stealth engaged",
|
|
183
|
+
"whiskers first, questions later",
|
|
184
|
+
"almost caught it... almost",
|
|
185
|
+
"following my favorite hooman",
|
|
186
|
+
"zoomies (polite edition)",
|
|
187
|
+
"this carpet has excellent traction",
|
|
188
|
+
"ear twitch: target acquired",
|
|
189
|
+
"main character energy, four paws",
|
|
190
|
+
"excuse me, I'm busy chasing vibes",
|
|
191
|
+
"the dot moved, I moved — it's science",
|
|
192
|
+
"low crouch, high drama",
|
|
193
|
+
"slink mode: activated",
|
|
194
|
+
"padding along with purpose",
|
|
195
|
+
"if I fits, I sprints",
|
|
196
|
+
"practicing my runway walk",
|
|
197
|
+
"big stretch, bigger chase",
|
|
198
|
+
"focused. fluffy. unstoppable.",
|
|
199
|
+
"trotting toward mild chaos",
|
|
200
|
+
"pounce trajectory: instinct only",
|
|
201
|
+
"hunting the invisible red dot",
|
|
202
|
+
"catch me if you can (you can't)",
|
|
203
|
+
"left paw, right paw, repeat",
|
|
204
|
+
"look at me go!",
|
|
205
|
+
"I'm not lost, I'm exploring",
|
|
206
|
+
"on my way... eventually",
|
|
207
|
+
"they call it wandering, I call it art",
|
|
208
|
+
"my paws are doing their best"
|
|
209
|
+
];
|
|
210
|
+
const CIRCLE_LOOP_MESSAGES = [
|
|
211
|
+
"dizzy hooman detected",
|
|
212
|
+
"are we training for the Olympics",
|
|
213
|
+
"this is not a merry-go-round",
|
|
214
|
+
"pick a direction. any direction.",
|
|
215
|
+
"my inner ear is offended",
|
|
216
|
+
"stop orbiting me like a moon",
|
|
217
|
+
"you're making the room spin",
|
|
218
|
+
"that's enough circles for one day",
|
|
219
|
+
"I'm getting motion sickness up here",
|
|
220
|
+
"chasing your own tail much?",
|
|
221
|
+
"whiskers say: please stop the loop",
|
|
222
|
+
"clockwise, counter… pick one forever",
|
|
223
|
+
"the cursor is not a hula hoop",
|
|
224
|
+
"I can't settle on a nap with this chaos",
|
|
225
|
+
"tiny circles, big drama",
|
|
226
|
+
"you're stressing the carpet out",
|
|
227
|
+
"I'm a cat not a centrifuge",
|
|
228
|
+
"round and round we do NOT go",
|
|
229
|
+
"save some spins for the washing machine",
|
|
230
|
+
"my brain is doing donuts too now",
|
|
231
|
+
"vertical tail = concerned",
|
|
232
|
+
"this is a desk not a racetrack",
|
|
233
|
+
"I need a ginger snap for my tummy",
|
|
234
|
+
"hooman.exe has entered spin cycle",
|
|
235
|
+
"one lap was cute. this is a lot.",
|
|
236
|
+
"the red dot retired. you replaced it."
|
|
237
|
+
];
|
|
238
|
+
const SLEEPING_MESSAGES = [
|
|
239
|
+
"zzz...",
|
|
240
|
+
"five more minutes...",
|
|
241
|
+
"dreaming of fish",
|
|
242
|
+
"according to my calculations... nap time",
|
|
243
|
+
"nap.exe has stopped working",
|
|
244
|
+
"the mouse will wait. it always does",
|
|
245
|
+
"sleep is the best superpower",
|
|
246
|
+
"do not wake the cat",
|
|
247
|
+
"currently offline",
|
|
248
|
+
"battery at 2%",
|
|
249
|
+
"in a meeting (with my pillow)",
|
|
250
|
+
"auto-reply: gone fishing (in my dreams)",
|
|
251
|
+
"shhh... genius at rest",
|
|
252
|
+
"consciousness is overrated",
|
|
253
|
+
"nap-driven development",
|
|
254
|
+
"out of office until further notice",
|
|
255
|
+
"REM cycle in progress",
|
|
256
|
+
"do not disturb sign: activated",
|
|
257
|
+
"napping is my cardio",
|
|
258
|
+
"subconscious loading...",
|
|
259
|
+
"snore.wav playing",
|
|
260
|
+
"zoning out professionally",
|
|
261
|
+
"dreams: 100%, productivity: 0%",
|
|
262
|
+
"powered off. goodbye.",
|
|
263
|
+
"the pillow is winning"
|
|
264
|
+
];
|
|
265
|
+
const SCRATCHING_MESSAGES = [
|
|
266
|
+
"itchy spot right here",
|
|
267
|
+
"don't judge me",
|
|
268
|
+
"self-care is important",
|
|
269
|
+
"spa day!",
|
|
270
|
+
"found a speck... on me",
|
|
271
|
+
"adjusting the floof distribution",
|
|
272
|
+
"something tickled under the fur",
|
|
273
|
+
"scratching away a whole Tuesday",
|
|
274
|
+
"exfoliating like a pro",
|
|
275
|
+
"this is a feature not a bug"
|
|
276
|
+
];
|
|
277
|
+
const TIRED_MESSAGES = [
|
|
278
|
+
"I need a mass break",
|
|
279
|
+
"running on vibes and caffeine",
|
|
280
|
+
"can someone carry me",
|
|
281
|
+
"this is exhausting",
|
|
282
|
+
"why is the cursor so far",
|
|
283
|
+
"legs.exe needs an update",
|
|
284
|
+
"low power mode activated",
|
|
285
|
+
"I'm too pretty for this",
|
|
286
|
+
"yawn... where was I going",
|
|
287
|
+
"energy level: potato"
|
|
288
|
+
];
|
|
289
|
+
const WALL_SCRATCH_MESSAGES = [
|
|
290
|
+
"this wall has it coming",
|
|
291
|
+
"redecorating!",
|
|
292
|
+
"abstract art in progress",
|
|
293
|
+
"your walls needed texture",
|
|
294
|
+
"sharpening my claws for later",
|
|
295
|
+
"leaving my mark on this windowsill",
|
|
296
|
+
"vandalism? I call it expression",
|
|
297
|
+
"the wall started it",
|
|
298
|
+
"claiming this territory",
|
|
299
|
+
"I do my best work on walls"
|
|
300
|
+
];
|
|
301
|
+
const ALERT_MESSAGES = [
|
|
302
|
+
"what was that?!",
|
|
303
|
+
"I heard something!",
|
|
304
|
+
"ears: activated",
|
|
305
|
+
"threat level: probably nothing",
|
|
306
|
+
"investigating...",
|
|
307
|
+
"something moved!",
|
|
308
|
+
"on high alert!",
|
|
309
|
+
"defense mode engaged",
|
|
310
|
+
"my spidey senses are tingling",
|
|
311
|
+
"I swear I saw something"
|
|
312
|
+
];
|
|
313
|
+
const FREERUN_MESSAGES = [
|
|
314
|
+
"I don't follow rules ya silly hooman",
|
|
315
|
+
"corners are optional in my book",
|
|
316
|
+
"watch me ignore the floor plan",
|
|
317
|
+
"obstacles? you mean decorations?",
|
|
318
|
+
"rules are for dogs",
|
|
319
|
+
"I go where I please",
|
|
320
|
+
"your furniture layout is a suggestion",
|
|
321
|
+
"try to stop me hooman",
|
|
322
|
+
"boundaries are a social construct",
|
|
323
|
+
"I am the monarch of this room",
|
|
324
|
+
"shortcut time!",
|
|
325
|
+
"physics is optional... probably",
|
|
326
|
+
"forbidden spots taste better",
|
|
327
|
+
"you can't out-sneak a cat",
|
|
328
|
+
"slipping through impossible gaps",
|
|
329
|
+
"walls are just vertical carpets",
|
|
330
|
+
"catch me if you can",
|
|
331
|
+
"ghost mode: highly unsneaky",
|
|
332
|
+
"free roam, full drama"
|
|
333
|
+
];
|
|
334
|
+
const FREERUN_CHANCE = 0.06;
|
|
335
|
+
const FREERUN_DURATION = 40;
|
|
336
|
+
const DEFAULT_SPEED = 10;
|
|
337
|
+
const DEFAULT_SCALE = 1;
|
|
338
|
+
const DEFAULT_OPACITY = 1;
|
|
339
|
+
const DEFAULT_ROTATION_AMOUNT = 15;
|
|
340
|
+
const DEFAULT_IDLE_THRESHOLD_MS = 1e3;
|
|
341
|
+
const DEFAULT_VOLUME = 0.5;
|
|
342
|
+
const DEFAULT_BUBBLE_CHANCE = 0.5;
|
|
343
|
+
const DEFAULT_FOLLOW_DISTANCE = 20;
|
|
344
|
+
const DEFAULT_ANIMATION_SPEED = 1;
|
|
345
|
+
const PET_MESSAGES = [
|
|
346
|
+
"purrrr",
|
|
347
|
+
"right there. yes. there.",
|
|
348
|
+
"you may continue",
|
|
349
|
+
"head scritches accepted",
|
|
350
|
+
"*slow blink*",
|
|
351
|
+
"best hooman"
|
|
352
|
+
];
|
|
353
|
+
const PET_SOUND_POOL = [
|
|
354
|
+
"/cat-sounds/Cat_purr1.ogg",
|
|
355
|
+
"/cat-sounds/Cat_purr2.ogg",
|
|
356
|
+
"/cat-sounds/Cat_purreow1.ogg"
|
|
357
|
+
];
|
|
358
|
+
const LASER_CATCH_POOL = ["/cat-sounds/Cat_eat1.ogg", "/cat-sounds/Cat_eat2.ogg"];
|
|
359
|
+
const HISS_POOL = [
|
|
360
|
+
"/cat-sounds/Cat_hiss1.ogg",
|
|
361
|
+
"/cat-sounds/Cat_hiss2.ogg",
|
|
362
|
+
"/cat-sounds/Cat_hiss3.ogg"
|
|
363
|
+
];
|
|
364
|
+
const SOUND_POOLS = {
|
|
365
|
+
idle: [
|
|
366
|
+
"/cat-sounds/Cat_idle1.ogg",
|
|
367
|
+
"/cat-sounds/Cat_idle2.ogg",
|
|
368
|
+
"/cat-sounds/Cat_idle3.ogg",
|
|
369
|
+
"/cat-sounds/Cat_idle4.ogg"
|
|
370
|
+
],
|
|
371
|
+
moving: [
|
|
372
|
+
"/cat-sounds/Cat_baby_ambient1.ogg",
|
|
373
|
+
"/cat-sounds/Cat_baby_ambient2.ogg",
|
|
374
|
+
"/cat-sounds/Cat_baby_ambient3.ogg",
|
|
375
|
+
"/cat-sounds/Cat_baby_ambient4.ogg",
|
|
376
|
+
"/cat-sounds/Cat_baby_ambient5.ogg",
|
|
377
|
+
"/cat-sounds/Cat_baby_ambient6.ogg",
|
|
378
|
+
"/cat-sounds/Cat_baby_ambient7.ogg"
|
|
379
|
+
],
|
|
380
|
+
sleeping: ["/cat-sounds/Cat_purr1.ogg", "/cat-sounds/Cat_purr2.ogg", "/cat-sounds/Cat_purr3.ogg"],
|
|
381
|
+
scratchSelf: ["/cat-sounds/Cat_beg1.ogg", "/cat-sounds/Cat_beg2.ogg", "/cat-sounds/Cat_beg3.ogg"],
|
|
382
|
+
scratchWallN: HISS_POOL,
|
|
383
|
+
scratchWallS: HISS_POOL,
|
|
384
|
+
scratchWallE: HISS_POOL,
|
|
385
|
+
scratchWallW: HISS_POOL,
|
|
386
|
+
tired: ["/cat-sounds/Cat_purreow1.ogg", "/cat-sounds/Cat_purreow2.ogg"],
|
|
387
|
+
freerun: [
|
|
388
|
+
"/cat-sounds/Cat_royal_ambient1.ogg",
|
|
389
|
+
"/cat-sounds/Cat_royal_ambient2.ogg",
|
|
390
|
+
"/cat-sounds/Cat_royal_ambient3.ogg",
|
|
391
|
+
"/cat-sounds/Cat_royal_ambient4.ogg",
|
|
392
|
+
"/cat-sounds/Cat_royal_ambient5.ogg",
|
|
393
|
+
"/cat-sounds/Cat_royal_ambient6.ogg"
|
|
394
|
+
],
|
|
395
|
+
alert: [
|
|
396
|
+
"/cat-sounds/Stray_cat_idle1.ogg",
|
|
397
|
+
"/cat-sounds/Stray_cat_idle2.ogg",
|
|
398
|
+
"/cat-sounds/Stray_cat_idle3.ogg",
|
|
399
|
+
"/cat-sounds/Stray_cat_idle4.ogg"
|
|
400
|
+
]
|
|
401
|
+
};
|
|
402
|
+
const KONAMI_SEQUENCE = [
|
|
403
|
+
"ArrowUp",
|
|
404
|
+
"ArrowUp",
|
|
405
|
+
"ArrowDown",
|
|
406
|
+
"ArrowDown",
|
|
407
|
+
"ArrowLeft",
|
|
408
|
+
"ArrowRight",
|
|
409
|
+
"ArrowLeft",
|
|
410
|
+
"ArrowRight",
|
|
411
|
+
"KeyB",
|
|
412
|
+
"KeyA"
|
|
413
|
+
];
|
|
414
|
+
const KONAMI_TARGET = KONAMI_SEQUENCE.join(",");
|
|
415
|
+
const DIRECTION_RANGES = [
|
|
416
|
+
{ min: -22.5, max: 22.5, dir: "E" },
|
|
417
|
+
{ min: 22.5, max: 67.5, dir: "SE" },
|
|
418
|
+
{ min: 67.5, max: 112.5, dir: "S" },
|
|
419
|
+
{ min: 112.5, max: 157.5, dir: "SW" },
|
|
420
|
+
{ min: -157.5, max: -112.5, dir: "NW" },
|
|
421
|
+
{ min: -112.5, max: -67.5, dir: "N" },
|
|
422
|
+
{ min: -67.5, max: -22.5, dir: "NE" }
|
|
423
|
+
];
|
|
424
|
+
const NEIGHBOR_OFFSETS = [
|
|
425
|
+
[-1, 0, 1],
|
|
426
|
+
[1, 0, 1],
|
|
427
|
+
[0, -1, 1],
|
|
428
|
+
[0, 1, 1],
|
|
429
|
+
[-1, -1, Math.SQRT2],
|
|
430
|
+
[-1, 1, Math.SQRT2],
|
|
431
|
+
[1, -1, Math.SQRT2],
|
|
432
|
+
[1, 1, Math.SQRT2]
|
|
433
|
+
];
|
|
434
|
+
const defaultSpriteSets = {
|
|
435
|
+
idle: [[-3, -3]],
|
|
436
|
+
alert: [[-7, -3]],
|
|
437
|
+
scratchSelf: [
|
|
438
|
+
[-5, 0],
|
|
439
|
+
[-6, 0],
|
|
440
|
+
[-7, 0]
|
|
441
|
+
],
|
|
442
|
+
scratchWallN: [
|
|
443
|
+
[0, 0],
|
|
444
|
+
[0, -1]
|
|
445
|
+
],
|
|
446
|
+
scratchWallS: [
|
|
447
|
+
[-7, -1],
|
|
448
|
+
[-6, -2]
|
|
449
|
+
],
|
|
450
|
+
scratchWallE: [
|
|
451
|
+
[-2, -2],
|
|
452
|
+
[-2, -3]
|
|
453
|
+
],
|
|
454
|
+
scratchWallW: [
|
|
455
|
+
[-4, 0],
|
|
456
|
+
[-4, -1]
|
|
457
|
+
],
|
|
458
|
+
tired: [[-3, -2]],
|
|
459
|
+
sleeping: [
|
|
460
|
+
[-2, 0],
|
|
461
|
+
[-2, -1]
|
|
462
|
+
],
|
|
463
|
+
N: [
|
|
464
|
+
[-1, -2],
|
|
465
|
+
[-1, -3]
|
|
466
|
+
],
|
|
467
|
+
NE: [
|
|
468
|
+
[0, -2],
|
|
469
|
+
[0, -3]
|
|
470
|
+
],
|
|
471
|
+
E: [
|
|
472
|
+
[-3, 0],
|
|
473
|
+
[-3, -1]
|
|
474
|
+
],
|
|
475
|
+
SE: [
|
|
476
|
+
[-5, -1],
|
|
477
|
+
[-5, -2]
|
|
478
|
+
],
|
|
479
|
+
S: [
|
|
480
|
+
[-6, -3],
|
|
481
|
+
[-7, -2]
|
|
482
|
+
],
|
|
483
|
+
SW: [
|
|
484
|
+
[-5, -3],
|
|
485
|
+
[-6, -1]
|
|
486
|
+
],
|
|
487
|
+
W: [
|
|
488
|
+
[-4, -2],
|
|
489
|
+
[-4, -3]
|
|
490
|
+
],
|
|
491
|
+
NW: [
|
|
492
|
+
[-1, 0],
|
|
493
|
+
[-1, -1]
|
|
494
|
+
]
|
|
495
|
+
};
|
|
496
|
+
const IDLE_ANIMATION_DURATIONS = {
|
|
497
|
+
sleeping: 80,
|
|
498
|
+
scratchSelf: 24,
|
|
499
|
+
tired: 40,
|
|
500
|
+
scratchWallN: 24,
|
|
501
|
+
scratchWallS: 24,
|
|
502
|
+
scratchWallE: 24,
|
|
503
|
+
scratchWallW: 24
|
|
504
|
+
};
|
|
505
|
+
const debugColor = {
|
|
506
|
+
panelBg: "color-mix(in srgb, var(--background) 88%, transparent)",
|
|
507
|
+
panelBorder: "color-mix(in srgb, var(--border) 70%, transparent)",
|
|
508
|
+
panelText: "var(--foreground)",
|
|
509
|
+
panelSubtle: "var(--muted-foreground)",
|
|
510
|
+
controlBg: "color-mix(in srgb, var(--muted) 72%, transparent)",
|
|
511
|
+
controlBorder: "var(--border)",
|
|
512
|
+
accent: "var(--ring)",
|
|
513
|
+
accentStrong: "var(--primary)",
|
|
514
|
+
danger: "var(--destructive)",
|
|
515
|
+
dangerSoft: "color-mix(in srgb, var(--destructive) 30%, transparent)",
|
|
516
|
+
trail: "color-mix(in srgb, var(--ring) 60%, transparent)",
|
|
517
|
+
trailDot: "color-mix(in srgb, var(--ring) 76%, transparent)",
|
|
518
|
+
gridStroke: "color-mix(in srgb, var(--muted-foreground) 18%, transparent)"
|
|
519
|
+
};
|
|
520
|
+
const DEFAULT_POSITION = {
|
|
521
|
+
x: typeof window !== "undefined" ? window.innerWidth / 2 : 512,
|
|
522
|
+
y: typeof window !== "undefined" ? window.innerHeight / 2 : 384
|
|
523
|
+
};
|
|
524
|
+
const ONEKO_DEFAULTS = {
|
|
525
|
+
paused: false,
|
|
526
|
+
followCursor: true,
|
|
527
|
+
sleepEnabled: true,
|
|
528
|
+
bubblePlacement: "auto",
|
|
529
|
+
bubbleScale: 1,
|
|
530
|
+
soundBasePath: "/cat-sounds",
|
|
531
|
+
storageKey: "oneko",
|
|
532
|
+
zoneAttractionChance: 0.3,
|
|
533
|
+
zoneAttractionDuration: 4e3,
|
|
534
|
+
skin: "classic",
|
|
535
|
+
persistPosition: true,
|
|
536
|
+
reducedMotion: "hide",
|
|
537
|
+
zIndex: DEFAULT_Z_INDEX,
|
|
538
|
+
speed: DEFAULT_SPEED,
|
|
539
|
+
scale: DEFAULT_SCALE,
|
|
540
|
+
opacity: DEFAULT_OPACITY,
|
|
541
|
+
rotationAmount: DEFAULT_ROTATION_AMOUNT,
|
|
542
|
+
idleThreshold: DEFAULT_IDLE_THRESHOLD_MS,
|
|
543
|
+
meow: false,
|
|
544
|
+
freerunChance: FREERUN_CHANCE,
|
|
545
|
+
freerunDuration: FREERUN_DURATION,
|
|
546
|
+
bubbleEnabled: true,
|
|
547
|
+
bubbleDisplayFrames: BUBBLE_DISPLAY_FRAMES,
|
|
548
|
+
bubbleCooldown: BUBBLE_COOLDOWN_FRAMES,
|
|
549
|
+
hueRotate: 0,
|
|
550
|
+
bubbleChance: DEFAULT_BUBBLE_CHANCE,
|
|
551
|
+
followDistance: DEFAULT_FOLLOW_DISTANCE,
|
|
552
|
+
animationSpeed: DEFAULT_ANIMATION_SPEED,
|
|
553
|
+
bubbleText: "",
|
|
554
|
+
volume: DEFAULT_VOLUME,
|
|
555
|
+
laserPointer: false,
|
|
556
|
+
chase: "cursor",
|
|
557
|
+
napWhileTyping: false,
|
|
558
|
+
followFocus: false,
|
|
559
|
+
perchOnHover: false,
|
|
560
|
+
pettable: false
|
|
561
|
+
};
|
|
562
|
+
function isValidRect(rect) {
|
|
563
|
+
return Object.values(rect).every(Number.isFinite) && rect.right > rect.left && rect.bottom > rect.top;
|
|
564
|
+
}
|
|
565
|
+
function expandRect(rect, padding) {
|
|
566
|
+
return {
|
|
567
|
+
left: rect.left - padding,
|
|
568
|
+
top: rect.top - padding,
|
|
569
|
+
right: rect.right + padding,
|
|
570
|
+
bottom: rect.bottom + padding
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function containsPoint(rect, point) {
|
|
574
|
+
return point.x > rect.left && point.x < rect.right && point.y > rect.top && point.y < rect.bottom;
|
|
575
|
+
}
|
|
576
|
+
function catZoneRadius(scale) {
|
|
577
|
+
return TILE / 2 * Math.max(0.1, Number.isFinite(scale) ? scale : 1) * Math.SQRT2 + 1;
|
|
578
|
+
}
|
|
579
|
+
const elementIds = /* @__PURE__ */ new WeakMap();
|
|
580
|
+
let nextElementId = 0;
|
|
581
|
+
function zoneElementId(element) {
|
|
582
|
+
if (!elementIds.has(element)) elementIds.set(element, ++nextElementId);
|
|
583
|
+
return elementIds.get(element);
|
|
584
|
+
}
|
|
585
|
+
function collectZones(zones) {
|
|
586
|
+
const result = [];
|
|
587
|
+
const add = (id, type, rect, padding = 0) => {
|
|
588
|
+
if (!isValidRect(rect)) return;
|
|
589
|
+
const extra = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
590
|
+
result.push({ id, type, rect: expandRect(rect, type === "avoid" ? extra : 0) });
|
|
591
|
+
};
|
|
592
|
+
const addElement = (id, type, element, padding = 0) => {
|
|
593
|
+
const style = getComputedStyle(element);
|
|
594
|
+
if (style.visibility === "hidden" || style.display === "none") return;
|
|
595
|
+
const { left, top, right, bottom } = element.getBoundingClientRect();
|
|
596
|
+
add(id, type, { left, top, right, bottom }, padding);
|
|
597
|
+
};
|
|
598
|
+
for (const zone of zones) {
|
|
599
|
+
if (zone.type !== "avoid" && zone.type !== "attract") continue;
|
|
600
|
+
if (zone.selector) {
|
|
601
|
+
try {
|
|
602
|
+
document.querySelectorAll(zone.selector).forEach(
|
|
603
|
+
(element) => addElement(`${zone.id}:${zoneElementId(element)}`, zone.type, element, zone.padding)
|
|
604
|
+
);
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
} else if (zone.rect) add(zone.id, zone.type, zone.rect, zone.padding);
|
|
608
|
+
}
|
|
609
|
+
document.querySelectorAll('[data-oneko-zone="avoid"], [data-oneko-zone="attract"]').forEach((element) => {
|
|
610
|
+
addElement(
|
|
611
|
+
`data-zone:${zoneElementId(element)}`,
|
|
612
|
+
element.getAttribute("data-oneko-zone"),
|
|
613
|
+
element
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
function nearestSafePoint(point, blocked, width, height, margin) {
|
|
619
|
+
if (width < margin * 2 || height < margin * 2) return null;
|
|
620
|
+
const clampX = (x) => Math.max(margin, Math.min(width - margin, x));
|
|
621
|
+
const clampY = (y) => Math.max(margin, Math.min(height - margin, y));
|
|
622
|
+
const origin = { x: clampX(point.x), y: clampY(point.y) };
|
|
623
|
+
if (!blocked.some((rect) => containsPoint(rect, origin))) return origin;
|
|
624
|
+
const xs = [
|
|
625
|
+
origin.x,
|
|
626
|
+
margin,
|
|
627
|
+
width - margin,
|
|
628
|
+
...blocked.flatMap((rect) => [rect.left, rect.right])
|
|
629
|
+
].map(clampX);
|
|
630
|
+
const ys = [
|
|
631
|
+
origin.y,
|
|
632
|
+
margin,
|
|
633
|
+
height - margin,
|
|
634
|
+
...blocked.flatMap((rect) => [rect.top, rect.bottom])
|
|
635
|
+
].map(clampY);
|
|
636
|
+
let closest = null;
|
|
637
|
+
let distance = Infinity;
|
|
638
|
+
for (const x of xs)
|
|
639
|
+
for (const y of ys) {
|
|
640
|
+
const candidate = { x, y };
|
|
641
|
+
if (blocked.some((rect) => containsPoint(rect, candidate))) continue;
|
|
642
|
+
const nextDistance = Math.hypot(x - point.x, y - point.y);
|
|
643
|
+
if (nextDistance < distance) {
|
|
644
|
+
distance = nextDistance;
|
|
645
|
+
closest = candidate;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return closest;
|
|
649
|
+
}
|
|
650
|
+
function constrainZoneMovement(from, to, blocked) {
|
|
651
|
+
let limit = 1;
|
|
652
|
+
const dx = to.x - from.x, dy = to.y - from.y;
|
|
653
|
+
for (const rect of blocked) {
|
|
654
|
+
let enter = -Infinity, leave = Infinity;
|
|
655
|
+
let misses = false;
|
|
656
|
+
for (const [position, delta, min, max] of [
|
|
657
|
+
[from.x, dx, rect.left, rect.right],
|
|
658
|
+
[from.y, dy, rect.top, rect.bottom]
|
|
659
|
+
]) {
|
|
660
|
+
if (Math.abs(delta) < 1e-9) {
|
|
661
|
+
if (position <= min || position >= max) {
|
|
662
|
+
misses = true;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
const a = (min - position) / delta, b = (max - position) / delta;
|
|
667
|
+
enter = Math.max(enter, Math.min(a, b));
|
|
668
|
+
leave = Math.min(leave, Math.max(a, b));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!misses && enter < leave && leave > 0 && enter >= 0 && enter < limit)
|
|
672
|
+
limit = Math.max(0, enter - 1e-3);
|
|
673
|
+
}
|
|
674
|
+
return { x: from.x + dx * limit, y: from.y + dy * limit };
|
|
675
|
+
}
|
|
676
|
+
const EMPTY_ZONES = [];
|
|
677
|
+
function createZoneState() {
|
|
678
|
+
return {
|
|
679
|
+
definitions: EMPTY_ZONES,
|
|
680
|
+
blocked: [],
|
|
681
|
+
favorites: [],
|
|
682
|
+
signature: "",
|
|
683
|
+
targetId: null,
|
|
684
|
+
visitFrames: 0,
|
|
685
|
+
cooldownFrames: 50,
|
|
686
|
+
chance: 0.3,
|
|
687
|
+
duration: 40,
|
|
688
|
+
movementTarget: { x: 0, y: 0 }
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function refreshZones(deps) {
|
|
692
|
+
const s = deps.stateRef.current, zones = s.zoneState;
|
|
693
|
+
const resolved = collectZones(zones.definitions);
|
|
694
|
+
const radius = catZoneRadius(s.scale);
|
|
695
|
+
const signature = JSON.stringify([resolved, radius]);
|
|
696
|
+
if (signature !== zones.signature) {
|
|
697
|
+
zones.signature = signature;
|
|
698
|
+
zones.blocked = resolved.filter((zone) => zone.type === "avoid").map((zone) => expandRect(zone.rect, radius));
|
|
699
|
+
zones.favorites = resolved.filter((zone) => zone.type === "attract");
|
|
700
|
+
s.lastObstacleRefresh = -OBSTACLE_INTERVAL;
|
|
701
|
+
s.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
|
|
702
|
+
}
|
|
703
|
+
if (!zones.blocked.length) {
|
|
704
|
+
deps.el.style.visibility = "";
|
|
705
|
+
deps.bubbleEl.style.visibility = "";
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
const safe = nearestSafePoint(
|
|
709
|
+
{ x: s.nekoPosX, y: s.nekoPosY },
|
|
710
|
+
zones.blocked,
|
|
711
|
+
window.innerWidth,
|
|
712
|
+
window.innerHeight,
|
|
713
|
+
radius
|
|
714
|
+
);
|
|
715
|
+
deps.el.style.visibility = safe ? "" : "hidden";
|
|
716
|
+
deps.bubbleEl.style.visibility = safe ? "" : "hidden";
|
|
717
|
+
if (!safe) {
|
|
718
|
+
s.nekoVelX = 0;
|
|
719
|
+
s.nekoVelY = 0;
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
if (safe.x !== s.nekoPosX || safe.y !== s.nekoPosY) {
|
|
723
|
+
s.nekoPosX = safe.x;
|
|
724
|
+
s.nekoPosY = safe.y;
|
|
725
|
+
s.nekoVelX = 0;
|
|
726
|
+
s.nekoVelY = 0;
|
|
727
|
+
deps.el.style.left = `${safe.x - TILE / 2}px`;
|
|
728
|
+
deps.el.style.top = `${safe.y - TILE / 2}px`;
|
|
729
|
+
s.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
|
|
730
|
+
}
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
function updateZoneTarget(s, width, height, random = Math.random) {
|
|
734
|
+
const zones = s.zoneState;
|
|
735
|
+
const oldId = zones.targetId;
|
|
736
|
+
const radius = catZoneRadius(s.scale);
|
|
737
|
+
const favorites = zones.favorites.filter((zone) => {
|
|
738
|
+
const point = {
|
|
739
|
+
x: (zone.rect.left + zone.rect.right) / 2,
|
|
740
|
+
y: (zone.rect.top + zone.rect.bottom) / 2
|
|
741
|
+
};
|
|
742
|
+
return point.x >= radius && point.x <= width - radius && point.y >= radius && point.y <= height - radius && !zones.blocked.some((rect) => containsPoint(rect, point));
|
|
743
|
+
});
|
|
744
|
+
if (!s.followCursorCfg || s.laserPointerCfg || s.noFollow || s.stateLocked || zones.chance === 0) {
|
|
745
|
+
zones.targetId = null;
|
|
746
|
+
zones.visitFrames = 0;
|
|
747
|
+
zones.cooldownFrames = 50;
|
|
748
|
+
} else if (zones.targetId) {
|
|
749
|
+
zones.visitFrames--;
|
|
750
|
+
if (zones.visitFrames <= 0 || !favorites.some((zone) => zone.id === zones.targetId)) {
|
|
751
|
+
zones.targetId = null;
|
|
752
|
+
zones.cooldownFrames = 80;
|
|
753
|
+
}
|
|
754
|
+
} else if (--zones.cooldownFrames <= 0) {
|
|
755
|
+
zones.cooldownFrames = 50;
|
|
756
|
+
if (favorites.length && random() < zones.chance) {
|
|
757
|
+
const index = Math.min(favorites.length - 1, Math.floor(random() * favorites.length));
|
|
758
|
+
zones.targetId = favorites[index].id;
|
|
759
|
+
zones.visitFrames = zones.duration;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const favorite = favorites.find((zone) => zone.id === zones.targetId);
|
|
763
|
+
const desired = !s.followCursorCfg || s.typingNap || s.held ? { x: s.nekoPosX, y: s.nekoPosY } : s.perchTarget ?? (favorite ? {
|
|
764
|
+
x: (favorite.rect.left + favorite.rect.right) / 2,
|
|
765
|
+
y: (favorite.rect.top + favorite.rect.bottom) / 2
|
|
766
|
+
} : s.leaderTarget ?? { x: s.mousePosX, y: s.mousePosY });
|
|
767
|
+
zones.movementTarget = zones.blocked.length ? nearestSafePoint(desired, zones.blocked, width, height, radius) ?? {
|
|
768
|
+
x: s.nekoPosX,
|
|
769
|
+
y: s.nekoPosY
|
|
770
|
+
} : desired;
|
|
771
|
+
if (oldId !== zones.targetId) s.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
|
|
772
|
+
}
|
|
773
|
+
function pickFromPool(deps, pool) {
|
|
774
|
+
const s = deps.stateRef.current;
|
|
775
|
+
let idx = Math.floor(Math.random() * pool.length);
|
|
776
|
+
if (idx === s.lastBubbleMsg) {
|
|
777
|
+
idx = (idx + 1) % pool.length;
|
|
778
|
+
}
|
|
779
|
+
s.lastBubbleMsg = idx;
|
|
780
|
+
return pool[idx];
|
|
781
|
+
}
|
|
782
|
+
function messageForIdleAnimation(deps) {
|
|
783
|
+
const s = deps.stateRef.current;
|
|
784
|
+
if (s.idleAnimation === "sleeping") {
|
|
785
|
+
return pickFromPool(deps, SLEEPING_MESSAGES);
|
|
786
|
+
}
|
|
787
|
+
if (typeof s.customBubbleText === "string") {
|
|
788
|
+
if (s.customBubbleText.trim()) return s.customBubbleText;
|
|
789
|
+
} else {
|
|
790
|
+
const thoughts = [...new Set(s.customBubbleText.filter((text) => text.trim()))];
|
|
791
|
+
if (thoughts.length) return pickFromPool(deps, thoughts);
|
|
792
|
+
}
|
|
793
|
+
if (s.idleAnimation === "tired") {
|
|
794
|
+
return pickFromPool(deps, TIRED_MESSAGES);
|
|
795
|
+
}
|
|
796
|
+
if (s.idleAnimation === "scratchSelf") {
|
|
797
|
+
return pickFromPool(deps, SCRATCHING_MESSAGES);
|
|
798
|
+
}
|
|
799
|
+
if (s.idleAnimation === "alert") {
|
|
800
|
+
return pickFromPool(deps, ALERT_MESSAGES);
|
|
801
|
+
}
|
|
802
|
+
if (s.idleAnimation?.startsWith("scratchWall")) {
|
|
803
|
+
return pickFromPool(deps, WALL_SCRATCH_MESSAGES);
|
|
804
|
+
}
|
|
805
|
+
return void 0;
|
|
806
|
+
}
|
|
807
|
+
function messageForActivity(deps) {
|
|
808
|
+
const s = deps.stateRef.current;
|
|
809
|
+
const idleAnimationMessage = messageForIdleAnimation(deps);
|
|
810
|
+
if (idleAnimationMessage) {
|
|
811
|
+
return idleAnimationMessage;
|
|
812
|
+
}
|
|
813
|
+
if (s.idleTime > 0) {
|
|
814
|
+
return pickFromPool(deps, IDLE_MESSAGES);
|
|
815
|
+
}
|
|
816
|
+
if (Math.abs(s.mouseCircleWinding) >= MOUSE_LOOP_WINDING_TRIGGER) {
|
|
817
|
+
return pickFromPool(deps, CIRCLE_LOOP_MESSAGES);
|
|
818
|
+
}
|
|
819
|
+
return pickFromPool(deps, MOVING_MESSAGES);
|
|
820
|
+
}
|
|
821
|
+
function pickBubbleMessage(deps) {
|
|
822
|
+
return messageForActivity(deps);
|
|
823
|
+
}
|
|
824
|
+
function showBubble(deps) {
|
|
825
|
+
const s = deps.stateRef.current;
|
|
826
|
+
s.bubbleVisible = true;
|
|
827
|
+
s.bubbleTimer = s.bubbleDisplayFramesCfg;
|
|
828
|
+
deps.bubbleTextEl.textContent = pickBubbleMessage(deps);
|
|
829
|
+
deps.bubbleEl.style.opacity = "1";
|
|
830
|
+
}
|
|
831
|
+
function hideBubble(deps) {
|
|
832
|
+
const s = deps.stateRef.current;
|
|
833
|
+
s.bubbleVisible = false;
|
|
834
|
+
deps.bubbleEl.style.opacity = "0";
|
|
835
|
+
s.bubbleCooldown = s.bubbleCooldownFramesCfg;
|
|
836
|
+
}
|
|
837
|
+
function applyBubblePlacement(deps, showBelow) {
|
|
838
|
+
deps.bubbleEl.style.transformOrigin = "top center";
|
|
839
|
+
deps.bubbleEl.style.flexDirection = showBelow ? "column-reverse" : "column";
|
|
840
|
+
deps.bubbleTail.style.cssText = showBelow ? "transform:scaleY(-1);margin-top:0;margin-bottom:-2px;" : "transform:none;margin-top:-2px;margin-bottom:0;";
|
|
841
|
+
}
|
|
842
|
+
function positionBubble(deps) {
|
|
843
|
+
const s = deps.stateRef.current;
|
|
844
|
+
const catX = Math.floor(s.nekoPosX);
|
|
845
|
+
const catY = Math.floor(s.nekoPosY);
|
|
846
|
+
const vw = window.innerWidth;
|
|
847
|
+
const vh = window.innerHeight;
|
|
848
|
+
const bubbleW = deps.bubbleEl.offsetWidth || deps.bubbleTextEl.offsetWidth || 100;
|
|
849
|
+
const bubbleH = deps.bubbleEl.offsetHeight || 34;
|
|
850
|
+
const bubbleScale = s.scale * s.bubbleScaleCfg;
|
|
851
|
+
const margin = 6;
|
|
852
|
+
const halfCat = TILE * s.scale / 2;
|
|
853
|
+
const aboveY = Math.floor(catY - halfCat - 8 - bubbleH * bubbleScale);
|
|
854
|
+
const belowY = Math.floor(catY + halfCat + 8);
|
|
855
|
+
const showBelow = s.bubblePlacementCfg === "below" || s.bubblePlacementCfg === "auto" && aboveY < margin && catY < vh / 2;
|
|
856
|
+
const top = Math.max(
|
|
857
|
+
margin,
|
|
858
|
+
Math.min(vh - margin - bubbleH * bubbleScale, showBelow ? belowY : aboveY)
|
|
859
|
+
);
|
|
860
|
+
deps.bubbleEl.style.top = `${Math.round(top)}px`;
|
|
861
|
+
deps.bubbleEl.style.transform = `translateX(-50%) scale(${bubbleScale})`;
|
|
862
|
+
applyBubblePlacement(deps, showBelow);
|
|
863
|
+
const halfBubble = bubbleW * bubbleScale / 2;
|
|
864
|
+
let left = catX;
|
|
865
|
+
if (halfBubble * 2 > vw - margin * 2) {
|
|
866
|
+
left = vw / 2;
|
|
867
|
+
} else if (catX - halfBubble < margin) {
|
|
868
|
+
left = margin + halfBubble;
|
|
869
|
+
} else if (catX + halfBubble > vw - margin) {
|
|
870
|
+
left = vw - margin - halfBubble;
|
|
871
|
+
}
|
|
872
|
+
deps.bubbleEl.style.left = `${Math.round(left)}px`;
|
|
873
|
+
}
|
|
874
|
+
function tickVisibleBubble(deps) {
|
|
875
|
+
const s = deps.stateRef.current;
|
|
876
|
+
s.bubbleTimer -= 1;
|
|
877
|
+
if (s.bubbleTimer <= 0) {
|
|
878
|
+
hideBubble(deps);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function tickBubbleCooldown(deps) {
|
|
882
|
+
deps.stateRef.current.bubbleCooldown -= 1;
|
|
883
|
+
}
|
|
884
|
+
function shouldTriggerBubble(deps) {
|
|
885
|
+
const s = deps.stateRef.current;
|
|
886
|
+
const idleFrameThreshold2 = Math.max(1, Math.round(s.idleThresholdMs / 100));
|
|
887
|
+
const idleRate = s.bubbleChanceCfg * 0.16;
|
|
888
|
+
const moveRate = s.bubbleChanceCfg * 0.01;
|
|
889
|
+
const loopW = Math.abs(s.mouseCircleWinding);
|
|
890
|
+
const loopComplaining = s.idleTime === 0 && loopW >= MOUSE_LOOP_WINDING_TRIGGER;
|
|
891
|
+
const moveRateBoost = loopComplaining ? 4.2 : 1;
|
|
892
|
+
if (s.idleTime > idleFrameThreshold2 && Math.random() < idleRate) {
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
return s.idleTime === 0 && Math.random() < moveRate * moveRateBoost;
|
|
896
|
+
}
|
|
897
|
+
function updateBubble(deps) {
|
|
898
|
+
const s = deps.stateRef.current;
|
|
899
|
+
if (!s.bubbleEnabledCfg) {
|
|
900
|
+
if (s.bubbleVisible) {
|
|
901
|
+
hideBubble(deps);
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
positionBubble(deps);
|
|
906
|
+
if (s.bubbleVisible) {
|
|
907
|
+
tickVisibleBubble(deps);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (s.bubbleCooldown > 0) {
|
|
911
|
+
tickBubbleCooldown(deps);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (shouldTriggerBubble(deps)) {
|
|
915
|
+
showBubble(deps);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const pack = [];
|
|
919
|
+
const firstSeen = /* @__PURE__ */ new WeakMap();
|
|
920
|
+
let arrivals = 0;
|
|
921
|
+
function joinPack(state) {
|
|
922
|
+
if (!firstSeen.has(state)) firstSeen.set(state, arrivals++);
|
|
923
|
+
if (!pack.includes(state)) {
|
|
924
|
+
pack.push(state);
|
|
925
|
+
pack.sort((a, b) => firstSeen.get(a) - firstSeen.get(b));
|
|
926
|
+
}
|
|
927
|
+
return () => {
|
|
928
|
+
const index = pack.indexOf(state);
|
|
929
|
+
if (index !== -1) pack.splice(index, 1);
|
|
930
|
+
for (const cat of pack) cat.leaderTarget = null;
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function leaderPosition(state) {
|
|
934
|
+
if (state.chaseCfg !== "cat") return null;
|
|
935
|
+
const leader = pack[pack.indexOf(state) - 1];
|
|
936
|
+
return leader ? { x: leader.nekoPosX, y: leader.nekoPosY } : null;
|
|
937
|
+
}
|
|
938
|
+
function napmateNearby(state) {
|
|
939
|
+
const reach = TILE * Math.max(1, state.scale) * 2;
|
|
940
|
+
return pack.some(
|
|
941
|
+
(cat) => cat !== state && cat.idleAnimation === "sleeping" && Math.hypot(cat.nekoPosX - state.nekoPosX, cat.nekoPosY - state.nekoPosY) < reach
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const HIDDEN_STORAGE_KEY = "oneko:hidden";
|
|
945
|
+
const HIDDEN_EVENT = "oneko:hiddenchange";
|
|
946
|
+
let hiddenWithoutStorage = false;
|
|
947
|
+
function areCatsHidden() {
|
|
948
|
+
try {
|
|
949
|
+
return localStorage.getItem(HIDDEN_STORAGE_KEY) === "1";
|
|
950
|
+
} catch {
|
|
951
|
+
return hiddenWithoutStorage;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function setCatsHidden(hidden) {
|
|
955
|
+
hiddenWithoutStorage = hidden;
|
|
956
|
+
try {
|
|
957
|
+
if (hidden) localStorage.setItem(HIDDEN_STORAGE_KEY, "1");
|
|
958
|
+
else localStorage.removeItem(HIDDEN_STORAGE_KEY);
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
window.dispatchEvent(new CustomEvent(HIDDEN_EVENT, { detail: { hidden } }));
|
|
962
|
+
}
|
|
963
|
+
function onCatsHiddenChange(callback) {
|
|
964
|
+
const onStorage = (event) => {
|
|
965
|
+
if (event.key === HIDDEN_STORAGE_KEY || event.key === null) callback();
|
|
966
|
+
};
|
|
967
|
+
window.addEventListener(HIDDEN_EVENT, callback);
|
|
968
|
+
window.addEventListener("storage", onStorage);
|
|
969
|
+
return () => {
|
|
970
|
+
window.removeEventListener(HIDDEN_EVENT, callback);
|
|
971
|
+
window.removeEventListener("storage", onStorage);
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function getActivityLabel(deps) {
|
|
975
|
+
const s = deps.stateRef.current;
|
|
976
|
+
if (s.freerunMode) {
|
|
977
|
+
return "freerun";
|
|
978
|
+
}
|
|
979
|
+
if (s.idleAnimation === "sleeping") {
|
|
980
|
+
return "sleeping";
|
|
981
|
+
}
|
|
982
|
+
if (s.idleAnimation) {
|
|
983
|
+
return s.idleAnimation;
|
|
984
|
+
}
|
|
985
|
+
if (s.idleTime > 0) {
|
|
986
|
+
return "idle";
|
|
987
|
+
}
|
|
988
|
+
return "moving";
|
|
989
|
+
}
|
|
990
|
+
function getBubbleStatus(s) {
|
|
991
|
+
if (s.bubbleVisible) {
|
|
992
|
+
return "showing";
|
|
993
|
+
}
|
|
994
|
+
if (s.bubbleCooldown > 0) {
|
|
995
|
+
return `cd ${s.bubbleCooldown}f`;
|
|
996
|
+
}
|
|
997
|
+
return "ready";
|
|
998
|
+
}
|
|
999
|
+
function playSound(deps, pool) {
|
|
1000
|
+
const s = deps.stateRef.current;
|
|
1001
|
+
if (!s.enableMeow || s.soundCooldown > 0 || pool.length === 0) return;
|
|
1002
|
+
const url = pool[Math.floor(Math.random() * pool.length)];
|
|
1003
|
+
const filename = url.slice(url.lastIndexOf("/") + 1);
|
|
1004
|
+
const audio = new Audio(`${s.soundBasePathCfg}/${filename}`);
|
|
1005
|
+
audio.volume = s.soundVolumeCfg;
|
|
1006
|
+
s.soundCooldown = 20;
|
|
1007
|
+
audio.play().catch(() => {
|
|
1008
|
+
s.soundCooldown = 0;
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
const PERCH_SELECTOR = "a[href],button,[role='button'],[data-oneko-perch]";
|
|
1012
|
+
const PERCH_DELAY_MS = 700;
|
|
1013
|
+
const NOT_TYPED_INPUTS = /* @__PURE__ */ new Set([
|
|
1014
|
+
"button",
|
|
1015
|
+
"checkbox",
|
|
1016
|
+
"color",
|
|
1017
|
+
"file",
|
|
1018
|
+
"image",
|
|
1019
|
+
"radio",
|
|
1020
|
+
"range",
|
|
1021
|
+
"reset",
|
|
1022
|
+
"submit"
|
|
1023
|
+
]);
|
|
1024
|
+
function stopDistance(s) {
|
|
1025
|
+
if (s.perchTarget) return Math.min(s.followDistanceCfg, 8);
|
|
1026
|
+
if (s.leaderTarget) return Math.max(s.followDistanceCfg, TILE * s.scale);
|
|
1027
|
+
return s.followDistanceCfg;
|
|
1028
|
+
}
|
|
1029
|
+
function tickAttention(deps) {
|
|
1030
|
+
const s = deps.stateRef.current;
|
|
1031
|
+
s.leaderTarget = leaderPosition(s);
|
|
1032
|
+
if (s.typingNap && s.sleepEnabledCfg && s.idleAnimation !== "sleeping") {
|
|
1033
|
+
s.freerunMode = false;
|
|
1034
|
+
s.idleAnimation = "sleeping";
|
|
1035
|
+
s.idleAnimationFrame = 0;
|
|
1036
|
+
s.idleTime = Math.max(s.idleTime, 1);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function isTypingTarget(target) {
|
|
1040
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
1041
|
+
if (target.isContentEditable || target instanceof HTMLTextAreaElement) return true;
|
|
1042
|
+
return target instanceof HTMLInputElement && !NOT_TYPED_INPUTS.has(target.type);
|
|
1043
|
+
}
|
|
1044
|
+
function isTypingKey(event) {
|
|
1045
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
1046
|
+
return event.key.length === 1 || event.key === "Backspace" || event.key === "Delete";
|
|
1047
|
+
}
|
|
1048
|
+
function matchesFocusVisible(el) {
|
|
1049
|
+
try {
|
|
1050
|
+
return el.matches(":focus-visible");
|
|
1051
|
+
} catch {
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function watchPage(deps) {
|
|
1056
|
+
const s = () => deps.stateRef.current;
|
|
1057
|
+
let perchTimer;
|
|
1058
|
+
let napAt = null;
|
|
1059
|
+
const onKeyDown = (event) => {
|
|
1060
|
+
const state = s();
|
|
1061
|
+
if (!state.napWhileTypingCfg || !isTypingTarget(event.target) || !isTypingKey(event)) return;
|
|
1062
|
+
state.typingNap = true;
|
|
1063
|
+
state.perchTarget = null;
|
|
1064
|
+
napAt = { x: state.mousePosX, y: state.mousePosY };
|
|
1065
|
+
};
|
|
1066
|
+
const onMouseMove = (event) => {
|
|
1067
|
+
const state = s();
|
|
1068
|
+
if (state.typingNap && (!napAt || Math.hypot(event.clientX - napAt.x, event.clientY - napAt.y) > 4)) {
|
|
1069
|
+
state.typingNap = false;
|
|
1070
|
+
}
|
|
1071
|
+
state.perchTarget = null;
|
|
1072
|
+
clearTimeout(perchTimer);
|
|
1073
|
+
if (!state.perchOnHoverCfg) return;
|
|
1074
|
+
const { clientX: x, clientY: y } = event;
|
|
1075
|
+
perchTimer = setTimeout(() => {
|
|
1076
|
+
const current = s();
|
|
1077
|
+
const target = document.elementFromPoint(x, y)?.closest(PERCH_SELECTOR);
|
|
1078
|
+
if (!current.perchOnHoverCfg || !target || target === deps.el) return;
|
|
1079
|
+
const rect = target.getBoundingClientRect();
|
|
1080
|
+
if (rect.width === 0 || rect.width > window.innerWidth * 0.6) return;
|
|
1081
|
+
current.perchTarget = {
|
|
1082
|
+
x: rect.left + rect.width / 2,
|
|
1083
|
+
y: Math.max(16, rect.top - TILE * current.scale / 2 + 4)
|
|
1084
|
+
};
|
|
1085
|
+
}, PERCH_DELAY_MS);
|
|
1086
|
+
};
|
|
1087
|
+
const onFocusIn = (event) => {
|
|
1088
|
+
const state = s();
|
|
1089
|
+
const target = event.target;
|
|
1090
|
+
if (!state.followFocusCfg || !(target instanceof Element) || target === deps.el) return;
|
|
1091
|
+
if (!matchesFocusVisible(target)) return;
|
|
1092
|
+
const rect = target.getBoundingClientRect();
|
|
1093
|
+
const reach = TILE * state.scale / 2 + 8;
|
|
1094
|
+
const below = rect.bottom + reach;
|
|
1095
|
+
state.mousePosX = Math.min(window.innerWidth - 16, Math.max(16, rect.left + rect.width / 2));
|
|
1096
|
+
state.mousePosY = below < window.innerHeight - 16 ? below : Math.max(16, rect.top - reach);
|
|
1097
|
+
state.perchTarget = null;
|
|
1098
|
+
};
|
|
1099
|
+
const onScroll = () => {
|
|
1100
|
+
s().perchTarget = null;
|
|
1101
|
+
};
|
|
1102
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1103
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
1104
|
+
document.addEventListener("focusin", onFocusIn);
|
|
1105
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
1106
|
+
return () => {
|
|
1107
|
+
clearTimeout(perchTimer);
|
|
1108
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1109
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
1110
|
+
document.removeEventListener("focusin", onFocusIn);
|
|
1111
|
+
window.removeEventListener("scroll", onScroll);
|
|
1112
|
+
const state = s();
|
|
1113
|
+
state.typingNap = false;
|
|
1114
|
+
state.perchTarget = null;
|
|
1115
|
+
state.held = false;
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap";
|
|
1119
|
+
function makePettable(deps) {
|
|
1120
|
+
const { el } = deps;
|
|
1121
|
+
const announcer = document.createElement("span");
|
|
1122
|
+
announcer.setAttribute("aria-live", "polite");
|
|
1123
|
+
announcer.style.cssText = VISUALLY_HIDDEN;
|
|
1124
|
+
el.appendChild(announcer);
|
|
1125
|
+
el.removeAttribute("aria-hidden");
|
|
1126
|
+
el.setAttribute("role", "button");
|
|
1127
|
+
el.setAttribute("aria-label", "Pet the cat");
|
|
1128
|
+
el.tabIndex = 0;
|
|
1129
|
+
let pets = 0;
|
|
1130
|
+
const onFocus = () => {
|
|
1131
|
+
deps.stateRef.current.held = true;
|
|
1132
|
+
if (matchesFocusVisible(el)) {
|
|
1133
|
+
el.style.outline = "2px solid var(--ring, Highlight)";
|
|
1134
|
+
el.style.outlineOffset = "2px";
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
const onBlur = () => {
|
|
1138
|
+
deps.stateRef.current.held = false;
|
|
1139
|
+
el.style.outline = "";
|
|
1140
|
+
};
|
|
1141
|
+
const onKeyDown = (event) => {
|
|
1142
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1143
|
+
event.preventDefault();
|
|
1144
|
+
if (!event.repeat) pet();
|
|
1145
|
+
};
|
|
1146
|
+
const pet = () => {
|
|
1147
|
+
const s = deps.stateRef.current;
|
|
1148
|
+
s.typingNap = false;
|
|
1149
|
+
s.freerunMode = false;
|
|
1150
|
+
s.idleAnimation = null;
|
|
1151
|
+
s.idleAnimationFrame = 0;
|
|
1152
|
+
s.idleTime = Math.max(s.idleTime, 1);
|
|
1153
|
+
if (s.bubbleEnabledCfg) {
|
|
1154
|
+
s.bubbleVisible = true;
|
|
1155
|
+
s.bubbleTimer = s.bubbleDisplayFramesCfg;
|
|
1156
|
+
deps.bubbleTextEl.textContent = pickFromPool(deps, PET_MESSAGES);
|
|
1157
|
+
deps.bubbleEl.style.opacity = "1";
|
|
1158
|
+
}
|
|
1159
|
+
playSound(deps, PET_SOUND_POOL);
|
|
1160
|
+
announcer.textContent = ++pets % 2 ? "The cat purrs." : "The cat purrs happily.";
|
|
1161
|
+
};
|
|
1162
|
+
el.addEventListener("focus", onFocus);
|
|
1163
|
+
el.addEventListener("blur", onBlur);
|
|
1164
|
+
el.addEventListener("keydown", onKeyDown);
|
|
1165
|
+
return () => {
|
|
1166
|
+
el.removeEventListener("focus", onFocus);
|
|
1167
|
+
el.removeEventListener("blur", onBlur);
|
|
1168
|
+
el.removeEventListener("keydown", onKeyDown);
|
|
1169
|
+
announcer.remove();
|
|
1170
|
+
deps.stateRef.current.held = false;
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const SVG_NS$1 = "http://www.w3.org/2000/svg";
|
|
1174
|
+
function clearSVGGroup(deps, id) {
|
|
1175
|
+
const group = deps.debugSVG.querySelector(`#${id}`);
|
|
1176
|
+
if (group) {
|
|
1177
|
+
group.innerHTML = "";
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function updateBlockedCells(deps, grid, cols, rows) {
|
|
1181
|
+
const group = deps.debugSVG.querySelector("#oneko-blocked");
|
|
1182
|
+
if (!group) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
group.innerHTML = "";
|
|
1186
|
+
for (let r = 0; r < rows; r++) {
|
|
1187
|
+
for (let c = 0; c < cols; c++) {
|
|
1188
|
+
if (grid[r * cols + c] === 1) {
|
|
1189
|
+
const rect = document.createElementNS(SVG_NS$1, "rect");
|
|
1190
|
+
rect.setAttribute("x", String(c * CELL_SIZE + 0.5));
|
|
1191
|
+
rect.setAttribute("y", String(r * CELL_SIZE + 0.5));
|
|
1192
|
+
rect.setAttribute("width", String(CELL_SIZE - 1));
|
|
1193
|
+
rect.setAttribute("height", String(CELL_SIZE - 1));
|
|
1194
|
+
rect.setAttribute("fill", "none");
|
|
1195
|
+
rect.setAttribute("stroke", debugColor.dangerSoft);
|
|
1196
|
+
rect.setAttribute("stroke-width", "0.5");
|
|
1197
|
+
group.appendChild(rect);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function updateObstacleOutlines(deps, rects) {
|
|
1203
|
+
const group = deps.debugSVG.querySelector("#oneko-obstacles");
|
|
1204
|
+
if (!group) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
group.innerHTML = "";
|
|
1208
|
+
for (const r of rects) {
|
|
1209
|
+
const rect = document.createElementNS(SVG_NS$1, "rect");
|
|
1210
|
+
rect.setAttribute("x", String(r.left));
|
|
1211
|
+
rect.setAttribute("y", String(r.top));
|
|
1212
|
+
rect.setAttribute("width", String(r.right - r.left));
|
|
1213
|
+
rect.setAttribute("height", String(r.bottom - r.top));
|
|
1214
|
+
rect.setAttribute("fill", "none");
|
|
1215
|
+
rect.setAttribute("stroke", debugColor.dangerSoft);
|
|
1216
|
+
rect.setAttribute("stroke-width", "0.5");
|
|
1217
|
+
rect.setAttribute("stroke-dasharray", "3 3");
|
|
1218
|
+
group.appendChild(rect);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function updateDebugPath(deps, path, catX, catY) {
|
|
1222
|
+
const group = deps.debugSVG.querySelector("#oneko-path");
|
|
1223
|
+
if (!group) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
group.innerHTML = "";
|
|
1227
|
+
if (path.length === 0) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const points = `${catX},${catY} ${path.map((p) => `${p.x},${p.y}`).join(" ")}`;
|
|
1231
|
+
const line = document.createElementNS(SVG_NS$1, "polyline");
|
|
1232
|
+
line.setAttribute("points", points);
|
|
1233
|
+
line.setAttribute("fill", "none");
|
|
1234
|
+
line.setAttribute("stroke", debugColor.trail);
|
|
1235
|
+
line.setAttribute("stroke-width", "1.5");
|
|
1236
|
+
line.style.transition = "all 0.15s ease";
|
|
1237
|
+
group.appendChild(line);
|
|
1238
|
+
for (const pt of path) {
|
|
1239
|
+
const dot = document.createElementNS(SVG_NS$1, "circle");
|
|
1240
|
+
dot.setAttribute("cx", String(pt.x));
|
|
1241
|
+
dot.setAttribute("cy", String(pt.y));
|
|
1242
|
+
dot.setAttribute("r", "2");
|
|
1243
|
+
dot.setAttribute("fill", debugColor.trailDot);
|
|
1244
|
+
group.appendChild(dot);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function updateDebugHUD(deps) {
|
|
1248
|
+
const s = deps.stateRef.current;
|
|
1249
|
+
const activity = getActivityLabel(deps);
|
|
1250
|
+
const stateColor = s.freerunMode ? debugColor.danger : debugColor.accent;
|
|
1251
|
+
const td = "padding:1px 0;";
|
|
1252
|
+
const labelTd = `${td}font-weight:bold;color:${debugColor.panelSubtle};padding-right:12px;`;
|
|
1253
|
+
const valueTd = `${td}text-align:right;`;
|
|
1254
|
+
const row = (label, value, color) => {
|
|
1255
|
+
const valStyle = color ? `${valueTd}color:${color}` : valueTd;
|
|
1256
|
+
return `<tr><td style="${labelTd}">${label}</td><td style="${valStyle}">${value}</td></tr>`;
|
|
1257
|
+
};
|
|
1258
|
+
const speed = Math.hypot(s.nekoVelX, s.nekoVelY);
|
|
1259
|
+
const distToMouse = Math.hypot(s.mousePosX - s.nekoPosX, s.mousePosY - s.nekoPosY);
|
|
1260
|
+
deps.debugHUD.innerHTML = `<table style="border-collapse:collapse;width:100%">${[
|
|
1261
|
+
row("state", activity, stateColor),
|
|
1262
|
+
row("pos", `${Math.round(s.nekoPosX)}, ${Math.round(s.nekoPosY)}`),
|
|
1263
|
+
row("vel", `${s.nekoVelX.toFixed(1)}, ${s.nekoVelY.toFixed(1)}`),
|
|
1264
|
+
row("speed", speed.toFixed(1)),
|
|
1265
|
+
row("target", `${Math.round(s.mousePosX)}, ${Math.round(s.mousePosY)}`),
|
|
1266
|
+
row("dist", `${Math.round(distToMouse)}px`),
|
|
1267
|
+
row("idle", `${s.idleTime}f`),
|
|
1268
|
+
row("frame", `${s.frameCount}`),
|
|
1269
|
+
row("trail", `${s.currentPath.length} pts`),
|
|
1270
|
+
row("step", `${s.pathWaypointIdx}/${s.currentPath.length}`),
|
|
1271
|
+
row("obstacles", `${s.obstacleRects.length}`),
|
|
1272
|
+
row("grid", `${s.gridCols} x ${s.gridRows}`),
|
|
1273
|
+
row("freerun", s.freerunMode ? `${s.freerunTimer}f left` : "off"),
|
|
1274
|
+
row("bubble", getBubbleStatus(s))
|
|
1275
|
+
].join("")}</table>`;
|
|
1276
|
+
}
|
|
1277
|
+
function createDebugRenderer(deps) {
|
|
1278
|
+
let lastBlockedGrid = null;
|
|
1279
|
+
const renderDebug = () => {
|
|
1280
|
+
const s = deps.stateRef.current;
|
|
1281
|
+
if (s.grid !== lastBlockedGrid) {
|
|
1282
|
+
lastBlockedGrid = s.grid;
|
|
1283
|
+
if (s.grid) {
|
|
1284
|
+
updateBlockedCells(deps, s.grid, s.gridCols, s.gridRows);
|
|
1285
|
+
} else {
|
|
1286
|
+
clearSVGGroup(deps, "oneko-blocked");
|
|
1287
|
+
}
|
|
1288
|
+
updateObstacleOutlines(deps, s.obstacleRects);
|
|
1289
|
+
}
|
|
1290
|
+
updateDebugPath(deps, s.currentPath, s.nekoPosX, s.nekoPosY);
|
|
1291
|
+
updateDebugHUD(deps);
|
|
1292
|
+
};
|
|
1293
|
+
const maybeRenderDebug = () => {
|
|
1294
|
+
if (deps.stateRef.current.debugMode) {
|
|
1295
|
+
renderDebug();
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
return { renderDebug, maybeRenderDebug };
|
|
1299
|
+
}
|
|
1300
|
+
function tickLiveState(deps, dist, currentState) {
|
|
1301
|
+
if (!deps.liveStateRef?.current) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const s = deps.stateRef.current;
|
|
1305
|
+
const ls = deps.liveStateRef.current;
|
|
1306
|
+
ls.state = currentState;
|
|
1307
|
+
ls.posX = s.nekoPosX;
|
|
1308
|
+
ls.posY = s.nekoPosY;
|
|
1309
|
+
ls.velMag = Math.hypot(s.nekoVelX, s.nekoVelY);
|
|
1310
|
+
ls.idleTime = s.idleTime;
|
|
1311
|
+
ls.distToMouse = dist;
|
|
1312
|
+
ls.frameCount = s.frameCount;
|
|
1313
|
+
ls.freerunActive = s.freerunMode;
|
|
1314
|
+
ls.freerunTimer = s.freerunTimer;
|
|
1315
|
+
ls.bubbleVisible = s.bubbleVisible;
|
|
1316
|
+
ls.pathLength = s.currentPath.length;
|
|
1317
|
+
ls.obstacleCount = s.obstacleRects.length;
|
|
1318
|
+
}
|
|
1319
|
+
function shouldIncludeObstacleRect(rect, viewportWidth, viewportHeight) {
|
|
1320
|
+
if (rect.width * rect.height < MIN_OBSTACLE_AREA) {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
if (rect.right < 0 || rect.bottom < 0 || rect.left > viewportWidth || rect.top > viewportHeight) {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
if (rect.width > viewportWidth * 0.85 && rect.height > viewportHeight * 0.5) {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
function toObstacleRect(rect) {
|
|
1332
|
+
return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
|
|
1333
|
+
}
|
|
1334
|
+
function neighborIndex(r, c, dr, dc, rows, cols) {
|
|
1335
|
+
const nr = r + dr;
|
|
1336
|
+
const nc = c + dc;
|
|
1337
|
+
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) return -1;
|
|
1338
|
+
return nr * cols + nc;
|
|
1339
|
+
}
|
|
1340
|
+
function collectObstacles() {
|
|
1341
|
+
const vw = window.innerWidth;
|
|
1342
|
+
const vh = window.innerHeight;
|
|
1343
|
+
const rects = [];
|
|
1344
|
+
for (const node of document.querySelectorAll(OBSTACLE_SELECTOR)) {
|
|
1345
|
+
if (node.closest('[data-oneko-zone="attract"]')) continue;
|
|
1346
|
+
const r = node.getBoundingClientRect();
|
|
1347
|
+
if (shouldIncludeObstacleRect(r, vw, vh)) {
|
|
1348
|
+
rects.push(toObstacleRect(r));
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return rects;
|
|
1352
|
+
}
|
|
1353
|
+
function buildGrid(rects, cols, rows) {
|
|
1354
|
+
const grid = new Uint8Array(cols * rows);
|
|
1355
|
+
for (const rect of rects) {
|
|
1356
|
+
const c0 = Math.max(0, Math.floor((rect.left - SPRITE_RADIUS) / CELL_SIZE));
|
|
1357
|
+
const c1 = Math.min(cols - 1, Math.ceil((rect.right + SPRITE_RADIUS) / CELL_SIZE));
|
|
1358
|
+
const r0 = Math.max(0, Math.floor((rect.top - SPRITE_RADIUS) / CELL_SIZE));
|
|
1359
|
+
const r1 = Math.min(rows - 1, Math.ceil((rect.bottom + SPRITE_RADIUS) / CELL_SIZE));
|
|
1360
|
+
for (let row = r0; row <= r1; row++) {
|
|
1361
|
+
for (let col = c0; col <= c1; col++) {
|
|
1362
|
+
grid[row * cols + col] = 1;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return grid;
|
|
1367
|
+
}
|
|
1368
|
+
function worldToCell(worldX, worldY, cols, rows) {
|
|
1369
|
+
const col = Math.max(0, Math.min(cols - 1, Math.floor(worldX / CELL_SIZE)));
|
|
1370
|
+
const row = Math.max(0, Math.min(rows - 1, Math.floor(worldY / CELL_SIZE)));
|
|
1371
|
+
return row * cols + col;
|
|
1372
|
+
}
|
|
1373
|
+
function cellToWorld(idx, cols) {
|
|
1374
|
+
return {
|
|
1375
|
+
x: (idx % cols + 0.5) * CELL_SIZE,
|
|
1376
|
+
y: (Math.floor(idx / cols) + 0.5) * CELL_SIZE
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function nearestWalkable(startIdx, grid, cols, rows) {
|
|
1380
|
+
if (grid[startIdx] === 0) {
|
|
1381
|
+
return startIdx;
|
|
1382
|
+
}
|
|
1383
|
+
const visited = new Uint8Array(grid.length);
|
|
1384
|
+
const queue = [startIdx];
|
|
1385
|
+
visited[startIdx] = 1;
|
|
1386
|
+
let head = 0;
|
|
1387
|
+
while (head < queue.length) {
|
|
1388
|
+
const idx = queue[head++];
|
|
1389
|
+
const r = Math.floor(idx / cols);
|
|
1390
|
+
const c = idx % cols;
|
|
1391
|
+
for (const [dr, dc] of NEIGHBOR_OFFSETS) {
|
|
1392
|
+
const nIdx = neighborIndex(r, c, dr, dc, rows, cols);
|
|
1393
|
+
if (nIdx === -1 || visited[nIdx]) {
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
visited[nIdx] = 1;
|
|
1397
|
+
if (grid[nIdx] === 0) {
|
|
1398
|
+
return nIdx;
|
|
1399
|
+
}
|
|
1400
|
+
queue.push(nIdx);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return startIdx;
|
|
1404
|
+
}
|
|
1405
|
+
function heapBubble(data, i) {
|
|
1406
|
+
let idx = i;
|
|
1407
|
+
while (idx > 0) {
|
|
1408
|
+
const parent = Math.floor((idx - 1) / 2);
|
|
1409
|
+
if (data[parent][0] <= data[idx][0]) {
|
|
1410
|
+
break;
|
|
1411
|
+
}
|
|
1412
|
+
const tmp = data[parent];
|
|
1413
|
+
data[parent] = data[idx];
|
|
1414
|
+
data[idx] = tmp;
|
|
1415
|
+
idx = parent;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function heapSink(data, i) {
|
|
1419
|
+
const n = data.length;
|
|
1420
|
+
let idx = i;
|
|
1421
|
+
for (let step = 0; step < n; step++) {
|
|
1422
|
+
let smallest = idx;
|
|
1423
|
+
const left = 2 * idx + 1;
|
|
1424
|
+
const right = 2 * idx + 2;
|
|
1425
|
+
if (left < n && data[left][0] < data[smallest][0]) {
|
|
1426
|
+
smallest = left;
|
|
1427
|
+
}
|
|
1428
|
+
if (right < n && data[right][0] < data[smallest][0]) {
|
|
1429
|
+
smallest = right;
|
|
1430
|
+
}
|
|
1431
|
+
if (smallest === idx) {
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
const tmp = data[smallest];
|
|
1435
|
+
data[smallest] = data[idx];
|
|
1436
|
+
data[idx] = tmp;
|
|
1437
|
+
idx = smallest;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
function smoothPath(rawPath, cols) {
|
|
1441
|
+
const points = rawPath.map((idx) => cellToWorld(idx, cols));
|
|
1442
|
+
if (points.length <= 2) {
|
|
1443
|
+
return points;
|
|
1444
|
+
}
|
|
1445
|
+
const result = [points[0]];
|
|
1446
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
1447
|
+
const prev = result.at(-1);
|
|
1448
|
+
const cur = points[i];
|
|
1449
|
+
const next = points[i + 1];
|
|
1450
|
+
const cross = (cur.x - prev.x) * (next.y - cur.y) - (cur.y - prev.y) * (next.x - cur.x);
|
|
1451
|
+
if (Math.abs(cross) > 1e-6) {
|
|
1452
|
+
result.push(cur);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
result.push(points.at(-1));
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
function relaxNeighbors(idx, cols, rows, grid, dist, prev, heap) {
|
|
1459
|
+
const r = Math.floor(idx / cols);
|
|
1460
|
+
const c = idx % cols;
|
|
1461
|
+
for (const [dr, dc, edgeCost] of NEIGHBOR_OFFSETS) {
|
|
1462
|
+
const nIdx = neighborIndex(r, c, dr, dc, rows, cols);
|
|
1463
|
+
if (nIdx === -1 || grid[nIdx] === 1) {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (dr !== 0 && dc !== 0 && (grid[r * cols + c + dc] || grid[(r + dr) * cols + c])) continue;
|
|
1467
|
+
const newDist = dist[idx] + edgeCost;
|
|
1468
|
+
if (newDist < dist[nIdx]) {
|
|
1469
|
+
dist[nIdx] = newDist;
|
|
1470
|
+
prev[nIdx] = idx;
|
|
1471
|
+
heap.push([newDist, nIdx]);
|
|
1472
|
+
heapBubble(heap, heap.length - 1);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function searchRoute(startIdx, goalIdx, grid, cols, rows) {
|
|
1477
|
+
const dist = new Float64Array(grid.length).fill(Number.POSITIVE_INFINITY);
|
|
1478
|
+
const prev = new Int32Array(grid.length).fill(-1);
|
|
1479
|
+
const heap = [];
|
|
1480
|
+
dist[startIdx] = 0;
|
|
1481
|
+
heap.push([0, startIdx]);
|
|
1482
|
+
while (heap.length > 0) {
|
|
1483
|
+
const top = heap.at(0);
|
|
1484
|
+
const last = heap.at(-1);
|
|
1485
|
+
heap[0] = last;
|
|
1486
|
+
heap.pop();
|
|
1487
|
+
heapSink(heap, 0);
|
|
1488
|
+
const [cost, idx] = top;
|
|
1489
|
+
if (idx === goalIdx) {
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
if (cost > dist[idx]) {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
relaxNeighbors(idx, cols, rows, grid, dist, prev, heap);
|
|
1496
|
+
}
|
|
1497
|
+
return Number.isFinite(dist[goalIdx]) ? prev : null;
|
|
1498
|
+
}
|
|
1499
|
+
function reconstructCellPath(prev, goalIdx) {
|
|
1500
|
+
const rawPath = [];
|
|
1501
|
+
let cur = goalIdx;
|
|
1502
|
+
while (cur !== -1) {
|
|
1503
|
+
rawPath.push(cur);
|
|
1504
|
+
cur = prev[cur];
|
|
1505
|
+
}
|
|
1506
|
+
rawPath.reverse();
|
|
1507
|
+
return rawPath;
|
|
1508
|
+
}
|
|
1509
|
+
function findRoute(startIdx, goalIdx, grid, cols, rows) {
|
|
1510
|
+
if (startIdx === goalIdx) {
|
|
1511
|
+
return [cellToWorld(startIdx, cols)];
|
|
1512
|
+
}
|
|
1513
|
+
const prev = searchRoute(startIdx, goalIdx, grid, cols, rows);
|
|
1514
|
+
if (!prev) {
|
|
1515
|
+
return [];
|
|
1516
|
+
}
|
|
1517
|
+
return smoothPath(reconstructCellPath(prev, goalIdx), cols);
|
|
1518
|
+
}
|
|
1519
|
+
function setSprite(deps, name, frame) {
|
|
1520
|
+
const sprites = defaultSpriteSets[name];
|
|
1521
|
+
if (!sprites?.length) {
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
const sprite = sprites[frame % sprites.length];
|
|
1525
|
+
deps.el.style.backgroundPosition = `${sprite[0] * TILE}px ${sprite[1] * TILE}px`;
|
|
1526
|
+
}
|
|
1527
|
+
function getDirectionFromAngle(angle) {
|
|
1528
|
+
if (angle >= 157.5 || angle < -157.5) {
|
|
1529
|
+
return "W";
|
|
1530
|
+
}
|
|
1531
|
+
for (const range of DIRECTION_RANGES) {
|
|
1532
|
+
if (angle >= range.min && angle < range.max) {
|
|
1533
|
+
return range.dir;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return "E";
|
|
1537
|
+
}
|
|
1538
|
+
function directionFromDelta(dx, dy) {
|
|
1539
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
return getDirectionFromAngle(Math.atan2(dy, dx) * (180 / Math.PI));
|
|
1543
|
+
}
|
|
1544
|
+
function clearRotation(deps) {
|
|
1545
|
+
const s = deps.stateRef.current;
|
|
1546
|
+
if (s.currentRotation !== 0) {
|
|
1547
|
+
s.currentRotation = 0;
|
|
1548
|
+
deps.el.style.transform = `scale(${s.scale}) rotate(0deg)`;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
function idleFrameThreshold(s) {
|
|
1552
|
+
return Math.max(1, Math.round(s.idleThresholdMs / 100));
|
|
1553
|
+
}
|
|
1554
|
+
function wallScratchAnimations(s) {
|
|
1555
|
+
return [
|
|
1556
|
+
s.nekoPosX <= 18 && "scratchWallW",
|
|
1557
|
+
s.nekoPosX >= window.innerWidth - 18 && "scratchWallE",
|
|
1558
|
+
s.nekoPosY <= 18 && "scratchWallN",
|
|
1559
|
+
s.nekoPosY >= window.innerHeight - 18 && "scratchWallS"
|
|
1560
|
+
].filter(Boolean);
|
|
1561
|
+
}
|
|
1562
|
+
function pickRandomIdleAnimation(s) {
|
|
1563
|
+
const available = [
|
|
1564
|
+
"scratchSelf",
|
|
1565
|
+
...s.sleepEnabledCfg ? ["tired"] : [],
|
|
1566
|
+
...wallScratchAnimations(s)
|
|
1567
|
+
];
|
|
1568
|
+
return available[Math.floor(Math.random() * available.length)];
|
|
1569
|
+
}
|
|
1570
|
+
function startSleepingAnimation(deps) {
|
|
1571
|
+
const s = deps.stateRef.current;
|
|
1572
|
+
s.idleAnimation = "sleeping";
|
|
1573
|
+
if (s.bubbleVisible) {
|
|
1574
|
+
deps.bubbleTextEl.textContent = pickFromPool(deps, SLEEPING_MESSAGES);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function maybeStartIdleAnimation(deps) {
|
|
1578
|
+
const s = deps.stateRef.current;
|
|
1579
|
+
const threshold = idleFrameThreshold(s);
|
|
1580
|
+
if (s.idleAnimation != null || s.idleTime < threshold || Math.random() >= 0.1) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
s.idleAnimationFrame = 0;
|
|
1584
|
+
if (s.sleepEnabledCfg && (s.idleTime > threshold * 3 || napmateNearby(s))) {
|
|
1585
|
+
startSleepingAnimation(deps);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
s.idleAnimation = pickRandomIdleAnimation(s);
|
|
1589
|
+
}
|
|
1590
|
+
function computeIdleSpriteIndex(animName, frame, animationSpeed) {
|
|
1591
|
+
const sprites = defaultSpriteSets[animName];
|
|
1592
|
+
const baseDuration = IDLE_ANIMATION_DURATIONS[animName] ?? (sprites ? sprites.length * 8 : 8);
|
|
1593
|
+
const duration = Math.max(1, Math.round(baseDuration / animationSpeed));
|
|
1594
|
+
const frameCount = sprites?.length || 1;
|
|
1595
|
+
const framesPerSprite = Math.max(1, Math.floor(duration / frameCount));
|
|
1596
|
+
return Math.floor(frame / framesPerSprite) % frameCount;
|
|
1597
|
+
}
|
|
1598
|
+
function getIdleAnimationDuration(animName, animationSpeed) {
|
|
1599
|
+
const sprites = defaultSpriteSets[animName];
|
|
1600
|
+
const baseDuration = IDLE_ANIMATION_DURATIONS[animName] ?? (sprites ? sprites.length * 8 : 8);
|
|
1601
|
+
return Math.max(1, Math.round(baseDuration / animationSpeed));
|
|
1602
|
+
}
|
|
1603
|
+
function resetOrLoopIdleAnimation(s, animName, duration) {
|
|
1604
|
+
if (s.idleAnimationFrame < duration || animName === "sleeping") {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (s.stateLocked || s.noFollow) {
|
|
1608
|
+
s.idleAnimationFrame = 0;
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
s.idleAnimation = null;
|
|
1612
|
+
s.idleAnimationFrame = 0;
|
|
1613
|
+
}
|
|
1614
|
+
function updateIdleAnimation(deps) {
|
|
1615
|
+
const s = deps.stateRef.current;
|
|
1616
|
+
if (!s.sleepEnabledCfg && (s.idleAnimation === "sleeping" || s.idleAnimation === "tired")) {
|
|
1617
|
+
s.idleAnimation = null;
|
|
1618
|
+
s.idleAnimationFrame = 0;
|
|
1619
|
+
s.idleTime = 0;
|
|
1620
|
+
}
|
|
1621
|
+
if (!s.idleAnimation) {
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
const animName = s.idleAnimation;
|
|
1625
|
+
const spriteIndex = computeIdleSpriteIndex(animName, s.idleAnimationFrame, s.animationSpeedCfg);
|
|
1626
|
+
setSprite(deps, animName, spriteIndex);
|
|
1627
|
+
clearRotation(deps);
|
|
1628
|
+
s.idleAnimationFrame += 1;
|
|
1629
|
+
resetOrLoopIdleAnimation(s, animName, getIdleAnimationDuration(animName, s.animationSpeedCfg));
|
|
1630
|
+
return true;
|
|
1631
|
+
}
|
|
1632
|
+
function freezeLockedState(deps) {
|
|
1633
|
+
const s = deps.stateRef.current;
|
|
1634
|
+
if (!(s.stateLocked || s.noFollow) || s.freerunMode && !s.noFollow) {
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
s.nekoVelX *= 0.5;
|
|
1638
|
+
s.nekoVelY *= 0.5;
|
|
1639
|
+
s.idleTime = Math.max(s.idleTime, 1);
|
|
1640
|
+
setSprite(deps, "idle", s.frameCount);
|
|
1641
|
+
clearRotation(deps);
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
function recalculatePath(deps) {
|
|
1645
|
+
const s = deps.stateRef.current;
|
|
1646
|
+
const { grid, gridCols: cols, gridRows: rows } = s;
|
|
1647
|
+
if (!grid || cols === 0 || rows === 0) {
|
|
1648
|
+
s.currentPath = [];
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const catCell = nearestWalkable(
|
|
1652
|
+
worldToCell(s.nekoPosX, s.nekoPosY, cols, rows),
|
|
1653
|
+
grid,
|
|
1654
|
+
cols,
|
|
1655
|
+
rows
|
|
1656
|
+
);
|
|
1657
|
+
const mouseCell = nearestWalkable(
|
|
1658
|
+
worldToCell(s.zoneState.movementTarget.x, s.zoneState.movementTarget.y, cols, rows),
|
|
1659
|
+
grid,
|
|
1660
|
+
cols,
|
|
1661
|
+
rows
|
|
1662
|
+
);
|
|
1663
|
+
s.currentPath = findRoute(catCell, mouseCell, grid, cols, rows);
|
|
1664
|
+
s.pathWaypointIdx = 0;
|
|
1665
|
+
s.lastPathRecalcFrame = s.frameCount;
|
|
1666
|
+
s.lastPathTargetCol = Math.floor(s.zoneState.movementTarget.x / CELL_SIZE);
|
|
1667
|
+
s.lastPathTargetRow = Math.floor(s.zoneState.movementTarget.y / CELL_SIZE);
|
|
1668
|
+
}
|
|
1669
|
+
function getNextWaypointTarget(deps) {
|
|
1670
|
+
const s = deps.stateRef.current;
|
|
1671
|
+
const { currentPath: path } = s;
|
|
1672
|
+
if (path.length === 0) {
|
|
1673
|
+
return { x: s.zoneState.movementTarget.x, y: s.zoneState.movementTarget.y };
|
|
1674
|
+
}
|
|
1675
|
+
while (s.pathWaypointIdx < path.length - 1) {
|
|
1676
|
+
const wp = path[s.pathWaypointIdx];
|
|
1677
|
+
if (Math.hypot(wp.x - s.nekoPosX, wp.y - s.nekoPosY) < WAYPOINT_REACH_DIST) {
|
|
1678
|
+
s.pathWaypointIdx++;
|
|
1679
|
+
} else {
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return path[s.pathWaypointIdx];
|
|
1684
|
+
}
|
|
1685
|
+
function moveToward(deps, targetX, targetY) {
|
|
1686
|
+
const s = deps.stateRef.current;
|
|
1687
|
+
const dx = targetX - s.nekoPosX;
|
|
1688
|
+
const dy = targetY - s.nekoPosY;
|
|
1689
|
+
const dist = Math.hypot(dx, dy);
|
|
1690
|
+
if (dist < 1) {
|
|
1691
|
+
s.nekoVelX *= 0.5;
|
|
1692
|
+
s.nekoVelY *= 0.5;
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const currentSpeed = s.currentSpeed;
|
|
1696
|
+
s.nekoVelX += (dx / dist * currentSpeed - s.nekoVelX) * STEER_LERP;
|
|
1697
|
+
s.nekoVelY += (dy / dist * currentSpeed - s.nekoVelY) * STEER_LERP;
|
|
1698
|
+
const velMag = Math.hypot(s.nekoVelX, s.nekoVelY);
|
|
1699
|
+
const maxVel = currentSpeed * MAX_VEL_FACTOR;
|
|
1700
|
+
const clampedMag = Math.min(velMag, maxVel);
|
|
1701
|
+
if (velMag > maxVel) {
|
|
1702
|
+
s.nekoVelX = s.nekoVelX / velMag * maxVel;
|
|
1703
|
+
s.nekoVelY = s.nekoVelY / velMag * maxVel;
|
|
1704
|
+
}
|
|
1705
|
+
const from = { x: s.nekoPosX, y: s.nekoPosY };
|
|
1706
|
+
const proposed = {
|
|
1707
|
+
x: Math.max(16, Math.min(window.innerWidth - 16, s.nekoPosX + s.nekoVelX)),
|
|
1708
|
+
y: Math.max(16, Math.min(window.innerHeight - 16, s.nekoPosY + s.nekoVelY))
|
|
1709
|
+
};
|
|
1710
|
+
const safe = constrainZoneMovement(from, proposed, s.zoneState.blocked);
|
|
1711
|
+
if (safe.x !== proposed.x || safe.y !== proposed.y) {
|
|
1712
|
+
s.nekoVelX = 0;
|
|
1713
|
+
s.nekoVelY = 0;
|
|
1714
|
+
}
|
|
1715
|
+
if (safe.x === from.x && safe.y === from.y && (safe.x !== proposed.x || safe.y !== proposed.y)) {
|
|
1716
|
+
restNearCursor(deps);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
s.nekoPosX = safe.x;
|
|
1720
|
+
s.nekoPosY = safe.y;
|
|
1721
|
+
s.nekoPosX = Math.max(16, Math.min(window.innerWidth - 16, s.nekoPosX));
|
|
1722
|
+
s.nekoPosY = Math.max(16, Math.min(window.innerHeight - 16, s.nekoPosY));
|
|
1723
|
+
deps.el.style.left = `${Math.floor(s.nekoPosX - TILE / 2)}px`;
|
|
1724
|
+
deps.el.style.top = `${Math.floor(s.nekoPosY - TILE / 2)}px`;
|
|
1725
|
+
const dir = directionFromDelta(s.nekoVelX, s.nekoVelY) ?? "idle";
|
|
1726
|
+
setSprite(deps, dir, s.frameCount);
|
|
1727
|
+
if (clampedMag > 0.5 && s.rotationAmount > 0) {
|
|
1728
|
+
const velAngle = Math.atan2(s.nekoVelY, s.nekoVelX) * (180 / Math.PI);
|
|
1729
|
+
s.currentRotation = velAngle / 180 * s.rotationAmount;
|
|
1730
|
+
} else {
|
|
1731
|
+
s.currentRotation = 0;
|
|
1732
|
+
}
|
|
1733
|
+
deps.el.style.transform = `scale(${s.scale}) rotate(${s.currentRotation}deg)`;
|
|
1734
|
+
s.idleTime = 0;
|
|
1735
|
+
}
|
|
1736
|
+
function pickFreerunMessage(deps) {
|
|
1737
|
+
const s = deps.stateRef.current;
|
|
1738
|
+
let idx = Math.floor(Math.random() * FREERUN_MESSAGES.length);
|
|
1739
|
+
if (idx === s.lastFreerunMsg) {
|
|
1740
|
+
idx = (idx + 1) % FREERUN_MESSAGES.length;
|
|
1741
|
+
}
|
|
1742
|
+
s.lastFreerunMsg = idx;
|
|
1743
|
+
return FREERUN_MESSAGES[idx];
|
|
1744
|
+
}
|
|
1745
|
+
function enterFreerun(deps) {
|
|
1746
|
+
const s = deps.stateRef.current;
|
|
1747
|
+
s.freerunMode = true;
|
|
1748
|
+
s.freerunTimer = s.freerunDurationCfg;
|
|
1749
|
+
s.bubbleVisible = true;
|
|
1750
|
+
s.bubbleTimer = s.freerunDurationCfg;
|
|
1751
|
+
s.bubbleCooldown = 0;
|
|
1752
|
+
deps.bubbleTextEl.textContent = pickFreerunMessage(deps);
|
|
1753
|
+
deps.bubbleEl.style.opacity = "1";
|
|
1754
|
+
}
|
|
1755
|
+
function tickFreerun(deps) {
|
|
1756
|
+
const s = deps.stateRef.current;
|
|
1757
|
+
s.freerunTimer -= 1;
|
|
1758
|
+
if (s.freerunTimer <= 0) {
|
|
1759
|
+
const keepAlive = s.stateLocked;
|
|
1760
|
+
s.freerunMode = keepAlive;
|
|
1761
|
+
if (keepAlive) {
|
|
1762
|
+
s.freerunTimer = s.freerunDurationCfg;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
moveToward(deps, s.zoneState.movementTarget.x, s.zoneState.movementTarget.y);
|
|
1766
|
+
}
|
|
1767
|
+
function restNearCursor(deps) {
|
|
1768
|
+
const s = deps.stateRef.current;
|
|
1769
|
+
setSprite(deps, "idle", s.frameCount);
|
|
1770
|
+
clearRotation(deps);
|
|
1771
|
+
s.nekoVelX *= 0.5;
|
|
1772
|
+
s.nekoVelY *= 0.5;
|
|
1773
|
+
s.idleTime += 1;
|
|
1774
|
+
if (!(s.stateLocked || s.noFollow)) {
|
|
1775
|
+
s.freerunMode = false;
|
|
1776
|
+
}
|
|
1777
|
+
maybeStartIdleAnimation(deps);
|
|
1778
|
+
}
|
|
1779
|
+
function chaseCursor(deps) {
|
|
1780
|
+
const s = deps.stateRef.current;
|
|
1781
|
+
if (s.freerunMode) {
|
|
1782
|
+
tickFreerun(deps);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (!s.noFollow && Math.random() < s.freerunChanceCfg) {
|
|
1786
|
+
enterFreerun(deps);
|
|
1787
|
+
moveToward(deps, s.zoneState.movementTarget.x, s.zoneState.movementTarget.y);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
const target = getNextWaypointTarget(deps);
|
|
1791
|
+
moveToward(deps, target.x, target.y);
|
|
1792
|
+
}
|
|
1793
|
+
function followPath(deps, overallDist) {
|
|
1794
|
+
const s = deps.stateRef.current;
|
|
1795
|
+
const lastWaypoint = s.currentPath.at(-1);
|
|
1796
|
+
const stop = stopDistance(s);
|
|
1797
|
+
const reachedPathEnd = !s.freerunMode && lastWaypoint && s.pathWaypointIdx === s.currentPath.length - 1 && Math.hypot(lastWaypoint.x - s.nekoPosX, lastWaypoint.y - s.nekoPosY) < stop;
|
|
1798
|
+
if (overallDist < stop || reachedPathEnd) {
|
|
1799
|
+
restNearCursor(deps);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
chaseCursor(deps);
|
|
1803
|
+
}
|
|
1804
|
+
function refreshGridIfNeeded(deps) {
|
|
1805
|
+
const s = deps.stateRef.current;
|
|
1806
|
+
if (s.frameCount - s.lastObstacleRefresh < OBSTACLE_INTERVAL) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const softObstacles = collectObstacles().filter(
|
|
1810
|
+
(rect) => !s.zoneState.favorites.some(
|
|
1811
|
+
({ rect: favorite }) => rect.left >= favorite.left && rect.right <= favorite.right && rect.top >= favorite.top && rect.bottom <= favorite.bottom
|
|
1812
|
+
)
|
|
1813
|
+
);
|
|
1814
|
+
s.obstacleRects = [...softObstacles, ...s.zoneState.blocked];
|
|
1815
|
+
s.lastObstacleRefresh = s.frameCount;
|
|
1816
|
+
s.gridCols = Math.ceil(window.innerWidth / CELL_SIZE);
|
|
1817
|
+
s.gridRows = Math.ceil(window.innerHeight / CELL_SIZE);
|
|
1818
|
+
s.grid = buildGrid(s.obstacleRects, s.gridCols, s.gridRows);
|
|
1819
|
+
s.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
|
|
1820
|
+
}
|
|
1821
|
+
function recalcPathIfNeeded(deps) {
|
|
1822
|
+
const s = deps.stateRef.current;
|
|
1823
|
+
const mouseCol = Math.floor(s.zoneState.movementTarget.x / CELL_SIZE);
|
|
1824
|
+
const mouseRow = Math.floor(s.zoneState.movementTarget.y / CELL_SIZE);
|
|
1825
|
+
const mouseMoved = Math.abs(mouseCol - s.lastPathTargetCol) > PATH_RECALC_MOUSE_CELLS || Math.abs(mouseRow - s.lastPathTargetRow) > PATH_RECALC_MOUSE_CELLS;
|
|
1826
|
+
if (s.frameCount - s.lastPathRecalcFrame >= PATH_RECALC_INTERVAL || mouseMoved) {
|
|
1827
|
+
recalculatePath(deps);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function interruptIdleIfChasing(s, dist) {
|
|
1831
|
+
const reach = Math.max(32, stopDistance(s) + 12);
|
|
1832
|
+
if (s.idleAnimation && dist > reach && !s.stateLocked && !s.noFollow) {
|
|
1833
|
+
s.idleAnimation = null;
|
|
1834
|
+
s.idleAnimationFrame = 0;
|
|
1835
|
+
s.idleTime = 0;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function tickLaser(deps, dist) {
|
|
1839
|
+
const s = deps.stateRef.current;
|
|
1840
|
+
if (!s.laserPointerCfg) {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const close = dist < s.followDistanceCfg + 4;
|
|
1844
|
+
if (close && !s.laserCaught) {
|
|
1845
|
+
s.laserCaught = true;
|
|
1846
|
+
playSound(deps, LASER_CATCH_POOL);
|
|
1847
|
+
} else if (!close && dist > s.followDistanceCfg + 24) {
|
|
1848
|
+
s.laserCaught = false;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function tickMovement(deps, dist) {
|
|
1852
|
+
const s = deps.stateRef.current;
|
|
1853
|
+
refreshGridIfNeeded(deps);
|
|
1854
|
+
const target = s.zoneState.movementTarget;
|
|
1855
|
+
const targetDistance = Math.hypot(target.x - s.nekoPosX, target.y - s.nekoPosY);
|
|
1856
|
+
interruptIdleIfChasing(s, targetDistance);
|
|
1857
|
+
recalcPathIfNeeded(deps);
|
|
1858
|
+
updateBubble(deps);
|
|
1859
|
+
tickLaser(deps, dist);
|
|
1860
|
+
if (!updateIdleAnimation(deps) && !freezeLockedState(deps)) {
|
|
1861
|
+
followPath(deps, targetDistance);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function tickPrelude(s) {
|
|
1865
|
+
s.frameCount += 1;
|
|
1866
|
+
s.mouseCircleWinding *= MOUSE_LOOP_WINDING_DECAY;
|
|
1867
|
+
if (Math.abs(s.mouseCircleWinding) < 0.35) {
|
|
1868
|
+
s.mouseCircleWinding = 0;
|
|
1869
|
+
s.loopPrevAngle = null;
|
|
1870
|
+
}
|
|
1871
|
+
s.soundCooldown = Math.max(0, s.soundCooldown - 1);
|
|
1872
|
+
}
|
|
1873
|
+
function tickAmbientSounds(deps, currentState) {
|
|
1874
|
+
const s = deps.stateRef.current;
|
|
1875
|
+
const { idleTime, frameCount } = s;
|
|
1876
|
+
if (currentState === "idle" && idleTime > 0 && idleTime % 60 === 0) {
|
|
1877
|
+
playSound(deps, SOUND_POOLS.idle);
|
|
1878
|
+
} else if (currentState === "sleeping" && frameCount % 100 === 0) {
|
|
1879
|
+
playSound(deps, SOUND_POOLS.sleeping);
|
|
1880
|
+
} else if (currentState === "moving" && frameCount % 80 === 0) {
|
|
1881
|
+
playSound(deps, SOUND_POOLS.moving);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
function tickSounds(deps) {
|
|
1885
|
+
const currentState = getActivityLabel(deps);
|
|
1886
|
+
if (currentState !== deps.lastStateRef.current) {
|
|
1887
|
+
deps.lastStateRef.current = currentState;
|
|
1888
|
+
deps.onStateChangeRef.current?.(currentState);
|
|
1889
|
+
const pool = SOUND_POOLS[currentState];
|
|
1890
|
+
if (pool) {
|
|
1891
|
+
playSound(deps, pool);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
tickAmbientSounds(deps, currentState);
|
|
1895
|
+
return currentState;
|
|
1896
|
+
}
|
|
1897
|
+
function createFrameLoop(deps) {
|
|
1898
|
+
const { maybeRenderDebug } = createDebugRenderer(deps);
|
|
1899
|
+
const frame = () => {
|
|
1900
|
+
const s = deps.stateRef.current;
|
|
1901
|
+
if (s.pausedCfg) return;
|
|
1902
|
+
if (!refreshZones(deps)) return;
|
|
1903
|
+
tickPrelude(s);
|
|
1904
|
+
tickAttention(deps);
|
|
1905
|
+
updateZoneTarget(s, window.innerWidth, window.innerHeight);
|
|
1906
|
+
const dist = Math.hypot(s.mousePosX - s.nekoPosX, s.mousePosY - s.nekoPosY);
|
|
1907
|
+
tickMovement(deps, dist);
|
|
1908
|
+
maybeRenderDebug();
|
|
1909
|
+
const currentState = tickSounds(deps);
|
|
1910
|
+
tickLiveState(deps, dist, currentState);
|
|
1911
|
+
};
|
|
1912
|
+
return frame;
|
|
1913
|
+
}
|
|
1914
|
+
function createMouseMoveHandler(deps) {
|
|
1915
|
+
return (ev) => {
|
|
1916
|
+
const mx = ev.clientX;
|
|
1917
|
+
const my = ev.clientY;
|
|
1918
|
+
const s = deps.stateRef.current;
|
|
1919
|
+
const px = s.mousePosX;
|
|
1920
|
+
const py = s.mousePosY;
|
|
1921
|
+
const dx = mx - px;
|
|
1922
|
+
const dy = my - py;
|
|
1923
|
+
const dist = Math.hypot(dx, dy);
|
|
1924
|
+
if (dist >= MOUSE_LOOP_MIN_STEP) {
|
|
1925
|
+
const ang = Math.atan2(dy, dx);
|
|
1926
|
+
if (s.loopPrevAngle !== null) {
|
|
1927
|
+
let d = ang - s.loopPrevAngle;
|
|
1928
|
+
while (d > Math.PI) {
|
|
1929
|
+
d -= 2 * Math.PI;
|
|
1930
|
+
}
|
|
1931
|
+
while (d < -Math.PI) {
|
|
1932
|
+
d += 2 * Math.PI;
|
|
1933
|
+
}
|
|
1934
|
+
s.mouseCircleWinding += d;
|
|
1935
|
+
if (s.mouseCircleWinding > MOUSE_LOOP_WINDING_CAP) {
|
|
1936
|
+
s.mouseCircleWinding = MOUSE_LOOP_WINDING_CAP;
|
|
1937
|
+
} else if (s.mouseCircleWinding < -MOUSE_LOOP_WINDING_CAP) {
|
|
1938
|
+
s.mouseCircleWinding = -MOUSE_LOOP_WINDING_CAP;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
s.loopPrevAngle = ang;
|
|
1942
|
+
}
|
|
1943
|
+
s.mousePosX = mx;
|
|
1944
|
+
s.mousePosY = my;
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function startAnimationLoop(stateRef, frame) {
|
|
1948
|
+
let timer;
|
|
1949
|
+
const syncVisibility = () => {
|
|
1950
|
+
clearInterval(timer);
|
|
1951
|
+
timer = void 0;
|
|
1952
|
+
if (document.hidden) return;
|
|
1953
|
+
timer = setInterval(() => {
|
|
1954
|
+
if (!stateRef.current.paused && !stateRef.current.pausedCfg) frame();
|
|
1955
|
+
}, 100);
|
|
1956
|
+
};
|
|
1957
|
+
document.addEventListener("visibilitychange", syncVisibility);
|
|
1958
|
+
syncVisibility();
|
|
1959
|
+
return () => {
|
|
1960
|
+
clearInterval(timer);
|
|
1961
|
+
document.removeEventListener("visibilitychange", syncVisibility);
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const BASE_STYLE = [
|
|
1965
|
+
"font-family:var(--font-mono), monospace",
|
|
1966
|
+
"font-size:11px",
|
|
1967
|
+
"line-height:1.6",
|
|
1968
|
+
"border-radius:6px",
|
|
1969
|
+
`background:${debugColor.panelBg}`,
|
|
1970
|
+
`border:1px solid ${debugColor.panelBorder}`,
|
|
1971
|
+
`color:${debugColor.panelText}`,
|
|
1972
|
+
"width:190px"
|
|
1973
|
+
].join(";");
|
|
1974
|
+
const btnStyle = [
|
|
1975
|
+
`background:${debugColor.controlBg}`,
|
|
1976
|
+
`border:1px solid ${debugColor.controlBorder}`,
|
|
1977
|
+
`color:${debugColor.panelText}`,
|
|
1978
|
+
"border-radius:4px",
|
|
1979
|
+
"padding:2px 8px",
|
|
1980
|
+
"font-family:inherit",
|
|
1981
|
+
"font-size:11px",
|
|
1982
|
+
"cursor:pointer"
|
|
1983
|
+
].join(";");
|
|
1984
|
+
const selectStyle = [
|
|
1985
|
+
`background:${debugColor.controlBg}`,
|
|
1986
|
+
`border:1px solid ${debugColor.controlBorder}`,
|
|
1987
|
+
`color:${debugColor.panelText}`,
|
|
1988
|
+
"border-radius:4px",
|
|
1989
|
+
"padding:2px 4px",
|
|
1990
|
+
"font-family:inherit",
|
|
1991
|
+
"font-size:11px",
|
|
1992
|
+
"cursor:pointer",
|
|
1993
|
+
"flex:1"
|
|
1994
|
+
].join(";");
|
|
1995
|
+
function sectionLabel(text) {
|
|
1996
|
+
const d = document.createElement("div");
|
|
1997
|
+
d.textContent = text;
|
|
1998
|
+
d.style.cssText = `color:${debugColor.panelSubtle};font-weight:bold;margin-top:8px;margin-bottom:3px;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;`;
|
|
1999
|
+
return d;
|
|
2000
|
+
}
|
|
2001
|
+
function makeRow(...children) {
|
|
2002
|
+
const r = document.createElement("div");
|
|
2003
|
+
r.style.cssText = "display:flex;gap:4px;align-items:center;margin-bottom:3px;";
|
|
2004
|
+
for (const c of children) {
|
|
2005
|
+
r.appendChild(c);
|
|
2006
|
+
}
|
|
2007
|
+
return r;
|
|
2008
|
+
}
|
|
2009
|
+
function makeBtn(label, onClick) {
|
|
2010
|
+
const btn = document.createElement("button");
|
|
2011
|
+
btn.textContent = label;
|
|
2012
|
+
btn.style.cssText = btnStyle;
|
|
2013
|
+
btn.addEventListener("click", onClick);
|
|
2014
|
+
return btn;
|
|
2015
|
+
}
|
|
2016
|
+
function appendBubbleSection(body, stateRef, bubbleEl, bubbleTextEl, showBubble2, hideBubble2, bubbleDisplayFrames) {
|
|
2017
|
+
body.appendChild(sectionLabel("bubble"));
|
|
2018
|
+
const showBubbleBtn = makeBtn("show", () => showBubble2());
|
|
2019
|
+
const hideBubbleBtn = makeBtn("hide", () => hideBubble2());
|
|
2020
|
+
body.appendChild(makeRow(showBubbleBtn, hideBubbleBtn));
|
|
2021
|
+
const bubbleInput = document.createElement("input");
|
|
2022
|
+
bubbleInput.type = "text";
|
|
2023
|
+
bubbleInput.placeholder = "custom text...";
|
|
2024
|
+
bubbleInput.style.cssText = [
|
|
2025
|
+
`background:${debugColor.controlBg}`,
|
|
2026
|
+
`border:1px solid ${debugColor.controlBorder}`,
|
|
2027
|
+
`color:${debugColor.panelText}`,
|
|
2028
|
+
"border-radius:4px",
|
|
2029
|
+
"padding:2px 6px",
|
|
2030
|
+
"font-family:inherit",
|
|
2031
|
+
"font-size:11px",
|
|
2032
|
+
"flex:1",
|
|
2033
|
+
"min-width:0"
|
|
2034
|
+
].join(";");
|
|
2035
|
+
const setTextBtn = makeBtn("set", () => {
|
|
2036
|
+
if (bubbleInput.value.trim()) {
|
|
2037
|
+
bubbleTextEl.textContent = bubbleInput.value.trim();
|
|
2038
|
+
stateRef.current.bubbleVisible = true;
|
|
2039
|
+
stateRef.current.bubbleTimer = bubbleDisplayFrames;
|
|
2040
|
+
bubbleEl.style.opacity = "1";
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
body.appendChild(makeRow(bubbleInput, setTextBtn));
|
|
2044
|
+
}
|
|
2045
|
+
const ALL_STATES = [
|
|
2046
|
+
"moving",
|
|
2047
|
+
"idle",
|
|
2048
|
+
"sleeping",
|
|
2049
|
+
"scratchSelf",
|
|
2050
|
+
"tired",
|
|
2051
|
+
"alert",
|
|
2052
|
+
"scratchWallN",
|
|
2053
|
+
"scratchWallS",
|
|
2054
|
+
"scratchWallE",
|
|
2055
|
+
"scratchWallW",
|
|
2056
|
+
"freerun"
|
|
2057
|
+
];
|
|
2058
|
+
function applyForcedState(s, chosen, locked) {
|
|
2059
|
+
s.idleAnimation = null;
|
|
2060
|
+
s.idleAnimationFrame = 0;
|
|
2061
|
+
s.freerunMode = false;
|
|
2062
|
+
s.freerunTimer = 0;
|
|
2063
|
+
if (chosen === "moving") {
|
|
2064
|
+
s.idleTime = 0;
|
|
2065
|
+
s.stateLocked = locked;
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (chosen === "idle") {
|
|
2069
|
+
s.idleTime = 200;
|
|
2070
|
+
s.stateLocked = locked;
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (chosen === "freerun") {
|
|
2074
|
+
s.freerunMode = true;
|
|
2075
|
+
s.freerunTimer = 600;
|
|
2076
|
+
s.stateLocked = locked;
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
s.idleTime = 200;
|
|
2080
|
+
s.idleAnimation = chosen;
|
|
2081
|
+
s.idleAnimationFrame = 0;
|
|
2082
|
+
s.stateLocked = locked;
|
|
2083
|
+
}
|
|
2084
|
+
function appendForceStateSection(body, stateRef) {
|
|
2085
|
+
body.appendChild(sectionLabel("force state"));
|
|
2086
|
+
const stateSelect = document.createElement("select");
|
|
2087
|
+
stateSelect.style.cssText = selectStyle;
|
|
2088
|
+
for (const name of ALL_STATES) {
|
|
2089
|
+
const opt = document.createElement("option");
|
|
2090
|
+
opt.value = name;
|
|
2091
|
+
opt.textContent = name;
|
|
2092
|
+
stateSelect.appendChild(opt);
|
|
2093
|
+
}
|
|
2094
|
+
const lockChk = document.createElement("input");
|
|
2095
|
+
lockChk.type = "checkbox";
|
|
2096
|
+
lockChk.title = "lock state (prevent auto-reset)";
|
|
2097
|
+
lockChk.style.cssText = `cursor:pointer;accent-color:${debugColor.accent};`;
|
|
2098
|
+
lockChk.addEventListener("change", () => {
|
|
2099
|
+
stateRef.current.stateLocked = lockChk.checked;
|
|
2100
|
+
});
|
|
2101
|
+
const lockLbl = document.createElement("label");
|
|
2102
|
+
lockLbl.title = "lock state";
|
|
2103
|
+
lockLbl.style.cssText = `display:flex;align-items:center;gap:3px;cursor:pointer;font-size:10px;color:${debugColor.accent};white-space:nowrap;`;
|
|
2104
|
+
lockLbl.appendChild(lockChk);
|
|
2105
|
+
lockLbl.append("lock");
|
|
2106
|
+
body.appendChild(makeRow(stateSelect));
|
|
2107
|
+
const forceStateBtn = makeBtn("apply", () => {
|
|
2108
|
+
applyForcedState(stateRef.current, stateSelect.value, lockChk.checked);
|
|
2109
|
+
});
|
|
2110
|
+
body.appendChild(makeRow(forceStateBtn, lockLbl));
|
|
2111
|
+
}
|
|
2112
|
+
function appendPlaybackSection(body, stateRef, frame) {
|
|
2113
|
+
body.appendChild(sectionLabel("playback"));
|
|
2114
|
+
const pauseBtn = makeBtn("⏸ pause", () => {
|
|
2115
|
+
stateRef.current.paused = !stateRef.current.paused;
|
|
2116
|
+
pauseBtn.textContent = stateRef.current.paused ? "▶ play" : "⏸ pause";
|
|
2117
|
+
stepBtn.style.opacity = stateRef.current.paused ? "1" : "0.3";
|
|
2118
|
+
stepBtn.style.pointerEvents = stateRef.current.paused ? "auto" : "none";
|
|
2119
|
+
});
|
|
2120
|
+
const stepBtn = makeBtn("→ step", () => {
|
|
2121
|
+
if (stateRef.current.paused) {
|
|
2122
|
+
frame();
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
stepBtn.style.opacity = "0.3";
|
|
2126
|
+
stepBtn.style.pointerEvents = "none";
|
|
2127
|
+
body.appendChild(makeRow(pauseBtn, stepBtn));
|
|
2128
|
+
const followBtn = makeBtn("⛶ stop following", () => {
|
|
2129
|
+
stateRef.current.noFollow = !stateRef.current.noFollow;
|
|
2130
|
+
const on = stateRef.current.noFollow;
|
|
2131
|
+
if (on) {
|
|
2132
|
+
stateRef.current.freerunMode = false;
|
|
2133
|
+
stateRef.current.freerunTimer = 0;
|
|
2134
|
+
}
|
|
2135
|
+
followBtn.textContent = on ? "⛶ resume following" : "⛶ stop following";
|
|
2136
|
+
followBtn.style.color = on ? debugColor.accentStrong : debugColor.panelText;
|
|
2137
|
+
});
|
|
2138
|
+
followBtn.style.width = "100%";
|
|
2139
|
+
body.appendChild(makeRow(followBtn));
|
|
2140
|
+
}
|
|
2141
|
+
function syncCatElementPosition(el, s) {
|
|
2142
|
+
el.style.left = `${s.nekoPosX - TILE / 2}px`;
|
|
2143
|
+
el.style.top = `${s.nekoPosY - TILE / 2}px`;
|
|
2144
|
+
}
|
|
2145
|
+
function makePosSlider(axis, el, stateRef, getMax, getVal, setVal) {
|
|
2146
|
+
const lbl = document.createElement("span");
|
|
2147
|
+
lbl.textContent = axis;
|
|
2148
|
+
lbl.style.cssText = `min-width:10px;color:${debugColor.panelSubtle};font-size:10px;font-weight:bold;`;
|
|
2149
|
+
const valLbl = document.createElement("span");
|
|
2150
|
+
valLbl.style.cssText = "min-width:28px;text-align:right;font-size:10px;";
|
|
2151
|
+
const slider = document.createElement("input");
|
|
2152
|
+
slider.type = "range";
|
|
2153
|
+
slider.min = "16";
|
|
2154
|
+
slider.style.cssText = `flex:1;cursor:pointer;accent-color:${debugColor.accent};`;
|
|
2155
|
+
const sync = () => {
|
|
2156
|
+
slider.max = String(Math.round(getMax()));
|
|
2157
|
+
slider.value = String(Math.round(getVal()));
|
|
2158
|
+
valLbl.textContent = String(Math.round(getVal()));
|
|
2159
|
+
};
|
|
2160
|
+
sync();
|
|
2161
|
+
slider.addEventListener("mousedown", sync);
|
|
2162
|
+
slider.addEventListener("input", () => {
|
|
2163
|
+
const v = Number(slider.value);
|
|
2164
|
+
valLbl.textContent = String(v);
|
|
2165
|
+
setVal(v);
|
|
2166
|
+
syncCatElementPosition(el, stateRef.current);
|
|
2167
|
+
});
|
|
2168
|
+
return makeRow(lbl, slider, valLbl);
|
|
2169
|
+
}
|
|
2170
|
+
function appendPositionSection(body, stateRef, el) {
|
|
2171
|
+
body.appendChild(sectionLabel("position"));
|
|
2172
|
+
const teleportCenterBtn = makeBtn("→ center", () => {
|
|
2173
|
+
stateRef.current.nekoPosX = window.innerWidth / 2;
|
|
2174
|
+
stateRef.current.nekoPosY = window.innerHeight / 2;
|
|
2175
|
+
syncCatElementPosition(el, stateRef.current);
|
|
2176
|
+
});
|
|
2177
|
+
const teleportMouseBtn = makeBtn("→ cursor", () => {
|
|
2178
|
+
const s = stateRef.current;
|
|
2179
|
+
s.nekoPosX = s.mousePosX;
|
|
2180
|
+
s.nekoPosY = s.mousePosY;
|
|
2181
|
+
syncCatElementPosition(el, s);
|
|
2182
|
+
});
|
|
2183
|
+
body.appendChild(makeRow(teleportCenterBtn, teleportMouseBtn));
|
|
2184
|
+
body.appendChild(
|
|
2185
|
+
makePosSlider(
|
|
2186
|
+
"X",
|
|
2187
|
+
el,
|
|
2188
|
+
stateRef,
|
|
2189
|
+
() => window.innerWidth,
|
|
2190
|
+
() => stateRef.current.nekoPosX,
|
|
2191
|
+
(v) => {
|
|
2192
|
+
stateRef.current.nekoPosX = v;
|
|
2193
|
+
}
|
|
2194
|
+
)
|
|
2195
|
+
);
|
|
2196
|
+
body.appendChild(
|
|
2197
|
+
makePosSlider(
|
|
2198
|
+
"Y",
|
|
2199
|
+
el,
|
|
2200
|
+
stateRef,
|
|
2201
|
+
() => window.innerHeight,
|
|
2202
|
+
() => stateRef.current.nekoPosY,
|
|
2203
|
+
(v) => {
|
|
2204
|
+
stateRef.current.nekoPosY = v;
|
|
2205
|
+
}
|
|
2206
|
+
)
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
function appendSpeedSection(body, stateRef) {
|
|
2210
|
+
body.appendChild(sectionLabel("speed"));
|
|
2211
|
+
const speedLabel = document.createElement("span");
|
|
2212
|
+
speedLabel.textContent = String(stateRef.current.currentSpeed);
|
|
2213
|
+
speedLabel.style.cssText = "min-width:20px;text-align:right;";
|
|
2214
|
+
const speedSlider = document.createElement("input");
|
|
2215
|
+
speedSlider.type = "range";
|
|
2216
|
+
speedSlider.min = "1";
|
|
2217
|
+
speedSlider.max = "40";
|
|
2218
|
+
speedSlider.value = String(stateRef.current.currentSpeed);
|
|
2219
|
+
speedSlider.style.cssText = `flex:1;cursor:pointer;accent-color:${debugColor.accent};`;
|
|
2220
|
+
speedSlider.addEventListener("input", () => {
|
|
2221
|
+
const val = Number(speedSlider.value);
|
|
2222
|
+
speedLabel.textContent = String(val);
|
|
2223
|
+
stateRef.current.currentSpeed = val;
|
|
2224
|
+
});
|
|
2225
|
+
body.appendChild(makeRow(speedSlider, speedLabel));
|
|
2226
|
+
}
|
|
2227
|
+
function createDebugControls({
|
|
2228
|
+
stateRef,
|
|
2229
|
+
el,
|
|
2230
|
+
bubbleEl,
|
|
2231
|
+
bubbleTextEl,
|
|
2232
|
+
frame,
|
|
2233
|
+
showBubble: showBubble2,
|
|
2234
|
+
hideBubble: hideBubble2,
|
|
2235
|
+
bubbleDisplayFrames
|
|
2236
|
+
}) {
|
|
2237
|
+
const panel = document.createElement("div");
|
|
2238
|
+
panel.setAttribute("aria-hidden", "true");
|
|
2239
|
+
panel.style.cssText = "user-select:none;pointer-events:auto;";
|
|
2240
|
+
const header = document.createElement("div");
|
|
2241
|
+
header.style.cssText = `${BASE_STYLE};padding:6px 14px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;`;
|
|
2242
|
+
const headerTitle = document.createElement("span");
|
|
2243
|
+
headerTitle.textContent = "controls";
|
|
2244
|
+
headerTitle.style.cssText = `font-weight:bold;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:${debugColor.panelSubtle};`;
|
|
2245
|
+
const chevron = document.createElement("span");
|
|
2246
|
+
chevron.textContent = "▾";
|
|
2247
|
+
chevron.style.cssText = `font-size:10px;color:${debugColor.panelSubtle};`;
|
|
2248
|
+
header.appendChild(headerTitle);
|
|
2249
|
+
header.appendChild(chevron);
|
|
2250
|
+
panel.appendChild(header);
|
|
2251
|
+
const body = document.createElement("div");
|
|
2252
|
+
body.style.cssText = `${BASE_STYLE};padding:6px 14px 10px;margin-top:4px;`;
|
|
2253
|
+
panel.appendChild(body);
|
|
2254
|
+
let collapsed = false;
|
|
2255
|
+
header.addEventListener("click", () => {
|
|
2256
|
+
collapsed = !collapsed;
|
|
2257
|
+
body.style.display = collapsed ? "none" : "block";
|
|
2258
|
+
chevron.textContent = collapsed ? "▸" : "▾";
|
|
2259
|
+
});
|
|
2260
|
+
appendPlaybackSection(body, stateRef, frame);
|
|
2261
|
+
appendForceStateSection(body, stateRef);
|
|
2262
|
+
appendBubbleSection(
|
|
2263
|
+
body,
|
|
2264
|
+
stateRef,
|
|
2265
|
+
bubbleEl,
|
|
2266
|
+
bubbleTextEl,
|
|
2267
|
+
showBubble2,
|
|
2268
|
+
hideBubble2,
|
|
2269
|
+
bubbleDisplayFrames
|
|
2270
|
+
);
|
|
2271
|
+
appendPositionSection(body, stateRef, el);
|
|
2272
|
+
appendSpeedSection(body, stateRef);
|
|
2273
|
+
return panel;
|
|
2274
|
+
}
|
|
2275
|
+
function createCatElement(state, zIndex) {
|
|
2276
|
+
const el = document.createElement("div");
|
|
2277
|
+
if (!document.getElementById("oneko-react")) el.id = "oneko-react";
|
|
2278
|
+
el.setAttribute("data-oneko-cat", "");
|
|
2279
|
+
el.setAttribute("aria-hidden", "true");
|
|
2280
|
+
el.style.cssText = [
|
|
2281
|
+
`width:${TILE}px`,
|
|
2282
|
+
`height:${TILE}px`,
|
|
2283
|
+
"position:fixed",
|
|
2284
|
+
"pointer-events:none",
|
|
2285
|
+
"image-rendering:pixelated",
|
|
2286
|
+
`left:${state.nekoPosX - TILE / 2}px`,
|
|
2287
|
+
`top:${state.nekoPosY - TILE / 2}px`,
|
|
2288
|
+
`z-index:${zIndex}`,
|
|
2289
|
+
"background-repeat:no-repeat",
|
|
2290
|
+
"background-size:256px 128px",
|
|
2291
|
+
"transform:scale(1)",
|
|
2292
|
+
"opacity:1"
|
|
2293
|
+
].join(";");
|
|
2294
|
+
return el;
|
|
2295
|
+
}
|
|
2296
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
2297
|
+
function createDebugSVG(zIndex) {
|
|
2298
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
2299
|
+
svg.setAttribute("aria-hidden", "true");
|
|
2300
|
+
svg.style.cssText = [
|
|
2301
|
+
"position:fixed",
|
|
2302
|
+
"top:0",
|
|
2303
|
+
"left:0",
|
|
2304
|
+
"width:100vw",
|
|
2305
|
+
"height:100vh",
|
|
2306
|
+
"pointer-events:none",
|
|
2307
|
+
`z-index:${zIndex - 1}`,
|
|
2308
|
+
"display:none"
|
|
2309
|
+
].join(";");
|
|
2310
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
2311
|
+
const pattern = document.createElementNS(SVG_NS, "pattern");
|
|
2312
|
+
pattern.id = "oneko-grid";
|
|
2313
|
+
pattern.setAttribute("width", String(CELL_SIZE));
|
|
2314
|
+
pattern.setAttribute("height", String(CELL_SIZE));
|
|
2315
|
+
pattern.setAttribute("patternUnits", "userSpaceOnUse");
|
|
2316
|
+
const patternRect = document.createElementNS(SVG_NS, "rect");
|
|
2317
|
+
patternRect.setAttribute("width", String(CELL_SIZE));
|
|
2318
|
+
patternRect.setAttribute("height", String(CELL_SIZE));
|
|
2319
|
+
patternRect.setAttribute("fill", "none");
|
|
2320
|
+
patternRect.setAttribute("stroke", debugColor.gridStroke);
|
|
2321
|
+
patternRect.setAttribute("stroke-width", "0.5");
|
|
2322
|
+
pattern.appendChild(patternRect);
|
|
2323
|
+
defs.appendChild(pattern);
|
|
2324
|
+
svg.appendChild(defs);
|
|
2325
|
+
const gridBg = document.createElementNS(SVG_NS, "rect");
|
|
2326
|
+
gridBg.setAttribute("width", "100%");
|
|
2327
|
+
gridBg.setAttribute("height", "100%");
|
|
2328
|
+
gridBg.setAttribute("fill", "url(#oneko-grid)");
|
|
2329
|
+
svg.appendChild(gridBg);
|
|
2330
|
+
const blockedGroup = document.createElementNS(SVG_NS, "g");
|
|
2331
|
+
blockedGroup.id = "oneko-blocked";
|
|
2332
|
+
svg.appendChild(blockedGroup);
|
|
2333
|
+
const obstacleGroup = document.createElementNS(SVG_NS, "g");
|
|
2334
|
+
obstacleGroup.id = "oneko-obstacles";
|
|
2335
|
+
svg.appendChild(obstacleGroup);
|
|
2336
|
+
const pathGroup = document.createElementNS(SVG_NS, "g");
|
|
2337
|
+
pathGroup.id = "oneko-path";
|
|
2338
|
+
svg.appendChild(pathGroup);
|
|
2339
|
+
return svg;
|
|
2340
|
+
}
|
|
2341
|
+
function createBubble(zIndex) {
|
|
2342
|
+
const wrapper = document.createElement("div");
|
|
2343
|
+
wrapper.setAttribute("aria-hidden", "true");
|
|
2344
|
+
wrapper.style.cssText = [
|
|
2345
|
+
"position:fixed",
|
|
2346
|
+
"pointer-events:none",
|
|
2347
|
+
`z-index:${zIndex}`,
|
|
2348
|
+
"opacity:0",
|
|
2349
|
+
"transition:opacity 0.2s ease-out",
|
|
2350
|
+
"transform:translateX(-50%)",
|
|
2351
|
+
"display:flex",
|
|
2352
|
+
"flex-direction:column",
|
|
2353
|
+
"align-items:center",
|
|
2354
|
+
"outline:none"
|
|
2355
|
+
].join(";");
|
|
2356
|
+
const bubble = document.createElement("div");
|
|
2357
|
+
bubble.style.cssText = [
|
|
2358
|
+
"padding:4px 8px",
|
|
2359
|
+
"border-radius:8px",
|
|
2360
|
+
"box-sizing:border-box",
|
|
2361
|
+
"font-size:9px",
|
|
2362
|
+
"line-height:1.25",
|
|
2363
|
+
"font-weight:500",
|
|
2364
|
+
"font-family:var(--font-geist-pixel-square), monospace",
|
|
2365
|
+
"color:var(--foreground)",
|
|
2366
|
+
"background-color:var(--background)",
|
|
2367
|
+
"border:1px solid var(--border)",
|
|
2368
|
+
"max-width:150px",
|
|
2369
|
+
"width:max-content",
|
|
2370
|
+
"white-space:normal",
|
|
2371
|
+
"word-break:break-word",
|
|
2372
|
+
"outline:none"
|
|
2373
|
+
].join(";");
|
|
2374
|
+
wrapper.appendChild(bubble);
|
|
2375
|
+
const tailWrap = document.createElement("div");
|
|
2376
|
+
tailWrap.style.cssText = "position:relative;line-height:0;flex-shrink:0;outline:none;";
|
|
2377
|
+
const tailSvg = document.createElementNS(SVG_NS, "svg");
|
|
2378
|
+
tailSvg.setAttribute("width", "12");
|
|
2379
|
+
tailSvg.setAttribute("height", "7");
|
|
2380
|
+
tailSvg.setAttribute("viewBox", "0 0 12 7");
|
|
2381
|
+
tailSvg.setAttribute("overflow", "visible");
|
|
2382
|
+
tailSvg.style.cssText = "display:block;outline:none;";
|
|
2383
|
+
const tailPath = document.createElementNS(SVG_NS, "path");
|
|
2384
|
+
tailPath.setAttribute("d", "M 6 7 L 1.5 1.5 L 10.5 1.5 Z");
|
|
2385
|
+
tailPath.setAttribute("fill", "var(--background)");
|
|
2386
|
+
tailPath.setAttribute("stroke", "var(--border)");
|
|
2387
|
+
tailPath.setAttribute("stroke-width", "1");
|
|
2388
|
+
tailPath.setAttribute("stroke-linejoin", "miter");
|
|
2389
|
+
tailPath.setAttribute("stroke-linecap", "butt");
|
|
2390
|
+
tailPath.setAttribute("stroke-miterlimit", "2");
|
|
2391
|
+
tailPath.style.paintOrder = "stroke fill";
|
|
2392
|
+
tailSvg.appendChild(tailPath);
|
|
2393
|
+
tailWrap.appendChild(tailSvg);
|
|
2394
|
+
wrapper.appendChild(tailWrap);
|
|
2395
|
+
return { wrapper, textEl: bubble, tailWrap };
|
|
2396
|
+
}
|
|
2397
|
+
function createDebugHUD() {
|
|
2398
|
+
const hud = document.createElement("div");
|
|
2399
|
+
hud.setAttribute("aria-hidden", "true");
|
|
2400
|
+
hud.style.cssText = [
|
|
2401
|
+
"pointer-events:none",
|
|
2402
|
+
"font-family:var(--font-mono), monospace",
|
|
2403
|
+
"font-size:11px",
|
|
2404
|
+
"line-height:1.6",
|
|
2405
|
+
"padding:10px 14px",
|
|
2406
|
+
"border-radius:6px",
|
|
2407
|
+
`background-color:${debugColor.panelBg}`,
|
|
2408
|
+
`border:1px solid ${debugColor.panelBorder}`,
|
|
2409
|
+
`color:${debugColor.panelText}`,
|
|
2410
|
+
"width:190px"
|
|
2411
|
+
].join(";");
|
|
2412
|
+
return hud;
|
|
2413
|
+
}
|
|
2414
|
+
function createKonamiHandler(stateRef, debugSVG, debugWrapper) {
|
|
2415
|
+
const konamiBuffer = [];
|
|
2416
|
+
return (e) => {
|
|
2417
|
+
konamiBuffer.push(e.code);
|
|
2418
|
+
if (konamiBuffer.length > KONAMI_SEQUENCE.length) {
|
|
2419
|
+
konamiBuffer.shift();
|
|
2420
|
+
}
|
|
2421
|
+
if (konamiBuffer.join(",") !== KONAMI_TARGET) {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
stateRef.current.debugMode = !stateRef.current.debugMode;
|
|
2425
|
+
if (stateRef.current.debugMode) {
|
|
2426
|
+
debugSVG.style.display = "block";
|
|
2427
|
+
debugWrapper.style.display = "flex";
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
debugSVG.style.display = "none";
|
|
2431
|
+
debugWrapper.style.display = "none";
|
|
2432
|
+
stateRef.current.paused = false;
|
|
2433
|
+
stateRef.current.stateLocked = false;
|
|
2434
|
+
stateRef.current.noFollow = false;
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
function parsePersistedPosition(value) {
|
|
2438
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2439
|
+
const record = value;
|
|
2440
|
+
if (record.version !== void 0 && record.version !== 1) return null;
|
|
2441
|
+
const x = record.version === 1 ? record.x : record.nekoPosX;
|
|
2442
|
+
const y = record.version === 1 ? record.y : record.nekoPosY;
|
|
2443
|
+
if (typeof x !== "number" || !Number.isFinite(x)) return null;
|
|
2444
|
+
if (typeof y !== "number" || !Number.isFinite(y)) return null;
|
|
2445
|
+
return { version: 1, x, y };
|
|
2446
|
+
}
|
|
2447
|
+
function readPersistedPosition(storageKey = ONEKO_DEFAULTS.storageKey) {
|
|
2448
|
+
let position;
|
|
2449
|
+
try {
|
|
2450
|
+
position = parsePersistedPosition(
|
|
2451
|
+
JSON.parse(window.localStorage.getItem(storageKey) ?? "null")
|
|
2452
|
+
);
|
|
2453
|
+
} catch {
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
if (!position) return null;
|
|
2457
|
+
const clamp = (value, extent) => {
|
|
2458
|
+
const inset = Math.min(TILE / 2, Math.max(0, extent / 2));
|
|
2459
|
+
return Math.max(inset, Math.min(Math.max(inset, extent - inset), value));
|
|
2460
|
+
};
|
|
2461
|
+
return {
|
|
2462
|
+
x: clamp(position.x, window.innerWidth),
|
|
2463
|
+
y: clamp(position.y, window.innerHeight)
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
function loadPersistedCatState(stateRef, el, storageKey = ONEKO_DEFAULTS.storageKey) {
|
|
2467
|
+
const position = readPersistedPosition(storageKey);
|
|
2468
|
+
if (!position) return;
|
|
2469
|
+
const state = stateRef.current;
|
|
2470
|
+
state.nekoPosX = state.mousePosX = position.x;
|
|
2471
|
+
state.nekoPosY = state.mousePosY = position.y;
|
|
2472
|
+
state.nekoVelX = state.nekoVelY = 0;
|
|
2473
|
+
state.currentPath = [];
|
|
2474
|
+
state.pathWaypointIdx = 0;
|
|
2475
|
+
el.style.left = `${state.nekoPosX - TILE / 2}px`;
|
|
2476
|
+
el.style.top = `${state.nekoPosY - TILE / 2}px`;
|
|
2477
|
+
}
|
|
2478
|
+
function createPersistHandler(stateRef, storageKey = ONEKO_DEFAULTS.storageKey) {
|
|
2479
|
+
return () => {
|
|
2480
|
+
const state = stateRef.current;
|
|
2481
|
+
const position = parsePersistedPosition({
|
|
2482
|
+
version: 1,
|
|
2483
|
+
x: state.nekoPosX,
|
|
2484
|
+
y: state.nekoPosY
|
|
2485
|
+
});
|
|
2486
|
+
if (!position) return;
|
|
2487
|
+
try {
|
|
2488
|
+
window.localStorage.setItem(storageKey, JSON.stringify(position));
|
|
2489
|
+
} catch {
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
function teardownCatAnimation({ el, debugSVG, debugWrapper, bubbleEl }, {
|
|
2494
|
+
onMouseMove,
|
|
2495
|
+
onPointerDown,
|
|
2496
|
+
onKeyDown,
|
|
2497
|
+
invalidateObstacles,
|
|
2498
|
+
persist,
|
|
2499
|
+
onVisibilityChange,
|
|
2500
|
+
stopAnimationLoop
|
|
2501
|
+
}) {
|
|
2502
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
2503
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
2504
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
2505
|
+
window.removeEventListener("scroll", invalidateObstacles);
|
|
2506
|
+
window.removeEventListener("resize", invalidateObstacles);
|
|
2507
|
+
if (persist) {
|
|
2508
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2509
|
+
window.removeEventListener("pagehide", persist);
|
|
2510
|
+
}
|
|
2511
|
+
stopAnimationLoop();
|
|
2512
|
+
for (const node of [el, debugSVG, debugWrapper, bubbleEl]) {
|
|
2513
|
+
if (node.parentNode) {
|
|
2514
|
+
node.parentNode.removeChild(node);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
function createDebugWrapper(zIndex, debugHUD, debugControls) {
|
|
2519
|
+
const debugWrapper = document.createElement("div");
|
|
2520
|
+
debugWrapper.setAttribute("aria-hidden", "true");
|
|
2521
|
+
debugWrapper.style.cssText = [
|
|
2522
|
+
"position:fixed",
|
|
2523
|
+
"top:10px",
|
|
2524
|
+
"left:10px",
|
|
2525
|
+
`z-index:${zIndex}`,
|
|
2526
|
+
"display:none",
|
|
2527
|
+
"flex-direction:column",
|
|
2528
|
+
"gap:6px",
|
|
2529
|
+
"pointer-events:none"
|
|
2530
|
+
].join(";");
|
|
2531
|
+
debugWrapper.appendChild(debugHUD);
|
|
2532
|
+
debugWrapper.appendChild(debugControls);
|
|
2533
|
+
return debugWrapper;
|
|
2534
|
+
}
|
|
2535
|
+
function startCatAnimation({
|
|
2536
|
+
stateRef,
|
|
2537
|
+
elRef,
|
|
2538
|
+
lastStateRef,
|
|
2539
|
+
onStateChangeRef,
|
|
2540
|
+
liveStateRef,
|
|
2541
|
+
persistPosition,
|
|
2542
|
+
storageKey = ONEKO_DEFAULTS.storageKey,
|
|
2543
|
+
zIndex,
|
|
2544
|
+
pettable = ONEKO_DEFAULTS.pettable
|
|
2545
|
+
}) {
|
|
2546
|
+
stateRef.current.bubbleVisible = false;
|
|
2547
|
+
stateRef.current.bubbleTimer = 0;
|
|
2548
|
+
const el = createCatElement(stateRef.current, zIndex);
|
|
2549
|
+
elRef.current = el;
|
|
2550
|
+
const debugSVG = createDebugSVG(zIndex);
|
|
2551
|
+
const debugHUD = createDebugHUD();
|
|
2552
|
+
const { wrapper: bubbleEl, textEl: bubbleTextEl, tailWrap: bubbleTail } = createBubble(zIndex);
|
|
2553
|
+
const deps = {
|
|
2554
|
+
stateRef,
|
|
2555
|
+
el,
|
|
2556
|
+
bubbleEl,
|
|
2557
|
+
bubbleTextEl,
|
|
2558
|
+
bubbleTail,
|
|
2559
|
+
debugSVG,
|
|
2560
|
+
debugHUD,
|
|
2561
|
+
lastStateRef,
|
|
2562
|
+
onStateChangeRef,
|
|
2563
|
+
liveStateRef
|
|
2564
|
+
};
|
|
2565
|
+
const frame = createFrameLoop(deps);
|
|
2566
|
+
const debugControls = createDebugControls({
|
|
2567
|
+
stateRef,
|
|
2568
|
+
el,
|
|
2569
|
+
bubbleEl,
|
|
2570
|
+
bubbleTextEl,
|
|
2571
|
+
frame,
|
|
2572
|
+
showBubble: () => showBubble(deps),
|
|
2573
|
+
hideBubble: () => hideBubble(deps),
|
|
2574
|
+
bubbleDisplayFrames: BUBBLE_DISPLAY_FRAMES
|
|
2575
|
+
});
|
|
2576
|
+
const debugWrapper = createDebugWrapper(zIndex, debugHUD, debugControls);
|
|
2577
|
+
if (persistPosition) {
|
|
2578
|
+
loadPersistedCatState(stateRef, el, storageKey);
|
|
2579
|
+
}
|
|
2580
|
+
refreshZones(deps);
|
|
2581
|
+
document.body.appendChild(debugSVG);
|
|
2582
|
+
document.body.appendChild(debugWrapper);
|
|
2583
|
+
document.body.appendChild(el);
|
|
2584
|
+
document.body.appendChild(bubbleEl);
|
|
2585
|
+
const onMouseMove = createMouseMoveHandler(deps);
|
|
2586
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
2587
|
+
const onPointerDown = (event) => {
|
|
2588
|
+
if (event.pointerType !== "touch") return;
|
|
2589
|
+
if (event.target instanceof Element && event.target.closest("button,a,input,select,textarea,label,summary,[role='button']"))
|
|
2590
|
+
return;
|
|
2591
|
+
onMouseMove(event);
|
|
2592
|
+
};
|
|
2593
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
2594
|
+
const invalidateObstacles = () => {
|
|
2595
|
+
stateRef.current.lastObstacleRefresh = -OBSTACLE_INTERVAL;
|
|
2596
|
+
stateRef.current.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
|
|
2597
|
+
};
|
|
2598
|
+
window.addEventListener("scroll", invalidateObstacles, { passive: true });
|
|
2599
|
+
window.addEventListener("resize", invalidateObstacles, { passive: true });
|
|
2600
|
+
const persist = persistPosition ? createPersistHandler(stateRef, storageKey) : null;
|
|
2601
|
+
const onVisibilityChange = () => {
|
|
2602
|
+
if (document.hidden) persist?.();
|
|
2603
|
+
};
|
|
2604
|
+
if (persist) {
|
|
2605
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2606
|
+
window.addEventListener("pagehide", persist);
|
|
2607
|
+
}
|
|
2608
|
+
const onKeyDown = createKonamiHandler(stateRef, debugSVG, debugWrapper);
|
|
2609
|
+
document.addEventListener("keydown", onKeyDown);
|
|
2610
|
+
const leavePack = joinPack(stateRef.current);
|
|
2611
|
+
const stopWatchingPage = watchPage(deps);
|
|
2612
|
+
const stopPetting = pettable ? makePettable(deps) : void 0;
|
|
2613
|
+
const stopAnimationLoop = startAnimationLoop(stateRef, frame);
|
|
2614
|
+
return () => {
|
|
2615
|
+
stopPetting?.();
|
|
2616
|
+
stopWatchingPage();
|
|
2617
|
+
leavePack();
|
|
2618
|
+
persist?.();
|
|
2619
|
+
teardownCatAnimation(
|
|
2620
|
+
{ el, debugSVG, debugWrapper, bubbleEl },
|
|
2621
|
+
{
|
|
2622
|
+
onMouseMove,
|
|
2623
|
+
onPointerDown,
|
|
2624
|
+
onKeyDown,
|
|
2625
|
+
invalidateObstacles,
|
|
2626
|
+
persist,
|
|
2627
|
+
onVisibilityChange,
|
|
2628
|
+
stopAnimationLoop
|
|
2629
|
+
}
|
|
2630
|
+
);
|
|
2631
|
+
elRef.current = null;
|
|
2632
|
+
stateRef.current.bubbleVisible = false;
|
|
2633
|
+
stateRef.current.bubbleTimer = 0;
|
|
2634
|
+
if (liveStateRef) liveStateRef.current.bubbleVisible = false;
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
function defaultRuntimeConfigState() {
|
|
2638
|
+
return {
|
|
2639
|
+
pausedCfg: ONEKO_DEFAULTS.paused,
|
|
2640
|
+
followCursorCfg: ONEKO_DEFAULTS.followCursor,
|
|
2641
|
+
sleepEnabledCfg: ONEKO_DEFAULTS.sleepEnabled,
|
|
2642
|
+
bubblePlacementCfg: ONEKO_DEFAULTS.bubblePlacement,
|
|
2643
|
+
bubbleScaleCfg: ONEKO_DEFAULTS.bubbleScale,
|
|
2644
|
+
soundBasePathCfg: ONEKO_DEFAULTS.soundBasePath,
|
|
2645
|
+
currentSpeed: ONEKO_DEFAULTS.speed,
|
|
2646
|
+
scale: ONEKO_DEFAULTS.scale,
|
|
2647
|
+
opacity: ONEKO_DEFAULTS.opacity,
|
|
2648
|
+
rotationAmount: ONEKO_DEFAULTS.rotationAmount,
|
|
2649
|
+
idleThresholdMs: ONEKO_DEFAULTS.idleThreshold,
|
|
2650
|
+
freerunChanceCfg: ONEKO_DEFAULTS.freerunChance,
|
|
2651
|
+
freerunDurationCfg: ONEKO_DEFAULTS.freerunDuration,
|
|
2652
|
+
bubbleEnabledCfg: ONEKO_DEFAULTS.bubbleEnabled,
|
|
2653
|
+
bubbleDisplayFramesCfg: ONEKO_DEFAULTS.bubbleDisplayFrames,
|
|
2654
|
+
bubbleCooldownFramesCfg: ONEKO_DEFAULTS.bubbleCooldown,
|
|
2655
|
+
bubbleChanceCfg: ONEKO_DEFAULTS.bubbleChance,
|
|
2656
|
+
followDistanceCfg: ONEKO_DEFAULTS.followDistance,
|
|
2657
|
+
animationSpeedCfg: ONEKO_DEFAULTS.animationSpeed,
|
|
2658
|
+
customBubbleText: ONEKO_DEFAULTS.bubbleText,
|
|
2659
|
+
currentRotation: 0,
|
|
2660
|
+
enableMeow: ONEKO_DEFAULTS.meow,
|
|
2661
|
+
soundVolumeCfg: ONEKO_DEFAULTS.volume,
|
|
2662
|
+
soundCooldown: 0,
|
|
2663
|
+
bubbleTimer: 0,
|
|
2664
|
+
bubbleCooldown: 0,
|
|
2665
|
+
bubbleVisible: false,
|
|
2666
|
+
lastBubbleMsg: -1,
|
|
2667
|
+
laserPointerCfg: ONEKO_DEFAULTS.laserPointer,
|
|
2668
|
+
laserCaught: false,
|
|
2669
|
+
chaseCfg: ONEKO_DEFAULTS.chase,
|
|
2670
|
+
napWhileTypingCfg: ONEKO_DEFAULTS.napWhileTyping,
|
|
2671
|
+
followFocusCfg: ONEKO_DEFAULTS.followFocus,
|
|
2672
|
+
perchOnHoverCfg: ONEKO_DEFAULTS.perchOnHover
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
function applyRuntimeConfig(state, config) {
|
|
2676
|
+
state.pausedCfg = config.paused ?? ONEKO_DEFAULTS.paused;
|
|
2677
|
+
state.followCursorCfg = config.followCursor ?? ONEKO_DEFAULTS.followCursor;
|
|
2678
|
+
state.sleepEnabledCfg = config.sleepEnabled ?? ONEKO_DEFAULTS.sleepEnabled;
|
|
2679
|
+
state.bubblePlacementCfg = config.bubblePlacement === "above" || config.bubblePlacement === "below" ? config.bubblePlacement : ONEKO_DEFAULTS.bubblePlacement;
|
|
2680
|
+
state.bubbleScaleCfg = Number.isFinite(config.bubbleScale) ? Math.max(0.5, Math.min(2, config.bubbleScale)) : ONEKO_DEFAULTS.bubbleScale;
|
|
2681
|
+
state.soundBasePathCfg = (config.soundBasePath ?? ONEKO_DEFAULTS.soundBasePath).replace(
|
|
2682
|
+
/\/+$/,
|
|
2683
|
+
""
|
|
2684
|
+
);
|
|
2685
|
+
if (!state.followCursorCfg) {
|
|
2686
|
+
state.freerunMode = false;
|
|
2687
|
+
state.freerunTimer = 0;
|
|
2688
|
+
state.currentPath = [];
|
|
2689
|
+
state.pathWaypointIdx = 0;
|
|
2690
|
+
state.nekoVelX = 0;
|
|
2691
|
+
state.nekoVelY = 0;
|
|
2692
|
+
}
|
|
2693
|
+
state.zoneState.definitions = config.zones ?? EMPTY_ZONES;
|
|
2694
|
+
state.zoneState.chance = Number.isFinite(config.zoneAttractionChance) ? Math.max(0, Math.min(1, config.zoneAttractionChance)) : ONEKO_DEFAULTS.zoneAttractionChance;
|
|
2695
|
+
state.zoneState.duration = Number.isFinite(config.zoneAttractionDuration) ? Math.max(1, Math.min(600, Math.round(config.zoneAttractionDuration / 100))) : ONEKO_DEFAULTS.zoneAttractionDuration / 100;
|
|
2696
|
+
state.currentSpeed = config.speed;
|
|
2697
|
+
state.scale = config.scale;
|
|
2698
|
+
state.opacity = config.opacity;
|
|
2699
|
+
state.rotationAmount = config.rotationAmount;
|
|
2700
|
+
state.idleThresholdMs = config.idleThreshold;
|
|
2701
|
+
state.freerunChanceCfg = config.freerunChance;
|
|
2702
|
+
state.freerunDurationCfg = config.freerunDuration;
|
|
2703
|
+
state.bubbleEnabledCfg = config.bubbleEnabled;
|
|
2704
|
+
state.bubbleDisplayFramesCfg = config.bubbleDisplayFrames;
|
|
2705
|
+
state.bubbleCooldownFramesCfg = config.bubbleCooldown;
|
|
2706
|
+
state.bubbleChanceCfg = config.bubbleChance;
|
|
2707
|
+
state.followDistanceCfg = config.followDistance;
|
|
2708
|
+
state.animationSpeedCfg = config.animationSpeed;
|
|
2709
|
+
state.customBubbleText = config.bubbleText;
|
|
2710
|
+
state.enableMeow = config.meow;
|
|
2711
|
+
state.soundVolumeCfg = config.volume;
|
|
2712
|
+
state.laserPointerCfg = config.laserPointer;
|
|
2713
|
+
if (!config.laserPointer) {
|
|
2714
|
+
state.laserCaught = false;
|
|
2715
|
+
}
|
|
2716
|
+
state.chaseCfg = config.chase === "cat" ? "cat" : "cursor";
|
|
2717
|
+
state.napWhileTypingCfg = config.napWhileTyping ?? ONEKO_DEFAULTS.napWhileTyping;
|
|
2718
|
+
state.followFocusCfg = config.followFocus ?? ONEKO_DEFAULTS.followFocus;
|
|
2719
|
+
state.perchOnHoverCfg = config.perchOnHover ?? ONEKO_DEFAULTS.perchOnHover;
|
|
2720
|
+
if (!state.napWhileTypingCfg) state.typingNap = false;
|
|
2721
|
+
if (!state.perchOnHoverCfg) state.perchTarget = null;
|
|
2722
|
+
}
|
|
2723
|
+
const classic = "data:image/png;base64,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";
|
|
2724
|
+
const classicSheet = {
|
|
2725
|
+
classic
|
|
2726
|
+
};
|
|
2727
|
+
const skinIds = [
|
|
2728
|
+
"classic",
|
|
2729
|
+
"black",
|
|
2730
|
+
"gray",
|
|
2731
|
+
"calico",
|
|
2732
|
+
"tora",
|
|
2733
|
+
"catppuccin",
|
|
2734
|
+
"ghost",
|
|
2735
|
+
"silver",
|
|
2736
|
+
"spirit",
|
|
2737
|
+
"valentine",
|
|
2738
|
+
"maia",
|
|
2739
|
+
"vaporwave",
|
|
2740
|
+
"ginger",
|
|
2741
|
+
"sage",
|
|
2742
|
+
"siamese",
|
|
2743
|
+
"strawberry-milk",
|
|
2744
|
+
"blue-frost",
|
|
2745
|
+
"lavender",
|
|
2746
|
+
"tuxedo",
|
|
2747
|
+
"peach",
|
|
2748
|
+
"honey",
|
|
2749
|
+
"mocha",
|
|
2750
|
+
"mint",
|
|
2751
|
+
"midnight-blue"
|
|
2752
|
+
];
|
|
2753
|
+
const SKIN_CATALOG = [
|
|
2754
|
+
{ id: "classic", name: "Classic", description: "The little cat that started it all" },
|
|
2755
|
+
{ id: "black", name: "Black", description: "A tiny midnight shadow" },
|
|
2756
|
+
{ id: "gray", name: "Gray", description: "Soft fur, softer footsteps" },
|
|
2757
|
+
{ id: "calico", name: "Calico", description: "A patchwork of mischief" },
|
|
2758
|
+
{ id: "tora", name: "Tora", description: "A little tiger at heart" },
|
|
2759
|
+
{ id: "catppuccin", name: "Catppuccin", description: "A warm cup of cat" },
|
|
2760
|
+
{ id: "ghost", name: "Ghost", description: "A pleasantly spooky companion" },
|
|
2761
|
+
{ id: "silver", name: "Silver", description: "Moonlight on little paws" },
|
|
2762
|
+
{ id: "spirit", name: "Spirit", description: "A curious little daydream" },
|
|
2763
|
+
{ id: "valentine", name: "Valentine", description: "Wears a heart on every paw" },
|
|
2764
|
+
{ id: "maia", name: "Maia", description: "Small paws, big personality" },
|
|
2765
|
+
{ id: "vaporwave", name: "Vaporwave", description: "Dreaming in pink and purple" },
|
|
2766
|
+
{ id: "ginger", name: "Ginger", description: "Golden fur and cream-colored paws" },
|
|
2767
|
+
{ id: "sage", name: "Sage", description: "A little garden companion" },
|
|
2768
|
+
{ id: "siamese", name: "Siamese", description: "Chocolate points and bright blue eyes" },
|
|
2769
|
+
{
|
|
2770
|
+
id: "strawberry-milk",
|
|
2771
|
+
name: "Strawberry Milk",
|
|
2772
|
+
description: "Soft pink with a splash of cream"
|
|
2773
|
+
},
|
|
2774
|
+
{ id: "blue-frost", name: "Blue Frost", description: "Snowy paws and an icy blue coat" },
|
|
2775
|
+
{ id: "lavender", name: "Lavender", description: "Lilac fur and a little daydream" },
|
|
2776
|
+
{ id: "tuxedo", name: "Tuxedo", description: "Dressed up in a white bib and socks" },
|
|
2777
|
+
{ id: "peach", name: "Peach", description: "Apricot fur with a touch of vanilla" },
|
|
2778
|
+
{ id: "honey", name: "Honey", description: "Golden fur and a sunny disposition" },
|
|
2779
|
+
{ id: "mocha", name: "Mocha", description: "Cocoa fur with a splash of milk" },
|
|
2780
|
+
{ id: "mint", name: "Mint", description: "Fresh turquoise and snowy little paws" },
|
|
2781
|
+
{ id: "midnight-blue", name: "Midnight Blue", description: "Navy fur with a silver lining" }
|
|
2782
|
+
];
|
|
2783
|
+
const installed = new Set(skinIds);
|
|
2784
|
+
const ONEKO_SKINS = SKIN_CATALOG.filter(
|
|
2785
|
+
(entry) => isOnekoSkin(entry.id)
|
|
2786
|
+
);
|
|
2787
|
+
function isOnekoSkin(value) {
|
|
2788
|
+
return typeof value === "string" && installed.has(value);
|
|
2789
|
+
}
|
|
2790
|
+
const loaded = /* @__PURE__ */ new Map([["classic", classicSheet.classic]]);
|
|
2791
|
+
let pending;
|
|
2792
|
+
function getSkinSource(skin = "classic") {
|
|
2793
|
+
return loaded.get(isOnekoSkin(skin) ? skin : "classic");
|
|
2794
|
+
}
|
|
2795
|
+
async function loadSkinSource(skin = "classic") {
|
|
2796
|
+
const id = isOnekoSkin(skin) ? skin : "classic";
|
|
2797
|
+
if (!loaded.has(id)) {
|
|
2798
|
+
pending ??= import("./skin-sheets-CG0yKeZb.js").then(({ default: sheets }) => {
|
|
2799
|
+
for (const [key, source] of Object.entries(sheets)) loaded.set(key, source);
|
|
2800
|
+
});
|
|
2801
|
+
await pending;
|
|
2802
|
+
}
|
|
2803
|
+
return loaded.get(id) ?? loaded.get("classic");
|
|
2804
|
+
}
|
|
2805
|
+
function createInitialCatState(options) {
|
|
2806
|
+
const startX = options.initialPos?.x ?? DEFAULT_POSITION.x;
|
|
2807
|
+
const startY = options.initialPos?.y ?? DEFAULT_POSITION.y;
|
|
2808
|
+
const state = {
|
|
2809
|
+
zoneState: createZoneState(),
|
|
2810
|
+
nekoPosX: startX,
|
|
2811
|
+
nekoPosY: startY,
|
|
2812
|
+
nekoVelX: 0,
|
|
2813
|
+
nekoVelY: 0,
|
|
2814
|
+
mousePosX: startX,
|
|
2815
|
+
mousePosY: startY,
|
|
2816
|
+
frameCount: 0,
|
|
2817
|
+
idleTime: 0,
|
|
2818
|
+
idleAnimation: null,
|
|
2819
|
+
idleAnimationFrame: 0,
|
|
2820
|
+
obstacleRects: [],
|
|
2821
|
+
lastObstacleRefresh: -OBSTACLE_INTERVAL,
|
|
2822
|
+
grid: null,
|
|
2823
|
+
gridCols: 0,
|
|
2824
|
+
gridRows: 0,
|
|
2825
|
+
currentPath: [],
|
|
2826
|
+
pathWaypointIdx: 0,
|
|
2827
|
+
lastPathRecalcFrame: -PATH_RECALC_INTERVAL,
|
|
2828
|
+
lastPathTargetCol: -1,
|
|
2829
|
+
lastPathTargetRow: -1,
|
|
2830
|
+
debugMode: false,
|
|
2831
|
+
paused: false,
|
|
2832
|
+
stateLocked: false,
|
|
2833
|
+
noFollow: false,
|
|
2834
|
+
...defaultRuntimeConfigState(),
|
|
2835
|
+
loopPrevAngle: null,
|
|
2836
|
+
mouseCircleWinding: 0,
|
|
2837
|
+
freerunMode: false,
|
|
2838
|
+
freerunTimer: 0,
|
|
2839
|
+
lastFreerunMsg: -1,
|
|
2840
|
+
typingNap: false,
|
|
2841
|
+
held: false,
|
|
2842
|
+
perchTarget: null,
|
|
2843
|
+
leaderTarget: null
|
|
2844
|
+
};
|
|
2845
|
+
applyRuntimeConfig(state, options);
|
|
2846
|
+
return state;
|
|
2847
|
+
}
|
|
2848
|
+
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
2849
|
+
function createOneko(initial = {}, base = {}) {
|
|
2850
|
+
const fallback = { ...ONEKO_DEFAULTS, ...base };
|
|
2851
|
+
const resolve = (next) => Object.fromEntries(
|
|
2852
|
+
Object.entries(next).map(([key, value]) => [
|
|
2853
|
+
key,
|
|
2854
|
+
value === void 0 ? fallback[key] : value
|
|
2855
|
+
])
|
|
2856
|
+
);
|
|
2857
|
+
let options = { ...fallback, ...resolve(initial) };
|
|
2858
|
+
const stateRef = { current: createInitialCatState(runtimeConfig(options)) };
|
|
2859
|
+
const elRef = { current: null };
|
|
2860
|
+
const lastStateRef = { current: "idle" };
|
|
2861
|
+
const onStateChangeRef = { current: options.onStateChange };
|
|
2862
|
+
const motionQuery = window.matchMedia?.(REDUCED_MOTION_QUERY);
|
|
2863
|
+
let stopEngine;
|
|
2864
|
+
let stopLaser;
|
|
2865
|
+
let resting;
|
|
2866
|
+
let skinRequest = 0;
|
|
2867
|
+
let destroyed = false;
|
|
2868
|
+
const reducedMotion = () => motionQuery?.matches ?? false;
|
|
2869
|
+
const paint = () => {
|
|
2870
|
+
const el = elRef.current ?? resting;
|
|
2871
|
+
if (!el) return;
|
|
2872
|
+
const rotation = resting ? 0 : stateRef.current.currentRotation;
|
|
2873
|
+
el.style.transform = `scale(${options.scale}) rotate(${rotation}deg)`;
|
|
2874
|
+
el.style.opacity = String(options.opacity);
|
|
2875
|
+
el.style.filter = options.hueRotate ? `hue-rotate(${options.hueRotate}deg)` : "";
|
|
2876
|
+
const source = options.spriteSrc || getSkinSource(options.skin);
|
|
2877
|
+
if (source) {
|
|
2878
|
+
el.style.backgroundImage = `url(${JSON.stringify(source)})`;
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
const request = ++skinRequest;
|
|
2882
|
+
loadSkinSource(options.skin).then(
|
|
2883
|
+
() => request === skinRequest && !destroyed && paint(),
|
|
2884
|
+
() => {
|
|
2885
|
+
}
|
|
2886
|
+
);
|
|
2887
|
+
};
|
|
2888
|
+
const stop = () => {
|
|
2889
|
+
stopEngine?.();
|
|
2890
|
+
stopLaser?.();
|
|
2891
|
+
resting?.remove();
|
|
2892
|
+
stopEngine = stopLaser = resting = void 0;
|
|
2893
|
+
elRef.current = null;
|
|
2894
|
+
};
|
|
2895
|
+
const start = () => {
|
|
2896
|
+
stop();
|
|
2897
|
+
if (areCatsHidden()) return;
|
|
2898
|
+
if (reducedMotion()) {
|
|
2899
|
+
if (options.reducedMotion === "rest") {
|
|
2900
|
+
resting = createRestingCat(options);
|
|
2901
|
+
document.body.appendChild(resting);
|
|
2902
|
+
paint();
|
|
2903
|
+
}
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
stopEngine = startCatAnimation({
|
|
2907
|
+
stateRef,
|
|
2908
|
+
elRef,
|
|
2909
|
+
lastStateRef,
|
|
2910
|
+
onStateChangeRef,
|
|
2911
|
+
persistPosition: options.persistPosition,
|
|
2912
|
+
storageKey: options.storageKey,
|
|
2913
|
+
zIndex: options.zIndex,
|
|
2914
|
+
pettable: options.pettable
|
|
2915
|
+
});
|
|
2916
|
+
if (options.laserPointer && options.followCursor && !options.paused)
|
|
2917
|
+
stopLaser = mountLaserCursor(options.zIndex);
|
|
2918
|
+
paint();
|
|
2919
|
+
};
|
|
2920
|
+
motionQuery?.addEventListener("change", start);
|
|
2921
|
+
const stopWatchingHidden = onCatsHiddenChange(start);
|
|
2922
|
+
start();
|
|
2923
|
+
return {
|
|
2924
|
+
update(next) {
|
|
2925
|
+
if (destroyed) return;
|
|
2926
|
+
const previous = options;
|
|
2927
|
+
options = { ...options, ...resolve(next) };
|
|
2928
|
+
onStateChangeRef.current = options.onStateChange;
|
|
2929
|
+
applyRuntimeConfig(stateRef.current, runtimeConfig(options));
|
|
2930
|
+
const restart = [
|
|
2931
|
+
"zIndex",
|
|
2932
|
+
"storageKey",
|
|
2933
|
+
"persistPosition",
|
|
2934
|
+
"reducedMotion",
|
|
2935
|
+
"laserPointer",
|
|
2936
|
+
"pettable"
|
|
2937
|
+
].some((key) => previous[key] !== options[key]);
|
|
2938
|
+
const laserChanged = previous.followCursor !== options.followCursor || previous.paused !== options.paused;
|
|
2939
|
+
if (restart || laserChanged && options.laserPointer) start();
|
|
2940
|
+
else paint();
|
|
2941
|
+
},
|
|
2942
|
+
destroy() {
|
|
2943
|
+
if (destroyed) return;
|
|
2944
|
+
destroyed = true;
|
|
2945
|
+
motionQuery?.removeEventListener("change", start);
|
|
2946
|
+
stopWatchingHidden();
|
|
2947
|
+
stop();
|
|
2948
|
+
}
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
function runtimeConfig(options) {
|
|
2952
|
+
return {
|
|
2953
|
+
...options,
|
|
2954
|
+
laserPointer: options.laserPointer && options.followCursor && !options.paused
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
function createRestingCat(options) {
|
|
2958
|
+
const position = options.persistPosition && readPersistedPosition(options.storageKey) || options.initialPos;
|
|
2959
|
+
const inset = TILE * options.scale / 2 + 16 - TILE / 2;
|
|
2960
|
+
const el = document.createElement("div");
|
|
2961
|
+
el.setAttribute("aria-hidden", "true");
|
|
2962
|
+
el.style.cssText = [
|
|
2963
|
+
"position:fixed",
|
|
2964
|
+
position ? `left:${position.x - TILE / 2}px` : `right:${inset}px`,
|
|
2965
|
+
position ? `top:${position.y - TILE / 2}px` : `bottom:${inset}px`,
|
|
2966
|
+
`width:${TILE}px`,
|
|
2967
|
+
`height:${TILE}px`,
|
|
2968
|
+
"pointer-events:none",
|
|
2969
|
+
"image-rendering:pixelated",
|
|
2970
|
+
"background-repeat:no-repeat",
|
|
2971
|
+
"background-size:256px 128px",
|
|
2972
|
+
`background-position:-${2 * TILE}px 0`,
|
|
2973
|
+
`z-index:${options.zIndex}`
|
|
2974
|
+
].join(";");
|
|
2975
|
+
return el;
|
|
2976
|
+
}
|
|
2977
|
+
export {
|
|
2978
|
+
ONEKO_DEFAULTS as O,
|
|
2979
|
+
TILE as T,
|
|
2980
|
+
applyRuntimeConfig as a,
|
|
2981
|
+
createInitialCatState as b,
|
|
2982
|
+
createOneko as c,
|
|
2983
|
+
areCatsHidden as d,
|
|
2984
|
+
ONEKO_SKINS as e,
|
|
2985
|
+
setCatsHidden as f,
|
|
2986
|
+
getSkinSource as g,
|
|
2987
|
+
isOnekoSkin as i,
|
|
2988
|
+
loadSkinSource as l,
|
|
2989
|
+
mountLaserCursor as m,
|
|
2990
|
+
onCatsHiddenChange as o,
|
|
2991
|
+
readPersistedPosition as r,
|
|
2992
|
+
startCatAnimation as s
|
|
2993
|
+
};
|