lilgotchi 0.0.0-stage → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +69 -3
- package/dist/LICENSE.md +21 -0
- package/dist/README.md +69 -0
- package/dist/design/evolution/README.md +7 -0
- package/dist/design/evolution/cypunk.js +3 -0
- package/dist/design/evolution/cypunk.md +20 -0
- package/dist/design/evolution/index.html +20 -0
- package/dist/design/evolution/lilgotchi.js +3 -0
- package/dist/design/evolution/lilgotchi.md +20 -0
- package/dist/design/evolution/preview.css +3 -0
- package/dist/design/evolution/preview.js +47 -0
- package/dist/design/evolution/slice.js +3 -0
- package/dist/design/evolution/slice.md +14 -0
- package/dist/design/games/README.md +52 -0
- package/dist/design/games/bump.md +23 -0
- package/dist/design/games/catalog.md +24 -0
- package/dist/design/games/flag.md +19 -0
- package/dist/design/games/get.md +21 -0
- package/dist/design/games/heading.md +23 -0
- package/dist/design/games/hoops.md +17 -0
- package/dist/design/games/left-right.md +30 -0
- package/dist/design/games/memory.md +21 -0
- package/dist/design/games/polish-review.md +11 -0
- package/dist/design/games/release-checklist.md +52 -0
- package/dist/design/games/sprint.md +21 -0
- package/dist/docs/README.md +14 -0
- package/dist/docs/care.md +51 -0
- package/dist/docs/characters.md +51 -0
- package/dist/docs/credits.md +17 -0
- package/dist/docs/development.md +87 -0
- package/dist/docs/frame-contract.md +38 -0
- package/dist/docs/images/cypunk-family.jpg +0 -0
- package/dist/docs/images/lilgotchi-family.jpg +0 -0
- package/dist/docs/images/original-family.jpg +0 -0
- package/dist/docs/images/slice-family.jpg +0 -0
- package/dist/docs/install.md +41 -0
- package/dist/docs/release-review.md +47 -0
- package/dist/embed.js +21 -0
- package/dist/examples/cross-origin.html +7 -0
- package/dist/examples/curated.html +12 -0
- package/dist/examples/duplicate.html +3 -0
- package/dist/examples/install.html +11 -0
- package/dist/examples/launcher-review.html +23 -0
- package/dist/examples/launchers.html +10 -0
- package/dist/examples/nested/index.html +6 -0
- package/dist/examples/one-line.html +9 -0
- package/dist/index.html +100 -0
- package/dist/lilgotchi.js +437 -0
- package/dist/pets/NOTICE.md +19 -0
- package/dist/pets/README.md +7 -0
- package/dist/pets/arcade-catalog.js +52 -0
- package/dist/pets/arcade-geometry.js +18 -0
- package/dist/pets/arcade-opponents.js +16 -0
- package/dist/pets/arcade-renderer.js +116 -0
- package/dist/pets/arcade-sprites.js +64 -0
- package/dist/pets/arcade.js +393 -0
- package/dist/pets/away.js +20 -0
- package/dist/pets/boot.js +161 -0
- package/dist/pets/catalog.js +32 -0
- package/dist/pets/celebrations.js +54 -0
- package/dist/pets/classic-sprites.js +672 -0
- package/dist/pets/clock.js +3 -0
- package/dist/pets/colors.js +38 -0
- package/dist/pets/constants.js +20 -0
- package/dist/pets/custom-themes.css +80 -0
- package/dist/pets/cyberpunk-sprites.js +68 -0
- package/dist/pets/cypunk-original.js +2 -0
- package/dist/pets/embed.css +130 -0
- package/dist/pets/engine.js +38 -0
- package/dist/pets/evolution.js +46 -0
- package/dist/pets/focus.js +27 -0
- package/dist/pets/font.js +185 -0
- package/dist/pets/game.css +275 -0
- package/dist/pets/game.html +148 -0
- package/dist/pets/game.js +626 -0
- package/dist/pets/layout.js +53 -0
- package/dist/pets/lilgotchi-sprites.js +62 -0
- package/dist/pets/minigames/bump.js +52 -0
- package/dist/pets/minigames/flag.js +87 -0
- package/dist/pets/minigames/get.js +59 -0
- package/dist/pets/minigames/heading.js +117 -0
- package/dist/pets/minigames/hoops.js +96 -0
- package/dist/pets/minigames/memory.js +81 -0
- package/dist/pets/minigames/model.js +40 -0
- package/dist/pets/minigames/sprint.js +103 -0
- package/dist/pets/original-roster.js +46 -0
- package/dist/pets/original-sprites.js +3861 -0
- package/dist/pets/overlay-size.js +13 -0
- package/dist/pets/pixel-size.js +6 -0
- package/dist/pets/presentation.js +80 -0
- package/dist/pets/renderer.js +100 -0
- package/dist/pets/scenery.css +24 -0
- package/dist/pets/simulation.js +185 -0
- package/dist/pets/skins.js +14 -0
- package/dist/pets/slice-sprites.js +38 -0
- package/dist/pets/sound.js +33 -0
- package/dist/pets/status.js +28 -0
- package/dist/pets/storage.js +49 -0
- package/dist/pets/valid-state.js +26 -0
- package/dist/pets/wellbeing.js +73 -0
- package/dist/reference/source.json +8 -0
- package/dist/version.json +5 -0
- package/dist/widget/care-guide.js +50 -0
- package/dist/widget/care-monitor.js +52 -0
- package/dist/widget/color-controls.js +15 -0
- package/dist/widget/configurator.js +24 -0
- package/dist/widget/egg.svg +1 -0
- package/dist/widget/focus.js +6 -0
- package/dist/widget/gallery.js +17 -0
- package/dist/widget/inactive.js +24 -0
- package/dist/widget/install-snippet.js +14 -0
- package/dist/widget/launcher.js +63 -0
- package/dist/widget/pet-history.js +57 -0
- package/dist/widget/pet-label.js +12 -0
- package/dist/widget/save-store.js +198 -0
- package/dist/widget/scroll-lock.js +11 -0
- package/dist/widget/settings.js +48 -0
- package/dist/widget/shell.css +63 -0
- package/package.json +43 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const FIELD_WIDTH = 32;
|
|
2
|
+
export const FIELD_HEIGHT = 24;
|
|
3
|
+
|
|
4
|
+
export function headingPose(sprite, center, jump = 0) {
|
|
5
|
+
const width = Math.max(...sprite.map(row => row.length));
|
|
6
|
+
const left = Math.max(0, Math.min(FIELD_WIDTH - width, Math.round(center - width / 2)));
|
|
7
|
+
const top = Math.round(FIELD_HEIGHT - sprite.length - jump);
|
|
8
|
+
const occupiedRows = sprite.flatMap((row, index) => row.some(Boolean) ? [index] : []);
|
|
9
|
+
const highest = Math.min(...occupiedRows);
|
|
10
|
+
const lowest = Math.max(...occupiedRows);
|
|
11
|
+
const headLimit = highest + Math.floor((lowest - highest) / 2);
|
|
12
|
+
const head = [];
|
|
13
|
+
for (let column = 0; column < width; column++) {
|
|
14
|
+
const row = sprite.findIndex(line => !!line[column]);
|
|
15
|
+
if (row >= 0 && row <= headLimit) head.push({x:left + column, y:top + row});
|
|
16
|
+
}
|
|
17
|
+
return {left, top, width, height:sprite.length, head};
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { originalSprites } from './original-sprites.js';
|
|
2
|
+
import { sliceSprites, slicePalette } from './slice-sprites.js';
|
|
3
|
+
import { cyberpunkSprites, cyberpunkPalette } from './cyberpunk-sprites.js';
|
|
4
|
+
import { lilgotchiSprites, lilgotchiPalette } from './lilgotchi-sprites.js';
|
|
5
|
+
|
|
6
|
+
const opponents = [
|
|
7
|
+
{family:'pizza',name:'Slice',frames:sliceSprites.mametchi,palette:slicePalette},
|
|
8
|
+
{family:'cyberpunk',name:'Cypunk',frames:cyberpunkSprites.ginjirotchi,palette:cyberpunkPalette},
|
|
9
|
+
{family:'lilgotchi',name:'Lilgotchi',frames:lilgotchiSprites.mametchi,palette:lilgotchiPalette},
|
|
10
|
+
{family:'classic',name:'Gen 1',frames:originalSprites.mametchi,palette:[null,'#c4cfa1']},
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function chooseOpponent(family, random = Math.random) {
|
|
14
|
+
const choices = opponents.filter(opponent => opponent.family !== family);
|
|
15
|
+
return choices[Math.min(choices.length-1, Math.floor(random()*choices.length))];
|
|
16
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { classicSprites } from './classic-sprites.js';
|
|
2
|
+
import { FIELD_WIDTH, FIELD_HEIGHT, headingPose } from './arcade-geometry.js';
|
|
3
|
+
|
|
4
|
+
const arrowPixels = {
|
|
5
|
+
left:[[2,0],[1,1],[0,2],[1,3],[2,4],[1,2],[2,2],[3,2],[4,2]],
|
|
6
|
+
right:[[2,0],[3,1],[4,2],[3,3],[2,4],[0,2],[1,2],[2,2],[3,2]],
|
|
7
|
+
up:[[2,0],[1,1],[3,1],[0,2],[4,2],[2,1],[2,2],[2,3],[2,4]],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function drawArcade(q, s, sprite, palette = [], options = {}) {
|
|
11
|
+
const reducedMotion = options.reducedMotion ?? globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
12
|
+
const frames = options.frames || {idle1:sprite};
|
|
13
|
+
const idle = frames.idle1 || sprite;
|
|
14
|
+
const opponent = options.opponent;
|
|
15
|
+
const rivalFrames = opponent?.frames || classicSprites.mametchi;
|
|
16
|
+
const foreground = options.foreground || '#0f380f';
|
|
17
|
+
const background = options.background || '#c4cfa1';
|
|
18
|
+
const field = q('.arcade-field');
|
|
19
|
+
field.width = FIELD_WIDTH; field.height = FIELD_HEIGHT;
|
|
20
|
+
const ctx = field.getContext('2d');
|
|
21
|
+
ctx.fillStyle = background; ctx.fillRect(0,0,FIELD_WIDTH,FIELD_HEIGHT);
|
|
22
|
+
const rect = (x,y,w,h,color=foreground) => {ctx.fillStyle=color;ctx.fillRect(Math.round(x),Math.round(y),w,h);};
|
|
23
|
+
const pose = direction => frames[{left:'lookLeft',right:'lookRight',up:'lookUp'}[direction]] || idle;
|
|
24
|
+
const pet = (frame, x, bottom, {rival=false,fallen=false}={}) => {
|
|
25
|
+
const width = fallen ? frame.length : Math.max(...frame.map(row=>row.length));
|
|
26
|
+
const height = fallen ? Math.max(...frame.map(row=>row.length)) : frame.length;
|
|
27
|
+
const left=Math.max(0,Math.min(FIELD_WIDTH-width,Math.round(x)));
|
|
28
|
+
const top=Math.max(0,Math.min(FIELD_HEIGHT-height,Math.round(bottom-height)));
|
|
29
|
+
for(let row=0;row<frame.length;row++)for(let col=0;col<frame[row].length;col++){
|
|
30
|
+
const pixel=frame[row][col];if(!pixel)continue;
|
|
31
|
+
rect(left+(fallen?frame.length-1-row:col),top+(fallen?col:row),1,1,rival?(opponent?.palette[pixel]||foreground):palette[pixel]||foreground);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const centered = (frame,bottom=24) => pet(frame,(FIELD_WIDTH-frame[0].length)/2,bottom);
|
|
35
|
+
const arrow = (direction,x,y,color=foreground) => {for(const [dx,dy] of arrowPixels[direction]||[])rect(x+dx,y+dy,1,1,color);};
|
|
36
|
+
const reaction = (x,y,good) => {
|
|
37
|
+
if(good){rect(x+1,y,1,1);rect(x,y+1,3,1);rect(x+1,y+2,1,1);}
|
|
38
|
+
else {rect(x+1,y,1,2);rect(x+1,y+3,1,1);}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if(s.id==='get'){
|
|
42
|
+
const frame=s.reaction==='happy'?frames.happy||idle:s.reaction==='startled'?frames.sad||idle:idle;
|
|
43
|
+
pet(frame,s.lane*10+5-frame[0].length/2,24);
|
|
44
|
+
if(s.object){
|
|
45
|
+
const occupiedTop=idle.findIndex(row=>row.some(Boolean));
|
|
46
|
+
const catchY=FIELD_HEIGHT-idle.length+Math.max(0,occupiedTop)-2;
|
|
47
|
+
const x=s.object.lane*10+4,y=s.object.y/10*catchY;
|
|
48
|
+
if(s.object.kind==='note'){rect(x+2,y,1,3);rect(x,y+2,3,1);}
|
|
49
|
+
else {rect(x+1,y,1,1,'#825b41');rect(x,y+1,3,2,'#825b41');}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if(s.id==='heading'){
|
|
53
|
+
const geometry=headingPose(idle,s.x,s.jump);
|
|
54
|
+
pet(idle,geometry.left,geometry.top+idle.length);
|
|
55
|
+
rect(s.ball.x,s.ball.y,2,2);
|
|
56
|
+
}
|
|
57
|
+
if(s.id==='bump'){
|
|
58
|
+
const reveal=s.phase==='reveal',time=s.phaseTime||0;
|
|
59
|
+
const contact=reveal&&time>=250&&time<1000;
|
|
60
|
+
const fallen=contact&&!reducedMotion&&time>=400;
|
|
61
|
+
const playerLoses=s.boutWon===false;
|
|
62
|
+
const player=frames.lookRight||idle;
|
|
63
|
+
pet(player,contact&&!playerLoses?5:1,24,{fallen:fallen&&playerLoses});
|
|
64
|
+
pet(rivalFrames.lookLeft||rivalFrames.idle1,contact&&playerLoses?17:21,24,{rival:true,fallen:fallen&&!playerLoses});
|
|
65
|
+
if(reveal)reaction(playerLoses?6:25,5,false);
|
|
66
|
+
}
|
|
67
|
+
if(s.id==='memory'){
|
|
68
|
+
const direction=s.phase==='watch'?s.cue:s.lastInput;
|
|
69
|
+
centered(pose(direction));
|
|
70
|
+
if(direction)arrow(direction,14,3);
|
|
71
|
+
if(s.result)reaction(14,3,s.result==='win');
|
|
72
|
+
}
|
|
73
|
+
if(s.id==='flag'){
|
|
74
|
+
centered(pose(s.response));
|
|
75
|
+
const flag=(left,raised)=>{
|
|
76
|
+
const x=left?6:25,y=raised?10:18;
|
|
77
|
+
rect(x,y,1,5);rect(left?x-3:x+1,y,3,2,left?'#365f54':'#934d42');
|
|
78
|
+
};
|
|
79
|
+
flag(true,s.response==='left'||s.response==='both');
|
|
80
|
+
flag(false,s.response==='right'||s.response==='both');
|
|
81
|
+
if(s.phase==='cue'){
|
|
82
|
+
if(s.cue==='fake'){
|
|
83
|
+
for(let i=0;i<5;i++){rect(14+i,2+i,1,1);rect(18-i,2+i,1,1);}
|
|
84
|
+
}else if(s.cue==='both'){arrow('left',8,2);arrow('right',19,2);}
|
|
85
|
+
else arrow(s.cue,14,2);
|
|
86
|
+
}
|
|
87
|
+
if(s.phase==='rest')reaction(14,3,s.correct);
|
|
88
|
+
}
|
|
89
|
+
q('.arcade-meter').hidden=!['bump','hoops'].includes(s.id);
|
|
90
|
+
if(['bump','hoops'].includes(s.id)){
|
|
91
|
+
const target=s.id==='bump'?s.target:[s.target.min*100,s.target.max*100];
|
|
92
|
+
q('.arcade-meter span').style.cssText=`left:${target[0]}%;width:${target[1]-target[0]}%`;
|
|
93
|
+
q('.arcade-meter i').style.left=`${s.id==='bump'?s.meter:s.marker*100}%`;
|
|
94
|
+
}
|
|
95
|
+
if(s.id==='sprint'){
|
|
96
|
+
for(let y=0;y<FIELD_HEIGHT;y++)rect(31,y,1,1,y%2?background:foreground);
|
|
97
|
+
const player=frames.lookRight||idle;
|
|
98
|
+
const end=30-player[0].length;
|
|
99
|
+
pet(player,Math.min(end,s.progress/s.finish*end),12);
|
|
100
|
+
const rival=rivalFrames.lookRight||rivalFrames.idle1;
|
|
101
|
+
pet(rival,Math.min(30-rival[0].length,s.opponent/s.finish*(30-rival[0].length)),24,{rival:true});
|
|
102
|
+
if(s.phase==='countdown'||(s.phase==='race'&&s.elapsed<1000)){
|
|
103
|
+
const active=s.phase==='race'?2:s.countdown>1500?0:1;
|
|
104
|
+
rect(23,1,5,11,'#17282e');
|
|
105
|
+
for(let i=0;i<3;i++)rect(24,2+i*3,3,2,i===active?['#ef7971','#f5d56e','#83da9c'][i]:'#455459');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if(s.id==='hoops'){
|
|
109
|
+
const frame=s.phase==='arc'?frames.eat||idle:frames.lookRight||idle;
|
|
110
|
+
pet(frame,0,24);
|
|
111
|
+
rect(30,3,1,20);rect(29,3,1,4);rect(27,23,5,1);
|
|
112
|
+
rect(24,9,7,1,'#ad5a35');
|
|
113
|
+
rect(25,10,1,2);rect(29,10,1,2);rect(26,12,1,2);rect(28,12,1,2);rect(26,13,3,1);
|
|
114
|
+
if(s.ball){rect(s.ball.x,s.ball.y,2,2,'#f6a64b');rect(s.ball.x,s.ball.y+1,1,1,'#8a4726');}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Compact playfield portraits are drawn separately from the care screen art.
|
|
2
|
+
// Each is eight pixels wide and six tall, leaving room for notes and balls.
|
|
3
|
+
const rows={
|
|
4
|
+
classic:{
|
|
5
|
+
babytchi:['00111100','01000010','10100101','10011001','01111110','00011000'],
|
|
6
|
+
marutchi:['00111100','01111110','11011011','11111111','11000011','01111110'],
|
|
7
|
+
tamatchi:['00011000','00111100','01011010','01111110','00100100','01000010'],
|
|
8
|
+
kuchitamatchi:['00111100','01000010','10100101','01111110','00111100','01000010'],
|
|
9
|
+
mametchi:['01000100','10111010','10000010','01010100','01111100','01000100'],
|
|
10
|
+
ginjirotchi:['00111100','01111110','10111011','11000011','01111110','01000010'],
|
|
11
|
+
maskutchi:['00111100','01000010','11011011','10100101','01111110','01000010'],
|
|
12
|
+
kuchipatchi:['00111100','01000010','10100101','10000111','01111110','01000010'],
|
|
13
|
+
nyorotchi:['01111000','10000100','10101100','10010100','01111000','00111110'],
|
|
14
|
+
tarakotchi:['01000010','00111100','01000010','10100101','10111101','01000010'],
|
|
15
|
+
oyajitchi:['00011000','00111100','01011010','11111111','01011010','00100100'],
|
|
16
|
+
},
|
|
17
|
+
pizza:{
|
|
18
|
+
babytchi:['00055000','00511500','05111150','05155150','00511500','00055000'],
|
|
19
|
+
marutchi:['00555500','05244250','52333325','52355325','05222250','00500500'],
|
|
20
|
+
tamatchi:['05555550','52222225','05355350','00533500','00055000','00500500'],
|
|
21
|
+
kuchitamatchi:['00555500','05222250','53355350','05333250','00533250','00055000'],
|
|
22
|
+
mametchi:['05555550','52222225','05155150','00546500','00035000','00500500'],
|
|
23
|
+
ginjirotchi:['05555550','52222225','05455450','00533500','00045000','00500500'],
|
|
24
|
+
maskutchi:['05555550','52222225','05755750','005AB500','000B5000','00500500'],
|
|
25
|
+
kuchipatchi:['05555550','52222225','53455435','53BB3B35','05222250','00500500'],
|
|
26
|
+
nyorotchi:['05555550','52222225','05355350','00599500','00035000','00500500'],
|
|
27
|
+
tarakotchi:['05555550','52222225','05855850','005C8500','000C5000','00500500'],
|
|
28
|
+
oyajitchi:['05555550','52222225','05155150','00566500','00065000','00500500'],
|
|
29
|
+
},
|
|
30
|
+
cyberpunk:{
|
|
31
|
+
babytchi:['00333300','03222300','03121300','03222300','00333300','00303000'],
|
|
32
|
+
marutchi:['00040000','00333300','03222330','03121230','00333300','00303000'],
|
|
33
|
+
tamatchi:['00333300','03222330','03333330','03121330','00344300','03000300'],
|
|
34
|
+
kuchitamatchi:['00400300','00333300','03121340','03222300','00344300','03000300'],
|
|
35
|
+
mametchi:['00700700','00333330','03122130','33377333','03222300','03303300'],
|
|
36
|
+
ginjirotchi:['00300300','00333330','03322330','33314333','03442300','03303300'],
|
|
37
|
+
maskutchi:['00600600','00666660','06122160','66611666','06216300','06606600'],
|
|
38
|
+
kuchipatchi:['04400000','00333330','03122130','34344343','03222300','03303300'],
|
|
39
|
+
nyorotchi:['03300400','03233330','04312133','03344330','00322330','03300303'],
|
|
40
|
+
tarakotchi:['05555550','59999995','59199195','59911995','05555550','00333000'],
|
|
41
|
+
oyajitchi:['00400300','00333340','43121330','00344300','04322304','03300330'],
|
|
42
|
+
},
|
|
43
|
+
lilgotchi:{
|
|
44
|
+
babytchi:['00000000','00022000','00211200','02133120','00211200','00022000'],
|
|
45
|
+
marutchi:['00022000','00211200','02111120','02133120','02111120','00222200'],
|
|
46
|
+
tamatchi:['00022000','00281200','02111120','02133120','02111120','00222200'],
|
|
47
|
+
kuchitamatchi:['00022000','00265200','02555520','02533520','02655620','00222200'],
|
|
48
|
+
mametchi:['00022000','00284200','02444420','02433420','02444120','00222200'],
|
|
49
|
+
ginjirotchi:['00066000','00656600','06555560','06533560','00665600','00066000'],
|
|
50
|
+
maskutchi:['00022000','00288200','02888820','02833820','02888820','00222200'],
|
|
51
|
+
kuchipatchi:['00222200','02111120','21111112','21133112','21111112','02222220'],
|
|
52
|
+
nyorotchi:['00066000','00682600','06222260','06266260','06222260','00666600'],
|
|
53
|
+
tarakotchi:['00066000','00647600','06777760','06788760','06777360','00666600'],
|
|
54
|
+
oyajitchi:['00022000','00288000','02883000','02833820','02888320','00222200'],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
export const arcadeSprites=Object.freeze(Object.fromEntries(Object.entries(rows).map(([family,pets])=>[
|
|
58
|
+
family,Object.freeze(Object.fromEntries(Object.entries(pets).map(([id,drawing])=>[
|
|
59
|
+
id,Object.freeze(drawing.map(row=>Object.freeze([...row].map(pixel=>parseInt(pixel,16)))))
|
|
60
|
+
])))
|
|
61
|
+
])));
|
|
62
|
+
export function arcadeSprite(family,identity){
|
|
63
|
+
return arcadeSprites[family]?.[identity]||arcadeSprites.classic[identity]||arcadeSprites.classic.babytchi;
|
|
64
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { chooseOpponent } from './arcade-opponents.js';
|
|
2
|
+
import { drawArcade } from "./arcade-renderer.js";
|
|
3
|
+
import { games } from "./arcade-catalog.js";
|
|
4
|
+
import { arcadeSprite } from "./arcade-sprites.js";
|
|
5
|
+
import { petType } from "./storage.js";
|
|
6
|
+
import { SP, palette } from "./skins.js";
|
|
7
|
+
import { applyWellbeingAction } from "./wellbeing.js";
|
|
8
|
+
import { now } from "./clock.js";
|
|
9
|
+
export class Arcade {
|
|
10
|
+
constructor(app) {
|
|
11
|
+
this.app = app;
|
|
12
|
+
this.model = null;
|
|
13
|
+
this.selection = null;
|
|
14
|
+
this.settled = false;
|
|
15
|
+
this.last = 0;
|
|
16
|
+
this.held = null;
|
|
17
|
+
this.repeatAt = 0;
|
|
18
|
+
this.root = document.createElement("div");
|
|
19
|
+
this.root.className = "overlay";
|
|
20
|
+
this.root.id = "arcade-overlay";
|
|
21
|
+
this.root.tabIndex = -1;
|
|
22
|
+
this.root.setAttribute("role", "dialog");
|
|
23
|
+
this.root.setAttribute("aria-modal", "true");
|
|
24
|
+
this.root.setAttribute("aria-labelledby", "arcade-title");
|
|
25
|
+
this.root.innerHTML =
|
|
26
|
+
'<h2 class="overlay-title" id="arcade-title">Play</h2><div class="arcade-picker"></div><p class="arcade-instruction"></p><label class="arcade-options"><input type="checkbox"><span> Slower cues</span></label><canvas class="arcade-pet" aria-label="Your pet"></canvas><div class="arcade-cue" aria-live="polite"></div><canvas class="arcade-field" width="32" height="24" aria-label="Game field"></canvas><div class="arcade-meter"><span></span><i></i></div><p class="arcade-score" aria-live="off"></p><span class="arcade-announcement" style="position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)" role="status" aria-atomic="true"></span><button class="overlay-btn arcade-start">Start</button><div class="arcade-actions"></div><button class="overlay-btn arcade-pause">Pause</button><button class="overlay-close arcade-exit">Back to games</button>';
|
|
27
|
+
document.body.append(this.root);
|
|
28
|
+
this.q = (selector) => this.root.querySelector(selector);
|
|
29
|
+
this.q(".arcade-start").onclick = () => this.start();
|
|
30
|
+
this.q(".arcade-pause").onclick = () => this.togglePause();
|
|
31
|
+
this.q(".arcade-exit").onclick = () => {
|
|
32
|
+
if (this.selection) this.open();
|
|
33
|
+
else app.closeOverlay("arcade-overlay");
|
|
34
|
+
};
|
|
35
|
+
document.addEventListener("keydown", (event) => {
|
|
36
|
+
if (
|
|
37
|
+
!this.root.classList.contains("show") ||
|
|
38
|
+
event.repeat ||
|
|
39
|
+
!this.model ||
|
|
40
|
+
["INPUT", "SELECT", "TEXTAREA"].includes(event.target.tagName) ||
|
|
41
|
+
(event.target.tagName === "BUTTON" &&
|
|
42
|
+
[" ", "Enter"].includes(event.key))
|
|
43
|
+
)
|
|
44
|
+
return;
|
|
45
|
+
const action = {
|
|
46
|
+
ArrowLeft: "left",
|
|
47
|
+
ArrowRight: "right",
|
|
48
|
+
ArrowUp: this.selection === "heading" ? "jump" : "up",
|
|
49
|
+
" ":
|
|
50
|
+
games[this.selection].actions.length === 1
|
|
51
|
+
? games[this.selection].actions[0]
|
|
52
|
+
: this.selection === "flag"
|
|
53
|
+
? "both"
|
|
54
|
+
: "jump",
|
|
55
|
+
}[event.key];
|
|
56
|
+
if (action) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
if (
|
|
59
|
+
["get", "heading"].includes(this.selection) &&
|
|
60
|
+
["left", "right"].includes(action)
|
|
61
|
+
) {
|
|
62
|
+
this.held = action;
|
|
63
|
+
this.repeatAt = 0;
|
|
64
|
+
}
|
|
65
|
+
this.input(action);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
document.addEventListener("keyup", (event) => {
|
|
69
|
+
if (["ArrowLeft", "ArrowRight"].includes(event.key)) this.held = null;
|
|
70
|
+
});
|
|
71
|
+
window.addEventListener("blur", () => {
|
|
72
|
+
this.held = null;
|
|
73
|
+
this.pause();
|
|
74
|
+
});
|
|
75
|
+
const loop = (time) => {
|
|
76
|
+
if (this.model) {
|
|
77
|
+
if(this.petChanged()){this.last=time;requestAnimationFrame(loop);return;}
|
|
78
|
+
if (app.state.isDead || app.state.vacationAt > 0) {
|
|
79
|
+
this.cancel();
|
|
80
|
+
this.last = time;
|
|
81
|
+
requestAnimationFrame(loop);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (document.hidden || app.panelVisible === false) this.pause();
|
|
85
|
+
else if (!this.model.snapshot().paused) {
|
|
86
|
+
if (this.last) this.model.advance(time - this.last);
|
|
87
|
+
if (this.held && time >= this.repeatAt) {
|
|
88
|
+
this.input(this.held);
|
|
89
|
+
this.repeatAt = time + 150;
|
|
90
|
+
}
|
|
91
|
+
this.paint();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.last = time;
|
|
95
|
+
requestAnimationFrame(loop);
|
|
96
|
+
};
|
|
97
|
+
requestAnimationFrame(loop);
|
|
98
|
+
}
|
|
99
|
+
open() {
|
|
100
|
+
this.cancel();
|
|
101
|
+
this.q(".arcade-announcement").textContent = "";
|
|
102
|
+
this.announcementKey=null;
|
|
103
|
+
this.selection = null;
|
|
104
|
+
this.app.openOverlay("arcade-overlay");
|
|
105
|
+
this.q(".overlay-title").textContent = "Choose a game";
|
|
106
|
+
this.q(".arcade-picker").replaceChildren();
|
|
107
|
+
for (const [id, name] of [
|
|
108
|
+
["guess", "Left and right"],
|
|
109
|
+
...Object.entries(games).map(([id, g]) => [id, g.name]),
|
|
110
|
+
]) {
|
|
111
|
+
const b = document.createElement("button");
|
|
112
|
+
b.className = "overlay-btn";
|
|
113
|
+
b.textContent = name;
|
|
114
|
+
b.onclick = () => {
|
|
115
|
+
if (id === "guess") {
|
|
116
|
+
const reason=this.unavailable();
|
|
117
|
+
if(reason){this.q(".arcade-instruction").textContent=reason;this.q(".arcade-announcement").textContent=reason;return;}
|
|
118
|
+
this.app.closeOverlay("arcade-overlay");
|
|
119
|
+
this.app.startGame();
|
|
120
|
+
} else this.select(id);
|
|
121
|
+
};
|
|
122
|
+
this.q(".arcade-picker").append(b);
|
|
123
|
+
}
|
|
124
|
+
this.showRun(false);
|
|
125
|
+
this.q(".arcade-instruction").hidden = false;
|
|
126
|
+
this.q(".arcade-instruction").textContent = this.unavailable() || "Pick something to play.";
|
|
127
|
+
this.q(".arcade-exit").textContent = "Close";
|
|
128
|
+
this.q(".arcade-picker button")?.focus();
|
|
129
|
+
}
|
|
130
|
+
showRun(selected) {
|
|
131
|
+
for (const selector of [
|
|
132
|
+
".arcade-options",
|
|
133
|
+
".arcade-start",
|
|
134
|
+
".arcade-pet",
|
|
135
|
+
".arcade-cue",
|
|
136
|
+
".arcade-field",
|
|
137
|
+
".arcade-score",
|
|
138
|
+
".arcade-meter",
|
|
139
|
+
".arcade-actions",
|
|
140
|
+
".arcade-pause",
|
|
141
|
+
])
|
|
142
|
+
this.q(selector).hidden = !selected;
|
|
143
|
+
this.q(".arcade-picker").hidden = selected;
|
|
144
|
+
}
|
|
145
|
+
select(id) {
|
|
146
|
+
this.selection = id;
|
|
147
|
+
this.q(".arcade-announcement").textContent = "";
|
|
148
|
+
this.announcementKey=null;
|
|
149
|
+
this.q(".arcade-options span").textContent =
|
|
150
|
+
id === "sprint" ? " Steady running" : ["bump","heading"].includes(id) ? " Slower pace" : " Slower cues";
|
|
151
|
+
this.showRun(true);
|
|
152
|
+
this.q(".overlay-title").textContent = games[id].name;
|
|
153
|
+
this.q(".arcade-instruction").hidden = false;
|
|
154
|
+
this.q(".arcade-instruction").textContent = games[id].instruction + (id === "sprint" ? " You: upper lane. Opponent: lower lane." : "");
|
|
155
|
+
this.q(".arcade-start").textContent = "Start";
|
|
156
|
+
this.q(".arcade-start").disabled = !!this.unavailable();
|
|
157
|
+
this.q(".arcade-start").hidden = false;
|
|
158
|
+
this.q(".arcade-options").hidden = ![
|
|
159
|
+
"memory",
|
|
160
|
+
"bump",
|
|
161
|
+
"heading",
|
|
162
|
+
"flag",
|
|
163
|
+
"sprint",
|
|
164
|
+
"hoops",
|
|
165
|
+
].includes(id);
|
|
166
|
+
this.q(".arcade-field").hidden = true;
|
|
167
|
+
this.q(".arcade-meter").hidden = true;
|
|
168
|
+
this.q(".arcade-pause").hidden = true;
|
|
169
|
+
this.q(".arcade-cue").textContent = "";
|
|
170
|
+
this.q(".arcade-score").textContent = "";
|
|
171
|
+
this.q(".arcade-actions").replaceChildren();
|
|
172
|
+
this.q(".arcade-exit").textContent = "Back to games";
|
|
173
|
+
this.drawPet();
|
|
174
|
+
if (this.unavailable()) this.q(".arcade-instruction").textContent = this.unavailable();
|
|
175
|
+
this.q(this.unavailable()?".arcade-exit":".arcade-start").focus();
|
|
176
|
+
}
|
|
177
|
+
unavailable() {
|
|
178
|
+
const st=this.app.state;
|
|
179
|
+
if(st.isDead)return "Start a new pet before playing.";
|
|
180
|
+
if(st.vacationAt>0)return "Turn Vacation off before playing.";
|
|
181
|
+
if(st.stage==="egg")return "Your pet can play after hatching.";
|
|
182
|
+
if(st.isSleeping)return "Wake your pet before playing.";
|
|
183
|
+
if(st.isSick)return "Your pet needs medicine before playing.";
|
|
184
|
+
if(st.hunger===0)return "Feed your pet before playing.";
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
start() {
|
|
188
|
+
const st = this.app.state;
|
|
189
|
+
if ((this.model && !this.model.snapshot().result) || !this.selection) return;
|
|
190
|
+
const reason=this.unavailable();
|
|
191
|
+
if(reason){
|
|
192
|
+
this.q(".arcade-instruction").textContent=reason;
|
|
193
|
+
this.q(".arcade-announcement").textContent=reason;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.cueSoundKey=null;
|
|
197
|
+
this.announcementKey=null;
|
|
198
|
+
this.runCharacter = st.character;
|
|
199
|
+
this.opponent = ['bump','sprint'].includes(this.selection) ? chooseOpponent(petType) : null;
|
|
200
|
+
this.model = games[this.selection].create({
|
|
201
|
+
sprite: SP[st.character]?.idle1,
|
|
202
|
+
slow: this.q("input").checked,
|
|
203
|
+
hold: this.q("input").checked,
|
|
204
|
+
});
|
|
205
|
+
this.settled = false;
|
|
206
|
+
this.last = 0;
|
|
207
|
+
applyWellbeingAction(st, "play", now());
|
|
208
|
+
st.save();
|
|
209
|
+
this.q(".arcade-start").hidden = true;
|
|
210
|
+
this.q(".arcade-instruction").hidden = true;
|
|
211
|
+
this.q(".arcade-options").hidden = true;
|
|
212
|
+
this.q(".arcade-field").hidden = false;
|
|
213
|
+
this.q(".arcade-pet").hidden = true;
|
|
214
|
+
this.q(".arcade-pause").hidden = false;
|
|
215
|
+
this.q(".arcade-actions").hidden = false;
|
|
216
|
+
this.q(".arcade-actions").replaceChildren();
|
|
217
|
+
for (const action of games[this.selection].actions) {
|
|
218
|
+
const b = document.createElement("button");
|
|
219
|
+
b.className = "overlay-btn";
|
|
220
|
+
b.textContent =
|
|
221
|
+
action === "run" && this.q("input").checked
|
|
222
|
+
? "Run steadily"
|
|
223
|
+
: action[0].toUpperCase() + action.slice(1);
|
|
224
|
+
b.onclick = () => this.input(action);
|
|
225
|
+
if (
|
|
226
|
+
["get", "heading"].includes(this.selection) &&
|
|
227
|
+
["left", "right"].includes(action)
|
|
228
|
+
) {
|
|
229
|
+
b.onpointerdown = () => {
|
|
230
|
+
this.held = action;
|
|
231
|
+
this.repeatAt = 0;
|
|
232
|
+
};
|
|
233
|
+
for (const type of ["pointerup", "pointercancel", "pointerleave"])
|
|
234
|
+
b.addEventListener(type, () => (this.held = null));
|
|
235
|
+
}
|
|
236
|
+
this.q(".arcade-actions").append(b);
|
|
237
|
+
}
|
|
238
|
+
this.paint();
|
|
239
|
+
this.root.focus();
|
|
240
|
+
}
|
|
241
|
+
input(action) {
|
|
242
|
+
if(this.petChanged())return;
|
|
243
|
+
if (this.app.state.isDead || this.app.state.vacationAt > 0) {
|
|
244
|
+
this.cancel();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!this.model) return;
|
|
248
|
+
if(document.hidden || this.app.panelVisible===false){this.pause();return;}
|
|
249
|
+
const s = this.model.snapshot();
|
|
250
|
+
if(s.paused || s.result)return;
|
|
251
|
+
if (this.selection === "sprint" && s.hold && action === "run")
|
|
252
|
+
action = "hold";
|
|
253
|
+
this.model.input(action);
|
|
254
|
+
this.paint();
|
|
255
|
+
}
|
|
256
|
+
pause() {
|
|
257
|
+
this.held = null;
|
|
258
|
+
this.cueSoundKey=null;
|
|
259
|
+
if (this.model && !this.model.snapshot().result) {
|
|
260
|
+
this.model.pause();
|
|
261
|
+
this.q(".arcade-pause").textContent = "Resume";
|
|
262
|
+
this.q(".arcade-cue").textContent = "Paused";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
togglePause() {
|
|
266
|
+
if (!this.model) return;
|
|
267
|
+
if(this.app.state.isDead || this.app.state.vacationAt>0){this.cancel();return;}
|
|
268
|
+
if(document.hidden || this.app.panelVisible===false)return;
|
|
269
|
+
if (this.model.snapshot().paused) {
|
|
270
|
+
this.model.resume();
|
|
271
|
+
this.last = 0;
|
|
272
|
+
this.q(".arcade-pause").textContent = "Pause";
|
|
273
|
+
this.paint();
|
|
274
|
+
} else this.pause();
|
|
275
|
+
}
|
|
276
|
+
petChanged() {
|
|
277
|
+
if(!this.model||!this.runCharacter||this.runCharacter===this.app.state.character)return false;
|
|
278
|
+
this.open();
|
|
279
|
+
this.q(".arcade-instruction").textContent="Your pet grew! Pick a game for your new look.";
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
cancel() {
|
|
283
|
+
this.runCharacter = null;
|
|
284
|
+
this.held = null;
|
|
285
|
+
this.model?.quit();
|
|
286
|
+
this.model = null;
|
|
287
|
+
this.last = 0;
|
|
288
|
+
}
|
|
289
|
+
drawPet() {
|
|
290
|
+
const sp = SP[this.app.state.character];
|
|
291
|
+
if (sp) this.app.renderer.renderToCanvas(this.q(".arcade-pet"), sp.idle1);
|
|
292
|
+
}
|
|
293
|
+
paint() {
|
|
294
|
+
if(this.petChanged())return;
|
|
295
|
+
const s = this.model.snapshot();
|
|
296
|
+
if(s.id === "sprint" && s.hold){
|
|
297
|
+
const run=this.q(".arcade-actions").children[0];
|
|
298
|
+
if(run){run.textContent=s.holding?"Stop running":"Run steadily";run.setAttribute("aria-pressed",String(s.holding));}
|
|
299
|
+
}
|
|
300
|
+
this.drawPet();
|
|
301
|
+
const accepting=!s.paused&&!s.result&&(s.id!=="memory"||s.phase==="answer")&&(s.id!=="flag"||(s.phase==="cue"&&s.response===null))&&(s.id!=="hoops"||["ready","aim"].includes(s.phase))&&(s.id!=="bump"||s.phase==="meter")&&(s.id!=="sprint"||s.phase==="race");
|
|
302
|
+
for(const button of this.q(".arcade-actions").children)button.disabled=!accepting;
|
|
303
|
+
this.q(".arcade-pause").textContent = s.paused ? "Resume" : "Pause";
|
|
304
|
+
const score = s.result
|
|
305
|
+
? s.result === "win"
|
|
306
|
+
? "You won! +1 Happy"
|
|
307
|
+
: "Round finished. Try again."
|
|
308
|
+
: s.id === "get"
|
|
309
|
+
? `${s.score}/12 notes · ${s.lives} lives · ${Math.ceil(s.remaining / 1000)}s`
|
|
310
|
+
: s.id === "bump"
|
|
311
|
+
? `Bout ${s.bout}/3 · ${s.wins} wins · Target ${s.target.join(" to ")}`
|
|
312
|
+
: s.id === "heading"
|
|
313
|
+
? `${s.score}/10 headers · ${s.misses}/3 misses · ${Math.ceil(s.remaining / 1000)}s`
|
|
314
|
+
: s.id === "memory"
|
|
315
|
+
? `Round ${s.round}/5 · ${s.entered}/${s.length}`
|
|
316
|
+
: s.id === "flag"
|
|
317
|
+
? `Round ${s.round}/15 · ${s.score} correct`
|
|
318
|
+
: s.id === "sprint"
|
|
319
|
+
? `Race to ${s.finish} steps`
|
|
320
|
+
: `Shot ${s.shot}/5 · ${s.score} baskets`;
|
|
321
|
+
if (this.q(".arcade-score").textContent !== score)
|
|
322
|
+
this.q(".arcade-score").textContent = score;
|
|
323
|
+
const cue = arcadeCue(s);
|
|
324
|
+
if (this.q(".arcade-cue").textContent !== cue)
|
|
325
|
+
this.q(".arcade-cue").textContent = cue;
|
|
326
|
+
// Announce outcomes and care changes, not the clock on every second.
|
|
327
|
+
const announcementKey=JSON.stringify([s.result,s.score,s.lives,s.misses,s.round,s.bout,s.shot]);
|
|
328
|
+
if(announcementKey!==this.announcementKey){
|
|
329
|
+
this.announcementKey=announcementKey;
|
|
330
|
+
this.q(".arcade-announcement").textContent=s.id==="get"&&!s.result
|
|
331
|
+
? `${s.score} notes. ${s.lives} lives.`
|
|
332
|
+
: s.id==="heading"&&!s.result
|
|
333
|
+
? `${s.score} headers. ${s.misses} misses.` : score;
|
|
334
|
+
}
|
|
335
|
+
if(s.id==="memory" && s.phase==="watch" && s.cue && !s.paused){
|
|
336
|
+
const key=`${s.round}:${s.cueIndex}`;
|
|
337
|
+
if(this.cueSoundKey!==key){
|
|
338
|
+
this.cueSoundKey=key;
|
|
339
|
+
this.app.sound.beep?.({left:440,up:660,right:880}[s.cue],100);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
drawArcade(
|
|
343
|
+
this.q,
|
|
344
|
+
s,
|
|
345
|
+
arcadeSprite(petType, this.app.state.character),
|
|
346
|
+
palette,
|
|
347
|
+
{opponent:this.opponent, reducedMotion:!!this.app.motionDisabled?.(), frames:SP[this.app.state.character], foreground:this.app.renderer.fg, background:this.app.renderer.bg},
|
|
348
|
+
);
|
|
349
|
+
if(s.result)this.q(".arcade-meter").hidden = true;
|
|
350
|
+
if (
|
|
351
|
+
s.result &&
|
|
352
|
+
!this.settled &&
|
|
353
|
+
!this.app.state.isDead &&
|
|
354
|
+
!(this.app.state.vacationAt > 0)
|
|
355
|
+
) {
|
|
356
|
+
this.settled = true;
|
|
357
|
+
if (s.result === "win") {
|
|
358
|
+
this.app.state.happiness = Math.min(4, this.app.state.happiness + 1);
|
|
359
|
+
this.app.sound.gameWin();
|
|
360
|
+
} else this.app.sound.gameLose();
|
|
361
|
+
this.app.state.save();
|
|
362
|
+
this.app.updateButtons();
|
|
363
|
+
this.q(".arcade-pause").hidden = true;
|
|
364
|
+
this.held = null;
|
|
365
|
+
this.q(".arcade-actions").hidden = true;
|
|
366
|
+
this.q(".arcade-meter").hidden = true;
|
|
367
|
+
this.q(".arcade-start").textContent = "Play again";
|
|
368
|
+
this.q(".arcade-start").hidden = false;
|
|
369
|
+
this.q(".arcade-start").disabled = !!this.unavailable();
|
|
370
|
+
this.q(".arcade-start").focus();
|
|
371
|
+
for (const b of this.q(".arcade-actions").children) b.disabled = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function arcadeCue(s) {
|
|
377
|
+
if(s.paused)return "Paused";
|
|
378
|
+
if(s.result)return s.result==="win"?"Nice work!":"Try another round";
|
|
379
|
+
if(s.id==="memory"&&s.phase==="between")return "Nice! Next round…";
|
|
380
|
+
if(s.id==="memory")return s.cue?`Watch: ${{left:"← Left",up:"↑ Up",right:"→ Right"}[s.cue]}`:s.phase==="answer"?"Your turn":"Watch";
|
|
381
|
+
if(s.id==="flag"){
|
|
382
|
+
if(s.phase==="prepare")return "Get ready for the next flag";
|
|
383
|
+
if(s.phase==="rest")return s.correct?"Correct!":"Missed that one";
|
|
384
|
+
return `Round ${s.round}: ${s.cue==="fake"?"Fake! Wait":s.cue?.toUpperCase()||"Wait"}`;
|
|
385
|
+
}
|
|
386
|
+
if(s.phase==="countdown")return s.id==="sprint"?"Wait for green":"Ready…";
|
|
387
|
+
if(s.id==="bump")return s.phase==="meter"?"Stop in the target":s.boutWon?"You won the bout!":"Opponent wins this bout";
|
|
388
|
+
if(s.id==="hoops")return s.phase==="ready"?"Press Shoot when ready":s.phase==="aim"?"Stop in the target":s.arcProgress<0.72?"In the air…":s.made?"Basket!":"Missed the basket";
|
|
389
|
+
if(s.id==="get")return "Catch notes. Dodge poop.";
|
|
390
|
+
if(s.id==="heading")return "Jump to meet the ball";
|
|
391
|
+
if(s.id==="sprint")return s.hold?(s.holding?"Running steadily":"Press Run steadily to go"):"Keep running!";
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Pause every relative game clock together so a holiday cannot age a pet.
|
|
2
|
+
const clocks=['stageStartTime','lastProcessed','lastHungerDecay','lastHappyDecay','lastPoopTime','lastAgeInc','snackWindowStart','attentionCallTime','wellbeingUpdatedAt'];
|
|
3
|
+
export const MAX_CATCHUP=8*60*60*1000;
|
|
4
|
+
export function shiftClocks(state,elapsed) {
|
|
5
|
+
if(!Number.isFinite(elapsed)||elapsed===0)return;
|
|
6
|
+
for(const key of clocks)if(Number.isFinite(state[key]))state[key]=Math.max(0,state[key]+elapsed);
|
|
7
|
+
if(state.sleepOverrideUntil>0)state.sleepOverrideUntil=Math.max(0,state.sleepOverrideUntil+elapsed);
|
|
8
|
+
}
|
|
9
|
+
export function resumeVacation(state,now) {
|
|
10
|
+
if(!Number.isFinite(state.vacationAt)||state.vacationAt<=0||!Number.isFinite(now)||now<0)return false;
|
|
11
|
+
// Rebase together even after a system clock correction, retaining elapsed phases.
|
|
12
|
+
shiftClocks(state,now-state.vacationAt);
|
|
13
|
+
state.vacationAt=0;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
export function limitAbsence(state,now) {
|
|
17
|
+
const skipped=Math.max(0,now-state.lastProcessed-MAX_CATCHUP);
|
|
18
|
+
shiftClocks(state,skipped);
|
|
19
|
+
return skipped;
|
|
20
|
+
}
|