oneko 0.3.1 → 0.4.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.
Files changed (31) hide show
  1. package/dist/chunks/{vanilla-BmeHFErE.js → vanilla-BNtPCdx5.js} +1074 -65
  2. package/dist/index.js +29 -7
  3. package/dist/solid.js +8 -3
  4. package/dist/svelte/Oneko.svelte +14 -4
  5. package/dist/svelte/Oneko.svelte.d.ts +2 -2
  6. package/dist/types/components/oneko.d.ts +12 -2
  7. package/dist/types/lib/oneko/affection.d.ts +16 -0
  8. package/dist/types/lib/oneko/animation/deps.d.ts +2 -0
  9. package/dist/types/lib/oneko/animation/errand-point.d.ts +7 -0
  10. package/dist/types/lib/oneko/animation/frame.d.ts +2 -1
  11. package/dist/types/lib/oneko/animation/play-types.d.ts +56 -0
  12. package/dist/types/lib/oneko/animation/play.d.ts +12 -0
  13. package/dist/types/lib/oneko/constants.d.ts +1 -0
  14. package/dist/types/lib/oneko/controls.d.ts +28 -0
  15. package/dist/types/lib/oneko/defaults.d.ts +5 -0
  16. package/dist/types/lib/oneko/initial-state.d.ts +4 -0
  17. package/dist/types/lib/oneko/mood.d.ts +7 -0
  18. package/dist/types/lib/oneko/pack.d.ts +4 -0
  19. package/dist/types/lib/oneko/pixel-art.d.ts +5 -0
  20. package/dist/types/lib/oneko/play-messages.d.ts +8 -0
  21. package/dist/types/lib/oneko/runtime-config.d.ts +2 -2
  22. package/dist/types/lib/oneko/seasons.d.ts +27 -0
  23. package/dist/types/lib/oneko/treats.d.ts +15 -0
  24. package/dist/types/lib/oneko/types.d.ts +61 -0
  25. package/dist/types/lib/oneko/vanilla.d.ts +3 -1
  26. package/dist/types/packages/oneko/src/solid.d.ts +5 -2
  27. package/dist/types/packages/oneko/src/vanilla.d.ts +2 -1
  28. package/dist/types/packages/oneko/src/vue.d.ts +71 -4
  29. package/dist/vanilla.js +4 -4
  30. package/dist/vue.js +38 -8
  31. package/package.json +1 -1
@@ -555,7 +555,12 @@ const ONEKO_DEFAULTS = {
555
555
  napWhileTyping: false,
556
556
  followFocus: false,
557
557
  perchOnHover: false,
558
- pettable: false
558
+ pettable: false,
559
+ seasonal: true,
560
+ grabbable: false,
561
+ treats: false,
562
+ timeOfDay: false,
563
+ affection: false
559
564
  };
560
565
  function isValidRect(rect) {
561
566
  return Object.values(rect).every(Number.isFinite) && rect.right > rect.left && rect.bottom > rect.top;
@@ -671,6 +676,55 @@ function constrainZoneMovement(from, to, blocked) {
671
676
  }
672
677
  return { x: from.x + dx * limit, y: from.y + dy * limit };
673
678
  }
679
+ const clamp = (value, min, max) => Math.max(min, Math.min(Math.max(min, max), value));
680
+ function visibleRect(el) {
681
+ if (!el.isConnected) return null;
682
+ const rect = el.getBoundingClientRect();
683
+ if (rect.width === 0 || rect.height === 0) return null;
684
+ if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight) return null;
685
+ if (rect.left > window.innerWidth) return null;
686
+ return rect;
687
+ }
688
+ function errandPoint(s, errand) {
689
+ const half = TILE * s.scale / 2;
690
+ const maxX = window.innerWidth - 16;
691
+ const maxY = window.innerHeight - 16;
692
+ switch (errand.kind) {
693
+ case "sit":
694
+ return { x: s.nekoPosX, y: s.nekoPosY };
695
+ case "treat":
696
+ return errand.treat.eaten ? null : { x: errand.treat.x, y: errand.treat.y };
697
+ case "goTo": {
698
+ const { target } = errand;
699
+ if (!(target instanceof Element))
700
+ return { x: clamp(target.x, 16, maxX), y: clamp(target.y, 16, maxY) };
701
+ const rect = visibleRect(target);
702
+ if (!rect) return null;
703
+ const below = rect.bottom + half + 4;
704
+ return {
705
+ x: clamp(rect.left + rect.width / 2, 16, maxX),
706
+ y: below < maxY ? below : clamp(rect.top - half - 4, 16, maxY)
707
+ };
708
+ }
709
+ case "box": {
710
+ const rect = visibleRect(errand.el);
711
+ if (!rect) return null;
712
+ return {
713
+ x: clamp(rect.left + rect.width / 2, 16, maxX),
714
+ y: clamp(Math.max(rect.top + half, rect.bottom - half - 2), 16, maxY)
715
+ };
716
+ }
717
+ case "knock": {
718
+ const rect = visibleRect(errand.el);
719
+ if (!rect) return null;
720
+ const x = errand.direction === 1 ? rect.left - half * 0.75 : rect.right + half * 0.75;
721
+ return {
722
+ x: clamp(x, 16, maxX),
723
+ y: clamp(Math.max(rect.top + half / 2, rect.bottom - half), 16, maxY)
724
+ };
725
+ }
726
+ }
727
+ }
674
728
  const EMPTY_ZONES = [];
675
729
  function createZoneState() {
676
730
  return {
@@ -758,7 +812,9 @@ function updateZoneTarget(s, width, height, random = Math.random) {
758
812
  }
759
813
  }
760
814
  const favorite = favorites.find((zone) => zone.id === zones.targetId);
761
- const desired = !s.followCursorCfg || s.typingNap || s.held ? { x: s.nekoPosX, y: s.nekoPosY } : s.perchTarget ?? (favorite ? {
815
+ const errand = s.errand && errandPoint(s, s.errand);
816
+ const requested = s.errand?.kind === "goTo" || s.errand?.kind === "sit";
817
+ const desired = errand && requested ? errand : !s.followCursorCfg || s.typingNap || s.held ? { x: s.nekoPosX, y: s.nekoPosY } : errand ?? s.perchTarget ?? (favorite ? {
762
818
  x: (favorite.rect.left + favorite.rect.right) / 2,
763
819
  y: (favorite.rect.top + favorite.rect.bottom) / 2
764
820
  } : s.leaderTarget ?? { x: s.mousePosX, y: s.mousePosY });
@@ -768,6 +824,161 @@ function updateZoneTarget(s, width, height, random = Math.random) {
768
824
  } : desired;
769
825
  if (oldId !== zones.targetId) s.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
770
826
  }
827
+ const OUTLINE = "#1b1b1b";
828
+ function pixelArtSVG(rows, colors) {
829
+ const width = rows[0].length + 2;
830
+ const height = rows.length + 2;
831
+ const filled = (x, y) => Boolean(colors[rows[y]?.[x] ?? "."]);
832
+ const rects = [];
833
+ for (let y = -1; y <= rows.length; y++) {
834
+ for (let x = -1; x <= rows[0].length; x++) {
835
+ const fill = filled(x, y) ? colors[rows[y][x]] : filled(x - 1, y) || filled(x + 1, y) || filled(x, y - 1) || filled(x, y + 1) ? OUTLINE : null;
836
+ if (fill) rects.push(`<rect x="${x + 1}" y="${y + 1}" width="1" height="1" fill="${fill}"/>`);
837
+ }
838
+ }
839
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges" style="display:block;width:100%;height:100%">${rects.join("")}</svg>`;
840
+ }
841
+ const ONEKO_SEASONS = ["halloween", "winter", "newYear"];
842
+ function isOnekoSeason(value) {
843
+ return ONEKO_SEASONS.includes(value);
844
+ }
845
+ function seasonForDate(date) {
846
+ const month = date.getMonth() + 1;
847
+ const day = date.getDate();
848
+ if (month === 10 && day >= 20) return "halloween";
849
+ if (month === 12 && day >= 18 && day <= 29) return "winter";
850
+ if (month === 12 && day >= 30 || month === 1 && day <= 2) return "newYear";
851
+ return null;
852
+ }
853
+ function resolveSeason(setting, date = /* @__PURE__ */ new Date()) {
854
+ if (setting === false) return null;
855
+ if (setting === true) return seasonForDate(date);
856
+ return isOnekoSeason(setting) ? setting : null;
857
+ }
858
+ const SEASON_MESSAGES = {
859
+ halloween: [
860
+ "trick or treat. mostly treat",
861
+ "I am the spooky one in this house",
862
+ "black cat energy, any coat",
863
+ "boo. did I scare you",
864
+ "this pumpkin smells like nap",
865
+ "I dressed up as a cat"
866
+ ],
867
+ winter: [
868
+ "is the tree for climbing",
869
+ "I helped wrap the presents",
870
+ "the box is the present",
871
+ "warm lap wanted, apply within",
872
+ "snow is just cold sand"
873
+ ],
874
+ newYear: [
875
+ "new year, same nap spot",
876
+ "resolution: more treats",
877
+ "the fireworks can stop now",
878
+ "I stayed up past midnight. barely",
879
+ "counting down to my next nap"
880
+ ]
881
+ };
882
+ const HATS = {
883
+ halloween: {
884
+ rows: [
885
+ ".......PP..",
886
+ "......PPP..",
887
+ ".....PPPP..",
888
+ ".....PPPP..",
889
+ "....PPPPPP.",
890
+ "....PPPPPP.",
891
+ "...POOOOOP.",
892
+ "PPPPPPPPPPP"
893
+ ],
894
+ colors: { P: "#4a2470", O: "#f28c28" }
895
+ },
896
+ winter: {
897
+ rows: [
898
+ "........WW.",
899
+ ".......RWW.",
900
+ "......RR...",
901
+ ".....RRRR..",
902
+ "....RRRRRR.",
903
+ "...RRRRRRR.",
904
+ "WWWWWWWWWWW",
905
+ "WWWWWWWWWWW"
906
+ ],
907
+ colors: { R: "#d62839", W: "#f4f1ea" }
908
+ },
909
+ newYear: {
910
+ rows: [
911
+ ".....YY....",
912
+ ".....GG....",
913
+ "....GBBG...",
914
+ "....BGGB...",
915
+ "...GBBBBG..",
916
+ "...BGGGGB..",
917
+ "..GBBBBBBG.",
918
+ "..BGGGGGGB."
919
+ ],
920
+ colors: { Y: "#ffd23f", G: "#ffb627", B: "#2e86de" }
921
+ }
922
+ };
923
+ const HAT_WIDTH = 13;
924
+ const HAT_HEIGHT = 10;
925
+ function hatSVG(season) {
926
+ return pixelArtSVG(HATS[season].rows, HATS[season].colors);
927
+ }
928
+ function hatOffset(spriteX, spriteY) {
929
+ const anchor = HAT_ANCHORS[`${spriteX},${spriteY}`];
930
+ if (!anchor) return null;
931
+ return { left: anchor[0] - Math.floor(HAT_WIDTH / 2), top: anchor[1] - (HAT_HEIGHT - 1) };
932
+ }
933
+ const HAT_ANCHORS = {
934
+ // idle, tired, alert: sitting, facing the viewer
935
+ "-3,-3": [16, 6],
936
+ "-3,-2": [16, 6],
937
+ "-7,-3": [16, 5],
938
+ // scratchSelf
939
+ "-5,0": [11, 7],
940
+ "-6,0": [11, 7],
941
+ "-7,0": [14, 5],
942
+ // scratchWallN: seen from behind
943
+ "0,0": [15, 4],
944
+ "0,-1": [15, 4],
945
+ // scratchWallS
946
+ "-7,-1": [16, 13],
947
+ "-6,-2": [16, 13],
948
+ // scratchWallE
949
+ "-2,-2": [25, 4],
950
+ "-2,-3": [25, 4],
951
+ // scratchWallW
952
+ "-4,0": [7, 4],
953
+ "-4,-1": [7, 4],
954
+ // sleeping
955
+ "-2,0": [18, 13],
956
+ "-2,-1": [19, 16],
957
+ // N
958
+ "-1,-2": [16, 4],
959
+ "-1,-3": [16, 4],
960
+ // NE
961
+ "0,-2": [22, 5],
962
+ "0,-3": [22, 4],
963
+ // E
964
+ "-3,0": [25, 7],
965
+ "-3,-1": [25, 7],
966
+ // SE
967
+ "-5,-1": [26, 12],
968
+ "-5,-2": [23, 10],
969
+ // S
970
+ "-6,-3": [16, 4],
971
+ "-7,-2": [16, 4],
972
+ // SW
973
+ "-5,-3": [7, 12],
974
+ "-6,-1": [7, 10],
975
+ // W
976
+ "-4,-2": [7, 7],
977
+ "-4,-3": [7, 7],
978
+ // NW
979
+ "-1,0": [10, 5],
980
+ "-1,-1": [10, 5]
981
+ };
771
982
  function pickFromPool(deps, pool) {
772
983
  const s = deps.stateRef.current;
773
984
  let idx = Math.floor(Math.random() * pool.length);
@@ -808,6 +1019,9 @@ function messageForActivity(deps) {
808
1019
  if (idleAnimationMessage) {
809
1020
  return idleAnimationMessage;
810
1021
  }
1022
+ if (s.season && Math.random() < 0.35) {
1023
+ return pickFromPool(deps, SEASON_MESSAGES[s.season]);
1024
+ }
811
1025
  if (s.idleTime > 0) {
812
1026
  return pickFromPool(deps, IDLE_MESSAGES);
813
1027
  }
@@ -829,6 +1043,7 @@ function showBubble(deps) {
829
1043
  function hideBubble(deps) {
830
1044
  const s = deps.stateRef.current;
831
1045
  s.bubbleVisible = false;
1046
+ s.bubbleForced = false;
832
1047
  deps.bubbleEl.style.opacity = "0";
833
1048
  s.bubbleCooldown = s.bubbleCooldownFramesCfg;
834
1049
  }
@@ -894,7 +1109,15 @@ function shouldTriggerBubble(deps) {
894
1109
  }
895
1110
  function updateBubble(deps) {
896
1111
  const s = deps.stateRef.current;
897
- if (!s.bubbleEnabledCfg) {
1112
+ if (s.sayRequest) {
1113
+ s.bubbleVisible = true;
1114
+ s.bubbleForced = true;
1115
+ s.bubbleTimer = s.sayRequest.frames;
1116
+ deps.bubbleTextEl.textContent = s.sayRequest.text;
1117
+ deps.bubbleEl.style.opacity = "1";
1118
+ s.sayRequest = null;
1119
+ }
1120
+ if (!s.bubbleEnabledCfg && !s.bubbleForced) {
898
1121
  if (s.bubbleVisible) {
899
1122
  hideBubble(deps);
900
1123
  }
@@ -933,6 +1156,17 @@ function leaderPosition(state) {
933
1156
  const leader = pack[pack.indexOf(state) - 1];
934
1157
  return leader ? { x: leader.nekoPosX, y: leader.nekoPosY } : null;
935
1158
  }
1159
+ function isNearestCat(state, point) {
1160
+ const distance = (cat) => Math.hypot(cat.nekoPosX - point.x, cat.nekoPosY - point.y);
1161
+ const mine = distance(state);
1162
+ const order = pack.indexOf(state);
1163
+ return pack.every(
1164
+ (cat, index) => cat === state || distance(cat) > mine || distance(cat) === mine && index > order
1165
+ );
1166
+ }
1167
+ function packSize() {
1168
+ return pack.length;
1169
+ }
936
1170
  function napmateNearby(state) {
937
1171
  const reach = TILE * Math.max(1, state.scale) * 2;
938
1172
  return pack.some(
@@ -969,8 +1203,181 @@ function onCatsHiddenChange(callback) {
969
1203
  window.removeEventListener("storage", onStorage);
970
1204
  };
971
1205
  }
1206
+ const GREETING_GAP_MS = 30 * 60 * 1e3;
1207
+ const MAX_AFFECTION = 999;
1208
+ function parseAffection(value) {
1209
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1210
+ const { version, affection, lastVisit } = value;
1211
+ if (version !== 1) return null;
1212
+ if (typeof affection !== "number" || !Number.isFinite(affection)) return null;
1213
+ if (typeof lastVisit !== "number" || !Number.isFinite(lastVisit)) return null;
1214
+ return {
1215
+ version: 1,
1216
+ affection: Math.max(0, Math.min(MAX_AFFECTION, Math.floor(affection))),
1217
+ lastVisit
1218
+ };
1219
+ }
1220
+ function readAffection(key) {
1221
+ try {
1222
+ return parseAffection(JSON.parse(window.localStorage.getItem(key) ?? "null"));
1223
+ } catch {
1224
+ return null;
1225
+ }
1226
+ }
1227
+ function writeAffection(key, affection, lastVisit = Date.now()) {
1228
+ try {
1229
+ window.localStorage.setItem(key, JSON.stringify({ version: 1, affection, lastVisit }));
1230
+ } catch {
1231
+ }
1232
+ }
1233
+ function affectionTier(affection) {
1234
+ if (affection >= 15) return 3;
1235
+ if (affection >= 5) return 2;
1236
+ if (affection >= 1) return 1;
1237
+ return 0;
1238
+ }
1239
+ const GREETINGS = {
1240
+ 0: ["oh. a new hooman", "who are you. do you have snacks", "hello, stranger"],
1241
+ 1: ["you again. acceptable", "I remember you. vaguely", "back so soon?"],
1242
+ 2: ["my hooman is back!", "I saved you a warm pixel", "there you are. I waited"],
1243
+ 3: [
1244
+ "I missed you. don't tell anyone",
1245
+ "best hooman has returned",
1246
+ "finally. I have so much to tell you"
1247
+ ]
1248
+ };
1249
+ function greetingFor(saved, now = Date.now()) {
1250
+ if (saved && now - saved.lastVisit < GREETING_GAP_MS) return null;
1251
+ const pool = GREETINGS[affectionTier(saved?.affection ?? 0)];
1252
+ return pool[Math.floor(Math.random() * pool.length)];
1253
+ }
1254
+ const NEUTRAL_MOOD = { speed: 1, freerun: 1, idle: 1 };
1255
+ function moodForHour(hour) {
1256
+ if (hour >= 5 && hour < 8) return { speed: 1.25, freerun: 2.5, idle: 1.5 };
1257
+ if (hour >= 8 && hour < 12) return { speed: 1.1, freerun: 1.4, idle: 1 };
1258
+ if (hour >= 12 && hour < 17) return { speed: 0.9, freerun: 0.7, idle: 0.6 };
1259
+ if (hour >= 17 && hour < 21) return { speed: 1.25, freerun: 2.5, idle: 1.5 };
1260
+ if (hour >= 21 && hour < 23) return { speed: 1, freerun: 0.8, idle: 0.8 };
1261
+ return { speed: 0.7, freerun: 0.25, idle: 0.35 };
1262
+ }
1263
+ const TREAT_MESSAGES = [
1264
+ "nom nom nom",
1265
+ "fish! my favorite food group",
1266
+ "more please",
1267
+ "this is why I keep you around",
1268
+ "crunchy. 5 stars"
1269
+ ];
1270
+ const TOSS_SOFT_MESSAGES = [
1271
+ "I meant to do that",
1272
+ "nailed the landing",
1273
+ "10/10 would fly again",
1274
+ "always land on my feet",
1275
+ "wheee. again?"
1276
+ ];
1277
+ const TOSS_HARD_MESSAGES = [
1278
+ "HEY. rude",
1279
+ "I was not done being held",
1280
+ "this is a lawsuit",
1281
+ "my dignity. gone",
1282
+ "do that again and I sit on your keyboard"
1283
+ ];
1284
+ const HELD_MESSAGES = [
1285
+ "put me down",
1286
+ "unhand me, hooman",
1287
+ "I am a liquid",
1288
+ "where are we going"
1289
+ ];
1290
+ const BOX_MESSAGES = [
1291
+ "if I fits, I sits",
1292
+ "this box is mine now",
1293
+ "perfect fit",
1294
+ "box. good."
1295
+ ];
1296
+ const KNOCK_STARE_MESSAGES = [
1297
+ "...",
1298
+ "don't you dare look away",
1299
+ "what's this",
1300
+ "watch this"
1301
+ ];
1302
+ const KNOCK_MESSAGES = [
1303
+ "oops",
1304
+ "it was in my way",
1305
+ "gravity check: still works",
1306
+ "I regret nothing",
1307
+ "you saw nothing"
1308
+ ];
1309
+ const FOND_PET_MESSAGES = [
1310
+ "purrrrrr",
1311
+ "you are my favorite",
1312
+ "more pets. now. forever",
1313
+ "this is the good stuff",
1314
+ "*slow blink*"
1315
+ ];
1316
+ const FISH = pixelArtSVG([".OO...O", "OOOO.OO", "OBOOOOO", "OOOO.OO", ".OO...O"], {
1317
+ O: "#f4a261",
1318
+ B: "#1b1b1b"
1319
+ });
1320
+ const PIXEL = 2;
1321
+ const MAX_TREATS = 5;
1322
+ const treats = [];
1323
+ function nearestTreat(x, y, skip) {
1324
+ let best;
1325
+ for (const treat of treats)
1326
+ if (skip?.(treat)) continue;
1327
+ else if (!best || Math.hypot(treat.x - x, treat.y - y) < Math.hypot(best.x - x, best.y - y))
1328
+ best = treat;
1329
+ return best;
1330
+ }
1331
+ function dropTreat(x, y, zIndex) {
1332
+ const el = document.createElement("div");
1333
+ el.setAttribute("aria-hidden", "true");
1334
+ el.setAttribute("data-oneko-treat", "");
1335
+ el.innerHTML = FISH;
1336
+ const width = 9 * PIXEL;
1337
+ const height = 7 * PIXEL;
1338
+ const clampedX = Math.max(width, Math.min(window.innerWidth - width, x));
1339
+ const clampedY = Math.max(height, Math.min(window.innerHeight - height, y));
1340
+ el.style.cssText = [
1341
+ "position:fixed",
1342
+ "pointer-events:none",
1343
+ `left:${clampedX - width / 2}px`,
1344
+ `top:${clampedY - height / 2}px`,
1345
+ `width:${width}px`,
1346
+ `height:${height}px`,
1347
+ `z-index:${zIndex}`
1348
+ ].join(";");
1349
+ document.body.appendChild(el);
1350
+ el.animate?.(
1351
+ [
1352
+ { transform: "translateY(-14px)", opacity: 0 },
1353
+ { transform: "none", opacity: 1 }
1354
+ ],
1355
+ { duration: 220, easing: "cubic-bezier(0.3, 1.4, 0.6, 1)" }
1356
+ );
1357
+ const treat = { x: clampedX, y: clampedY, el, eaten: false };
1358
+ treats.push(treat);
1359
+ while (treats.length > MAX_TREATS) removeTreat(treats[0]);
1360
+ return treat;
1361
+ }
1362
+ function removeTreat(treat) {
1363
+ treat.eaten = true;
1364
+ treat.el.remove();
1365
+ const index = treats.indexOf(treat);
1366
+ if (index !== -1) treats.splice(index, 1);
1367
+ }
1368
+ function eatTreat(treat) {
1369
+ if (treat.eaten) return false;
1370
+ removeTreat(treat);
1371
+ return true;
1372
+ }
1373
+ function clearTreats() {
1374
+ while (treats.length) removeTreat(treats[0]);
1375
+ }
972
1376
  function getActivityLabel(deps) {
973
1377
  const s = deps.stateRef.current;
1378
+ if (s.grab && s.grab.phase !== "landed") {
1379
+ return "alert";
1380
+ }
974
1381
  if (s.freerunMode) {
975
1382
  return "freerun";
976
1383
  }
@@ -1006,6 +1413,551 @@ function playSound(deps, pool) {
1006
1413
  s.soundCooldown = 0;
1007
1414
  });
1008
1415
  }
1416
+ function setSprite(deps, name, frame) {
1417
+ const sprites = defaultSpriteSets[name];
1418
+ if (!sprites?.length) {
1419
+ return;
1420
+ }
1421
+ const sprite = sprites[frame % sprites.length];
1422
+ deps.el.style.backgroundPosition = `${sprite[0] * TILE}px ${sprite[1] * TILE}px`;
1423
+ if (deps.hat) {
1424
+ const offset = hatOffset(sprite[0], sprite[1]);
1425
+ deps.hat.style.display = offset ? "" : "none";
1426
+ if (offset) deps.hat.style.transform = `translate(${offset.left}px, ${offset.top}px)`;
1427
+ }
1428
+ }
1429
+ function getDirectionFromAngle(angle) {
1430
+ if (angle >= 157.5 || angle < -157.5) {
1431
+ return "W";
1432
+ }
1433
+ for (const range of DIRECTION_RANGES) {
1434
+ if (angle >= range.min && angle < range.max) {
1435
+ return range.dir;
1436
+ }
1437
+ }
1438
+ return "E";
1439
+ }
1440
+ function directionFromDelta(dx, dy) {
1441
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
1442
+ return null;
1443
+ }
1444
+ return getDirectionFromAngle(Math.atan2(dy, dx) * (180 / Math.PI));
1445
+ }
1446
+ function clearRotation(deps) {
1447
+ const s = deps.stateRef.current;
1448
+ if (s.currentRotation !== 0) {
1449
+ s.currentRotation = 0;
1450
+ deps.el.style.transform = `scale(${s.scale}) rotate(0deg)`;
1451
+ }
1452
+ }
1453
+ const CLOCK_CHECK_FRAMES = 600;
1454
+ const ERRAND_CHECK_FRAMES = 50;
1455
+ const ERRAND_CHANCE = 0.25;
1456
+ const ERRAND_REST_FRAMES = 300;
1457
+ const BOX_NAP_FRAMES = 150;
1458
+ const KNOCK_STARE_FRAMES = 12;
1459
+ const KNOCKED_DOWN_MS = 5e3;
1460
+ const MAX_TOSS_SPEED = 3;
1461
+ const HARD_TOSS_SPEED = 1.6;
1462
+ const TYPING_TARGETS = "a,button,input,select,textarea,label,summary,[role='button'],[contenteditable]";
1463
+ function speak(deps, pool) {
1464
+ const s = deps.stateRef.current;
1465
+ if (!s.bubbleEnabledCfg) return;
1466
+ s.bubbleVisible = true;
1467
+ s.bubbleTimer = s.bubbleDisplayFramesCfg;
1468
+ s.bubbleCooldown = 0;
1469
+ deps.bubbleTextEl.textContent = pickFromPool(deps, pool);
1470
+ deps.bubbleEl.style.opacity = "1";
1471
+ }
1472
+ function bumpAffection(s) {
1473
+ s.affection = Math.min(MAX_AFFECTION, s.affection + 1);
1474
+ if (s.affectionCfg) writeAffection(s.affectionKey, s.affection);
1475
+ return s.affection;
1476
+ }
1477
+ function rouse(s) {
1478
+ s.typingNap = false;
1479
+ s.freerunMode = false;
1480
+ s.idleAnimation = null;
1481
+ s.idleAnimationFrame = 0;
1482
+ s.idleTime = Math.max(s.idleTime, 1);
1483
+ }
1484
+ function petCat(deps) {
1485
+ const s = deps.stateRef.current;
1486
+ rouse(s);
1487
+ const affection = bumpAffection(s);
1488
+ speak(deps, s.affectionCfg && affectionTier(affection) >= 2 ? FOND_PET_MESSAGES : PET_MESSAGES);
1489
+ playSound(deps, PET_SOUND_POOL);
1490
+ s.handlers.onPet?.({ affection });
1491
+ }
1492
+ const knocked = /* @__PURE__ */ new Map();
1493
+ function knockOver(el, direction) {
1494
+ if (knocked.has(el) || !(el instanceof HTMLElement || el instanceof SVGElement)) return;
1495
+ const rect = el.getBoundingClientRect();
1496
+ const fall = Math.max(0, window.innerHeight - rect.bottom - 4);
1497
+ const animation = el.animate(
1498
+ [
1499
+ { transform: "none", offset: 0 },
1500
+ {
1501
+ transform: `translateX(${direction * 6}px) rotate(${direction * 12}deg)`,
1502
+ offset: 0.25
1503
+ },
1504
+ {
1505
+ transform: `translate(${direction * 48}px, ${fall}px) rotate(${direction * 100}deg)`,
1506
+ offset: 1
1507
+ }
1508
+ ],
1509
+ {
1510
+ duration: 700,
1511
+ easing: "cubic-bezier(0.5, 0, 0.9, 0.6)",
1512
+ fill: "forwards"
1513
+ }
1514
+ );
1515
+ knocked.set(el, animation);
1516
+ setTimeout(() => {
1517
+ animation.playbackRate = 2;
1518
+ animation.reverse();
1519
+ animation.finished.then(
1520
+ () => {
1521
+ animation.cancel();
1522
+ knocked.delete(el);
1523
+ },
1524
+ () => knocked.delete(el)
1525
+ );
1526
+ }, KNOCKED_DOWN_MS);
1527
+ }
1528
+ function restoreKnocked() {
1529
+ for (const animation of knocked.values()) animation.cancel();
1530
+ knocked.clear();
1531
+ }
1532
+ function isVisible(el) {
1533
+ const rect = el.getBoundingClientRect();
1534
+ return rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
1535
+ }
1536
+ const handledEvents = /* @__PURE__ */ new WeakSet();
1537
+ function isOverText(x, y) {
1538
+ const node = document.caretRangeFromPoint?.(x, y)?.startContainer;
1539
+ if (!node || node.nodeType !== Node.TEXT_NODE || !node.textContent?.trim()) return false;
1540
+ const range = document.createRange();
1541
+ range.selectNodeContents(node);
1542
+ return [...range.getClientRects()].some(
1543
+ (rect) => x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
1544
+ );
1545
+ }
1546
+ function createPlay(deps) {
1547
+ const s = () => deps.stateRef.current;
1548
+ const { el } = deps;
1549
+ let seasonKey = "";
1550
+ let seasonCheckedFrame = -Infinity;
1551
+ let grabbable = null;
1552
+ let flight = 0;
1553
+ let sayTarget = null;
1554
+ let sayTimer;
1555
+ const unreachable = /* @__PURE__ */ new WeakSet();
1556
+ const wearHat = () => {
1557
+ const state = s();
1558
+ const key = String(state.seasonalCfg);
1559
+ if (key === seasonKey && state.frameCount - seasonCheckedFrame < CLOCK_CHECK_FRAMES) return;
1560
+ seasonKey = key;
1561
+ seasonCheckedFrame = state.frameCount;
1562
+ const season = resolveSeason(state.seasonalCfg);
1563
+ if (season === state.season && (deps.hat || !season)) return;
1564
+ state.season = season;
1565
+ deps.hat?.remove();
1566
+ deps.hat = void 0;
1567
+ if (!season) return;
1568
+ const hat = document.createElement("div");
1569
+ hat.setAttribute("data-oneko-hat", season);
1570
+ hat.style.cssText = `position:absolute;left:0;top:0;width:${HAT_WIDTH}px;height:${HAT_HEIGHT}px;pointer-events:none;display:none`;
1571
+ hat.innerHTML = hatSVG(season);
1572
+ el.appendChild(hat);
1573
+ deps.hat = hat;
1574
+ };
1575
+ const readClock = () => {
1576
+ const state = s();
1577
+ if (!state.timeOfDayCfg) {
1578
+ state.mood = NEUTRAL_MOOD;
1579
+ state.moodCheckedFrame = -Infinity;
1580
+ } else if (state.frameCount - state.moodCheckedFrame >= CLOCK_CHECK_FRAMES) {
1581
+ state.mood = moodForHour((/* @__PURE__ */ new Date()).getHours());
1582
+ state.moodCheckedFrame = state.frameCount;
1583
+ }
1584
+ };
1585
+ const greet = () => {
1586
+ const state = s();
1587
+ if (!state.affectionCfg) {
1588
+ state.affectionLoaded = false;
1589
+ return;
1590
+ }
1591
+ if (state.affectionLoaded) return;
1592
+ state.affectionLoaded = true;
1593
+ const saved = readAffection(state.affectionKey);
1594
+ state.affection = Math.max(state.affection, saved?.affection ?? 0);
1595
+ const greeting = greetingFor(saved);
1596
+ writeAffection(state.affectionKey, state.affection);
1597
+ if (greeting && state.bubbleEnabledCfg)
1598
+ state.sayRequest = {
1599
+ text: greeting,
1600
+ frames: state.bubbleDisplayFramesCfg
1601
+ };
1602
+ };
1603
+ const syncGrabbable = () => {
1604
+ const on = s().grabbableCfg;
1605
+ if (on === grabbable) return;
1606
+ grabbable = on;
1607
+ el.style.pointerEvents = on ? "auto" : "none";
1608
+ el.style.cursor = on ? "grab" : "";
1609
+ el.style.touchAction = on ? "none" : "";
1610
+ if (!on) el.removeAttribute("title");
1611
+ else if (!el.hasAttribute("aria-label")) el.title = "Drag to toss, click to pet";
1612
+ };
1613
+ const chooseErrand = () => {
1614
+ const state = s();
1615
+ if (state.grab || !state.followCursorCfg || state.typingNap || state.held) return;
1616
+ const current = state.errand?.kind;
1617
+ if (current === "goTo" || current === "sit") return;
1618
+ const treat = nearestTreat(state.nekoPosX, state.nekoPosY, (item) => unreachable.has(item));
1619
+ if (treat && current !== "treat") {
1620
+ rouse(state);
1621
+ state.errand = { kind: "treat", treat, frames: 0 };
1622
+ return;
1623
+ }
1624
+ if (state.errand || state.laserPointerCfg || state.perchTarget || state.zoneState.targetId)
1625
+ return;
1626
+ if (--state.errandCooldown > 0) return;
1627
+ state.errandCooldown = ERRAND_CHECK_FRAMES;
1628
+ if (state.idleTime === 0 || Math.random() >= ERRAND_CHANCE) return;
1629
+ const size = TILE * state.scale;
1630
+ const boxes = [...document.querySelectorAll("[data-oneko-box]")].filter((box) => {
1631
+ const rect = box.getBoundingClientRect();
1632
+ return isVisible(box) && rect.width >= size * 0.75 && rect.height >= size * 0.5;
1633
+ });
1634
+ const knockables = [...document.querySelectorAll("[data-oneko-knockable]")].filter(
1635
+ (item) => isVisible(item) && !knocked.has(item)
1636
+ );
1637
+ const pick = [...boxes, ...knockables][Math.floor(Math.random() * (boxes.length + knockables.length))];
1638
+ if (!pick) return;
1639
+ if (boxes.includes(pick)) {
1640
+ state.errand = { kind: "box", el: pick, frames: 0, arrived: false };
1641
+ } else {
1642
+ const rect = pick.getBoundingClientRect();
1643
+ state.errand = {
1644
+ kind: "knock",
1645
+ el: pick,
1646
+ frames: 0,
1647
+ phase: "approach",
1648
+ // Approach from the side the cat is already on, then push the other way.
1649
+ direction: state.nekoPosX <= rect.left + rect.width / 2 ? 1 : -1
1650
+ };
1651
+ }
1652
+ };
1653
+ const upkeep = () => {
1654
+ const state = s();
1655
+ wearHat();
1656
+ readClock();
1657
+ greet();
1658
+ syncGrabbable();
1659
+ if (state.petRequest) {
1660
+ state.petRequest = false;
1661
+ petCat(deps);
1662
+ }
1663
+ chooseErrand();
1664
+ };
1665
+ const endErrand = (cooldown = 0) => {
1666
+ const state = s();
1667
+ state.errand = null;
1668
+ if (cooldown) state.errandCooldown = cooldown;
1669
+ };
1670
+ const tickErrand = () => {
1671
+ const state = s();
1672
+ const errand = state.errand;
1673
+ const chasingCursor = !errand && state.followCursorCfg && !state.typingNap && !state.held && !state.perchTarget && !state.leaderTarget && !state.zoneState.targetId;
1674
+ const toCursor = Math.hypot(state.mousePosX - state.nekoPosX, state.mousePosY - state.nekoPosY);
1675
+ if (!chasingCursor) state.chasingFar = false;
1676
+ else if (toCursor > 160 && state.idleTime === 0) state.chasingFar = true;
1677
+ else if (state.chasingFar && state.idleTime > 0) {
1678
+ state.chasingFar = false;
1679
+ state.handlers.onCatchCursor?.();
1680
+ }
1681
+ if (!errand || errand.kind === "sit") return;
1682
+ const point = errandPoint(state, errand);
1683
+ if (!point) {
1684
+ if (errand.kind === "goTo") errand.resolve?.(false);
1685
+ endErrand();
1686
+ return;
1687
+ }
1688
+ const distance = Math.hypot(point.x - state.nekoPosX, point.y - state.nekoPosY);
1689
+ const arrived = distance < 14 || state.idleTime > 2 && distance < 64;
1690
+ switch (errand.kind) {
1691
+ case "goTo":
1692
+ if (!errand.arrived && (arrived || state.idleTime > 2 && ++errand.frames > 30)) {
1693
+ errand.arrived = true;
1694
+ errand.resolve?.(true);
1695
+ errand.resolve = void 0;
1696
+ }
1697
+ return;
1698
+ case "treat":
1699
+ if (arrived) {
1700
+ if (eatTreat(errand.treat)) {
1701
+ const affection = bumpAffection(state);
1702
+ speak(deps, TREAT_MESSAGES);
1703
+ playSound(deps, LASER_CATCH_POOL);
1704
+ state.handlers.onTreat?.({ affection });
1705
+ }
1706
+ endErrand();
1707
+ } else if (++errand.frames > 300 || state.idleTime > 20) {
1708
+ unreachable.add(errand.treat);
1709
+ endErrand();
1710
+ }
1711
+ return;
1712
+ case "box":
1713
+ if (!errand.arrived) {
1714
+ if (arrived) {
1715
+ errand.arrived = true;
1716
+ errand.frames = 0;
1717
+ speak(deps, BOX_MESSAGES);
1718
+ if (state.sleepEnabledCfg) {
1719
+ state.idleAnimation = "sleeping";
1720
+ state.idleAnimationFrame = 0;
1721
+ }
1722
+ } else if (++errand.frames > 100) endErrand(ERRAND_REST_FRAMES);
1723
+ } else if (++errand.frames > BOX_NAP_FRAMES) endErrand(ERRAND_REST_FRAMES);
1724
+ return;
1725
+ case "knock":
1726
+ if (errand.phase === "approach") {
1727
+ if (arrived) {
1728
+ errand.phase = "stare";
1729
+ errand.frames = 0;
1730
+ rouse(state);
1731
+ speak(deps, KNOCK_STARE_MESSAGES);
1732
+ } else if (++errand.frames > 100) endErrand(ERRAND_REST_FRAMES);
1733
+ return;
1734
+ }
1735
+ setSprite(
1736
+ deps,
1737
+ ++errand.frames >= KNOCK_STARE_FRAMES ? errand.direction === 1 ? "E" : "W" : "idle",
1738
+ errand.frames
1739
+ );
1740
+ if (errand.frames >= KNOCK_STARE_FRAMES) {
1741
+ knockOver(errand.el, errand.direction);
1742
+ speak(deps, KNOCK_MESSAGES);
1743
+ playSound(deps, HISS_POOL);
1744
+ endErrand(ERRAND_REST_FRAMES);
1745
+ }
1746
+ return;
1747
+ }
1748
+ };
1749
+ const place = (x, y) => {
1750
+ const state = s();
1751
+ state.nekoPosX = x;
1752
+ state.nekoPosY = y;
1753
+ el.style.left = `${Math.floor(x - TILE / 2)}px`;
1754
+ el.style.top = `${Math.floor(y - TILE / 2)}px`;
1755
+ };
1756
+ const land = (hard) => {
1757
+ const state = s();
1758
+ cancelAnimationFrame(flight);
1759
+ flight = 0;
1760
+ state.grab = { phase: "landed", frames: 8 };
1761
+ state.nekoVelX = state.nekoVelY = 0;
1762
+ state.currentPath = [];
1763
+ clearRotation(deps);
1764
+ speak(deps, hard ? TOSS_HARD_MESSAGES : TOSS_SOFT_MESSAGES);
1765
+ playSound(deps, hard ? HISS_POOL : PET_SOUND_POOL);
1766
+ };
1767
+ const fly = (vx, vy, hard) => {
1768
+ let last = performance.now();
1769
+ let spin = 0;
1770
+ const step = (now) => {
1771
+ const state = s();
1772
+ if (state.grab?.phase !== "flying") return;
1773
+ const dt = Math.min(50, now - last);
1774
+ last = now;
1775
+ if (state.pausedCfg) {
1776
+ flight = requestAnimationFrame(step);
1777
+ return;
1778
+ }
1779
+ const from = { x: state.nekoPosX, y: state.nekoPosY };
1780
+ let x = from.x + vx * dt;
1781
+ let y = from.y + vy * dt;
1782
+ const maxX = window.innerWidth - 16;
1783
+ const maxY = window.innerHeight - 16;
1784
+ if (x < 16 || x > maxX) {
1785
+ vx *= -0.5;
1786
+ x = Math.max(16, Math.min(maxX, x));
1787
+ }
1788
+ if (y < 16 || y > maxY) {
1789
+ vy *= -0.5;
1790
+ y = Math.max(16, Math.min(maxY, y));
1791
+ }
1792
+ const safe = constrainZoneMovement(from, { x, y }, state.zoneState.blocked);
1793
+ if (safe.x !== x || safe.y !== y) vx = vy = 0;
1794
+ place(safe.x, safe.y);
1795
+ const drag = Math.exp(-dt / 260);
1796
+ vx *= drag;
1797
+ vy *= drag;
1798
+ spin += vx * dt * 0.9;
1799
+ el.style.transform = `scale(${state.scale}) rotate(${spin}deg)`;
1800
+ if (Math.hypot(vx, vy) < 0.06) land(hard);
1801
+ else flight = requestAnimationFrame(step);
1802
+ };
1803
+ flight = requestAnimationFrame(step);
1804
+ };
1805
+ const tickGrab = () => {
1806
+ const state = s();
1807
+ const grab = state.grab;
1808
+ if (!grab) return false;
1809
+ state.idleAnimation = null;
1810
+ state.freerunMode = false;
1811
+ if (grab.phase === "landed") {
1812
+ setSprite(deps, "idle", 0);
1813
+ state.idleTime = Math.max(state.idleTime, 1);
1814
+ if (--grab.frames <= 0) state.grab = null;
1815
+ return true;
1816
+ }
1817
+ setSprite(deps, "alert", 0);
1818
+ state.idleTime = 0;
1819
+ return true;
1820
+ };
1821
+ const onPointerDown = (event) => {
1822
+ const state = s();
1823
+ if (!state.grabbableCfg || event.button > 0 || state.pausedCfg) return;
1824
+ event.preventDefault();
1825
+ el.setPointerCapture?.(event.pointerId);
1826
+ cancelAnimationFrame(flight);
1827
+ rouse(state);
1828
+ if (state.errand?.kind === "box" || state.errand?.kind === "knock") endErrand();
1829
+ state.grab = {
1830
+ phase: "held",
1831
+ samples: [{ x: event.clientX, y: event.clientY, t: event.timeStamp }],
1832
+ startT: event.timeStamp,
1833
+ travel: 0
1834
+ };
1835
+ el.style.cursor = "grabbing";
1836
+ setSprite(deps, "alert", 0);
1837
+ clearRotation(deps);
1838
+ };
1839
+ const onPointerMove = (event) => {
1840
+ const state = s();
1841
+ const grab = state.grab;
1842
+ if (grab?.phase !== "held") return;
1843
+ const previous = grab.samples.at(-1);
1844
+ grab.travel += Math.hypot(event.clientX - previous.x, event.clientY - previous.y);
1845
+ grab.samples.push({
1846
+ x: event.clientX,
1847
+ y: event.clientY,
1848
+ t: event.timeStamp
1849
+ });
1850
+ if (grab.samples.length > 8) grab.samples.shift();
1851
+ place(
1852
+ Math.max(16, Math.min(window.innerWidth - 16, event.clientX)),
1853
+ Math.max(16, Math.min(window.innerHeight - 16, event.clientY))
1854
+ );
1855
+ state.mousePosX = event.clientX;
1856
+ state.mousePosY = event.clientY;
1857
+ if (grab.travel > 40 && !state.bubbleVisible && Math.random() < 0.02)
1858
+ speak(deps, HELD_MESSAGES);
1859
+ };
1860
+ const onPointerUp = (event) => {
1861
+ const state = s();
1862
+ const grab = state.grab;
1863
+ if (grab?.phase !== "held") return;
1864
+ el.style.cursor = "grab";
1865
+ if (grab.travel < 6 && event.timeStamp - grab.startT < 500) {
1866
+ state.grab = null;
1867
+ petCat(deps);
1868
+ return;
1869
+ }
1870
+ const recent = grab.samples.filter((sample) => event.timeStamp - sample.t < 100);
1871
+ const first = recent[0] ?? grab.samples.at(-1);
1872
+ const elapsed = Math.max(16, event.timeStamp - first.t);
1873
+ let vx = (event.clientX - first.x) / elapsed;
1874
+ let vy = (event.clientY - first.y) / elapsed;
1875
+ const speed = Math.hypot(vx, vy);
1876
+ if (speed > MAX_TOSS_SPEED) {
1877
+ vx = vx / speed * MAX_TOSS_SPEED;
1878
+ vy = vy / speed * MAX_TOSS_SPEED;
1879
+ }
1880
+ if (speed < 0.25) {
1881
+ land(false);
1882
+ return;
1883
+ }
1884
+ const hard = speed > HARD_TOSS_SPEED;
1885
+ state.grab = { phase: "flying", vx, vy, spin: 0, hard };
1886
+ state.handlers.onToss?.({
1887
+ speed: Math.round(Math.min(speed, MAX_TOSS_SPEED) * 1e3)
1888
+ });
1889
+ fly(vx, vy, hard);
1890
+ };
1891
+ const onPointerCancel = () => {
1892
+ if (s().grab?.phase === "held") land(false);
1893
+ };
1894
+ const onDoubleClick = (event) => {
1895
+ const state = s();
1896
+ if (!state.treatsCfg || handledEvents.has(event)) return;
1897
+ if (event.target instanceof Element && event.target.closest(TYPING_TARGETS)) return;
1898
+ if (window.getSelection()?.toString().trim() || isOverText(event.clientX, event.clientY))
1899
+ return;
1900
+ const point = { x: event.clientX, y: event.clientY };
1901
+ if (state.zoneState.blocked.some((rect) => containsPoint(rect, point))) return;
1902
+ handledEvents.add(event);
1903
+ dropTreat(point.x, point.y, state.zIndex);
1904
+ };
1905
+ const requestSay = (target) => {
1906
+ const state = s();
1907
+ const text = target.getAttribute("data-oneko-say")?.trim();
1908
+ if (!text || !state.bubbleEnabledCfg || state.pausedCfg) return;
1909
+ const rect = target.getBoundingClientRect();
1910
+ if (!isNearestCat(state, {
1911
+ x: rect.left + rect.width / 2,
1912
+ y: rect.top + rect.height / 2
1913
+ }))
1914
+ return;
1915
+ state.sayRequest = {
1916
+ text: text.slice(0, 200),
1917
+ frames: state.bubbleDisplayFramesCfg
1918
+ };
1919
+ };
1920
+ const onMouseOver = (event) => {
1921
+ const target = event.target instanceof Element ? event.target.closest("[data-oneko-say]") : null;
1922
+ if (target === sayTarget) return;
1923
+ sayTarget = target;
1924
+ clearTimeout(sayTimer);
1925
+ if (target) sayTimer = setTimeout(() => requestSay(target), 350);
1926
+ };
1927
+ const onFocusIn = (event) => {
1928
+ const target = event.target instanceof Element ? event.target.closest("[data-oneko-say]") : null;
1929
+ if (target) requestSay(target);
1930
+ };
1931
+ syncGrabbable();
1932
+ el.addEventListener("pointerdown", onPointerDown);
1933
+ el.addEventListener("pointermove", onPointerMove);
1934
+ el.addEventListener("pointerup", onPointerUp);
1935
+ el.addEventListener("pointercancel", onPointerCancel);
1936
+ document.addEventListener("dblclick", onDoubleClick);
1937
+ document.addEventListener("mouseover", onMouseOver);
1938
+ document.addEventListener("focusin", onFocusIn);
1939
+ const dispose = () => {
1940
+ const state = s();
1941
+ cancelAnimationFrame(flight);
1942
+ clearTimeout(sayTimer);
1943
+ el.removeEventListener("pointerdown", onPointerDown);
1944
+ el.removeEventListener("pointermove", onPointerMove);
1945
+ el.removeEventListener("pointerup", onPointerUp);
1946
+ el.removeEventListener("pointercancel", onPointerCancel);
1947
+ document.removeEventListener("dblclick", onDoubleClick);
1948
+ document.removeEventListener("mouseover", onMouseOver);
1949
+ document.removeEventListener("focusin", onFocusIn);
1950
+ if (state.affectionCfg && state.affectionLoaded)
1951
+ writeAffection(state.affectionKey, state.affection);
1952
+ if (state.errand?.kind === "goTo") state.errand.resolve?.(false);
1953
+ state.errand = null;
1954
+ state.grab = null;
1955
+ state.season = null;
1956
+ deps.hat?.remove();
1957
+ deps.hat = void 0;
1958
+ };
1959
+ return { upkeep, tickGrab, tickErrand, dispose };
1960
+ }
1009
1961
  const PERCH_SELECTOR = "a[href],button,[role='button'],[data-oneko-perch]";
1010
1962
  const PERCH_DELAY_MS = 700;
1011
1963
  const NOT_TYPED_INPUTS = /* @__PURE__ */ new Set([
@@ -1020,7 +1972,7 @@ const NOT_TYPED_INPUTS = /* @__PURE__ */ new Set([
1020
1972
  "submit"
1021
1973
  ]);
1022
1974
  function stopDistance(s) {
1023
- if (s.perchTarget) return Math.min(s.followDistanceCfg, 8);
1975
+ if (s.errand || s.perchTarget) return Math.min(s.followDistanceCfg, 8);
1024
1976
  if (s.leaderTarget) return Math.max(s.followDistanceCfg, TILE * s.scale);
1025
1977
  return s.followDistanceCfg;
1026
1978
  }
@@ -1142,19 +2094,7 @@ function makePettable(deps) {
1142
2094
  if (!event.repeat) pet();
1143
2095
  };
1144
2096
  const pet = () => {
1145
- const s = deps.stateRef.current;
1146
- s.typingNap = false;
1147
- s.freerunMode = false;
1148
- s.idleAnimation = null;
1149
- s.idleAnimationFrame = 0;
1150
- s.idleTime = Math.max(s.idleTime, 1);
1151
- if (s.bubbleEnabledCfg) {
1152
- s.bubbleVisible = true;
1153
- s.bubbleTimer = s.bubbleDisplayFramesCfg;
1154
- deps.bubbleTextEl.textContent = pickFromPool(deps, PET_MESSAGES);
1155
- deps.bubbleEl.style.opacity = "1";
1156
- }
1157
- playSound(deps, PET_SOUND_POOL);
2097
+ petCat(deps);
1158
2098
  announcer.textContent = ++pets % 2 ? "The cat purrs." : "The cat purrs happily.";
1159
2099
  };
1160
2100
  el.addEventListener("focus", onFocus);
@@ -1340,7 +2280,7 @@ function collectObstacles() {
1340
2280
  const vh = window.innerHeight;
1341
2281
  const rects = [];
1342
2282
  for (const node of document.querySelectorAll(OBSTACLE_SELECTOR)) {
1343
- if (node.closest('[data-oneko-zone="attract"]')) continue;
2283
+ if (node.closest('[data-oneko-zone="attract"],[data-oneko-box]')) continue;
1344
2284
  const r = node.getBoundingClientRect();
1345
2285
  if (shouldIncludeObstacleRect(r, vw, vh)) {
1346
2286
  rects.push(toObstacleRect(r));
@@ -1514,40 +2454,8 @@ function findRoute(startIdx, goalIdx, grid, cols, rows) {
1514
2454
  }
1515
2455
  return smoothPath(reconstructCellPath(prev, goalIdx), cols);
1516
2456
  }
1517
- function setSprite(deps, name, frame) {
1518
- const sprites = defaultSpriteSets[name];
1519
- if (!sprites?.length) {
1520
- return;
1521
- }
1522
- const sprite = sprites[frame % sprites.length];
1523
- deps.el.style.backgroundPosition = `${sprite[0] * TILE}px ${sprite[1] * TILE}px`;
1524
- }
1525
- function getDirectionFromAngle(angle) {
1526
- if (angle >= 157.5 || angle < -157.5) {
1527
- return "W";
1528
- }
1529
- for (const range of DIRECTION_RANGES) {
1530
- if (angle >= range.min && angle < range.max) {
1531
- return range.dir;
1532
- }
1533
- }
1534
- return "E";
1535
- }
1536
- function directionFromDelta(dx, dy) {
1537
- if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
1538
- return null;
1539
- }
1540
- return getDirectionFromAngle(Math.atan2(dy, dx) * (180 / Math.PI));
1541
- }
1542
- function clearRotation(deps) {
1543
- const s = deps.stateRef.current;
1544
- if (s.currentRotation !== 0) {
1545
- s.currentRotation = 0;
1546
- deps.el.style.transform = `scale(${s.scale}) rotate(0deg)`;
1547
- }
1548
- }
1549
2457
  function idleFrameThreshold(s) {
1550
- return Math.max(1, Math.round(s.idleThresholdMs / 100));
2458
+ return Math.max(1, Math.round(s.idleThresholdMs * s.mood.idle / 100));
1551
2459
  }
1552
2460
  function wallScratchAnimations(s) {
1553
2461
  return [
@@ -1690,7 +2598,7 @@ function moveToward(deps, targetX, targetY) {
1690
2598
  s.nekoVelY *= 0.5;
1691
2599
  return;
1692
2600
  }
1693
- const currentSpeed = s.currentSpeed;
2601
+ const currentSpeed = s.currentSpeed * s.mood.speed;
1694
2602
  s.nekoVelX += (dx / dist * currentSpeed - s.nekoVelX) * STEER_LERP;
1695
2603
  s.nekoVelY += (dy / dist * currentSpeed - s.nekoVelY) * STEER_LERP;
1696
2604
  const velMag = Math.hypot(s.nekoVelX, s.nekoVelY);
@@ -1780,7 +2688,7 @@ function chaseCursor(deps) {
1780
2688
  tickFreerun(deps);
1781
2689
  return;
1782
2690
  }
1783
- if (!s.noFollow && Math.random() < s.freerunChanceCfg) {
2691
+ if (!s.noFollow && !s.errand && Math.random() < s.freerunChanceCfg * s.mood.freerun) {
1784
2692
  enterFreerun(deps);
1785
2693
  moveToward(deps, s.zoneState.movementTarget.x, s.zoneState.movementTarget.y);
1786
2694
  return;
@@ -1884,6 +2792,7 @@ function tickSounds(deps) {
1884
2792
  if (currentState !== deps.lastStateRef.current) {
1885
2793
  deps.lastStateRef.current = currentState;
1886
2794
  deps.onStateChangeRef.current?.(currentState);
2795
+ if (currentState === "sleeping") deps.stateRef.current.handlers.onSleep?.();
1887
2796
  const pool = SOUND_POOLS[currentState];
1888
2797
  if (pool) {
1889
2798
  playSound(deps, pool);
@@ -1892,17 +2801,23 @@ function tickSounds(deps) {
1892
2801
  tickAmbientSounds(deps, currentState);
1893
2802
  return currentState;
1894
2803
  }
1895
- function createFrameLoop(deps) {
2804
+ function createFrameLoop(deps, play) {
1896
2805
  const { maybeRenderDebug } = createDebugRenderer(deps);
1897
2806
  const frame = () => {
1898
2807
  const s = deps.stateRef.current;
1899
2808
  if (s.pausedCfg) return;
1900
2809
  if (!refreshZones(deps)) return;
1901
2810
  tickPrelude(s);
2811
+ play?.upkeep();
1902
2812
  tickAttention(deps);
1903
2813
  updateZoneTarget(s, window.innerWidth, window.innerHeight);
1904
2814
  const dist = Math.hypot(s.mousePosX - s.nekoPosX, s.mousePosY - s.nekoPosY);
1905
- tickMovement(deps, dist);
2815
+ if (play?.tickGrab()) {
2816
+ updateBubble(deps);
2817
+ } else {
2818
+ tickMovement(deps, dist);
2819
+ play?.tickErrand();
2820
+ }
1906
2821
  maybeRenderDebug();
1907
2822
  const currentState = tickSounds(deps);
1908
2823
  tickLiveState(deps, dist, currentState);
@@ -2452,13 +3367,13 @@ function readPersistedPosition(storageKey = ONEKO_DEFAULTS.storageKey) {
2452
3367
  return null;
2453
3368
  }
2454
3369
  if (!position) return null;
2455
- const clamp = (value, extent) => {
3370
+ const clamp2 = (value, extent) => {
2456
3371
  const inset = Math.min(TILE / 2, Math.max(0, extent / 2));
2457
3372
  return Math.max(inset, Math.min(Math.max(inset, extent - inset), value));
2458
3373
  };
2459
3374
  return {
2460
- x: clamp(position.x, window.innerWidth),
2461
- y: clamp(position.y, window.innerHeight)
3375
+ x: clamp2(position.x, window.innerWidth),
3376
+ y: clamp2(position.y, window.innerHeight)
2462
3377
  };
2463
3378
  }
2464
3379
  function loadPersistedCatState(stateRef, el, storageKey = ONEKO_DEFAULTS.storageKey) {
@@ -2543,6 +3458,8 @@ function startCatAnimation({
2543
3458
  }) {
2544
3459
  stateRef.current.bubbleVisible = false;
2545
3460
  stateRef.current.bubbleTimer = 0;
3461
+ stateRef.current.zIndex = zIndex;
3462
+ stateRef.current.affectionKey = `${storageKey}:affection`;
2546
3463
  const el = createCatElement(stateRef.current, zIndex);
2547
3464
  elRef.current = el;
2548
3465
  const debugSVG = createDebugSVG(zIndex);
@@ -2560,7 +3477,8 @@ function startCatAnimation({
2560
3477
  onStateChangeRef,
2561
3478
  liveStateRef
2562
3479
  };
2563
- const frame = createFrameLoop(deps);
3480
+ const play = createPlay(deps);
3481
+ const frame = createFrameLoop(deps, play);
2564
3482
  const debugControls = createDebugControls({
2565
3483
  stateRef,
2566
3484
  el,
@@ -2584,7 +3502,9 @@ function startCatAnimation({
2584
3502
  document.addEventListener("mousemove", onMouseMove);
2585
3503
  const onPointerDown = (event) => {
2586
3504
  if (event.pointerType !== "touch") return;
2587
- if (event.target instanceof Element && event.target.closest("button,a,input,select,textarea,label,summary,[role='button']"))
3505
+ if (event.target instanceof Element && event.target.closest(
3506
+ "button,a,input,select,textarea,label,summary,[role='button'],[data-oneko-cat]"
3507
+ ))
2588
3508
  return;
2589
3509
  onMouseMove(event);
2590
3510
  };
@@ -2612,7 +3532,12 @@ function startCatAnimation({
2612
3532
  return () => {
2613
3533
  stopPetting?.();
2614
3534
  stopWatchingPage();
3535
+ play.dispose();
2615
3536
  leavePack();
3537
+ if (packSize() === 0) {
3538
+ clearTreats();
3539
+ restoreKnocked();
3540
+ }
2616
3541
  persist?.();
2617
3542
  teardownCatAnimation(
2618
3543
  { el, debugSVG, debugWrapper, bubbleEl },
@@ -2667,7 +3592,13 @@ function defaultRuntimeConfigState() {
2667
3592
  chaseCfg: ONEKO_DEFAULTS.chase,
2668
3593
  napWhileTypingCfg: ONEKO_DEFAULTS.napWhileTyping,
2669
3594
  followFocusCfg: ONEKO_DEFAULTS.followFocus,
2670
- perchOnHoverCfg: ONEKO_DEFAULTS.perchOnHover
3595
+ perchOnHoverCfg: ONEKO_DEFAULTS.perchOnHover,
3596
+ seasonalCfg: ONEKO_DEFAULTS.seasonal,
3597
+ grabbableCfg: ONEKO_DEFAULTS.grabbable,
3598
+ treatsCfg: ONEKO_DEFAULTS.treats,
3599
+ timeOfDayCfg: ONEKO_DEFAULTS.timeOfDay,
3600
+ affectionCfg: ONEKO_DEFAULTS.affection,
3601
+ handlers: {}
2671
3602
  };
2672
3603
  }
2673
3604
  function applyRuntimeConfig(state, config) {
@@ -2717,6 +3648,20 @@ function applyRuntimeConfig(state, config) {
2717
3648
  state.perchOnHoverCfg = config.perchOnHover ?? ONEKO_DEFAULTS.perchOnHover;
2718
3649
  if (!state.napWhileTypingCfg) state.typingNap = false;
2719
3650
  if (!state.perchOnHoverCfg) state.perchTarget = null;
3651
+ const seasonal = config.seasonal ?? ONEKO_DEFAULTS.seasonal;
3652
+ state.seasonalCfg = typeof seasonal === "boolean" || isOnekoSeason(seasonal) ? seasonal : true;
3653
+ state.grabbableCfg = config.grabbable ?? ONEKO_DEFAULTS.grabbable;
3654
+ state.treatsCfg = config.treats ?? ONEKO_DEFAULTS.treats;
3655
+ state.timeOfDayCfg = config.timeOfDay ?? ONEKO_DEFAULTS.timeOfDay;
3656
+ state.affectionCfg = config.affection ?? ONEKO_DEFAULTS.affection;
3657
+ state.handlers = {
3658
+ onPet: config.onPet,
3659
+ onCatchCursor: config.onCatchCursor,
3660
+ onSleep: config.onSleep,
3661
+ onToss: config.onToss,
3662
+ onTreat: config.onTreat
3663
+ };
3664
+ if (!state.grabbableCfg && state.grab?.phase === "held") state.grab = null;
2720
3665
  }
2721
3666
  const classic = "data:image/png;base64,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";
2722
3667
  const classicSheet = {
@@ -2800,6 +3745,67 @@ async function loadSkinSource(skin = "classic") {
2800
3745
  }
2801
3746
  return loaded.get(id) ?? loaded.get("classic");
2802
3747
  }
3748
+ function createCatControls(stateRef) {
3749
+ const s = () => stateRef.current;
3750
+ const release = () => {
3751
+ const errand = s().errand;
3752
+ if (errand?.kind === "goTo") errand.resolve?.(false);
3753
+ s().errand = null;
3754
+ };
3755
+ return {
3756
+ goTo(target) {
3757
+ const resolved = typeof target === "string" ? document.querySelector(target) : target;
3758
+ release();
3759
+ if (!resolved) return Promise.resolve(false);
3760
+ return new Promise((resolve) => {
3761
+ const state = s();
3762
+ state.errand = {
3763
+ kind: "goTo",
3764
+ target: resolved,
3765
+ frames: 0,
3766
+ arrived: false,
3767
+ resolve
3768
+ };
3769
+ state.typingNap = false;
3770
+ });
3771
+ },
3772
+ say(text, options = {}) {
3773
+ const state = s();
3774
+ const frames = Number.isFinite(options.duration) ? Math.max(1, Math.round(options.duration / 100)) : state.bubbleDisplayFramesCfg;
3775
+ state.sayRequest = { text: String(text).slice(0, 280), frames };
3776
+ },
3777
+ sit() {
3778
+ release();
3779
+ s().errand = { kind: "sit" };
3780
+ },
3781
+ follow: release,
3782
+ pet() {
3783
+ s().petRequest = true;
3784
+ },
3785
+ dropTreat(x, y) {
3786
+ const state = s();
3787
+ dropTreat(x ?? state.mousePosX, y ?? state.mousePosY, state.zIndex);
3788
+ }
3789
+ };
3790
+ }
3791
+ function defaultPlayState() {
3792
+ return {
3793
+ season: null,
3794
+ grab: null,
3795
+ errand: null,
3796
+ errandCooldown: 50,
3797
+ sayRequest: null,
3798
+ bubbleForced: false,
3799
+ petRequest: false,
3800
+ chasingFar: false,
3801
+ mood: { speed: 1, freerun: 1, idle: 1 },
3802
+ moodCheckedFrame: -Infinity,
3803
+ affection: 0,
3804
+ affectionLoaded: false,
3805
+ affectionKey: "oneko:affection",
3806
+ zIndex: DEFAULT_Z_INDEX
3807
+ };
3808
+ }
2803
3809
  function createInitialCatState(options) {
2804
3810
  const startX = options.initialPos?.x ?? DEFAULT_POSITION.x;
2805
3811
  const startY = options.initialPos?.y ?? DEFAULT_POSITION.y;
@@ -2838,7 +3844,8 @@ function createInitialCatState(options) {
2838
3844
  typingNap: false,
2839
3845
  held: false,
2840
3846
  perchTarget: null,
2841
- leaderTarget: null
3847
+ leaderTarget: null,
3848
+ ...defaultPlayState()
2842
3849
  };
2843
3850
  applyRuntimeConfig(state, options);
2844
3851
  return state;
@@ -2919,6 +3926,7 @@ function createOneko(initial = {}, base = {}) {
2919
3926
  const stopWatchingHidden = onCatsHiddenChange(start);
2920
3927
  start();
2921
3928
  return {
3929
+ ...createCatControls(stateRef),
2922
3930
  update(next) {
2923
3931
  if (destroyed) return;
2924
3932
  const previous = options;
@@ -2978,10 +3986,11 @@ export {
2978
3986
  applyRuntimeConfig as a,
2979
3987
  createInitialCatState as b,
2980
3988
  createOneko as c,
2981
- areCatsHidden as d,
2982
- ONEKO_SKINS as e,
2983
- setCatsHidden as f,
3989
+ createCatControls as d,
3990
+ areCatsHidden as e,
3991
+ ONEKO_SKINS as f,
2984
3992
  getSkinSource as g,
3993
+ setCatsHidden as h,
2985
3994
  isOnekoSkin as i,
2986
3995
  loadSkinSource as l,
2987
3996
  mountLaserCursor as m,