@mingxy/opencode-mascot 0.7.11 → 0.8.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/package.json +1 -1
- package/src/builtins/props/box.ts +5 -7
- package/src/builtins/props/laptop.ts +13 -1
- package/src/builtins/props/pad.ts +5 -5
- package/src/components/home-mascot.tsx +123 -103
- package/src/components/sidebar-mascot.tsx +223 -128
- package/src/core/ascii-renderer.tsx +88 -28
- package/src/core/celebration-bus.ts +11 -0
package/package.json
CHANGED
|
@@ -35,23 +35,21 @@ const frames: string[][] = [
|
|
|
35
35
|
"╰──────────╯─┐",
|
|
36
36
|
" ╲ ",
|
|
37
37
|
],
|
|
38
|
-
// 帧3: 打开(露出月儿剪影)
|
|
39
38
|
[
|
|
40
39
|
" ╭~~~~~~╮ ",
|
|
41
|
-
" ╱
|
|
40
|
+
" ╱ ╲ ",
|
|
42
41
|
" ╱~~────~~╲ ",
|
|
43
|
-
"│░
|
|
44
|
-
"
|
|
42
|
+
"│░ ☆ ^-^ ░│ ",
|
|
43
|
+
"│░ ┃█┃ ░░░│ ",
|
|
45
44
|
"│░░░░░░░░░│ ",
|
|
46
45
|
"╰──────────╯─┐",
|
|
47
46
|
" ╲ ",
|
|
48
47
|
],
|
|
49
|
-
// 帧4: 月儿冒头
|
|
50
48
|
[
|
|
51
49
|
" ╭~~~~~~╮ ",
|
|
52
|
-
" ╱
|
|
50
|
+
" ╱ ☆ ╲ ",
|
|
53
51
|
" ╱(^-^)───╲ ",
|
|
54
|
-
"
|
|
52
|
+
"│░ ┃█┃ ░░░│ ",
|
|
55
53
|
"│░░░░░░░░░│ ",
|
|
56
54
|
"│░░░░░░░░░│ ",
|
|
57
55
|
"╰──────────╯─┐",
|
|
@@ -33,6 +33,18 @@ const states = [
|
|
|
33
33
|
">ᵇᵘᵍ!ᵇᵘᵍ!",
|
|
34
34
|
">ⁿᵖᵐⁱⁿˢᵗᵃˡˡ",
|
|
35
35
|
">ᵈᵒⁿᵉ✓",
|
|
36
|
+
">ᶜᵒᵐᵖⁱˡⁱⁿᵍ",
|
|
37
|
+
">ᵗᵉˢᵗⁱⁿᵍ...",
|
|
38
|
+
">ʳᵉᶠᵃᶜᵗᵒʳ",
|
|
39
|
+
">ᵈᵉᵖˡᵒʸⁱⁿᵍ",
|
|
40
|
+
">ᵐᵉʳᵍᵉ...",
|
|
41
|
+
">ˡⁱⁿᵗ...",
|
|
42
|
+
">ᶠᵒʳᵐᵃᵗ...",
|
|
43
|
+
">ʳᵉᵛⁱᵉʷ...",
|
|
44
|
+
">ᵒᵖˢ...",
|
|
45
|
+
">ʰᵐᵐ~...",
|
|
46
|
+
">ʰᵉˡᵖ...",
|
|
47
|
+
">⁻ᵒ⁻",
|
|
36
48
|
];
|
|
37
49
|
|
|
38
50
|
const frames: string[][] = states.map((st) => [
|
|
@@ -49,5 +61,5 @@ export const laptopProp: PropPack = {
|
|
|
49
61
|
frameInterval: 800,
|
|
50
62
|
trigger: "busy",
|
|
51
63
|
position: "side-right",
|
|
52
|
-
weight: 0.
|
|
64
|
+
weight: 0.65,
|
|
53
65
|
};
|
|
@@ -15,17 +15,17 @@ const games: string[][] = [
|
|
|
15
15
|
[" ☆ ᵇᵉᵍⁱⁿ ", "(^-^) ◯→ ●", " ┃█┃ ˢ:0 ", " "],
|
|
16
16
|
[" ☆ ", "(^-^) ◯◯→ ●", " ┃█┃ ˢ:1 ", " "],
|
|
17
17
|
[" ☆ ", "(×_×) ◯◯💥 ", " ┃█┃ ᵍᵍ! ", " "],
|
|
18
|
-
[" ☆ ", "(╥_╥)
|
|
18
|
+
[" ☆ ", "(╥_╥) ", " ┃█┃ ˢ:1 ", " ᵍᵃᵐᵉᵒᵛᵉʳ "],
|
|
19
19
|
[" ☆ ", "(¬_¬) ᵃᵍᵃⁱⁿ", " ┃█┃ ", " "],
|
|
20
20
|
[" ☆ ᵍᵒ! ", "(^-^) ◯→ ●", " ┃█┃ ˢ:0 ", " "],
|
|
21
21
|
[" ☆ ᵇᵉᵍⁱⁿ ", "(^-^) ▣ ", " ┃█┃ ", " "],
|
|
22
22
|
[" ☆ ", "(^-^) ▣▣ ", " ┃█┃ ▣▣▣▣ ", " "],
|
|
23
23
|
[" ☆ ", "(×_×) ", " ┃█┃ ▣▣▣▣▣▣", " ᶠᵘˡˡ! "],
|
|
24
|
-
[" ☆ ", "(╥_╥)
|
|
24
|
+
[" ☆ ", "(╥_╥) ", " ┃█┃ ", " ᵍᵃᵐᵉᵒᵛᵉʳ "],
|
|
25
25
|
[" ☆ ᵇᵉᵍⁱⁿ ", "(^-^) [ 2 ]", " ┃█┃ ", " "],
|
|
26
26
|
[" ☆ ", "(^-^) [ 4 ]", " ┃█┃ ᵒᵒⁿ ", " "],
|
|
27
27
|
[" ☆ ", "(⊙_⊙) [128]", " ┃█┃ ʷᵒʷ ", " "],
|
|
28
|
-
[" ☆ ", "(╥_╥)
|
|
28
|
+
[" ☆ ", "(╥_╥) ", " ┃█┃ ", " ᵍᵃᵐᵉᵒᵛᵉʳ "],
|
|
29
29
|
[" ☆ ", "(^-^) ⁿᵉˣᵗ!", " ┃█┃ ", " "],
|
|
30
30
|
];
|
|
31
31
|
|
|
@@ -41,8 +41,8 @@ const frames: string[][] = games.map((rows) => [
|
|
|
41
41
|
export const padProp: PropPack = {
|
|
42
42
|
name: "pad",
|
|
43
43
|
frames,
|
|
44
|
-
frameInterval:
|
|
44
|
+
frameInterval: 1000,
|
|
45
45
|
trigger: "busy",
|
|
46
46
|
position: "front",
|
|
47
|
-
weight: 0.
|
|
47
|
+
weight: 0.35,
|
|
48
48
|
};
|
|
@@ -16,20 +16,26 @@ interface HomeMascotProps {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
let homeSingletonRenderers: Record<string, ReturnType<typeof createAnimatedRenderer>> | null = null;
|
|
20
|
+
let homeStartupTriggered = false;
|
|
21
|
+
const [homeCurX, setHomeCurX] = createSignal(0);
|
|
22
|
+
const [homeCurY, setHomeCurY] = createSignal(7);
|
|
23
|
+
|
|
19
24
|
export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
20
25
|
const names = Object.keys(props.mascots);
|
|
21
26
|
const initialName = props.mascots["yueer"] ? "yueer" : names[0];
|
|
22
27
|
|
|
23
28
|
const cw = (typeof process !== "undefined" && process.stdout?.columns) || 80;
|
|
24
29
|
|
|
25
|
-
const initX = Math.floor((Math.random() - 0.5) * Math.max(0, cw - 20));
|
|
26
|
-
const initY = 0;
|
|
27
|
-
|
|
28
30
|
const [currentName, setCurrentName] = createSignal(initialName);
|
|
29
31
|
const [zBoost, setZBoost] = createSignal(false);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const curX = homeCurX;
|
|
33
|
+
const setCurX = setHomeCurX;
|
|
34
|
+
const curY = homeCurY;
|
|
35
|
+
const setCurY = setHomeCurY;
|
|
36
|
+
if (!homeStartupTriggered) {
|
|
37
|
+
setCurX(Math.floor(cw / 2) - 5);
|
|
38
|
+
}
|
|
33
39
|
let dragStartX = 0;
|
|
34
40
|
let dragStartY = 0;
|
|
35
41
|
let dragAnchorX = 0;
|
|
@@ -37,10 +43,13 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
37
43
|
let lastClickTime = 0;
|
|
38
44
|
let isDragging = false;
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
if (!homeSingletonRenderers) {
|
|
47
|
+
homeSingletonRenderers = {};
|
|
48
|
+
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
49
|
+
homeSingletonRenderers[name] = createAnimatedRenderer(pack);
|
|
50
|
+
}
|
|
43
51
|
}
|
|
52
|
+
const renderers = homeSingletonRenderers;
|
|
44
53
|
|
|
45
54
|
const switchToNext = () => {
|
|
46
55
|
const cur = currentName();
|
|
@@ -48,13 +57,6 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
48
57
|
setCurrentName(names[(idx + 1) % names.length]);
|
|
49
58
|
};
|
|
50
59
|
|
|
51
|
-
const applyPos = () => {
|
|
52
|
-
if (boxRef) {
|
|
53
|
-
boxRef.translateX = curX;
|
|
54
|
-
boxRef.translateY = curY;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
60
|
onCelebrate((newVersion) => {
|
|
59
61
|
setZBoost(true);
|
|
60
62
|
renderers[currentName()].celebrateUpdate(newVersion);
|
|
@@ -70,106 +72,124 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
70
72
|
onScatter(() => {
|
|
71
73
|
});
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
75
|
+
if (!homeStartupTriggered) {
|
|
76
|
+
homeStartupTriggered = true;
|
|
77
|
+
renderers[currentName()].setCharacterHidden(true);
|
|
78
|
+
renderers[currentName()].setProp(getProp("box") ?? null);
|
|
79
|
+
|
|
80
|
+
const finalY = curY();
|
|
81
|
+
const finalX = curX();
|
|
82
|
+
const fallStartY = finalY - 15;
|
|
83
|
+
const fallDuration = 500;
|
|
84
|
+
const fallStartTime = Date.now();
|
|
85
|
+
setCurY(fallStartY);
|
|
86
|
+
|
|
87
|
+
const fallInterval = setInterval(() => {
|
|
88
|
+
const elapsed = Date.now() - fallStartTime;
|
|
89
|
+
const t = Math.min(elapsed / fallDuration, 1);
|
|
90
|
+
const eased = t * t;
|
|
91
|
+
setCurY(Math.round(fallStartY + (finalY - fallStartY) * eased));
|
|
92
|
+
if (t >= 1) {
|
|
93
|
+
clearInterval(fallInterval);
|
|
94
|
+
setCurY(finalY);
|
|
95
|
+
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
const shakeSeq = [1, -1, 1, -1, 0];
|
|
98
|
+
let shakeIdx = 0;
|
|
99
|
+
const shakeInterval = setInterval(() => {
|
|
100
|
+
if (shakeIdx >= shakeSeq.length) {
|
|
101
|
+
clearInterval(shakeInterval);
|
|
102
|
+
setCurX(finalX);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setCurX(finalX + shakeSeq[shakeIdx]);
|
|
106
|
+
shakeIdx++;
|
|
107
|
+
}, 60);
|
|
108
|
+
}, 2000);
|
|
109
|
+
}
|
|
110
|
+
}, 16);
|
|
111
|
+
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
renderers[currentName()].setProp(null);
|
|
114
|
+
renderers[currentName()].setCharacterHidden(false);
|
|
115
|
+
const dropStart = curY();
|
|
116
|
+
const dropEnd = 10;
|
|
117
|
+
const dropDuration = 300;
|
|
118
|
+
const dropStartTime = Date.now();
|
|
119
|
+
const dropInterval = setInterval(() => {
|
|
120
|
+
const elapsed = Date.now() - dropStartTime;
|
|
121
|
+
const t = Math.min(elapsed / dropDuration, 1);
|
|
122
|
+
const eased = t * t;
|
|
123
|
+
setCurY(Math.round(dropStart + (dropEnd - dropStart) * eased));
|
|
124
|
+
if (t >= 1) {
|
|
125
|
+
clearInterval(dropInterval);
|
|
126
|
+
setCurY(dropEnd);
|
|
127
|
+
}
|
|
128
|
+
}, 16);
|
|
129
|
+
}, 6000);
|
|
130
|
+
}
|
|
117
131
|
|
|
118
132
|
const stopDrag = () => {
|
|
119
133
|
isDragging = false;
|
|
120
134
|
renderers[currentName()].setDragging(false);
|
|
121
135
|
};
|
|
122
136
|
|
|
137
|
+
const handleMouseDown = (e: any) => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (now - lastClickTime < 300) {
|
|
140
|
+
switchToNext();
|
|
141
|
+
lastClickTime = 0;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
lastClickTime = now;
|
|
145
|
+
if (e.modifiers?.alt) {
|
|
146
|
+
dragStartX = e.x;
|
|
147
|
+
dragStartY = e.y;
|
|
148
|
+
dragAnchorX = curX();
|
|
149
|
+
dragAnchorY = curY();
|
|
150
|
+
isDragging = true;
|
|
151
|
+
renderers[currentName()].setDragging(true);
|
|
152
|
+
props.api.renderer.clearSelection();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const handleMouseDrag = (e: any) => {
|
|
156
|
+
if (e.modifiers?.alt && isDragging) {
|
|
157
|
+
setCurX(dragAnchorX + (e.x - dragStartX));
|
|
158
|
+
setCurY(dragAnchorY + (e.y - dragStartY));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const handleMouseUp = () => { stopDrag(); };
|
|
162
|
+
|
|
123
163
|
return (
|
|
124
|
-
|
|
125
|
-
zIndex={zBoost() ? 9999 : 100}
|
|
126
|
-
flexDirection="column"
|
|
127
|
-
ref={(node: any) => {
|
|
128
|
-
boxRef = node;
|
|
129
|
-
applyPos();
|
|
130
|
-
}}
|
|
131
|
-
onMouseDown={(e: any) => {
|
|
132
|
-
const now = Date.now();
|
|
133
|
-
if (now - lastClickTime < 300) {
|
|
134
|
-
switchToNext();
|
|
135
|
-
lastClickTime = 0;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
lastClickTime = now;
|
|
139
|
-
|
|
140
|
-
if (e.modifiers?.alt) {
|
|
141
|
-
dragStartX = e.x;
|
|
142
|
-
dragStartY = e.y;
|
|
143
|
-
dragAnchorX = curX;
|
|
144
|
-
dragAnchorY = curY;
|
|
145
|
-
isDragging = true;
|
|
146
|
-
renderers[currentName()].setDragging(true);
|
|
147
|
-
props.api.renderer.clearSelection();
|
|
148
|
-
}
|
|
149
|
-
}}
|
|
150
|
-
onMouseDrag={(e: any) => {
|
|
151
|
-
if (e.modifiers?.alt && isDragging) {
|
|
152
|
-
curX = dragAnchorX + (e.x - dragStartX);
|
|
153
|
-
curY = dragAnchorY + (e.y - dragStartY);
|
|
154
|
-
applyPos();
|
|
155
|
-
}
|
|
156
|
-
}}
|
|
157
|
-
onMouseUp={() => { stopDrag(); }}
|
|
158
|
-
onMouseDragEnd={() => { stopDrag(); }}
|
|
159
|
-
>
|
|
164
|
+
<>
|
|
160
165
|
{renderers[currentName()]?.propElement() ? (
|
|
161
166
|
<box
|
|
162
167
|
position="absolute"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
left={renderers[currentName()].getPropPosition() === "front" ? curX() - 4 : curX()}
|
|
169
|
+
top={renderers[currentName()].getPropPosition() === "front" ? curY() - 5 : curY()}
|
|
170
|
+
zIndex={zBoost() ? 9998 : 50}
|
|
171
|
+
onMouseDown={handleMouseDown}
|
|
172
|
+
onMouseDrag={handleMouseDrag}
|
|
173
|
+
onMouseUp={handleMouseUp}
|
|
174
|
+
onMouseDragEnd={handleMouseUp}
|
|
166
175
|
>
|
|
167
176
|
{renderers[currentName()].propElement()}
|
|
168
177
|
</box>
|
|
169
178
|
) : null}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
{renderers[currentName()]?.element() ? (
|
|
180
|
+
<box
|
|
181
|
+
position="absolute"
|
|
182
|
+
left={curX()}
|
|
183
|
+
top={curY()}
|
|
184
|
+
zIndex={zBoost() ? 9999 : 100}
|
|
185
|
+
onMouseDown={handleMouseDown}
|
|
186
|
+
onMouseDrag={handleMouseDrag}
|
|
187
|
+
onMouseUp={handleMouseUp}
|
|
188
|
+
onMouseDragEnd={handleMouseUp}
|
|
189
|
+
>
|
|
190
|
+
{renderers[currentName()].element()}
|
|
191
|
+
</box>
|
|
192
|
+
) : null}
|
|
193
|
+
</>
|
|
174
194
|
);
|
|
175
195
|
}
|
|
@@ -4,7 +4,7 @@ import { createSignal, onCleanup } from "solid-js";
|
|
|
4
4
|
import type { JSX } from "@opentui/solid";
|
|
5
5
|
import type { MascotPack, MascotState } from "../core/types";
|
|
6
6
|
import { createAnimatedRenderer } from "../core/ascii-renderer";
|
|
7
|
-
import { onCelebrate, onVersion, onScatter } from "../core/celebration-bus";
|
|
7
|
+
import { onCelebrate, onVersion, onScatter, onPropShow } from "../core/celebration-bus";
|
|
8
8
|
import { pickPropByTrigger } from "../core/prop-loader";
|
|
9
9
|
import { log } from "../core/logger";
|
|
10
10
|
|
|
@@ -34,7 +34,80 @@ const PEEK = 2;
|
|
|
34
34
|
const PEEK_INTERVAL = 1200;
|
|
35
35
|
const EDGE_THRESHOLD = 3;
|
|
36
36
|
|
|
37
|
+
let singletonRenderers: Record<string, ReturnType<typeof createAnimatedRenderer>> | null = null;
|
|
38
|
+
let singletonListener = false;
|
|
39
|
+
const [globalCurrentName, setGlobalCurrentName] = createSignal<string>("yueer");
|
|
40
|
+
const [globalUserOverride, setGlobalUserOverride] = createSignal(false);
|
|
41
|
+
const [globalPosX, setGlobalPosX] = createSignal(20);
|
|
42
|
+
const [globalPosY, setGlobalPosY] = createSignal(2);
|
|
43
|
+
const [globalPacingX, setGlobalPacingX] = createSignal(0);
|
|
44
|
+
const [globalZBoost, setGlobalZBoost] = createSignal(false);
|
|
45
|
+
let globalScattered = false;
|
|
46
|
+
let globalLastUserY: number | null = null;
|
|
47
|
+
let globalLastUserX: number | null = null;
|
|
48
|
+
let globalFallTimer: ReturnType<typeof setInterval> | null = null;
|
|
49
|
+
|
|
50
|
+
const fallToWorkY = () => {
|
|
51
|
+
const targetY = globalLastUserY ?? 25;
|
|
52
|
+
const targetX = globalLastUserX ?? 5;
|
|
53
|
+
const startY = globalPosY();
|
|
54
|
+
const startX = globalPosX();
|
|
55
|
+
const needMove = Math.abs(startY - targetY) >= 2 || Math.abs(startX - targetX) >= 2;
|
|
56
|
+
if (!needMove) return;
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
const duration = 500;
|
|
59
|
+
if (globalFallTimer) clearInterval(globalFallTimer);
|
|
60
|
+
globalFallTimer = setInterval(() => {
|
|
61
|
+
const elapsed = Date.now() - startTime;
|
|
62
|
+
const t = Math.min(elapsed / duration, 1);
|
|
63
|
+
const eased = t * t;
|
|
64
|
+
setGlobalPosY(Math.round(startY + (targetY - startY) * eased));
|
|
65
|
+
setGlobalPosX(Math.round(startX + (targetX - startX) * eased));
|
|
66
|
+
if (t >= 1) {
|
|
67
|
+
if (globalFallTimer) { clearInterval(globalFallTimer); globalFallTimer = null; }
|
|
68
|
+
}
|
|
69
|
+
}, 16);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let busyPacingTimer: ReturnType<typeof setInterval> | null = null;
|
|
73
|
+
|
|
74
|
+
const startBusyPacing = () => {
|
|
75
|
+
if (busyPacingTimer) clearInterval(busyPacingTimer);
|
|
76
|
+
let step = 0;
|
|
77
|
+
let direction = 1;
|
|
78
|
+
let phaseTimer = 0;
|
|
79
|
+
let walking = true;
|
|
80
|
+
busyPacingTimer = setInterval(() => {
|
|
81
|
+
const prop = singletonRenderers?.[globalCurrentName()]?.getProp();
|
|
82
|
+
if (prop && prop.position === "front") return;
|
|
83
|
+
phaseTimer += 100;
|
|
84
|
+
if (walking) {
|
|
85
|
+
if (phaseTimer >= 3000) {
|
|
86
|
+
walking = false;
|
|
87
|
+
phaseTimer = 0;
|
|
88
|
+
setGlobalPacingX(0);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
step += direction;
|
|
92
|
+
if (Math.abs(step) >= 3) direction *= -1;
|
|
93
|
+
setGlobalPacingX(step);
|
|
94
|
+
} else {
|
|
95
|
+
if (phaseTimer >= 2000) {
|
|
96
|
+
walking = true;
|
|
97
|
+
phaseTimer = 0;
|
|
98
|
+
step = 0;
|
|
99
|
+
direction = Math.random() < 0.5 ? -1 : 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, 100);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const stopBusyPacing = () => {
|
|
106
|
+
if (busyPacingTimer) { clearInterval(busyPacingTimer); busyPacingTimer = null; }
|
|
107
|
+
};
|
|
108
|
+
|
|
37
109
|
export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
110
|
+
log("DEBUG", "SidebarMascot mount");
|
|
38
111
|
const names = Object.keys(props.mascots);
|
|
39
112
|
const initialName =
|
|
40
113
|
props.initialMascot && props.mascots[props.initialMascot]
|
|
@@ -43,12 +116,24 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
43
116
|
? "yueer"
|
|
44
117
|
: names[0];
|
|
45
118
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
119
|
+
if (!singletonRenderers) {
|
|
120
|
+
singletonRenderers = {};
|
|
121
|
+
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
122
|
+
singletonRenderers[name] = createAnimatedRenderer(pack);
|
|
123
|
+
}
|
|
124
|
+
setGlobalCurrentName(initialName);
|
|
125
|
+
}
|
|
126
|
+
const renderers = singletonRenderers;
|
|
127
|
+
|
|
128
|
+
const currentName = globalCurrentName;
|
|
129
|
+
const setCurrentName = setGlobalCurrentName;
|
|
130
|
+
const setUserOverride = setGlobalUserOverride;
|
|
131
|
+
const posX = globalPosX;
|
|
132
|
+
const setPosX = setGlobalPosX;
|
|
133
|
+
const posY = globalPosY;
|
|
134
|
+
const setPosY = setGlobalPosY;
|
|
135
|
+
|
|
50
136
|
const [containerWidth, setContainerWidth] = createSignal(0);
|
|
51
|
-
const [zBoost, setZBoost] = createSignal(false);
|
|
52
137
|
let dragStartX = 0;
|
|
53
138
|
let dragStartY = 0;
|
|
54
139
|
let dragAnchorX = 0;
|
|
@@ -59,11 +144,6 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
59
144
|
let peekTimer: ReturnType<typeof setInterval> | null = null;
|
|
60
145
|
let returnTimer: ReturnType<typeof setInterval> | null = null;
|
|
61
146
|
|
|
62
|
-
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
63
|
-
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
64
|
-
renderers[name] = createAnimatedRenderer(pack);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
147
|
const stopPeek = () => {
|
|
68
148
|
if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
|
|
69
149
|
};
|
|
@@ -143,81 +223,90 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
143
223
|
}, 16);
|
|
144
224
|
};
|
|
145
225
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
renderers[cur].setState(s);
|
|
149
|
-
if (userOverride()) return;
|
|
150
|
-
const target = DEFAULT_STATE_MAP[s];
|
|
151
|
-
if (target && target !== cur && props.mascots[target]) {
|
|
152
|
-
setCurrentName(target);
|
|
153
|
-
renderers[target].setState(s);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
226
|
+
if (!singletonListener) {
|
|
227
|
+
singletonListener = true;
|
|
156
228
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const name = target.name;
|
|
183
|
-
if (props.mascots[name] && name !== currentName()) {
|
|
184
|
-
setCurrentName(name);
|
|
185
|
-
setUserOverride(true);
|
|
229
|
+
props.api.event.on("session.status", (data: unknown) => {
|
|
230
|
+
const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
|
|
231
|
+
const statusType = payload?.properties?.status?.type;
|
|
232
|
+
log("DEBUG", `session.status: statusType=${statusType}`);
|
|
233
|
+
if (statusType === "busy" || statusType === "retry") {
|
|
234
|
+
renderers[globalCurrentName()].setState("busy");
|
|
235
|
+
if (!renderers[globalCurrentName()].getProp()) {
|
|
236
|
+
const busyProp = pickPropByTrigger("busy");
|
|
237
|
+
renderers[globalCurrentName()].setProp(busyProp);
|
|
238
|
+
renderers[globalCurrentName()].setCharacterHidden(busyProp?.position === "front");
|
|
239
|
+
fallToWorkY();
|
|
240
|
+
}
|
|
241
|
+
startBusyPacing();
|
|
242
|
+
} else {
|
|
243
|
+
renderers[globalCurrentName()].setState("idle");
|
|
244
|
+
stopBusyPacing();
|
|
245
|
+
if (!globalUserOverride()) {
|
|
246
|
+
const target = DEFAULT_STATE_MAP["idle" as MascotState];
|
|
247
|
+
if (target && target !== globalCurrentName() && singletonRenderers && singletonRenderers[target]) {
|
|
248
|
+
setGlobalCurrentName(target);
|
|
249
|
+
singletonRenderers[target].setState("idle");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
renderers[globalCurrentName()].setProp(null);
|
|
253
|
+
renderers[globalCurrentName()].setCharacterHidden(false);
|
|
186
254
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
props.api.event.on("session.idle", () => {
|
|
258
|
+
renderers[globalCurrentName()].setState("happy");
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
renderers[globalCurrentName()].setState("idle");
|
|
261
|
+
}, 3000);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
props.api.event.on("mascot.switch", (data: unknown) => {
|
|
265
|
+
const target = data as { name?: string } | null;
|
|
266
|
+
if (target?.name) {
|
|
267
|
+
const name = target.name;
|
|
268
|
+
if (singletonRenderers && singletonRenderers[name] && name !== globalCurrentName()) {
|
|
269
|
+
setGlobalCurrentName(name);
|
|
270
|
+
setGlobalUserOverride(true);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
const cur = globalCurrentName();
|
|
274
|
+
const allNames = Object.keys(singletonRenderers ?? {});
|
|
275
|
+
const idx = allNames.indexOf(cur);
|
|
276
|
+
setGlobalCurrentName(allNames[(idx + 1) % allNames.length]);
|
|
277
|
+
setGlobalUserOverride(true);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
props.api.event.on("mascot.toggleWalk", () => {
|
|
282
|
+
renderers[globalCurrentName()].toggleWalk();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
onCelebrate((newVersion) => {
|
|
286
|
+
setGlobalZBoost(true);
|
|
287
|
+
renderers[globalCurrentName()].celebrateUpdate(newVersion);
|
|
288
|
+
setTimeout(() => setGlobalZBoost(false), 2000);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
onVersion((version) => {
|
|
292
|
+
setGlobalZBoost(true);
|
|
293
|
+
renderers[globalCurrentName()].showVersion(version);
|
|
294
|
+
setTimeout(() => setGlobalZBoost(false), 3500);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
onScatter(() => {
|
|
298
|
+
if (globalScattered) return;
|
|
299
|
+
globalScattered = true;
|
|
300
|
+
renderers[globalCurrentName()].scatterIn();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
onPropShow(() => {
|
|
304
|
+
fallToWorkY();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
globalScattered = true;
|
|
308
|
+
renderers[globalCurrentName()].scatterIn();
|
|
309
|
+
}
|
|
221
310
|
|
|
222
311
|
const propOffset = () => {
|
|
223
312
|
const pos = renderers[currentName()]?.getPropPosition();
|
|
@@ -226,6 +315,39 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
226
315
|
return 0;
|
|
227
316
|
};
|
|
228
317
|
|
|
318
|
+
const handleMouseDown = (e: any) => {
|
|
319
|
+
if (hideSide) { returnToView(); return; }
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
if (now - lastClickTime < 300) {
|
|
322
|
+
switchToNext();
|
|
323
|
+
lastClickTime = 0;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
lastClickTime = now;
|
|
327
|
+
if (e.modifiers?.alt) {
|
|
328
|
+
dragStartX = e.x;
|
|
329
|
+
dragStartY = e.y;
|
|
330
|
+
dragAnchorX = posX();
|
|
331
|
+
dragAnchorY = posY();
|
|
332
|
+
isDragging = true;
|
|
333
|
+
renderers[currentName()].setDragging(true);
|
|
334
|
+
props.api.renderer.clearSelection();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const handleMouseDrag = (e: any) => {
|
|
338
|
+
if (e.modifiers?.alt && isDragging) {
|
|
339
|
+
setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
|
|
340
|
+
setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
|
|
341
|
+
globalLastUserY = posY();
|
|
342
|
+
globalLastUserX = posX();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const handleMouseUp = () => {
|
|
346
|
+
isDragging = false;
|
|
347
|
+
renderers[currentName()].setDragging(false);
|
|
348
|
+
checkEdge();
|
|
349
|
+
};
|
|
350
|
+
|
|
229
351
|
return (
|
|
230
352
|
<>
|
|
231
353
|
{renderers[currentName()]?.propElement() ? (
|
|
@@ -233,17 +355,22 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
233
355
|
position="absolute"
|
|
234
356
|
left={posX() + propOffset()}
|
|
235
357
|
top={posY()}
|
|
236
|
-
zIndex={
|
|
358
|
+
zIndex={globalZBoost() ? 9998 : 50}
|
|
359
|
+
onMouseDown={handleMouseDown}
|
|
360
|
+
onMouseDrag={handleMouseDrag}
|
|
361
|
+
onMouseUp={handleMouseUp}
|
|
362
|
+
onMouseDragEnd={handleMouseUp}
|
|
237
363
|
>
|
|
238
364
|
{renderers[currentName()].propElement()}
|
|
239
365
|
</box>
|
|
240
366
|
) : null}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
367
|
+
{renderers[currentName()].getPropPosition() !== "front" ? (
|
|
368
|
+
<box
|
|
369
|
+
position="absolute"
|
|
370
|
+
left={posX() + globalPacingX()}
|
|
371
|
+
top={posY()}
|
|
372
|
+
alignItems="center"
|
|
373
|
+
zIndex={globalZBoost() ? 9999 : 100}
|
|
247
374
|
flexDirection="column"
|
|
248
375
|
ref={(node: any) => {
|
|
249
376
|
if (node) {
|
|
@@ -255,46 +382,14 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
255
382
|
}
|
|
256
383
|
}
|
|
257
384
|
}}
|
|
258
|
-
onMouseDown={
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (now - lastClickTime < 300) {
|
|
263
|
-
switchToNext();
|
|
264
|
-
lastClickTime = 0;
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
lastClickTime = now;
|
|
268
|
-
|
|
269
|
-
if (e.modifiers?.alt) {
|
|
270
|
-
dragStartX = e.x;
|
|
271
|
-
dragStartY = e.y;
|
|
272
|
-
dragAnchorX = posX();
|
|
273
|
-
dragAnchorY = posY();
|
|
274
|
-
isDragging = true;
|
|
275
|
-
renderers[currentName()].setDragging(true);
|
|
276
|
-
props.api.renderer.clearSelection();
|
|
277
|
-
}
|
|
278
|
-
}}
|
|
279
|
-
onMouseDrag={(e: any) => {
|
|
280
|
-
if (e.modifiers?.alt && isDragging) {
|
|
281
|
-
setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
|
|
282
|
-
setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
|
|
283
|
-
}
|
|
284
|
-
}}
|
|
285
|
-
onMouseUp={() => {
|
|
286
|
-
isDragging = false;
|
|
287
|
-
renderers[currentName()].setDragging(false);
|
|
288
|
-
checkEdge();
|
|
289
|
-
}}
|
|
290
|
-
onMouseDragEnd={() => {
|
|
291
|
-
isDragging = false;
|
|
292
|
-
renderers[currentName()].setDragging(false);
|
|
293
|
-
checkEdge();
|
|
294
|
-
}}
|
|
385
|
+
onMouseDown={handleMouseDown}
|
|
386
|
+
onMouseDrag={handleMouseDrag}
|
|
387
|
+
onMouseUp={handleMouseUp}
|
|
388
|
+
onMouseDragEnd={handleMouseUp}
|
|
295
389
|
>
|
|
296
390
|
{renderers[currentName()]?.element() ?? null}
|
|
297
391
|
</box>
|
|
392
|
+
) : null}
|
|
298
393
|
</>
|
|
299
394
|
);
|
|
300
395
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { createSignal, onCleanup } from "solid-js";
|
|
4
4
|
import type { JSX } from "@opentui/solid";
|
|
5
5
|
import type { MascotPack, MascotState, EffectTimerCtx, EffectRenderCtx, PropPack, PropPosition } from "./types";
|
|
6
|
+
import { emitPropShow } from "./celebration-bus";
|
|
7
|
+
import { log } from "./logger";
|
|
6
8
|
|
|
7
9
|
const SUPERSCRIPT: Record<string, string> = {
|
|
8
10
|
"0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴",
|
|
@@ -141,10 +143,17 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
141
143
|
};
|
|
142
144
|
|
|
143
145
|
let idleSleepTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
146
|
+
let idlePadTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
147
|
+
let idleBoxTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
148
|
+
let lastBoxEvent = 0;
|
|
144
149
|
|
|
145
150
|
const resetIdleSleep = () => {
|
|
146
151
|
if (idleSleepTimeout) clearTimeout(idleSleepTimeout);
|
|
152
|
+
if (idlePadTimeout) clearTimeout(idlePadTimeout);
|
|
153
|
+
if (idleBoxTimeout) clearTimeout(idleBoxTimeout);
|
|
147
154
|
idleSleepTimeout = null;
|
|
155
|
+
idlePadTimeout = null;
|
|
156
|
+
idleBoxTimeout = null;
|
|
148
157
|
if (currentState() !== "idle") return;
|
|
149
158
|
idleSleepTimeout = setTimeout(() => {
|
|
150
159
|
if (currentState() === "idle") {
|
|
@@ -152,6 +161,49 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
152
161
|
stopWalk();
|
|
153
162
|
}
|
|
154
163
|
}, anim.idleTimeout);
|
|
164
|
+
|
|
165
|
+
idlePadTimeout = setTimeout(async () => {
|
|
166
|
+
const { log } = await import("./logger");
|
|
167
|
+
log("DEBUG", `idlePadTimeout fired, state=${currentState()}, prop=${activeProp()?.name ?? "null"}`);
|
|
168
|
+
if (currentState() !== "idle") return;
|
|
169
|
+
if (activeProp()) return;
|
|
170
|
+
if (Math.random() >= 0.4) {
|
|
171
|
+
log("DEBUG", "idle Pad skipped (70% miss)");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { getProp } = await import("./prop-loader");
|
|
175
|
+
const pad = getProp("pad");
|
|
176
|
+
if (!pad) return;
|
|
177
|
+
log("DEBUG", "idle Pad triggered!");
|
|
178
|
+
setProp(pad);
|
|
179
|
+
setCharacterHiddenSignal(true);
|
|
180
|
+
emitPropShow();
|
|
181
|
+
const duration = 10000 + Math.random() * 10000;
|
|
182
|
+
idlePadTimeout = setTimeout(() => {
|
|
183
|
+
setProp(null);
|
|
184
|
+
setCharacterHiddenSignal(false);
|
|
185
|
+
}, duration);
|
|
186
|
+
}, 30000);
|
|
187
|
+
|
|
188
|
+
idleBoxTimeout = setTimeout(async () => {
|
|
189
|
+
if (currentState() !== "idle") return;
|
|
190
|
+
if (activeProp()) return;
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
if (now - lastBoxEvent < 60000) return;
|
|
193
|
+
if (Math.random() >= 0.05) return;
|
|
194
|
+
lastBoxEvent = now;
|
|
195
|
+
const { getProp } = await import("./prop-loader");
|
|
196
|
+
const box = getProp("box");
|
|
197
|
+
if (!box) return;
|
|
198
|
+
setProp(box);
|
|
199
|
+
setCharacterHiddenSignal(true);
|
|
200
|
+
emitPropShow();
|
|
201
|
+
idleBoxTimeout = setTimeout(() => {
|
|
202
|
+
setProp(null);
|
|
203
|
+
setCharacterHiddenSignal(false);
|
|
204
|
+
bounce();
|
|
205
|
+
}, 3000);
|
|
206
|
+
}, 60000);
|
|
155
207
|
};
|
|
156
208
|
|
|
157
209
|
resetIdleSleep();
|
|
@@ -181,7 +233,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
181
233
|
// 1. Blink
|
|
182
234
|
const hasBlink = (pack.frames as Record<string, string[] | undefined>)["blink"] !== undefined;
|
|
183
235
|
|
|
184
|
-
|
|
236
|
+
setInterval(() => {
|
|
185
237
|
if (currentState() !== "sleeping" && Math.random() < anim.blinkChance && hasBlink) {
|
|
186
238
|
setFrameOverride("blink");
|
|
187
239
|
setTimeout(() => setFrameOverride(null), 150);
|
|
@@ -193,7 +245,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
193
245
|
(k) => k !== "default" && k !== "blink",
|
|
194
246
|
);
|
|
195
247
|
|
|
196
|
-
|
|
248
|
+
setInterval(() => {
|
|
197
249
|
if (currentState() === "idle" && !frameOverride()) {
|
|
198
250
|
const pick = availableExpressions[Math.floor(Math.random() * availableExpressions.length)];
|
|
199
251
|
if (pick) {
|
|
@@ -204,7 +256,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
204
256
|
}, anim.expressionInterval);
|
|
205
257
|
|
|
206
258
|
// 3. Breathing
|
|
207
|
-
|
|
259
|
+
setInterval(() => {
|
|
208
260
|
if (currentState() === "idle") {
|
|
209
261
|
setBreathPhase((v) => !v);
|
|
210
262
|
}
|
|
@@ -243,30 +295,28 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
243
295
|
setWalkOffset(0);
|
|
244
296
|
};
|
|
245
297
|
|
|
246
|
-
let walkTimeout: ReturnType<typeof setTimeout>;
|
|
247
|
-
|
|
248
298
|
function scheduleNextWalk() {
|
|
249
299
|
if (!walkEnabled()) return setTimeout(() => {}, 60000);
|
|
250
300
|
const delay = anim.walkMinDelay + Math.floor(Math.random() * (anim.walkMaxDelay - anim.walkMinDelay));
|
|
251
301
|
return setTimeout(() => {
|
|
252
302
|
if (currentState() === "idle" && !frameOverride() && walkStep === -1 && walkEnabled()) {
|
|
253
|
-
|
|
303
|
+
const boom = Math.random() < 0.1;
|
|
304
|
+
log("DEBUG", `walk callback: boom=${boom}`);
|
|
305
|
+
if (boom) {
|
|
254
306
|
explode();
|
|
255
307
|
} else {
|
|
256
308
|
startWalk();
|
|
257
309
|
}
|
|
258
310
|
}
|
|
259
311
|
if (currentState() !== "sleeping") {
|
|
260
|
-
|
|
312
|
+
scheduleNextWalk();
|
|
261
313
|
}
|
|
262
314
|
}, delay);
|
|
263
315
|
}
|
|
264
316
|
|
|
265
|
-
|
|
317
|
+
scheduleNextWalk();
|
|
266
318
|
|
|
267
319
|
// 5. Jump
|
|
268
|
-
let jumpTimeout: ReturnType<typeof setTimeout>;
|
|
269
|
-
|
|
270
320
|
function scheduleNextJump() {
|
|
271
321
|
const delay = anim.jumpMinDelay + Math.floor(Math.random() * (anim.jumpMaxDelay - anim.jumpMinDelay));
|
|
272
322
|
return setTimeout(() => {
|
|
@@ -281,12 +331,12 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
281
331
|
}, 2000);
|
|
282
332
|
}
|
|
283
333
|
if (currentState() !== "sleeping") {
|
|
284
|
-
|
|
334
|
+
scheduleNextJump();
|
|
285
335
|
}
|
|
286
336
|
}, delay);
|
|
287
337
|
}
|
|
288
338
|
|
|
289
|
-
|
|
339
|
+
scheduleNextJump();
|
|
290
340
|
|
|
291
341
|
// ─── Pack-defined effect timers ───
|
|
292
342
|
const effectTimers: ReturnType<typeof setInterval>[] = [];
|
|
@@ -296,16 +346,10 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
296
346
|
}
|
|
297
347
|
}
|
|
298
348
|
|
|
349
|
+
resetIdleSleep();
|
|
350
|
+
|
|
299
351
|
// ─── Cleanup ───
|
|
300
352
|
onCleanup(() => {
|
|
301
|
-
clearInterval(blinkTimer);
|
|
302
|
-
clearInterval(expressionTimer);
|
|
303
|
-
clearInterval(breathTimer);
|
|
304
|
-
clearTimeout(walkTimeout);
|
|
305
|
-
clearTimeout(jumpTimeout);
|
|
306
|
-
if (idleSleepTimeout) clearTimeout(idleSleepTimeout);
|
|
307
|
-
if (walkInterval) clearInterval(walkInterval);
|
|
308
|
-
for (const t of effectTimers) clearInterval(t);
|
|
309
353
|
stopFlash();
|
|
310
354
|
stopDragMsg();
|
|
311
355
|
stopBounce();
|
|
@@ -315,7 +359,6 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
315
359
|
stopFall();
|
|
316
360
|
stopBomb();
|
|
317
361
|
if (zzzTimer) { clearInterval(zzzTimer); zzzTimer = null; }
|
|
318
|
-
stopPropTimer();
|
|
319
362
|
});
|
|
320
363
|
|
|
321
364
|
// ─── Render ───
|
|
@@ -333,6 +376,9 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
333
376
|
scatter();
|
|
334
377
|
bomb();
|
|
335
378
|
versionMsg();
|
|
379
|
+
characterHidden();
|
|
380
|
+
activeProp();
|
|
381
|
+
propPosition();
|
|
336
382
|
|
|
337
383
|
for (const [, [get]] of extraSignals) {
|
|
338
384
|
get();
|
|
@@ -345,7 +391,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
345
391
|
const width = rawLines[0]?.length ?? 10;
|
|
346
392
|
const blank = " ".repeat(width);
|
|
347
393
|
|
|
348
|
-
|
|
394
|
+
const hideForProp = propPosition() === "front";
|
|
395
|
+
let lines: string[] = (characterHidden() || hideForProp)
|
|
349
396
|
? rawLines.map(() => blank)
|
|
350
397
|
: rawLines.map((line, i) => {
|
|
351
398
|
if (!breathPhase()) {
|
|
@@ -422,8 +469,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
422
469
|
if (s !== "idle") {
|
|
423
470
|
stopWalk();
|
|
424
471
|
} else if (walkEnabled()) {
|
|
425
|
-
|
|
426
|
-
|
|
472
|
+
scheduleNextWalk();
|
|
473
|
+
scheduleNextJump();
|
|
427
474
|
}
|
|
428
475
|
|
|
429
476
|
if (s === "thinking" || s === "busy") {
|
|
@@ -455,7 +502,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
455
502
|
if (!next) {
|
|
456
503
|
stopWalk();
|
|
457
504
|
} else if (currentState() === "idle") {
|
|
458
|
-
|
|
505
|
+
scheduleNextWalk();
|
|
459
506
|
}
|
|
460
507
|
};
|
|
461
508
|
|
|
@@ -596,15 +643,22 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
596
643
|
clearInterval(fuseTimer);
|
|
597
644
|
setBomb(null);
|
|
598
645
|
setFrameOverride(null);
|
|
599
|
-
|
|
646
|
+
const explosionColors = ["#FF0000", "#FF6600", "#FFCC00"];
|
|
647
|
+
let expColorIdx = 0;
|
|
648
|
+
setFlashColor(explosionColors[0]);
|
|
649
|
+
const expColorTimer = setInterval(() => {
|
|
650
|
+
expColorIdx++;
|
|
651
|
+
setFlashColor(explosionColors[expColorIdx % explosionColors.length]);
|
|
652
|
+
}, 100);
|
|
600
653
|
const lineCount = getFrameLines(pack, "default").length;
|
|
601
654
|
const offsets = Array.from({ length: lineCount }, () => ({
|
|
602
655
|
dx: Math.floor((Math.random() - 0.5) * 30),
|
|
603
656
|
dy: Math.floor((Math.random() - 0.5) * 15),
|
|
604
657
|
}));
|
|
605
658
|
setScatter(offsets);
|
|
606
|
-
setCelebrate({ text: "
|
|
659
|
+
setCelebrate({ text: "ᵇᵒᵒᵐ~💥", count: 0 });
|
|
607
660
|
explodeTimer = setTimeout(() => {
|
|
661
|
+
clearInterval(expColorTimer);
|
|
608
662
|
setFlashColor(null);
|
|
609
663
|
setCelebrate(null);
|
|
610
664
|
scatterIn();
|
|
@@ -624,8 +678,14 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
624
678
|
setPropPosition(pos);
|
|
625
679
|
stopPropTimer();
|
|
626
680
|
if (Array.isArray(prop.frames[0]) && prop.frameInterval) {
|
|
681
|
+
const totalFrames = (prop.frames as string[][]).length;
|
|
682
|
+
const randomize = prop.name === "laptop";
|
|
627
683
|
propTimer = setInterval(() => {
|
|
628
|
-
|
|
684
|
+
if (randomize) {
|
|
685
|
+
setPropFrameIdx(Math.floor(Math.random() * totalFrames));
|
|
686
|
+
} else {
|
|
687
|
+
setPropFrameIdx((idx) => (idx + 1) % totalFrames);
|
|
688
|
+
}
|
|
629
689
|
}, prop.frameInterval);
|
|
630
690
|
}
|
|
631
691
|
} else {
|
|
@@ -3,6 +3,7 @@ const bus = new EventTarget();
|
|
|
3
3
|
const CELEBRATE_EVENT = "mascot:celebrate";
|
|
4
4
|
const VERSION_EVENT = "mascot:version";
|
|
5
5
|
const SCATTER_EVENT = "mascot:scatter";
|
|
6
|
+
const PROP_SHOW_EVENT = "mascot:propshow";
|
|
6
7
|
|
|
7
8
|
export function emitCelebrate(newVersion: string): void {
|
|
8
9
|
bus.dispatchEvent(new CustomEvent(CELEBRATE_EVENT, { detail: { newVersion } }));
|
|
@@ -39,3 +40,13 @@ export function onScatter(handler: () => void): () => void {
|
|
|
39
40
|
bus.addEventListener(SCATTER_EVENT, listener);
|
|
40
41
|
return () => bus.removeEventListener(SCATTER_EVENT, listener);
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
export function emitPropShow(): void {
|
|
45
|
+
bus.dispatchEvent(new CustomEvent(PROP_SHOW_EVENT));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function onPropShow(handler: () => void): () => void {
|
|
49
|
+
const listener = () => { handler(); };
|
|
50
|
+
bus.addEventListener(PROP_SHOW_EVENT, listener);
|
|
51
|
+
return () => bus.removeEventListener(PROP_SHOW_EVENT, listener);
|
|
52
|
+
}
|