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.
- package/dist/chunks/{vanilla-BmeHFErE.js → vanilla-BNtPCdx5.js} +1074 -65
- 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
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
2461
|
-
y:
|
|
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
|
|
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(
|
|
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
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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,
|