@mingxy/opencode-mascot 0.7.7 → 0.7.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/opencode-mascot",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "OpenCode TUI mascot plugin framework - customizable ASCII mascots for your terminal",
5
5
  "author": "mingxy",
6
6
  "license": "MIT",
@@ -1,19 +1,6 @@
1
1
  import type { PropPack } from "../../core/types";
2
2
 
3
- /**
4
- * Pad 道具 — busy 状态变体道具
5
- *
6
- * 尺寸: 18 宽 × 9 高(双层框+.•摄像头+底部◉Home键)
7
- * 屏幕区: 12 宽 × 4 高
8
- * 叙事: 月儿小人玩游戏,又菜又爱玩
9
- * 贪吃蛇 6帧: 开局→走→撞墙→哭→不服→重开
10
- * 俄罗斯方块 4帧: 开局→堆积→满→over
11
- * 2048 4帧: 开局→合并→128→over
12
- * 切换 1帧: next!
13
- * 月儿小人: ☆ 呆毛 + (^-^) 圆脸 + ┃█┃ 身体(迷你版)
14
- */
15
-
16
- const W = 12; // 屏幕内容区宽
3
+ const W = 12;
17
4
 
18
5
  const OUTER_TOP = "╭.•" + "─".repeat(14) + "╮";
19
6
  const INNER_TOP = "│ ┌" + "─".repeat(W) + "┐ │";
@@ -25,24 +12,20 @@ const scr = (rows: string[]) =>
25
12
  rows.map((r) => "│ │" + r.padEnd(W) + "│ │");
26
13
 
27
14
  const games: string[][] = [
28
- // 贪吃蛇 6帧
29
15
  [" ☆ ᵇᵉᵍⁱⁿ ", "(^-^) ◯→ ●", " ┃█┃ ˢ:0 ", " "],
30
16
  [" ☆ ", "(^-^) ◯◯→ ●", " ┃█┃ ˢ:1 ", " "],
31
17
  [" ☆ ", "(×_×) ◯◯💥 ", " ┃█┃ ᵍᵍ! ", " "],
32
18
  [" ☆ ", "(╥_╥) ᵒᵛᵉʳ ", " ┃█┃ ˢ:1 ", " "],
33
19
  [" ☆ ", "(¬_¬) ᵃᵍᵃⁱⁿ", " ┃█┃ ", " "],
34
20
  [" ☆ ᵍᵒ! ", "(^-^) ◯→ ●", " ┃█┃ ˢ:0 ", " "],
35
- // 俄罗斯方块 4帧
36
21
  [" ☆ ᵇᵉᵍⁱⁿ ", "(^-^) ▣ ", " ┃█┃ ", " "],
37
22
  [" ☆ ", "(^-^) ▣▣ ", " ┃█┃ ▣▣▣▣ ", " "],
38
23
  [" ☆ ", "(×_×) ", " ┃█┃ ▣▣▣▣▣▣", " ᶠᵘˡˡ! "],
39
24
  [" ☆ ", "(╥_╥) ᵒᵛᵉʳ ", " ┃█┃ ", " "],
40
- // 2048 4帧
41
25
  [" ☆ ᵇᵉᵍⁱⁿ ", "(^-^) [ 2 ]", " ┃█┃ ", " "],
42
26
  [" ☆ ", "(^-^) [ 4 ]", " ┃█┃ ᵒᵒⁿ ", " "],
43
27
  [" ☆ ", "(⊙_⊙) [128]", " ┃█┃ ʷᵒʷ ", " "],
44
28
  [" ☆ ", "(╥_╥) ᵒᵛᵉʳ ", " ┃█┃ ", " "],
45
- // 切换
46
29
  [" ☆ ", "(^-^) ⁿᵉˣᵗ!", " ┃█┃ ", " "],
47
30
  ];
48
31
 
@@ -157,7 +157,19 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
157
157
  onMouseUp={() => { stopDrag(); }}
158
158
  onMouseDragEnd={() => { stopDrag(); }}
159
159
  >
160
- {renderers[currentName()]?.element() ?? null}
160
+ {renderers[currentName()]?.propElement() ? (
161
+ <box
162
+ position="absolute"
163
+ zIndex={50}
164
+ left={renderers[currentName()].getPropPosition() === "side-left" ? -16 : renderers[currentName()].getPropPosition() === "side-right" ? 12 : 0}
165
+ top={0}
166
+ >
167
+ {renderers[currentName()].propElement()}
168
+ </box>
169
+ ) : null}
170
+ <box zIndex={100}>
171
+ {renderers[currentName()]?.element() ?? null}
172
+ </box>
161
173
  </box>
162
174
  );
163
175
  }
@@ -159,16 +159,8 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
159
159
  if (statusType === "busy" || statusType === "retry") {
160
160
  if (hideSide) returnToView();
161
161
  renderers[currentName()].setState("busy");
162
- // 先显示箱子"打开"动画300ms,再切换到 busy 道具(道具从箱子里掉出来)
163
162
  const busyProp = pickPropByTrigger("busy");
164
- if (busyProp) {
165
- renderers[currentName()].setProp(getProp("box") ?? null);
166
- setTimeout(() => {
167
- renderers[currentName()].setProp(busyProp);
168
- }, 300);
169
- } else {
170
- renderers[currentName()].setProp(null);
171
- }
163
+ renderers[currentName()].setProp(busyProp);
172
164
  } else {
173
165
  setStateWithSwitch("idle");
174
166
  renderers[currentName()].setProp(null);
@@ -209,7 +201,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
209
201
  setTimeout(() => setZBoost(false), 3500);
210
202
  });
211
203
 
212
- let scattered = true;
204
+ let scattered = false;
213
205
 
214
206
  onScatter(() => {
215
207
  if (scattered) return;
@@ -217,45 +209,11 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
217
209
  renderers[currentName()].scatterIn();
218
210
  });
219
211
 
220
- renderers[currentName()].setCharacterHidden(true);
221
- renderers[currentName()].setProp(getProp("box") ?? null);
222
-
223
- const finalY = posY();
224
- const finalX = posX();
225
- const fallStartY = finalY - 15;
226
- const fallDuration = 500;
227
- const fallStartTime = Date.now();
228
- setPosY(fallStartY);
229
-
230
- const fallInterval = setInterval(() => {
231
- const elapsed = Date.now() - fallStartTime;
232
- const t = Math.min(elapsed / fallDuration, 1);
233
- const eased = t * t;
234
- setPosY(Math.round(fallStartY + (finalY - fallStartY) * eased));
235
- if (t >= 1) {
236
- clearInterval(fallInterval);
237
- setPosY(finalY);
238
-
239
- setTimeout(() => {
240
- const shakeSeq = [1, -1, 1, -1, 0];
241
- let shakeIdx = 0;
242
- const shakeInterval = setInterval(() => {
243
- if (shakeIdx >= shakeSeq.length) {
244
- clearInterval(shakeInterval);
245
- setPosX(finalX);
246
- return;
247
- }
248
- setPosX(finalX + shakeSeq[shakeIdx]);
249
- shakeIdx++;
250
- }, 60);
251
- }, 2000);
252
- }
253
- }, 16);
254
-
255
212
  setTimeout(() => {
256
- renderers[currentName()].setProp(null);
257
- renderers[currentName()].setCharacterHidden(false);
258
- }, 6000);
213
+ if (scattered) return;
214
+ scattered = true;
215
+ renderers[currentName()].scatterIn();
216
+ }, 2000);
259
217
 
260
218
  return (
261
219
  <box
@@ -313,7 +271,19 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
313
271
  checkEdge();
314
272
  }}
315
273
  >
316
- {renderers[currentName()]?.element() ?? null}
274
+ {renderers[currentName()]?.propElement() ? (
275
+ <box
276
+ position="absolute"
277
+ zIndex={50}
278
+ left={renderers[currentName()].getPropPosition() === "side-left" ? -16 : renderers[currentName()].getPropPosition() === "side-right" ? 12 : 0}
279
+ top={0}
280
+ >
281
+ {renderers[currentName()].propElement()}
282
+ </box>
283
+ ) : null}
284
+ <box zIndex={100}>
285
+ {renderers[currentName()]?.element() ?? null}
286
+ </box>
317
287
  </box>
318
288
  );
319
289
  }
@@ -46,6 +46,8 @@ function getFrameLines(pack: MascotPack, frameName: string): string[] {
46
46
 
47
47
  export function createAnimatedRenderer(pack: MascotPack): {
48
48
  element: () => JSX.Element;
49
+ propElement: () => JSX.Element | null;
50
+ getPropPosition: () => PropPosition | null;
49
51
  getState: () => MascotState;
50
52
  setState: (s: MascotState) => void;
51
53
  toggleWalk: () => void;
@@ -331,9 +333,6 @@ export function createAnimatedRenderer(pack: MascotPack): {
331
333
  scatter();
332
334
  bomb();
333
335
  versionMsg();
334
- activeProp();
335
- propFrameIdx();
336
- propPosition();
337
336
 
338
337
  for (const [, [get]] of extraSignals) {
339
338
  get();
@@ -368,47 +367,6 @@ export function createAnimatedRenderer(pack: MascotPack): {
368
367
  lines = effects.render(lines, renderCtx);
369
368
  }
370
369
 
371
- // ─── Prop overlay ───
372
- const prop = activeProp();
373
- if (prop) {
374
- const propFramesRaw = Array.isArray(prop.frames[0])
375
- ? (prop.frames as string[][])
376
- : [prop.frames as string[]];
377
- const propLines = propFramesRaw[propFrameIdx() % propFramesRaw.length] ?? propFramesRaw[0];
378
-
379
- if (propLines.length > 0) {
380
- const pos = propPosition();
381
- if (pos === 'front') {
382
- const overlayCount = Math.min(propLines.length, lines.length);
383
- const startRow = Math.floor((lines.length - overlayCount) / 2);
384
- for (let i = 0; i < overlayCount; i++) {
385
- lines[startRow + i] = propLines[i];
386
- }
387
- } else {
388
- const charWidth = lines[0]?.length ?? 0;
389
- const propWidth = propLines[0]?.length ?? 0;
390
- const charHeight = lines.length;
391
- const propHeight = propLines.length;
392
- const maxLines = Math.max(charHeight, propHeight);
393
- const charPad = Math.floor((maxLines - charHeight) / 2);
394
- const propPad = Math.floor((maxLines - propHeight) / 2);
395
- const sep = " ";
396
-
397
- const merged: string[] = [];
398
- for (let i = 0; i < maxLines; i++) {
399
- const cLine = (i >= charPad && i < charPad + charHeight)
400
- ? lines[i - charPad]
401
- : " ".repeat(charWidth);
402
- const pLine = (i >= propPad && i < propPad + propHeight)
403
- ? propLines[i - propPad]
404
- : " ".repeat(propWidth);
405
- merged.push(pos === 'side-right' ? cLine + sep + pLine : pLine + sep + cLine);
406
- }
407
- lines = merged;
408
- }
409
- }
410
- }
411
-
412
370
  const top = jumpOffset();
413
371
  const left = offset > 0 ? offset : 0;
414
372
  const cel = celebrate();
@@ -435,6 +393,26 @@ export function createAnimatedRenderer(pack: MascotPack): {
435
393
  );
436
394
  };
437
395
 
396
+ const propElement = () => {
397
+ activeProp();
398
+ propFrameIdx();
399
+ const prop = activeProp();
400
+ if (!prop) return null;
401
+
402
+ const propFramesRaw = Array.isArray(prop.frames[0])
403
+ ? (prop.frames as string[][])
404
+ : [prop.frames as string[]];
405
+ const propLines = propFramesRaw[propFrameIdx() % propFramesRaw.length] ?? propFramesRaw[0];
406
+
407
+ return (
408
+ <box flexDirection="column" alignItems="flex-start">
409
+ {propLines.map((line: string) => (
410
+ <text fg={fg}>{line}</text>
411
+ ))}
412
+ </box>
413
+ );
414
+ };
415
+
438
416
  // ─── State control ───
439
417
  const setState = (s: MascotState) => {
440
418
  setCurrentState(s);
@@ -658,5 +636,5 @@ export function createAnimatedRenderer(pack: MascotPack): {
658
636
 
659
637
  const getProp = () => activeProp();
660
638
 
661
- return { element, getState: currentState, setState, toggleWalk, setDragging, setCharacterHidden, celebrateUpdate, bounce, showVersion, scatterIn, explode, setProp, getProp };
639
+ return { element, propElement, getPropPosition: () => propPosition(), getState: currentState, setState, toggleWalk, setDragging, setCharacterHidden, celebrateUpdate, bounce, showVersion, scatterIn, explode, setProp, getProp };
662
640
  }