rive-react-setup
<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->
Agent definition
rive-react-setup.mdRive React Setup Reference
<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->
`@rive-app/react-canvas` — React wrapper over Rive Web runtime. Handles canvas lifecycle, WASM loading, resize. Version 4.x supports React 16.8–19. WASM bundle ~150KB gzip.
Installation
npm install @rive-app/react-canvas
WASM bundled in npm package, loaded at runtime on first mount.
Basic useRive Pattern
import { useRive } from '@rive-app/react-canvas';
function PlayerCharacter() {
const { RiveComponent, rive } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
});
// rive is null until the .riv file loads — always guard before use
useEffect(() => {
if (!rive) return;
// rive instance is ready here
}, [rive]);
return (
<div style={{ width: 400, height: 400, position: 'relative' }}>
<RiveComponent />
</div>
);
}`RiveComponent` fills its container. Wrap in explicit-sized div. Do not set width/height on `RiveComponent` directly.
useRive Parameters
useRive({
src: string, // path to .riv file, or ArrayBuffer
artboard?: string, // specific artboard (default: first artboard)
animations?: string | string[], // animation clip names to play directly (no state machine)
stateMachines?: string | string[], // state machine name(s) to activate
autoplay?: boolean, // default: false — set true for immediate playback
layout?: Layout, // canvas fit/alignment — import Layout from @rive-app/react-canvas
onLoad?: () => void, // fires when .riv finishes loading
onStateChange?: (event: StateChangeEvent) => void, // fires on state machine transition
onPlay?: () => void,
onPause?: () => void,
onStop?: () => void,
})`stateMachines` name is case-sensitive, must match Rive Editor exactly.
Return Values
const {
rive, // Rive instance — null until loaded
RiveComponent, // JSX element — mount this in render
setCanvasRef, // ref setter for custom canvas placement
setContainerRef, // ref setter for custom container placement
} = useRive(params);`rive` exposes Web runtime API: `rive.play()`, `rive.pause()`, `rive.reset()`, `rive.stop()`.
State Machine Inputs
`useStateMachineInput` returns a reference to a named input from the active state machine.
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
const SM = 'CombatStateMachine'; // exact name from Rive Editor
function PlayerCharacter() {
const { RiveComponent, rive } = useRive({
src: playerRiv,
stateMachines: SM,
autoplay: true,
});
// Trigger — one-shot event (attack, hit, signature)
const attackTrigger = useStateMachineInput(rive, SM, 'attack');
const hitTrigger = useStateMachineInput(rive, SM, 'hit');
// Boolean — sustained state (blocking, stunned)
const blockInput = useStateMachineInput(rive, SM, 'isBlocking');
// Number — health, charge level, anger meter
const healthInput = useStateMachineInput(rive, SM, 'health');
// All inputs are null until rive loads — always guard
const fireAttack = () => { if (attackTrigger) attackTrigger.fire(); };
const setBlock = (v: boolean) => { if (blockInput) blockInput.value = v; };
const setHealth = (hp: number) => { if (healthInput) healthInput.value = hp; };
return <div style={{ width: 400, height: 400 }}><RiveComponent /></div>;
}Input names are case-sensitive, must match Rive Editor. Mismatch returns `null` silently — log return values in dev to catch typos.
Input Object Types
| Input type | Hook return type | API | |------------|-----------------|-----| | Trigger | `SMITrigger \| null` | `.fire()` — one-shot event | | Boolean | `SMIBoolean \| null` | `.value` (get/set boolean) | | Number | `SMINumber \| null` | `.value` (get/set number) |
Replacing img + motion.div
**Before:**
<motion.div
animate={{ y: [0, -3, 0] }}
transition={{ duration: 4, repeat: Infinity }}
style={{ width: 400, height: 400 }}
>
<img src="/sprites/player.png" alt="player" style={{ width: '100%' }} />
</motion.div>**After (Rive):**
<div style={{ width: 400, height: 400 }}>
<RiveComponent />
</div>Remove `motion.div` entirely. Idle bob lives in the `.riv` file. Do not wrap `RiveComponent` in `motion.div`.
Wiring to Zustand Combat Store
Zustand is source of truth. Bridge to Rive inputs via `useEffect`. Never call `input.fire()` inside Zustand actions — keep Rive coupling in the component.
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
import { useCombatStore } from '../stores/combatStore';
import playerRiv from '../assets/characters/player.riv?url';
const SM = 'CombatStateMachine';
function PlayerCharacter() {
const { RiveComponent, rive } = useRive({ src: playerRiv, stateMachines: SM, autoplay: true });
const attackTrigger = useStateMachineInput(rive, SM, 'attack');
const hitTrigger = useStateMachineInput(rive, SM, 'hit');
const blockInput = useStateMachineInput(rive, SM, 'isBlocking');
// Subscribe only to what this component needs
const lastAction = useCombatStore(s => s.playerLastAction);
const isBlocking = useCombatStore(s => s.playerIsBlocking);
useEffect(() => {
if (lastAction.type === 'attack' && attackTrigger) attackTrigger.fire();
if (lastAction.type === 'hit' && hitTrigger) hitTrigger.fire();
}, [lastAction, attackTrigger, hitTrigger]);
useEffect(() => {
if (blockInput) blockInput.value = isBlocking;
}, [isBlocking, blockInput]);
return <div style={{ width: 400, height: 400 }}><RiveComponent /></div>;
}Timestamp trick for re-triggering same action type
`useEffect` won't re-fire for consecutive identica
Read more
Rive React Setup Reference
<!-- Loaded by rive-skeletal-animator when task involves: installing Rive, mounting canvas, useRive hook, useStateMachineInput, Zustand wiring, CombatEngine events, lazy loading, Vite WASM config -->
`@rive-app/react-canvas` — React wrapper over Rive Web runtime. Handles canvas lifecycle, WASM loading, resize. Version 4.x supports React 16.8–19. WASM bundle ~150KB gzip.
Installation
npm install @rive-app/react-canvas
WASM bundled in npm package, loaded at runtime on first mount.
Basic useRive Pattern
import { useRive } from '@rive-app/react-canvas';
function PlayerCharacter() {
const { RiveComponent, rive } = useRive({
src: '/assets/characters/player.riv',
stateMachines: 'CombatStateMachine',
autoplay: true,
});
// rive is null until the .riv file loads — always guard before use
useEffect(() => {
if (!rive) return;
// rive instance is ready here
}, [rive]);
return (
<div style={{ width: 400, height: 400, position: 'relative' }}>
<RiveComponent />
</div>
);
}`RiveComponent` fills its container. Wrap in explicit-sized div. Do not set width/height on `RiveComponent` directly.
useRive Parameters
useRive({
src: string, // path to .riv file, or ArrayBuffer
artboard?: string, // specific artboard (default: first artboard)
animations?: string | string[], // animation clip names to play directly (no state machine)
stateMachines?: string | string[], // state machine name(s) to activate
autoplay?: boolean, // default: false — set true for immediate playback
layout?: Layout, // canvas fit/alignment — import Layout from @rive-app/react-canvas
onLoad?: () => void, // fires when .riv finishes loading
onStateChange?: (event: StateChangeEvent) => void, // fires on state machine transition
onPlay?: () => void,
onPause?: () => void,
onStop?: () => void,
})`stateMachines` name is case-sensitive, must match Rive Editor exactly.
Return Values
const {
rive, // Rive instance — null until loaded
RiveComponent, // JSX element — mount this in render
setCanvasRef, // ref setter for custom canvas placement
setContainerRef, // ref setter for custom container placement
} = useRive(params);`rive` exposes Web runtime API: `rive.play()`, `rive.pause()`, `rive.reset()`, `rive.stop()`.
State Machine Inputs
`useStateMachineInput` returns a reference to a named input from the active state machine.
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
const SM = 'CombatStateMachine'; // exact name from Rive Editor
function PlayerCharacter() {
const { RiveComponent, rive } = useRive({
src: playerRiv,
stateMachines: SM,
autoplay: true,
});
// Trigger — one-shot event (attack, hit, signature)
const attackTrigger = useStateMachineInput(rive, SM, 'attack');
const hitTrigger = useStateMachineInput(rive, SM, 'hit');
// Boolean — sustained state (blocking, stunned)
const blockInput = useStateMachineInput(rive, SM, 'isBlocking');
// Number — health, charge level, anger meter
const healthInput = useStateMachineInput(rive, SM, 'health');
// All inputs are null until rive loads — always guard
const fireAttack = () => { if (attackTrigger) attackTrigger.fire(); };
const setBlock = (v: boolean) => { if (blockInput) blockInput.value = v; };
const setHealth = (hp: number) => { if (healthInput) healthInput.value = hp; };
return <div style={{ width: 400, height: 400 }}><RiveComponent /></div>;
}Input names are case-sensitive, must match Rive Editor. Mismatch returns `null` silently — log return values in dev to catch typos.
Input Object Types
| Input type | Hook return type | API | |------------|-----------------|-----| | Trigger | `SMITrigger \| null` | `.fire()` — one-shot event | | Boolean | `SMIBoolean \| null` | `.value` (get/set boolean) | | Number | `SMINumber \| null` | `.value` (get/set number) |
Replacing img + motion.div
**Before:**
<motion.div
animate={{ y: [0, -3, 0] }}
transition={{ duration: 4, repeat: Infinity }}
style={{ width: 400, height: 400 }}
>
<img src="/sprites/player.png" alt="player" style={{ width: '100%' }} />
</motion.div>**After (Rive):**
<div style={{ width: 400, height: 400 }}>
<RiveComponent />
</div>Remove `motion.div` entirely. Idle bob lives in the `.riv` file. Do not wrap `RiveComponent` in `motion.div`.
Wiring to Zustand Combat Store
Zustand is source of truth. Bridge to Rive inputs via `useEffect`. Never call `input.fire()` inside Zustand actions — keep Rive coupling in the component.
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
import { useCombatStore } from '../stores/combatStore';
import playerRiv from '../assets/characters/player.riv?url';
const SM = 'CombatStateMachine';
function PlayerCharacter() {
const { RiveComponent, rive } = useRive({ src: playerRiv, stateMachines: SM, autoplay: true });
const attackTrigger = useStateMachineInput(rive, SM, 'attack');
const hitTrigger = useStateMachineInput(rive, SM, 'hit');
const blockInput = useStateMachineInput(rive, SM, 'isBlocking');
// Subscribe only to what this component needs
const lastAction = useCombatStore(s => s.playerLastAction);
const isBlocking = useCombatStore(s => s.playerIsBlocking);
useEffect(() => {
if (lastAction.type === 'attack' && attackTrigger) attackTrigger.fire();
if (lastAction.type === 'hit' && hitTrigger) hitTrigger.fire();
}, [lastAction, attackTrigger, hitTrigger]);
useEffect(() => {
if (blockInput) blockInput.value = isBlocking;
}, [isBlocking, blockInput]);
return <div style={{ width: 400, height: 400 }}><RiveComponent /></div>;
}Timestamp trick for re-triggering same action type
`useEffect` won't re-fire for consecutive identica
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

