@jcendal/opencode-digital-pet 0.2.3 → 0.4.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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-10-07
11
+
12
+ ### Changed
13
+
14
+ - feat(ci): dispatch web deploy from merged release plan
15
+
16
+
17
+ ## [0.3.0] - 2026-10-06
18
+
19
+ ### Changed
20
+
21
+ - Feat/cursor pet display
22
+
23
+
10
24
  ## [0.2.3] - 2026-10-06
11
25
 
12
26
  ### Changed
@@ -5,4 +5,4 @@ export type EvolutionArtworkScene = {
5
5
  readonly progress: number;
6
6
  };
7
7
  export declare const renderEvolutionArtwork: (catalog: MonsterFrameCatalog, fromSprite: string, toSprite: string, scene: EvolutionArtworkScene, viewportWidth: number, tick?: number) => string;
8
- export declare const runEvolutionAnimation: (catalog: MonsterFrameCatalog, fromSprite: string, toSprite: string, viewportWidth: number, onFrame: (artwork: string) => Promise<void>) => Promise<void>;
8
+ export declare const runEvolutionAnimation: (catalog: MonsterFrameCatalog, fromSprite: string, toSprite: string, viewportWidth: number, onFrame: (artwork: string) => Promise<void>, onReveal?: () => Promise<void>) => Promise<void>;
@@ -8,6 +8,18 @@ export type EvolutionBattleShooter = "player" | "opponent";
8
8
  export type EvolutionBattleOutcome = EvolutionBattleShooter;
9
9
  export type BattleSpritePose = "attack" | "injured" | "happy" | "idle";
10
10
  export type EvolutionBattleResultLabel = "hit" | "miss" | null;
11
+ /** Scores and caption accompanying the shared frame; frontends choose their HUD style. */
12
+ export type BattleFrameHud = {
13
+ readonly playerHits: number;
14
+ readonly opponentHits: number;
15
+ readonly hitsToWin: number;
16
+ readonly caption: string | null;
17
+ readonly gapStartColumn: number;
18
+ readonly gapColumns: number;
19
+ readonly scoreRow: number;
20
+ readonly captionRow: number;
21
+ };
22
+ export type BattleFrameListener = (artwork: string, hud?: BattleFrameHud) => Promise<void>;
11
23
  export type EvolutionBattleShot = {
12
24
  readonly shooter: EvolutionBattleShooter;
13
25
  readonly hit: boolean;
@@ -30,4 +42,4 @@ export declare const planEvolutionBattle: (outcome: EvolutionBattleOutcome, rand
30
42
  export declare const defaultBattleScene: (overrides?: Partial<EvolutionBattleScene>) => EvolutionBattleScene;
31
43
  export declare const renderBattleIntroArtwork: (viewportWidth: number, _progress: number, tick?: number) => string;
32
44
  export declare const renderEvolutionBattleArtwork: (catalog: MonsterFrameCatalog, playerSprite: string, opponentSprite: string, scene: EvolutionBattleScene, viewportWidth: number) => string;
33
- export declare const runEvolutionBattleAnimation: (catalog: MonsterFrameCatalog, playerSprite: string, opponentSprite: string, viewportWidth: number, outcome: EvolutionBattleOutcome, onFrame: (artwork: string) => Promise<void>, random?: () => number) => Promise<EvolutionBattleOutcome>;
45
+ export declare const runEvolutionBattleAnimation: (catalog: MonsterFrameCatalog, playerSprite: string, opponentSprite: string, viewportWidth: number, outcome: EvolutionBattleOutcome, onFrame: BattleFrameListener, random?: () => number) => Promise<EvolutionBattleOutcome>;
@@ -3,12 +3,15 @@ import type { SidebarSnapshot } from "../../digital-pet-core/application/ports/s
3
3
  import { type EvolutionBattleRepository } from "../../digital-pet-core/application/use-cases/resolve-evolution-battle.js";
4
4
  import type { DigimonCatalog } from "../../digital-pet-core/data/catalog.js";
5
5
  import type { MonsterFrameCatalog } from "../../digital-pet-core/data/monster-frame-catalog.js";
6
+ import { type BattleFrameListener } from "../sequences/evolution-battle-artwork.js";
7
+ import type { PresentationStateListener } from "./presentation-state.js";
6
8
  export type EvolutionBattleSessionDependencies = {
7
9
  readonly frameCatalog: MonsterFrameCatalog;
8
10
  readonly digimonCatalog: DigimonCatalog;
9
11
  readonly repository: EvolutionBattleRepository;
10
12
  readonly random?: () => number;
11
- readonly onArtwork: (artwork: string) => Promise<void>;
13
+ readonly onState?: PresentationStateListener;
14
+ readonly onArtwork: BattleFrameListener;
12
15
  readonly onResolved?: (result: ResolveEvolutionBattleOutcome) => Promise<void>;
13
16
  };
14
17
  export declare const runEvolutionBattleSession: (snapshot: SidebarSnapshot, viewportWidth: number, dependencies: EvolutionBattleSessionDependencies) => Promise<boolean>;
@@ -1,9 +1,11 @@
1
1
  import type { UsageEvolutionTransition } from "../../digital-pet-core/application/models/usage.js";
2
2
  import type { DigimonCatalog } from "../../digital-pet-core/data/catalog.js";
3
3
  import type { MonsterFrameCatalog } from "../../digital-pet-core/data/monster-frame-catalog.js";
4
+ import type { PresentationStateListener } from "./presentation-state.js";
4
5
  export type EvolutionRevealSessionDependencies = {
5
6
  readonly frameCatalog: MonsterFrameCatalog;
6
7
  readonly digimonCatalog: DigimonCatalog;
8
+ readonly onState?: PresentationStateListener;
7
9
  readonly onArtwork: (artwork: string) => Promise<void>;
8
10
  };
9
11
  export declare const runEvolutionRevealSession: (evolution: UsageEvolutionTransition, viewportWidth: number, dependencies: EvolutionRevealSessionDependencies) => Promise<boolean>;
@@ -0,0 +1,16 @@
1
+ /** Semantic animation phases; each frontend chooses its own layout and styling. */
2
+ export type PresentationState = {
3
+ readonly phase: "idle";
4
+ } | {
5
+ readonly phase: "battle";
6
+ readonly fromNodeId: string;
7
+ readonly opponentNodeId: string;
8
+ } | {
9
+ readonly phase: "evolving" | "evolved";
10
+ readonly fromNodeId: string;
11
+ readonly toNodeId: string;
12
+ } | {
13
+ readonly phase: "defeated";
14
+ readonly fromNodeId: string;
15
+ };
16
+ export type PresentationStateListener = (state: PresentationState) => Promise<void>;
@@ -1,9 +1,11 @@
1
+ import type { PresentationState } from "../digital-pet-animation/sessions/presentation-state.js";
1
2
  import { type Accessor } from "solid-js";
2
3
  import type { SidebarCardModel } from "../digital-pet-core/view-models/sidebar-view-model.js";
3
4
  import type { MonsterAnimationOutput, MonsterAnimationResult } from "../digital-pet-animation/idle/monster-animation.js";
4
5
  export declare const DigitalPetSidebarCard: (props: {
5
6
  readonly model: Accessor<SidebarCardModel>;
6
7
  readonly animation: Accessor<MonsterAnimationOutput | MonsterAnimationResult>;
8
+ readonly presentationState?: Accessor<PresentationState>;
7
9
  readonly customArtwork?: Accessor<string | undefined>;
8
10
  readonly onArtworkWidthChange?: (width: number) => void;
9
11
  readonly onUrlClick?: (url: string) => void;
package/dist/tui.js CHANGED
@@ -7680,7 +7680,17 @@ var renderEvolutionBattleArtwork = (catalog, playerSprite, opponentSprite, scene
7680
7680
  return rows.map((row) => `${" ".repeat(padding)}${row}`).join("\n");
7681
7681
  };
7682
7682
  var postScene = async (catalog, playerSprite, opponentSprite, viewportWidth, scene, onFrame) => {
7683
- await onFrame(renderEvolutionBattleArtwork(catalog, playerSprite, opponentSprite, scene, viewportWidth));
7683
+ const battleWidth = MONSTER_FRAME_COLUMNS * 2 + BATTLE_GAP_COLUMNS;
7684
+ await onFrame(renderEvolutionBattleArtwork(catalog, playerSprite, opponentSprite, scene, viewportWidth), {
7685
+ playerHits: scene.playerHits,
7686
+ opponentHits: scene.opponentHits,
7687
+ hitsToWin: EVOLUTION_BATTLE_HITS_TO_WIN,
7688
+ caption: scene.outcomeLabel ?? (scene.lastResult === null ? null : scene.lastResult === "hit" ? "HIT!" : "MISS"),
7689
+ gapStartColumn: Math.max(Math.floor((viewportWidth - battleWidth) / 2), 0) + MONSTER_FRAME_COLUMNS,
7690
+ gapColumns: BATTLE_GAP_COLUMNS,
7691
+ scoreRow: 0,
7692
+ captionRow: MONSTER_FRAME_ROWS - 1
7693
+ });
7684
7694
  };
7685
7695
  var animateBattleIntro = async (viewportWidth, onFrame) => {
7686
7696
  const steps = Math.max(1, Math.ceil(EVOLUTION_BATTLE_INTRO_MS / EVOLUTION_BATTLE_TICK_MS));
@@ -7868,10 +7878,11 @@ var animatePhase = async (catalog, fromSprite, toSprite, viewportWidth, phase, d
7868
7878
  if (step < steps) await sleep(EVOLUTION_TICK_MS);
7869
7879
  }
7870
7880
  };
7871
- var runEvolutionAnimation = async (catalog, fromSprite, toSprite, viewportWidth, onFrame) => {
7881
+ var runEvolutionAnimation = async (catalog, fromSprite, toSprite, viewportWidth, onFrame, onReveal) => {
7872
7882
  await sleep(EVOLUTION_PRE_ANIMATION_MS);
7873
7883
  await animatePhase(catalog, fromSprite, toSprite, viewportWidth, "glow", EVOLUTION_GLOW_MS, onFrame);
7874
7884
  await animatePhase(catalog, fromSprite, toSprite, viewportWidth, "morph", EVOLUTION_MORPH_MS, onFrame);
7885
+ await onReveal?.();
7875
7886
  await animatePhase(catalog, fromSprite, toSprite, viewportWidth, "reveal", EVOLUTION_REVEAL_MS, onFrame);
7876
7887
  };
7877
7888
 
@@ -7880,12 +7891,16 @@ var runEvolutionRevealSession = async (evolution, viewportWidth, dependencies) =
7880
7891
  const from = dependencies.digimonCatalog.byId.get(evolution.fromNodeId);
7881
7892
  const to = dependencies.digimonCatalog.byId.get(evolution.toNodeId);
7882
7893
  if (from === void 0 || to === void 0) return false;
7894
+ await dependencies.onState?.({ phase: "evolving", ...evolution });
7883
7895
  await runEvolutionAnimation(
7884
7896
  dependencies.frameCatalog,
7885
7897
  from.sprite,
7886
7898
  to.sprite,
7887
7899
  viewportWidth,
7888
- async (artwork) => dependencies.onArtwork(artwork)
7900
+ dependencies.onArtwork,
7901
+ async () => {
7902
+ await dependencies.onState?.({ phase: "evolved", ...evolution });
7903
+ }
7889
7904
  );
7890
7905
  return true;
7891
7906
  };
@@ -7898,7 +7913,8 @@ var runEvolutionBattleSession = async (snapshot, viewportWidth, dependencies) =>
7898
7913
  if (player === void 0 || opponent === void 0) return false;
7899
7914
  const random = dependencies.random ?? Math.random;
7900
7915
  const outcome = random() < 0.5 ? "player" : "opponent";
7901
- const postArtwork = async (artwork) => dependencies.onArtwork(artwork);
7916
+ const postArtwork = async (artwork, hud) => dependencies.onArtwork(artwork, hud);
7917
+ await dependencies.onState?.({ phase: "battle", fromNodeId: player.id, opponentNodeId: opponent.id });
7902
7918
  await runEvolutionBattleAnimation(
7903
7919
  dependencies.frameCatalog,
7904
7920
  player.sprite,
@@ -7915,10 +7931,12 @@ var runEvolutionBattleSession = async (snapshot, viewportWidth, dependencies) =>
7915
7931
  {
7916
7932
  frameCatalog: dependencies.frameCatalog,
7917
7933
  digimonCatalog: dependencies.digimonCatalog,
7918
- onArtwork: postArtwork
7934
+ onArtwork: postArtwork,
7935
+ ...dependencies.onState === void 0 ? {} : { onState: dependencies.onState }
7919
7936
  }
7920
7937
  );
7921
7938
  } else {
7939
+ await dependencies.onState?.({ phase: "defeated", fromNodeId: player.id });
7922
7940
  await runDefeatAnimation(dependencies.frameCatalog, player.sprite, viewportWidth, postArtwork);
7923
7941
  }
7924
7942
  const result = resolveEvolutionBattleForPartner(
@@ -8958,13 +8976,18 @@ var DigitalPetSidebarCard = (props) => {
8958
8976
  updateText(gauge, "");
8959
8977
  updateText(url, "");
8960
8978
  return;
8961
- case "partner":
8979
+ case "partner": {
8962
8980
  updateText(name, model.isSetOverride ? `${model.name} (set)` : model.name);
8963
8981
  updateText(stage, model.frozen ? `${model.stage} (frozen)` : model.stage);
8964
- updateText(nextCheck, buildNextCheckLine(model, nextCheckWidth?.width ?? 0));
8982
+ const phase = props.presentationState?.().phase ?? "idle";
8983
+ updateText(
8984
+ nextCheck,
8985
+ phase === "idle" ? buildNextCheckLine(model, nextCheckWidth?.width ?? 0) : phase.toUpperCase()
8986
+ );
8965
8987
  updateText(gauge, model.isTerminal ? "-/-" : `${formatCount(model.gauge)}/${formatCount(model.threshold)}`);
8966
8988
  updateText(url, URL_LABEL);
8967
8989
  return;
8990
+ }
8968
8991
  }
8969
8992
  };
8970
8993
  createEffect(renderCard);
@@ -9429,6 +9452,7 @@ var createTui = (loadInputs, settings, scheduling = {}) => async (api) => {
9429
9452
  offset: 0,
9430
9453
  facing: "left"
9431
9454
  });
9455
+ const [presentationState, setPresentationState] = createSignal({ phase: "idle" });
9432
9456
  const [customArtwork, setCustomArtwork] = createSignal();
9433
9457
  const controller = new MonsterAnimationController(
9434
9458
  MONSTER_FRAME_CATALOG,
@@ -9458,6 +9482,15 @@ var createTui = (loadInputs, settings, scheduling = {}) => async (api) => {
9458
9482
  digimonCatalog: DIGIMON_CATALOG,
9459
9483
  repository: scheduling.battleRepository,
9460
9484
  ...scheduling.random === void 0 ? {} : { random: scheduling.random },
9485
+ onState: async (state) => {
9486
+ setPresentationState(state);
9487
+ if (state.phase === "evolved") {
9488
+ const current = inputs();
9489
+ const node = DIGIMON_CATALOG.byId.get(state.toNodeId);
9490
+ if (current.kind === "partner" && node !== void 0) setInputs({ ...current, node });
9491
+ }
9492
+ api.renderer.requestRender();
9493
+ },
9461
9494
  onArtwork: async (artwork) => {
9462
9495
  setCustomArtwork(artwork);
9463
9496
  api.renderer.requestRender();
@@ -9481,6 +9514,7 @@ var createTui = (loadInputs, settings, scheduling = {}) => async (api) => {
9481
9514
  return battled;
9482
9515
  } finally {
9483
9516
  battleInProgress = false;
9517
+ setPresentationState({ phase: "idle" });
9484
9518
  setCustomArtwork(void 0);
9485
9519
  }
9486
9520
  };
@@ -9498,6 +9532,15 @@ var createTui = (loadInputs, settings, scheduling = {}) => async (api) => {
9498
9532
  const revealed = await runEvolutionRevealSession(evolution, artworkWidth, {
9499
9533
  frameCatalog: MONSTER_FRAME_CATALOG,
9500
9534
  digimonCatalog: DIGIMON_CATALOG,
9535
+ onState: async (state) => {
9536
+ setPresentationState(state);
9537
+ if (state.phase === "evolved") {
9538
+ const current = inputs();
9539
+ const node = DIGIMON_CATALOG.byId.get(state.toNodeId);
9540
+ if (current.kind === "partner" && node !== void 0) setInputs({ ...current, node });
9541
+ }
9542
+ api.renderer.requestRender();
9543
+ },
9501
9544
  onArtwork: async (artwork) => {
9502
9545
  setCustomArtwork(artwork);
9503
9546
  api.renderer.requestRender();
@@ -9512,6 +9555,7 @@ var createTui = (loadInputs, settings, scheduling = {}) => async (api) => {
9512
9555
  return true;
9513
9556
  } finally {
9514
9557
  battleInProgress = false;
9558
+ setPresentationState({ phase: "idle" });
9515
9559
  setCustomArtwork(void 0);
9516
9560
  }
9517
9561
  };
@@ -9651,6 +9695,7 @@ var createTui = (loadInputs, settings, scheduling = {}) => async (api) => {
9651
9695
  model: () => buildSidebarCardModel(inputs(), settings),
9652
9696
  animation,
9653
9697
  customArtwork,
9698
+ presentationState,
9654
9699
  onArtworkWidthChange: (width) => {
9655
9700
  if (disposed) return;
9656
9701
  artworkWidth = width;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcendal/opencode-digital-pet",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "An OpenCode virtual-pet plugin with /digital-pet-spawn, /digital-pet-freeze, /digital-pet-unfreeze, /digital-pet-set, /digital-pet-dex, and /digital-pet-history commands.",
5
5
  "author": "Jorge Cendal",
6
6
  "contributors": [