Skip to content

Commit c04111c

Browse files
committed
Add React random pet actions
1 parent 122a4be commit c04111c

5 files changed

Lines changed: 118 additions & 3 deletions

File tree

apps/demo/src/main.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react";
22
import { createRoot } from "react-dom/client";
3-
import { CODEX_PET_STATES, type CodexPetManifest, type CodexPetState } from "codex-pet-web";
3+
import {
4+
CODEX_PET_STATES,
5+
createCodexPetRandomActionRunner,
6+
type CodexPetManifest,
7+
type CodexPetSnapshot,
8+
type CodexPetState
9+
} from "codex-pet-web";
410
import { CodexPet, type CodexPetHandle } from "codex-pet-web-react";
511
import "./styles.css";
612

@@ -28,7 +34,7 @@ function App() {
2834
const [pets, setPets] = useState<DemoPet[]>([]);
2935
const [selectedPetId, setSelectedPetId] = useState("");
3036
const [state, setState] = useState<CodexPetState>("idle");
31-
const [scale, setScale] = useState(0.5);
37+
const [scale, setScale] = useState(0.45);
3238
const [fps, setFps] = useState(8);
3339
const [paused, setPaused] = useState(false);
3440
const [reducedMotion, setReducedMotion] = useState(false);
@@ -59,6 +65,47 @@ function App() {
5965
});
6066
}, []);
6167

68+
useEffect(() => {
69+
const runner = createCodexPetRandomActionRunner(
70+
{
71+
getSnapshot: () =>
72+
petRef.current?.getSnapshot() ??
73+
({
74+
id: "preview",
75+
spritesheetUrl: "",
76+
state: "idle",
77+
baseState: "idle",
78+
frame: 0,
79+
scale,
80+
fps,
81+
position: null,
82+
hidden: false,
83+
mounted: false,
84+
paused,
85+
removed: false
86+
} satisfies CodexPetSnapshot),
87+
play: (nextState, options) => petRef.current?.play(nextState, options)
88+
},
89+
{
90+
averageIntervalSeconds: 120,
91+
minIntervalSeconds: 45,
92+
maxIntervalSeconds: 300,
93+
actions: [
94+
{ state: "waving", weight: 3 },
95+
{ state: "jumping", weight: 2 },
96+
{ state: "waiting", weight: 1 },
97+
{ state: "review", weight: 1 }
98+
]
99+
}
100+
);
101+
102+
runner.start();
103+
104+
return () => {
105+
runner.destroy();
106+
};
107+
}, []);
108+
62109
const selectedPet = useMemo(
63110
() => pets.find((pet) => pet.id === selectedPetId) ?? pets[0],
64111
[pets, selectedPetId]

apps/demo/test/defaults.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("demo defaults", () => {
1515
);
1616

1717
it("starts pets at a compact size", () => {
18-
expect(source).toContain("useState(0.5)");
18+
expect(source).toContain("useState(0.45)");
1919
});
2020

2121
it("keeps idle calmer while action states stay responsive", () => {
@@ -45,4 +45,10 @@ describe("demo defaults", () => {
4545
expect(localScript).toContain('"packages", "core", "example-pets"');
4646
expect(localScript).toContain('".codex", "pets"');
4747
});
48+
49+
it("runs occasional idle-only random actions", () => {
50+
expect(source).toContain("createCodexPetRandomActionRunner");
51+
expect(source).toContain("averageIntervalSeconds: 120");
52+
expect(source).toContain('{ state: "waving", weight: 3 }');
53+
});
4854
});

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export {
55
useCodexPet
66
} from "./CodexPetProvider.js";
77
export type { CodexPetProviderProps } from "./CodexPetProvider.js";
8+
export { useCodexPetRandomActions } from "./useCodexPetRandomActions.js";
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
createCodexPetRandomActionRunner,
3+
type CodexPetId,
4+
type CodexPetRandomActionRunnerOptions
5+
} from "codex-pet-web";
6+
import { useEffect } from "react";
7+
import { useCodexPet } from "./CodexPetProvider.js";
8+
9+
export function useCodexPetRandomActions(
10+
id: CodexPetId | undefined,
11+
options: CodexPetRandomActionRunnerOptions
12+
): void {
13+
const pet = useCodexPet(id);
14+
15+
useEffect(() => {
16+
const runner = createCodexPetRandomActionRunner(pet, options);
17+
runner.start();
18+
19+
return () => {
20+
runner.destroy();
21+
};
22+
}, [pet, options]);
23+
}

packages/react/test/CodexPet.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CodexPet,
66
CodexPetProvider,
77
useCodexPet,
8+
useCodexPetRandomActions,
89
type CodexPetHandle
910
} from "../src/index";
1011
import type { CodexPetController, CodexPetManifest } from "codex-pet-web";
@@ -462,4 +463,41 @@ describe("CodexPet", () => {
462463

463464
unmount();
464465
});
466+
467+
it("runs random provider actions through the React hook", () => {
468+
function AmbientActions() {
469+
useCodexPetRandomActions("assistant", {
470+
averageIntervalSeconds: 120,
471+
minIntervalSeconds: 45,
472+
maxIntervalSeconds: 300,
473+
actions: [{ state: "waving", weight: 1 }],
474+
random: () => 0
475+
});
476+
477+
return null;
478+
}
479+
480+
const { host, unmount } = render(
481+
<CodexPetProvider
482+
pets={{
483+
assistant: {
484+
spritesheetUrl: "/pets/sapling/spritesheet.webp"
485+
}
486+
}}
487+
>
488+
<AmbientActions />
489+
<CodexPet id="assistant" />
490+
</CodexPetProvider>
491+
);
492+
493+
const pet = host.querySelector("[data-codex-pet='assistant']") as HTMLDivElement;
494+
495+
act(() => {
496+
vi.advanceTimersByTime(45_000);
497+
});
498+
499+
expect(pet.style.backgroundPosition).toBe("0px -624px");
500+
501+
unmount();
502+
});
465503
});

0 commit comments

Comments
 (0)