@mingxy/opencode-mascot 0.1.2 → 0.2.1
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 +15 -6
- package/src/builtins/baozi/frames.ts +43 -0
- package/src/builtins/baozi/index.ts +105 -0
- package/src/builtins/yueer/index.ts +186 -10
- package/src/components/home-mascot.tsx +103 -0
- package/src/components/sidebar-mascot.tsx +130 -15
- package/src/core/ascii-renderer.tsx +156 -139
- package/src/core/celebration-bus.ts +16 -0
- package/src/core/mascot-loader.ts +26 -9
- package/src/core/types.ts +73 -23
- package/src/core/updater.ts +109 -0
- package/tui.tsx +26 -11
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createSignal, onCleanup } from "solid-js";
|
|
4
4
|
import type { JSX } from "@opentui/solid";
|
|
5
|
-
import type { MascotPack, MascotState } from "./types";
|
|
5
|
+
import type { MascotPack, MascotState, EffectTimerCtx, EffectRenderCtx } from "./types";
|
|
6
6
|
|
|
7
7
|
const STATE_TO_FRAME: Record<MascotState, string> = {
|
|
8
8
|
idle: "default",
|
|
@@ -18,11 +18,11 @@ const DEFAULT_ANIM = {
|
|
|
18
18
|
expressionInterval: 8000,
|
|
19
19
|
idleTimeout: 120000,
|
|
20
20
|
breathInterval: 3000,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
walkEnabled: true,
|
|
22
|
+
walkMinDelay: 20000,
|
|
23
|
+
walkMaxDelay: 40000,
|
|
23
24
|
jumpMinDelay: 20000,
|
|
24
25
|
jumpMaxDelay: 40000,
|
|
25
|
-
stompThreshold: 30000,
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
const WALK_PATH = [1, 2, 3, 4, 3, 2, 1, 0, -1, -2, -3, -2, -1, 0];
|
|
@@ -45,35 +45,72 @@ function renderLines(lines: string[], fg?: string): JSX.Element {
|
|
|
45
45
|
export function createAnimatedRenderer(pack: MascotPack): {
|
|
46
46
|
element: () => JSX.Element;
|
|
47
47
|
setState: (s: MascotState) => void;
|
|
48
|
+
toggleWalk: () => void;
|
|
49
|
+
setDragging: (v: boolean) => void;
|
|
50
|
+
celebrateUpdate: (newVersion: string) => void;
|
|
48
51
|
} {
|
|
49
52
|
const anim = { ...DEFAULT_ANIM, ...pack.animations };
|
|
50
53
|
const fg = pack.colors?.defaultFg || undefined;
|
|
54
|
+
const effects = pack.effects;
|
|
51
55
|
|
|
52
56
|
const [currentState, setCurrentState] = createSignal<MascotState>("idle");
|
|
53
57
|
const [frameOverride, setFrameOverride] = createSignal<string | null>(null);
|
|
54
|
-
const [
|
|
58
|
+
const [breathPhase, setBreathPhase] = createSignal(true);
|
|
55
59
|
const [walkOffset, setWalkOffset] = createSignal(0);
|
|
56
|
-
const [ahogeAlt, setAhogeAlt] = createSignal(false);
|
|
57
|
-
const [braidAlt, setBraidAlt] = createSignal(false);
|
|
58
|
-
const [waveSide, setWaveSide] = createSignal(false);
|
|
59
|
-
const [zzzPhase, setZzzPhase] = createSignal(0);
|
|
60
60
|
const [jumpOffset, setJumpOffset] = createSignal(0);
|
|
61
|
-
const [
|
|
62
|
-
const [
|
|
61
|
+
const [walkEnabled, setWalkEnabled] = createSignal(anim.walkEnabled ?? true);
|
|
62
|
+
const [dragging, setDraggingSignal] = createSignal(false);
|
|
63
|
+
const [celebrate, setCelebrate] = createSignal<{ text: string; count: number } | null>(null);
|
|
64
|
+
|
|
65
|
+
let idleSleepTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
|
|
67
|
+
const resetIdleSleep = () => {
|
|
68
|
+
if (idleSleepTimeout) clearTimeout(idleSleepTimeout);
|
|
69
|
+
idleSleepTimeout = null;
|
|
70
|
+
if (currentState() !== "idle") return;
|
|
71
|
+
idleSleepTimeout = setTimeout(() => {
|
|
72
|
+
if (currentState() === "idle") {
|
|
73
|
+
setCurrentState("sleeping");
|
|
74
|
+
stopWalk();
|
|
75
|
+
}
|
|
76
|
+
}, anim.idleTimeout);
|
|
77
|
+
};
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
resetIdleSleep();
|
|
65
80
|
|
|
66
|
-
//
|
|
81
|
+
// ─── Extra signals from pack effects ───
|
|
82
|
+
const extraSignals = new Map<string, [() => unknown, (v: unknown) => void]>();
|
|
83
|
+
if (effects?.signals) {
|
|
84
|
+
for (const sig of effects.signals) {
|
|
85
|
+
const [get, set] = createSignal(sig.initial);
|
|
86
|
+
extraSignals.set(sig.name, [get, set]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const getExtra = (name: string): unknown => extraSignals.get(name)?.[0]() ?? null;
|
|
91
|
+
const setExtra = (name: string, value: unknown) => extraSignals.get(name)?.[1](value);
|
|
92
|
+
|
|
93
|
+
const timerCtx: EffectTimerCtx = {
|
|
94
|
+
get: getExtra,
|
|
95
|
+
set: setExtra,
|
|
96
|
+
get state() { return currentState(); },
|
|
97
|
+
get frameOverride() { return frameOverride(); },
|
|
98
|
+
setFrameOverride: (name) => setFrameOverride(name),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ─── Built-in timers ───
|
|
102
|
+
|
|
103
|
+
// 1. Blink
|
|
67
104
|
const hasBlink = (pack.frames as Record<string, string[] | undefined>)["blink"] !== undefined;
|
|
68
105
|
|
|
69
106
|
const blinkTimer = setInterval(() => {
|
|
70
|
-
if (Math.random() < anim.blinkChance && hasBlink) {
|
|
107
|
+
if (currentState() !== "sleeping" && Math.random() < anim.blinkChance && hasBlink) {
|
|
71
108
|
setFrameOverride("blink");
|
|
72
109
|
setTimeout(() => setFrameOverride(null), 150);
|
|
73
110
|
}
|
|
74
111
|
}, anim.blinkInterval);
|
|
75
112
|
|
|
76
|
-
//
|
|
113
|
+
// 2. Random expression
|
|
77
114
|
const availableExpressions = Object.keys(pack.frames).filter(
|
|
78
115
|
(k) => k !== "default" && k !== "blink",
|
|
79
116
|
);
|
|
@@ -83,37 +120,30 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
83
120
|
const pick = availableExpressions[Math.floor(Math.random() * availableExpressions.length)];
|
|
84
121
|
if (pick) {
|
|
85
122
|
setFrameOverride(pick);
|
|
86
|
-
|
|
87
|
-
setTimeout(() => {
|
|
88
|
-
setFrameOverride(null);
|
|
89
|
-
stopWave();
|
|
90
|
-
}, 2000);
|
|
123
|
+
setTimeout(() => setFrameOverride(null), 2000);
|
|
91
124
|
}
|
|
92
125
|
}
|
|
93
126
|
}, anim.expressionInterval);
|
|
94
127
|
|
|
95
|
-
//
|
|
128
|
+
// 3. Breathing
|
|
96
129
|
const breathTimer = setInterval(() => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}, anim.breathInterval);
|
|
100
|
-
|
|
101
|
-
// ============ 4. Ahoge wobble (呆毛晃) ============
|
|
102
|
-
const ahogeTimer = setInterval(() => {
|
|
103
|
-
if (Math.random() < anim.ahogeChance) {
|
|
104
|
-
setAhogeAlt(true);
|
|
105
|
-
setTimeout(() => setAhogeAlt(false), 200);
|
|
130
|
+
if (currentState() === "idle") {
|
|
131
|
+
setBreathPhase((v) => !v);
|
|
106
132
|
}
|
|
107
|
-
}, anim.
|
|
133
|
+
}, anim.breathInterval);
|
|
108
134
|
|
|
109
|
-
//
|
|
135
|
+
// 4. Walk
|
|
110
136
|
let walkStep = -1;
|
|
111
137
|
let walkInterval: ReturnType<typeof setInterval> | null = null;
|
|
112
138
|
|
|
113
139
|
const startWalk = () => {
|
|
114
|
-
if (walkInterval) return;
|
|
140
|
+
if (walkInterval || !walkEnabled()) return;
|
|
115
141
|
walkStep = 0;
|
|
116
142
|
walkInterval = setInterval(() => {
|
|
143
|
+
if (currentState() !== "idle") {
|
|
144
|
+
stopWalk();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
117
147
|
if (walkStep < WALK_PATH.length) {
|
|
118
148
|
setWalkOffset(WALK_PATH[walkStep]);
|
|
119
149
|
walkStep++;
|
|
@@ -138,18 +168,21 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
138
168
|
let walkTimeout: ReturnType<typeof setTimeout>;
|
|
139
169
|
|
|
140
170
|
function scheduleNextWalk() {
|
|
141
|
-
|
|
171
|
+
if (!walkEnabled()) return setTimeout(() => {}, 60000);
|
|
172
|
+
const delay = anim.walkMinDelay + Math.floor(Math.random() * (anim.walkMaxDelay - anim.walkMinDelay));
|
|
142
173
|
return setTimeout(() => {
|
|
143
|
-
if (currentState() === "idle" && !frameOverride() && walkStep === -1) {
|
|
174
|
+
if (currentState() === "idle" && !frameOverride() && walkStep === -1 && walkEnabled()) {
|
|
144
175
|
startWalk();
|
|
145
176
|
}
|
|
146
|
-
|
|
177
|
+
if (currentState() !== "sleeping") {
|
|
178
|
+
walkTimeout = scheduleNextWalk();
|
|
179
|
+
}
|
|
147
180
|
}, delay);
|
|
148
181
|
}
|
|
149
182
|
|
|
150
183
|
walkTimeout = scheduleNextWalk();
|
|
151
184
|
|
|
152
|
-
//
|
|
185
|
+
// 5. Jump
|
|
153
186
|
let jumpTimeout: ReturnType<typeof setTimeout>;
|
|
154
187
|
|
|
155
188
|
function scheduleNextJump() {
|
|
@@ -157,77 +190,50 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
157
190
|
return setTimeout(() => {
|
|
158
191
|
if (currentState() === "idle" && !frameOverride() && walkStep === -1) {
|
|
159
192
|
setJumpOffset(-2);
|
|
160
|
-
setTimeout(() => setJumpOffset(
|
|
193
|
+
setTimeout(() => setJumpOffset(-1), 1500);
|
|
194
|
+
setTimeout(() => setJumpOffset(0), 2000);
|
|
195
|
+
}
|
|
196
|
+
if (currentState() !== "sleeping") {
|
|
197
|
+
jumpTimeout = scheduleNextJump();
|
|
161
198
|
}
|
|
162
|
-
jumpTimeout = scheduleNextJump();
|
|
163
199
|
}, delay);
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
jumpTimeout = scheduleNextJump();
|
|
167
203
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
waveTimer = setInterval(() => setWaveSide((v) => !v), 300);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const stopWave = () => {
|
|
177
|
-
if (waveTimer) {
|
|
178
|
-
clearInterval(waveTimer);
|
|
179
|
-
waveTimer = null;
|
|
180
|
-
}
|
|
181
|
-
setWaveSide(false);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// ============ 8. Zzz bubbles (sleeping state) ============
|
|
185
|
-
const zzzTimer = setInterval(() => {
|
|
186
|
-
if (currentState() === "sleeping") {
|
|
187
|
-
setZzzPhase((p) => (p + 1) % 4);
|
|
188
|
-
} else {
|
|
189
|
-
setZzzPhase(0);
|
|
204
|
+
// ─── Pack-defined effect timers ───
|
|
205
|
+
const effectTimers: ReturnType<typeof setInterval>[] = [];
|
|
206
|
+
if (effects?.timers) {
|
|
207
|
+
for (const t of effects.timers) {
|
|
208
|
+
effectTimers.push(setInterval(() => t.update(timerCtx), t.interval));
|
|
190
209
|
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ============ 9. Stomp (跺脚 — thinking > threshold) ============
|
|
194
|
-
const stompTimer = setInterval(() => {
|
|
195
|
-
if (
|
|
196
|
-
currentState() === "thinking" &&
|
|
197
|
-
thinkingStartTime > 0 &&
|
|
198
|
-
Date.now() - thinkingStartTime > anim.stompThreshold
|
|
199
|
-
) {
|
|
200
|
-
setStompActive(true);
|
|
201
|
-
setStompAlt((a) => !a);
|
|
202
|
-
} else {
|
|
203
|
-
setStompActive(false);
|
|
204
|
-
}
|
|
205
|
-
}, 200);
|
|
210
|
+
}
|
|
206
211
|
|
|
207
|
-
//
|
|
212
|
+
// ─── Cleanup ───
|
|
208
213
|
onCleanup(() => {
|
|
209
214
|
clearInterval(blinkTimer);
|
|
210
215
|
clearInterval(expressionTimer);
|
|
211
216
|
clearInterval(breathTimer);
|
|
212
|
-
clearInterval(ahogeTimer);
|
|
213
|
-
clearInterval(zzzTimer);
|
|
214
|
-
clearInterval(stompTimer);
|
|
215
217
|
clearTimeout(walkTimeout);
|
|
216
218
|
clearTimeout(jumpTimeout);
|
|
219
|
+
if (idleSleepTimeout) clearTimeout(idleSleepTimeout);
|
|
217
220
|
if (walkInterval) clearInterval(walkInterval);
|
|
218
|
-
|
|
221
|
+
for (const t of effectTimers) clearInterval(t);
|
|
219
222
|
});
|
|
220
223
|
|
|
221
|
-
//
|
|
224
|
+
// ─── Render ───
|
|
222
225
|
const element = () => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
braidAlt();
|
|
226
|
-
waveSide();
|
|
227
|
-
zzzPhase();
|
|
226
|
+
breathPhase();
|
|
227
|
+
walkOffset();
|
|
228
228
|
jumpOffset();
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
frameOverride();
|
|
230
|
+
currentState();
|
|
231
|
+
dragging();
|
|
232
|
+
celebrate();
|
|
233
|
+
|
|
234
|
+
for (const [, [get]] of extraSignals) {
|
|
235
|
+
get();
|
|
236
|
+
}
|
|
231
237
|
|
|
232
238
|
const frameName = frameOverride() ?? STATE_TO_FRAME[currentState()] ?? "default";
|
|
233
239
|
const rawLines = getFrameLines(pack, frameName);
|
|
@@ -236,85 +242,96 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
236
242
|
const width = rawLines[0]?.length ?? 10;
|
|
237
243
|
const blank = " ".repeat(width);
|
|
238
244
|
|
|
239
|
-
let lines = rawLines.map((line, i) => {
|
|
240
|
-
if (!
|
|
245
|
+
let lines: string[] = rawLines.map((line, i) => {
|
|
246
|
+
if (!breathPhase()) {
|
|
241
247
|
if (i === 0) return blank;
|
|
242
248
|
return rawLines[i - 1];
|
|
243
249
|
}
|
|
244
250
|
return line;
|
|
245
251
|
});
|
|
246
252
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// 4. Wave: braids become hands ╲ / ╱ on face row
|
|
258
|
-
if (frameName === "happy") {
|
|
259
|
-
const faceIdx = lines.findIndex((l) => l.includes("^ω^"));
|
|
260
|
-
if (faceIdx >= 0) {
|
|
261
|
-
lines[faceIdx] = waveSide()
|
|
262
|
-
? "╲( ^ω^ )╱ "
|
|
263
|
-
: " ~( ^ω^ )~";
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 5. Stomp: legs alternate ╲ ╱ ↔ ╱ ╲
|
|
268
|
-
if (stompActive()) {
|
|
269
|
-
const legIdx = lines.length - 1;
|
|
270
|
-
if (legIdx >= 0) {
|
|
271
|
-
lines[legIdx] = stompAlt() ? " ╲ ╱ " : " ╱ ╲ ";
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 6. Zzz: floating Z bubbles beside face
|
|
276
|
-
const zzz = zzzPhase();
|
|
277
|
-
if (zzz > 0 && currentState() === "sleeping") {
|
|
278
|
-
for (let i = 0; i < lines.length; i++) {
|
|
279
|
-
if (lines[i].includes("-.-")) {
|
|
280
|
-
lines[i] = lines[i] + " " + "Z".repeat(zzz);
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
253
|
+
if (effects?.render) {
|
|
254
|
+
const renderCtx: EffectRenderCtx = {
|
|
255
|
+
state: currentState(),
|
|
256
|
+
frameName,
|
|
257
|
+
breathPhase: breathPhase(),
|
|
258
|
+
jumpOffset: jumpOffset(),
|
|
259
|
+
dragging: dragging(),
|
|
260
|
+
get: getExtra,
|
|
261
|
+
};
|
|
262
|
+
lines = effects.render(lines, renderCtx);
|
|
284
263
|
}
|
|
285
264
|
|
|
286
265
|
const top = jumpOffset();
|
|
287
266
|
const left = offset > 0 ? offset : 0;
|
|
267
|
+
const cel = celebrate();
|
|
288
268
|
|
|
289
269
|
return (
|
|
290
270
|
<box flexDirection="column" left={left} top={top}>
|
|
291
271
|
{renderLines(lines, fg)}
|
|
272
|
+
{cel ? <text fg={fg}>{cel.text}</text> : null}
|
|
292
273
|
</box>
|
|
293
274
|
);
|
|
294
275
|
};
|
|
295
276
|
|
|
296
|
-
//
|
|
277
|
+
// ─── State control ───
|
|
297
278
|
const setState = (s: MascotState) => {
|
|
298
|
-
const prev = currentState();
|
|
299
279
|
setCurrentState(s);
|
|
280
|
+
setBreathPhase(true);
|
|
281
|
+
resetIdleSleep();
|
|
300
282
|
|
|
301
283
|
if (s !== "idle") {
|
|
302
284
|
stopWalk();
|
|
285
|
+
} else if (walkEnabled()) {
|
|
286
|
+
walkTimeout = scheduleNextWalk();
|
|
287
|
+
jumpTimeout = scheduleNextJump();
|
|
303
288
|
}
|
|
289
|
+
};
|
|
304
290
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
291
|
+
const toggleWalk = () => {
|
|
292
|
+
const next = !walkEnabled();
|
|
293
|
+
setWalkEnabled(next);
|
|
294
|
+
if (!next) {
|
|
295
|
+
stopWalk();
|
|
296
|
+
} else if (currentState() === "idle") {
|
|
297
|
+
walkTimeout = scheduleNextWalk();
|
|
310
298
|
}
|
|
299
|
+
};
|
|
311
300
|
|
|
312
|
-
|
|
313
|
-
|
|
301
|
+
const setDragging = (v: boolean) => {
|
|
302
|
+
setDraggingSignal(v);
|
|
303
|
+
if (v) {
|
|
304
|
+
// 睡着时被拖拽 → 惊醒到 idle,切回 default 帧后手臂 ┃███┃ 才能被扇手渲染匹配
|
|
305
|
+
if (currentState() === "sleeping") {
|
|
306
|
+
setState("idle");
|
|
307
|
+
}
|
|
308
|
+
setJumpOffset(-1);
|
|
314
309
|
} else {
|
|
315
|
-
|
|
310
|
+
setJumpOffset(0);
|
|
316
311
|
}
|
|
317
312
|
};
|
|
318
313
|
|
|
319
|
-
|
|
314
|
+
// 连续跳跃 + 吐火星文泡泡庆祝更新成功
|
|
315
|
+
const celebrateUpdate = (newVersion: string) => {
|
|
316
|
+
const bubbles = pack.bubbleTexts ?? ["ᵘᵖ~"];
|
|
317
|
+
if (currentState() === "sleeping") setState("idle");
|
|
318
|
+
|
|
319
|
+
let step = 0;
|
|
320
|
+
const JUMPS = 3;
|
|
321
|
+
const tick = () => {
|
|
322
|
+
if (step >= JUMPS) {
|
|
323
|
+
setJumpOffset(0);
|
|
324
|
+
setCelebrate(null);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
setJumpOffset(step % 2 === 0 ? -2 : 0);
|
|
328
|
+
const word = bubbles[Math.floor(Math.random() * bubbles.length)];
|
|
329
|
+
setCelebrate({ text: `${word} ᵘᵖ→ᵛ${newVersion}`, count: step });
|
|
330
|
+
step++;
|
|
331
|
+
setTimeout(tick, 600);
|
|
332
|
+
};
|
|
333
|
+
tick();
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return { element, setState, toggleWalk, setDragging, celebrateUpdate };
|
|
320
337
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const bus = new EventTarget();
|
|
2
|
+
|
|
3
|
+
const CELEBRATE_EVENT = "mascot:celebrate";
|
|
4
|
+
|
|
5
|
+
export function emitCelebrate(newVersion: string): void {
|
|
6
|
+
bus.dispatchEvent(new CustomEvent(CELEBRATE_EVENT, { detail: { newVersion } }));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function onCelebrate(handler: (newVersion: string) => void): () => void {
|
|
10
|
+
const listener = (e: Event) => {
|
|
11
|
+
const detail = (e as CustomEvent).detail as { newVersion: string };
|
|
12
|
+
handler(detail.newVersion);
|
|
13
|
+
};
|
|
14
|
+
bus.addEventListener(CELEBRATE_EVENT, listener);
|
|
15
|
+
return () => bus.removeEventListener(CELEBRATE_EVENT, listener);
|
|
16
|
+
}
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import type { MascotPack } from "./types"
|
|
2
2
|
import { yueerPack } from "../builtins/yueer"
|
|
3
|
+
import { baoziPack } from "../builtins/baozi"
|
|
3
4
|
|
|
4
5
|
const BUILTINS: Record<string, MascotPack> = {
|
|
5
6
|
yueer: yueerPack,
|
|
7
|
+
baozi: baoziPack,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ALL_NAMES = Object.keys(BUILTINS)
|
|
11
|
+
|
|
12
|
+
function randomPick<T>(arr: T[]): T {
|
|
13
|
+
return arr[Math.floor(Math.random() * arr.length)]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadAllMascots(): Record<string, MascotPack> {
|
|
17
|
+
return { ...BUILTINS }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getMascotNames(): string[] {
|
|
21
|
+
return [...ALL_NAMES]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getRandomMascot(): MascotPack {
|
|
25
|
+
return randomPick(Object.values(BUILTINS))
|
|
6
26
|
}
|
|
7
27
|
|
|
8
28
|
export async function loadMascot(options?: Record<string, unknown>): Promise<MascotPack> {
|
|
9
|
-
const mascotName = (options?.mascot as string) || "
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// Future: npm package loading, local file loading
|
|
16
|
-
// Fallback to yueer
|
|
17
|
-
return yueerPack
|
|
29
|
+
const mascotName = (options?.mascot as string) || "random"
|
|
30
|
+
|
|
31
|
+
if (mascotName === "random") return getRandomMascot()
|
|
32
|
+
if (BUILTINS[mascotName]) return BUILTINS[mascotName]
|
|
33
|
+
|
|
34
|
+
return getRandomMascot()
|
|
18
35
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -17,18 +17,12 @@ export interface AnimationConfig {
|
|
|
17
17
|
blinkChance?: number;
|
|
18
18
|
expressionInterval?: number;
|
|
19
19
|
idleTimeout?: number;
|
|
20
|
-
/** Breathing cycle in ms (default: 3000) */
|
|
21
20
|
breathInterval?: number;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ahogeChance?: number;
|
|
26
|
-
/** Minimum delay between jumps in ms (default: 20000) */
|
|
21
|
+
walkEnabled?: boolean;
|
|
22
|
+
walkMinDelay?: number;
|
|
23
|
+
walkMaxDelay?: number;
|
|
27
24
|
jumpMinDelay?: number;
|
|
28
|
-
/** Maximum delay between jumps in ms (default: 40000) */
|
|
29
25
|
jumpMaxDelay?: number;
|
|
30
|
-
/** Stomp trigger: ms in thinking state before stomping (default: 30000) */
|
|
31
|
-
stompThreshold?: number;
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
/**
|
|
@@ -39,26 +33,72 @@ export interface SidebarConfig {
|
|
|
39
33
|
busyPhrases?: string[];
|
|
40
34
|
}
|
|
41
35
|
|
|
36
|
+
// ─── Effect system types ───
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A single extra signal managed by the effect system.
|
|
40
|
+
* The renderer creates a Solid signal for each and exposes get/set to timers and render.
|
|
41
|
+
*/
|
|
42
|
+
export interface SignalDef {
|
|
43
|
+
name: string;
|
|
44
|
+
initial: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
/**
|
|
43
|
-
*
|
|
48
|
+
* Timer context passed to EffectTimer.update().
|
|
44
49
|
*/
|
|
45
|
-
export interface
|
|
50
|
+
export interface EffectTimerCtx {
|
|
51
|
+
/** Read an extra signal by name */
|
|
52
|
+
get: (name: string) => unknown;
|
|
53
|
+
/** Write an extra signal by name */
|
|
54
|
+
set: (name: string, value: unknown) => void;
|
|
55
|
+
/** Current mascot state */
|
|
46
56
|
state: MascotState;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
/** Current frame override (null = use state mapping) */
|
|
58
|
+
frameOverride: string | null;
|
|
59
|
+
/** Override the displayed frame */
|
|
60
|
+
setFrameOverride: (name: string | null) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A recurring timer that updates extra signals.
|
|
65
|
+
*/
|
|
66
|
+
export interface EffectTimer {
|
|
67
|
+
/** Interval in ms */
|
|
68
|
+
interval: number;
|
|
69
|
+
/** Called on each tick */
|
|
70
|
+
update: (ctx: EffectTimerCtx) => void;
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
/**
|
|
57
|
-
*
|
|
74
|
+
* Context passed to EffectRenderFn.
|
|
58
75
|
*/
|
|
59
|
-
export interface
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
export interface EffectRenderCtx {
|
|
77
|
+
state: MascotState;
|
|
78
|
+
frameName: string;
|
|
79
|
+
breathPhase: boolean;
|
|
80
|
+
jumpOffset: number;
|
|
81
|
+
dragging: boolean;
|
|
82
|
+
get: (name: string) => unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pack-defined render effect. Called after built-in rendering (breathing),
|
|
87
|
+
* before final output. Return modified lines.
|
|
88
|
+
*/
|
|
89
|
+
export type EffectRenderFn = (lines: string[], ctx: EffectRenderCtx) => string[];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Effect bundle: extra signals, timers, and render function.
|
|
93
|
+
* Packs use this to define mascot-specific animations.
|
|
94
|
+
*/
|
|
95
|
+
export interface MascotEffects {
|
|
96
|
+
/** Extra signals (name → initial value) */
|
|
97
|
+
signals?: SignalDef[];
|
|
98
|
+
/** Recurring timers that drive signal changes */
|
|
99
|
+
timers?: EffectTimer[];
|
|
100
|
+
/** Render hook — modify lines based on signals */
|
|
101
|
+
render: EffectRenderFn;
|
|
62
102
|
}
|
|
63
103
|
|
|
64
104
|
/**
|
|
@@ -91,7 +131,16 @@ export interface MascotPack {
|
|
|
91
131
|
|
|
92
132
|
animations?: AnimationConfig;
|
|
93
133
|
sidebar?: SidebarConfig;
|
|
94
|
-
|
|
134
|
+
|
|
135
|
+
/** Mascot-specific animation effects (timers + render) */
|
|
136
|
+
effects?: MascotEffects;
|
|
137
|
+
|
|
138
|
+
/** Thinking bubble phrases, shown when mascot is in busy/thinking state */
|
|
139
|
+
bubbleTexts?: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SwitchConfig {
|
|
143
|
+
onState?: Partial<Record<MascotState, string>>;
|
|
95
144
|
}
|
|
96
145
|
|
|
97
146
|
/**
|
|
@@ -101,4 +150,5 @@ export interface MascotConfig {
|
|
|
101
150
|
mascot: string;
|
|
102
151
|
animations: boolean;
|
|
103
152
|
idleSleep: boolean;
|
|
153
|
+
switchConfig?: SwitchConfig;
|
|
104
154
|
}
|