@mingxy/opencode-mascot 0.7.8 → 0.7.10

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.8",
3
+ "version": "0.7.10",
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
  }
@@ -5,7 +5,7 @@ import type { JSX } from "@opentui/solid";
5
5
  import type { MascotPack, MascotState } from "../core/types";
6
6
  import { createAnimatedRenderer } from "../core/ascii-renderer";
7
7
  import { onCelebrate, onVersion, onScatter } from "../core/celebration-bus";
8
- import { pickPropByTrigger, getProp } from "../core/prop-loader";
8
+ import { pickPropByTrigger } from "../core/prop-loader";
9
9
 
10
10
  interface SidebarMascotProps {
11
11
  mascots: Record<string, MascotPack>;
@@ -159,19 +159,13 @@ 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);
164
+ renderers[currentName()].setCharacterHidden(busyProp?.position === "front");
172
165
  } else {
173
166
  setStateWithSwitch("idle");
174
167
  renderers[currentName()].setProp(null);
168
+ renderers[currentName()].setCharacterHidden(false);
175
169
  }
176
170
  });
177
171
 
@@ -223,63 +217,82 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
223
217
  renderers[currentName()].scatterIn();
224
218
  }, 2000);
225
219
 
220
+ const propOffset = () => {
221
+ const pos = renderers[currentName()]?.getPropPosition();
222
+ if (pos === "side-left") return -18;
223
+ if (pos === "side-right") return 12;
224
+ return 0;
225
+ };
226
+
226
227
  return (
227
- <box
228
- position="absolute"
229
- left={posX()}
230
- top={posY()}
231
- alignItems="center"
232
- zIndex={zBoost() ? 9999 : 100}
233
- flexDirection="column"
234
- ref={(node: any) => {
235
- if (node) {
236
- setContainerWidth(node.width || 0);
237
- if (node.onSizeChange !== undefined) {
238
- node.onSizeChange = () => {
239
- setContainerWidth(node.width || 0);
240
- };
228
+ <>
229
+ {renderers[currentName()]?.propElement() ? (
230
+ <box
231
+ position="absolute"
232
+ left={posX() + propOffset()}
233
+ top={posY()}
234
+ zIndex={zBoost() ? 9998 : 50}
235
+ >
236
+ {renderers[currentName()].propElement()}
237
+ </box>
238
+ ) : null}
239
+ <box
240
+ position="absolute"
241
+ left={posX()}
242
+ top={posY()}
243
+ alignItems="center"
244
+ zIndex={zBoost() ? 9999 : 100}
245
+ flexDirection="column"
246
+ ref={(node: any) => {
247
+ if (node) {
248
+ setContainerWidth(node.width || 0);
249
+ if (node.onSizeChange !== undefined) {
250
+ node.onSizeChange = () => {
251
+ setContainerWidth(node.width || 0);
252
+ };
253
+ }
241
254
  }
242
- }
243
- }}
244
- onMouseDown={(e: any) => {
245
- if (hideSide) { returnToView(); return; }
255
+ }}
256
+ onMouseDown={(e: any) => {
257
+ if (hideSide) { returnToView(); return; }
246
258
 
247
- const now = Date.now();
248
- if (now - lastClickTime < 300) {
249
- switchToNext();
250
- lastClickTime = 0;
251
- return;
252
- }
253
- lastClickTime = now;
259
+ const now = Date.now();
260
+ if (now - lastClickTime < 300) {
261
+ switchToNext();
262
+ lastClickTime = 0;
263
+ return;
264
+ }
265
+ lastClickTime = now;
254
266
 
255
- if (e.modifiers?.alt) {
256
- dragStartX = e.x;
257
- dragStartY = e.y;
258
- dragAnchorX = posX();
259
- dragAnchorY = posY();
260
- isDragging = true;
261
- renderers[currentName()].setDragging(true);
262
- props.api.renderer.clearSelection();
263
- }
264
- }}
265
- onMouseDrag={(e: any) => {
266
- if (e.modifiers?.alt && isDragging) {
267
- setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
268
- setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
269
- }
270
- }}
271
- onMouseUp={() => {
272
- isDragging = false;
273
- renderers[currentName()].setDragging(false);
274
- checkEdge();
275
- }}
276
- onMouseDragEnd={() => {
277
- isDragging = false;
278
- renderers[currentName()].setDragging(false);
279
- checkEdge();
280
- }}
281
- >
282
- {renderers[currentName()]?.element() ?? null}
283
- </box>
267
+ if (e.modifiers?.alt) {
268
+ dragStartX = e.x;
269
+ dragStartY = e.y;
270
+ dragAnchorX = posX();
271
+ dragAnchorY = posY();
272
+ isDragging = true;
273
+ renderers[currentName()].setDragging(true);
274
+ props.api.renderer.clearSelection();
275
+ }
276
+ }}
277
+ onMouseDrag={(e: any) => {
278
+ if (e.modifiers?.alt && isDragging) {
279
+ setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
280
+ setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
281
+ }
282
+ }}
283
+ onMouseUp={() => {
284
+ isDragging = false;
285
+ renderers[currentName()].setDragging(false);
286
+ checkEdge();
287
+ }}
288
+ onMouseDragEnd={() => {
289
+ isDragging = false;
290
+ renderers[currentName()].setDragging(false);
291
+ checkEdge();
292
+ }}
293
+ >
294
+ {renderers[currentName()]?.element() ?? null}
295
+ </box>
296
+ </>
284
297
  );
285
298
  }
@@ -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
  }