oneko 0.3.0 → 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.
- package/dist/chunks/{vanilla-CUrawvWx.js → vanilla-BNtPCdx5.js} +1079 -72
- package/dist/index.js +29 -7
- package/dist/solid.js +8 -3
- package/dist/svelte/Oneko.svelte +14 -4
- package/dist/svelte/Oneko.svelte.d.ts +2 -2
- package/dist/types/components/oneko.d.ts +12 -2
- package/dist/types/lib/oneko/affection.d.ts +16 -0
- package/dist/types/lib/oneko/animation/deps.d.ts +2 -0
- package/dist/types/lib/oneko/animation/errand-point.d.ts +7 -0
- package/dist/types/lib/oneko/animation/frame.d.ts +2 -1
- package/dist/types/lib/oneko/animation/play-types.d.ts +56 -0
- package/dist/types/lib/oneko/animation/play.d.ts +12 -0
- package/dist/types/lib/oneko/constants.d.ts +1 -0
- package/dist/types/lib/oneko/controls.d.ts +28 -0
- package/dist/types/lib/oneko/defaults.d.ts +5 -0
- package/dist/types/lib/oneko/initial-state.d.ts +4 -0
- package/dist/types/lib/oneko/mood.d.ts +7 -0
- package/dist/types/lib/oneko/pack.d.ts +4 -0
- package/dist/types/lib/oneko/pixel-art.d.ts +5 -0
- package/dist/types/lib/oneko/play-messages.d.ts +8 -0
- package/dist/types/lib/oneko/runtime-config.d.ts +2 -2
- package/dist/types/lib/oneko/seasons.d.ts +27 -0
- package/dist/types/lib/oneko/treats.d.ts +15 -0
- package/dist/types/lib/oneko/types.d.ts +61 -0
- package/dist/types/lib/oneko/vanilla.d.ts +3 -1
- package/dist/types/packages/oneko/src/solid.d.ts +5 -2
- package/dist/types/packages/oneko/src/vanilla.d.ts +2 -1
- package/dist/types/packages/oneko/src/vue.d.ts +71 -4
- package/dist/vanilla.js +4 -4
- package/dist/vue.js +38 -8
- package/package.json +1 -1
- package/dist/cat-sounds/Cat_royal_ambient5.ogg +0 -1
- package/dist/cat-sounds/Cat_royal_ambient6.ogg +0 -1
- package/dist/cat-sounds/Cat_royal_death.ogg +0 -1
- package/dist/cat-sounds/Cat_royal_hurt1.ogg +0 -1
- package/dist/cat-sounds/Cat_royal_hurt2.ogg +0 -1
- package/dist/cat-sounds/Cat_royal_hurt3.ogg +0 -1
- package/dist/cat-sounds/Stray_cat_idle1.ogg +0 -1
- package/dist/cat-sounds/Stray_cat_idle2.ogg +0 -1
- package/dist/cat-sounds/Stray_cat_idle3.ogg +0 -1
- package/dist/cat-sounds/Stray_cat_idle4.ogg +0 -1
|
@@ -388,15 +388,13 @@ const SOUND_POOLS = {
|
|
|
388
388
|
"/cat-sounds/Cat_royal_ambient1.ogg",
|
|
389
389
|
"/cat-sounds/Cat_royal_ambient2.ogg",
|
|
390
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"
|
|
391
|
+
"/cat-sounds/Cat_royal_ambient4.ogg"
|
|
394
392
|
],
|
|
395
393
|
alert: [
|
|
396
|
-
"/cat-sounds/
|
|
397
|
-
"/cat-sounds/
|
|
398
|
-
"/cat-sounds/
|
|
399
|
-
"/cat-sounds/
|
|
394
|
+
"/cat-sounds/Cat_idle1.ogg",
|
|
395
|
+
"/cat-sounds/Cat_idle2.ogg",
|
|
396
|
+
"/cat-sounds/Cat_idle3.ogg",
|
|
397
|
+
"/cat-sounds/Cat_idle4.ogg"
|
|
400
398
|
]
|
|
401
399
|
};
|
|
402
400
|
const KONAMI_SEQUENCE = [
|
|
@@ -557,7 +555,12 @@ const ONEKO_DEFAULTS = {
|
|
|
557
555
|
napWhileTyping: false,
|
|
558
556
|
followFocus: false,
|
|
559
557
|
perchOnHover: false,
|
|
560
|
-
pettable: false
|
|
558
|
+
pettable: false,
|
|
559
|
+
seasonal: true,
|
|
560
|
+
grabbable: false,
|
|
561
|
+
treats: false,
|
|
562
|
+
timeOfDay: false,
|
|
563
|
+
affection: false
|
|
561
564
|
};
|
|
562
565
|
function isValidRect(rect) {
|
|
563
566
|
return Object.values(rect).every(Number.isFinite) && rect.right > rect.left && rect.bottom > rect.top;
|
|
@@ -673,6 +676,55 @@ function constrainZoneMovement(from, to, blocked) {
|
|
|
673
676
|
}
|
|
674
677
|
return { x: from.x + dx * limit, y: from.y + dy * limit };
|
|
675
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
|
+
}
|
|
676
728
|
const EMPTY_ZONES = [];
|
|
677
729
|
function createZoneState() {
|
|
678
730
|
return {
|
|
@@ -760,7 +812,9 @@ function updateZoneTarget(s, width, height, random = Math.random) {
|
|
|
760
812
|
}
|
|
761
813
|
}
|
|
762
814
|
const favorite = favorites.find((zone) => zone.id === zones.targetId);
|
|
763
|
-
const
|
|
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 ? {
|
|
764
818
|
x: (favorite.rect.left + favorite.rect.right) / 2,
|
|
765
819
|
y: (favorite.rect.top + favorite.rect.bottom) / 2
|
|
766
820
|
} : s.leaderTarget ?? { x: s.mousePosX, y: s.mousePosY });
|
|
@@ -770,6 +824,161 @@ function updateZoneTarget(s, width, height, random = Math.random) {
|
|
|
770
824
|
} : desired;
|
|
771
825
|
if (oldId !== zones.targetId) s.lastPathRecalcFrame = -PATH_RECALC_INTERVAL;
|
|
772
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
|
+
};
|
|
773
982
|
function pickFromPool(deps, pool) {
|
|
774
983
|
const s = deps.stateRef.current;
|
|
775
984
|
let idx = Math.floor(Math.random() * pool.length);
|
|
@@ -810,6 +1019,9 @@ function messageForActivity(deps) {
|
|
|
810
1019
|
if (idleAnimationMessage) {
|
|
811
1020
|
return idleAnimationMessage;
|
|
812
1021
|
}
|
|
1022
|
+
if (s.season && Math.random() < 0.35) {
|
|
1023
|
+
return pickFromPool(deps, SEASON_MESSAGES[s.season]);
|
|
1024
|
+
}
|
|
813
1025
|
if (s.idleTime > 0) {
|
|
814
1026
|
return pickFromPool(deps, IDLE_MESSAGES);
|
|
815
1027
|
}
|
|
@@ -831,6 +1043,7 @@ function showBubble(deps) {
|
|
|
831
1043
|
function hideBubble(deps) {
|
|
832
1044
|
const s = deps.stateRef.current;
|
|
833
1045
|
s.bubbleVisible = false;
|
|
1046
|
+
s.bubbleForced = false;
|
|
834
1047
|
deps.bubbleEl.style.opacity = "0";
|
|
835
1048
|
s.bubbleCooldown = s.bubbleCooldownFramesCfg;
|
|
836
1049
|
}
|
|
@@ -896,7 +1109,15 @@ function shouldTriggerBubble(deps) {
|
|
|
896
1109
|
}
|
|
897
1110
|
function updateBubble(deps) {
|
|
898
1111
|
const s = deps.stateRef.current;
|
|
899
|
-
if (
|
|
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) {
|
|
900
1121
|
if (s.bubbleVisible) {
|
|
901
1122
|
hideBubble(deps);
|
|
902
1123
|
}
|
|
@@ -935,6 +1156,17 @@ function leaderPosition(state) {
|
|
|
935
1156
|
const leader = pack[pack.indexOf(state) - 1];
|
|
936
1157
|
return leader ? { x: leader.nekoPosX, y: leader.nekoPosY } : null;
|
|
937
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
|
+
}
|
|
938
1170
|
function napmateNearby(state) {
|
|
939
1171
|
const reach = TILE * Math.max(1, state.scale) * 2;
|
|
940
1172
|
return pack.some(
|
|
@@ -971,8 +1203,181 @@ function onCatsHiddenChange(callback) {
|
|
|
971
1203
|
window.removeEventListener("storage", onStorage);
|
|
972
1204
|
};
|
|
973
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
|
+
}
|
|
974
1376
|
function getActivityLabel(deps) {
|
|
975
1377
|
const s = deps.stateRef.current;
|
|
1378
|
+
if (s.grab && s.grab.phase !== "landed") {
|
|
1379
|
+
return "alert";
|
|
1380
|
+
}
|
|
976
1381
|
if (s.freerunMode) {
|
|
977
1382
|
return "freerun";
|
|
978
1383
|
}
|
|
@@ -1008,6 +1413,551 @@ function playSound(deps, pool) {
|
|
|
1008
1413
|
s.soundCooldown = 0;
|
|
1009
1414
|
});
|
|
1010
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
|
+
}
|
|
1011
1961
|
const PERCH_SELECTOR = "a[href],button,[role='button'],[data-oneko-perch]";
|
|
1012
1962
|
const PERCH_DELAY_MS = 700;
|
|
1013
1963
|
const NOT_TYPED_INPUTS = /* @__PURE__ */ new Set([
|
|
@@ -1022,7 +1972,7 @@ const NOT_TYPED_INPUTS = /* @__PURE__ */ new Set([
|
|
|
1022
1972
|
"submit"
|
|
1023
1973
|
]);
|
|
1024
1974
|
function stopDistance(s) {
|
|
1025
|
-
if (s.perchTarget) return Math.min(s.followDistanceCfg, 8);
|
|
1975
|
+
if (s.errand || s.perchTarget) return Math.min(s.followDistanceCfg, 8);
|
|
1026
1976
|
if (s.leaderTarget) return Math.max(s.followDistanceCfg, TILE * s.scale);
|
|
1027
1977
|
return s.followDistanceCfg;
|
|
1028
1978
|
}
|
|
@@ -1144,19 +2094,7 @@ function makePettable(deps) {
|
|
|
1144
2094
|
if (!event.repeat) pet();
|
|
1145
2095
|
};
|
|
1146
2096
|
const pet = () => {
|
|
1147
|
-
|
|
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);
|
|
2097
|
+
petCat(deps);
|
|
1160
2098
|
announcer.textContent = ++pets % 2 ? "The cat purrs." : "The cat purrs happily.";
|
|
1161
2099
|
};
|
|
1162
2100
|
el.addEventListener("focus", onFocus);
|
|
@@ -1342,7 +2280,7 @@ function collectObstacles() {
|
|
|
1342
2280
|
const vh = window.innerHeight;
|
|
1343
2281
|
const rects = [];
|
|
1344
2282
|
for (const node of document.querySelectorAll(OBSTACLE_SELECTOR)) {
|
|
1345
|
-
if (node.closest('[data-oneko-zone="attract"]')) continue;
|
|
2283
|
+
if (node.closest('[data-oneko-zone="attract"],[data-oneko-box]')) continue;
|
|
1346
2284
|
const r = node.getBoundingClientRect();
|
|
1347
2285
|
if (shouldIncludeObstacleRect(r, vw, vh)) {
|
|
1348
2286
|
rects.push(toObstacleRect(r));
|
|
@@ -1516,40 +2454,8 @@ function findRoute(startIdx, goalIdx, grid, cols, rows) {
|
|
|
1516
2454
|
}
|
|
1517
2455
|
return smoothPath(reconstructCellPath(prev, goalIdx), cols);
|
|
1518
2456
|
}
|
|
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
2457
|
function idleFrameThreshold(s) {
|
|
1552
|
-
return Math.max(1, Math.round(s.idleThresholdMs / 100));
|
|
2458
|
+
return Math.max(1, Math.round(s.idleThresholdMs * s.mood.idle / 100));
|
|
1553
2459
|
}
|
|
1554
2460
|
function wallScratchAnimations(s) {
|
|
1555
2461
|
return [
|
|
@@ -1692,7 +2598,7 @@ function moveToward(deps, targetX, targetY) {
|
|
|
1692
2598
|
s.nekoVelY *= 0.5;
|
|
1693
2599
|
return;
|
|
1694
2600
|
}
|
|
1695
|
-
const currentSpeed = s.currentSpeed;
|
|
2601
|
+
const currentSpeed = s.currentSpeed * s.mood.speed;
|
|
1696
2602
|
s.nekoVelX += (dx / dist * currentSpeed - s.nekoVelX) * STEER_LERP;
|
|
1697
2603
|
s.nekoVelY += (dy / dist * currentSpeed - s.nekoVelY) * STEER_LERP;
|
|
1698
2604
|
const velMag = Math.hypot(s.nekoVelX, s.nekoVelY);
|
|
@@ -1782,7 +2688,7 @@ function chaseCursor(deps) {
|
|
|
1782
2688
|
tickFreerun(deps);
|
|
1783
2689
|
return;
|
|
1784
2690
|
}
|
|
1785
|
-
if (!s.noFollow && Math.random() < s.freerunChanceCfg) {
|
|
2691
|
+
if (!s.noFollow && !s.errand && Math.random() < s.freerunChanceCfg * s.mood.freerun) {
|
|
1786
2692
|
enterFreerun(deps);
|
|
1787
2693
|
moveToward(deps, s.zoneState.movementTarget.x, s.zoneState.movementTarget.y);
|
|
1788
2694
|
return;
|
|
@@ -1886,6 +2792,7 @@ function tickSounds(deps) {
|
|
|
1886
2792
|
if (currentState !== deps.lastStateRef.current) {
|
|
1887
2793
|
deps.lastStateRef.current = currentState;
|
|
1888
2794
|
deps.onStateChangeRef.current?.(currentState);
|
|
2795
|
+
if (currentState === "sleeping") deps.stateRef.current.handlers.onSleep?.();
|
|
1889
2796
|
const pool = SOUND_POOLS[currentState];
|
|
1890
2797
|
if (pool) {
|
|
1891
2798
|
playSound(deps, pool);
|
|
@@ -1894,17 +2801,23 @@ function tickSounds(deps) {
|
|
|
1894
2801
|
tickAmbientSounds(deps, currentState);
|
|
1895
2802
|
return currentState;
|
|
1896
2803
|
}
|
|
1897
|
-
function createFrameLoop(deps) {
|
|
2804
|
+
function createFrameLoop(deps, play) {
|
|
1898
2805
|
const { maybeRenderDebug } = createDebugRenderer(deps);
|
|
1899
2806
|
const frame = () => {
|
|
1900
2807
|
const s = deps.stateRef.current;
|
|
1901
2808
|
if (s.pausedCfg) return;
|
|
1902
2809
|
if (!refreshZones(deps)) return;
|
|
1903
2810
|
tickPrelude(s);
|
|
2811
|
+
play?.upkeep();
|
|
1904
2812
|
tickAttention(deps);
|
|
1905
2813
|
updateZoneTarget(s, window.innerWidth, window.innerHeight);
|
|
1906
2814
|
const dist = Math.hypot(s.mousePosX - s.nekoPosX, s.mousePosY - s.nekoPosY);
|
|
1907
|
-
|
|
2815
|
+
if (play?.tickGrab()) {
|
|
2816
|
+
updateBubble(deps);
|
|
2817
|
+
} else {
|
|
2818
|
+
tickMovement(deps, dist);
|
|
2819
|
+
play?.tickErrand();
|
|
2820
|
+
}
|
|
1908
2821
|
maybeRenderDebug();
|
|
1909
2822
|
const currentState = tickSounds(deps);
|
|
1910
2823
|
tickLiveState(deps, dist, currentState);
|
|
@@ -2454,13 +3367,13 @@ function readPersistedPosition(storageKey = ONEKO_DEFAULTS.storageKey) {
|
|
|
2454
3367
|
return null;
|
|
2455
3368
|
}
|
|
2456
3369
|
if (!position) return null;
|
|
2457
|
-
const
|
|
3370
|
+
const clamp2 = (value, extent) => {
|
|
2458
3371
|
const inset = Math.min(TILE / 2, Math.max(0, extent / 2));
|
|
2459
3372
|
return Math.max(inset, Math.min(Math.max(inset, extent - inset), value));
|
|
2460
3373
|
};
|
|
2461
3374
|
return {
|
|
2462
|
-
x:
|
|
2463
|
-
y:
|
|
3375
|
+
x: clamp2(position.x, window.innerWidth),
|
|
3376
|
+
y: clamp2(position.y, window.innerHeight)
|
|
2464
3377
|
};
|
|
2465
3378
|
}
|
|
2466
3379
|
function loadPersistedCatState(stateRef, el, storageKey = ONEKO_DEFAULTS.storageKey) {
|
|
@@ -2545,6 +3458,8 @@ function startCatAnimation({
|
|
|
2545
3458
|
}) {
|
|
2546
3459
|
stateRef.current.bubbleVisible = false;
|
|
2547
3460
|
stateRef.current.bubbleTimer = 0;
|
|
3461
|
+
stateRef.current.zIndex = zIndex;
|
|
3462
|
+
stateRef.current.affectionKey = `${storageKey}:affection`;
|
|
2548
3463
|
const el = createCatElement(stateRef.current, zIndex);
|
|
2549
3464
|
elRef.current = el;
|
|
2550
3465
|
const debugSVG = createDebugSVG(zIndex);
|
|
@@ -2562,7 +3477,8 @@ function startCatAnimation({
|
|
|
2562
3477
|
onStateChangeRef,
|
|
2563
3478
|
liveStateRef
|
|
2564
3479
|
};
|
|
2565
|
-
const
|
|
3480
|
+
const play = createPlay(deps);
|
|
3481
|
+
const frame = createFrameLoop(deps, play);
|
|
2566
3482
|
const debugControls = createDebugControls({
|
|
2567
3483
|
stateRef,
|
|
2568
3484
|
el,
|
|
@@ -2586,7 +3502,9 @@ function startCatAnimation({
|
|
|
2586
3502
|
document.addEventListener("mousemove", onMouseMove);
|
|
2587
3503
|
const onPointerDown = (event) => {
|
|
2588
3504
|
if (event.pointerType !== "touch") return;
|
|
2589
|
-
if (event.target instanceof Element && event.target.closest(
|
|
3505
|
+
if (event.target instanceof Element && event.target.closest(
|
|
3506
|
+
"button,a,input,select,textarea,label,summary,[role='button'],[data-oneko-cat]"
|
|
3507
|
+
))
|
|
2590
3508
|
return;
|
|
2591
3509
|
onMouseMove(event);
|
|
2592
3510
|
};
|
|
@@ -2614,7 +3532,12 @@ function startCatAnimation({
|
|
|
2614
3532
|
return () => {
|
|
2615
3533
|
stopPetting?.();
|
|
2616
3534
|
stopWatchingPage();
|
|
3535
|
+
play.dispose();
|
|
2617
3536
|
leavePack();
|
|
3537
|
+
if (packSize() === 0) {
|
|
3538
|
+
clearTreats();
|
|
3539
|
+
restoreKnocked();
|
|
3540
|
+
}
|
|
2618
3541
|
persist?.();
|
|
2619
3542
|
teardownCatAnimation(
|
|
2620
3543
|
{ el, debugSVG, debugWrapper, bubbleEl },
|
|
@@ -2669,7 +3592,13 @@ function defaultRuntimeConfigState() {
|
|
|
2669
3592
|
chaseCfg: ONEKO_DEFAULTS.chase,
|
|
2670
3593
|
napWhileTypingCfg: ONEKO_DEFAULTS.napWhileTyping,
|
|
2671
3594
|
followFocusCfg: ONEKO_DEFAULTS.followFocus,
|
|
2672
|
-
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: {}
|
|
2673
3602
|
};
|
|
2674
3603
|
}
|
|
2675
3604
|
function applyRuntimeConfig(state, config) {
|
|
@@ -2719,6 +3648,20 @@ function applyRuntimeConfig(state, config) {
|
|
|
2719
3648
|
state.perchOnHoverCfg = config.perchOnHover ?? ONEKO_DEFAULTS.perchOnHover;
|
|
2720
3649
|
if (!state.napWhileTypingCfg) state.typingNap = false;
|
|
2721
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;
|
|
2722
3665
|
}
|
|
2723
3666
|
const classic = "data:image/png;base64,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";
|
|
2724
3667
|
const classicSheet = {
|
|
@@ -2802,6 +3745,67 @@ async function loadSkinSource(skin = "classic") {
|
|
|
2802
3745
|
}
|
|
2803
3746
|
return loaded.get(id) ?? loaded.get("classic");
|
|
2804
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
|
+
}
|
|
2805
3809
|
function createInitialCatState(options) {
|
|
2806
3810
|
const startX = options.initialPos?.x ?? DEFAULT_POSITION.x;
|
|
2807
3811
|
const startY = options.initialPos?.y ?? DEFAULT_POSITION.y;
|
|
@@ -2840,7 +3844,8 @@ function createInitialCatState(options) {
|
|
|
2840
3844
|
typingNap: false,
|
|
2841
3845
|
held: false,
|
|
2842
3846
|
perchTarget: null,
|
|
2843
|
-
leaderTarget: null
|
|
3847
|
+
leaderTarget: null,
|
|
3848
|
+
...defaultPlayState()
|
|
2844
3849
|
};
|
|
2845
3850
|
applyRuntimeConfig(state, options);
|
|
2846
3851
|
return state;
|
|
@@ -2921,6 +3926,7 @@ function createOneko(initial = {}, base = {}) {
|
|
|
2921
3926
|
const stopWatchingHidden = onCatsHiddenChange(start);
|
|
2922
3927
|
start();
|
|
2923
3928
|
return {
|
|
3929
|
+
...createCatControls(stateRef),
|
|
2924
3930
|
update(next) {
|
|
2925
3931
|
if (destroyed) return;
|
|
2926
3932
|
const previous = options;
|
|
@@ -2980,10 +3986,11 @@ export {
|
|
|
2980
3986
|
applyRuntimeConfig as a,
|
|
2981
3987
|
createInitialCatState as b,
|
|
2982
3988
|
createOneko as c,
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3989
|
+
createCatControls as d,
|
|
3990
|
+
areCatsHidden as e,
|
|
3991
|
+
ONEKO_SKINS as f,
|
|
2986
3992
|
getSkinSource as g,
|
|
3993
|
+
setCatsHidden as h,
|
|
2987
3994
|
isOnekoSkin as i,
|
|
2988
3995
|
loadSkinSource as l,
|
|
2989
3996
|
mountLaserCursor as m,
|