Skip to content

Instantly share code, notes, and snippets.

@lukevanin
Created March 16, 2025 16:22
Show Gist options
  • Save lukevanin/67f0555271b9223bce7dc4c785c32f14 to your computer and use it in GitHub Desktop.
Save lukevanin/67f0555271b9223bce7dc4c785c32f14 to your computer and use it in GitHub Desktop.
Vibe coded animation state machine
import { getAnimationDebugInfo } from './engine';
// Animation state class
export class AnimationState {
context: AnimationContext | null = null;
private retryCount: number = 0;
private static MAX_RETRIES: number = 5;
private audioEffect: string | null = null;
private animationName: string;
private transitionDuration: number;
private animationSpeed: number; // Animation playback speed multiplier
private loop: boolean; // Whether the animation should loop
constructor(
animationName: string,
audioEffect: string | null = null,
transitionDuration: number = 0.2,
animationSpeed: number = 1.0,
loop: boolean = true
) {
this.animationName = animationName;
this.audioEffect = audioEffect;
this.transitionDuration = transitionDuration;
this.animationSpeed = animationSpeed;
this.loop = loop;
}
// Called when entering this state
enter(): void {
// Reset retry count on each enter
this.retryCount = 0;
this.tryPlayAnimation();
}
// Try to play the animation, with retry support
private tryPlayAnimation(): void {
// Get animation name for this state
const animName = this.getAnimationName();
// Check if animation exists before trying to play it
const engineInfo = getAnimationDebugInfo();
// Animation playing is now handled by the Actor class.
// The animation is triggered by the Actor's fadeToAction method when it
// calls animationContext.setAnimation()
console.log(`%c[STATE] Entering ${this.getAnimationName()} state`,
'background: #4CAF50; color: white; padding: 2px 4px; border-radius: 2px;');
}
// Called when exiting this state
exit(): void {
console.log(`%c[STATE] Exiting ${this.getAnimationName()} state`, 'background: #F44336; color: white; padding: 2px 4px; border-radius: 2px;');
}
// Get the animation name for this state
getAnimationName(): string {
return this.animationName;
}
// Get the transition duration for fading to this animation
getTransitionDuration(): number {
return this.transitionDuration;
}
// Update method called each frame when this state is active
update(deltaTime: number): void {
// Default implementation does nothing
}
// Get a descriptive name for this state for debugging
getStateName(): string {
return this.animationName;
}
// Set the audio effect for this state
setAudioEffect(effectName: string | null): void {
this.audioEffect = effectName;
}
// Get the audio effect for this state
getAudioEffect(): string | null {
return this.audioEffect;
}
// Get the animation speed
getAnimationSpeed(): number {
return this.animationSpeed;
}
// Set the animation speed
setAnimationSpeed(speed: number): void {
this.animationSpeed = speed;
}
// Get whether this animation should loop
getLoop(): boolean {
return this.loop;
}
// Set whether this animation should loop
setLoop(loop: boolean): void {
this.loop = loop;
}
}
// Interface for animation state collection
export interface AnimationStates {
[key: string]: AnimationState;
}
// Animation context manages the current animation state
export class AnimationContext {
currentAnimationState: AnimationState | null = null;
private debugInterval: number | null = null;
private debugCounter: number = 0;
private animationStates: AnimationStates;
constructor(animationStates: AnimationStates) {
this.animationStates = animationStates;
// Setup periodic debug output every ~3 seconds
this.debugInterval = window.setInterval(() => this.debugCurrentState(), 3000);
}
// Get an animation state by name
getState(stateName: string): AnimationState {
const state = this.animationStates[stateName];
if (!state) {
throw new Error(`Animation state "${stateName}" not found`);
}
return state;
}
// Get all available animation states
getStates(): AnimationStates {
return this.animationStates;
}
// Change to a new animation state
setAnimation(newAnimationState: AnimationState): void {
// Don't change if it's the same state type
if (this.currentAnimationState &&
this.currentAnimationState.getAnimationName() === newAnimationState.getAnimationName()) {
return;
}
console.log(`%c[TRANSITION] ${this.currentAnimationState ? this.currentAnimationState.getStateName() : 'None'} -> ${newAnimationState.getStateName()} (Animation: ${newAnimationState.getAnimationName()})`,
'background: #2196F3; color: white; padding: 2px 4px; border-radius: 2px;');
// Exit current animation state if it exists
if (this.currentAnimationState) {
this.currentAnimationState.exit();
this.currentAnimationState.context = null;
}
// Set new animation state
this.currentAnimationState = newAnimationState;
// Set context on the new animation state
this.currentAnimationState.context = this;
// Enter the new animation state
this.currentAnimationState.enter();
// Immediately show debug info on state change
this.debugCurrentState();
}
// Set animation by state name
setAnimationByName(stateName: string): void {
// Try to get the state directly by name
try {
const state = this.getState(stateName);
this.setAnimation(state);
} catch (error) {
// If not found by state name, try to find a state with matching animation name
console.log(`Failed to find state by name "${stateName}", trying to find by animation name`);
const states = this.getStates();
const matchingStateKey = Object.keys(states).find(key =>
states[key].getAnimationName() === stateName
);
if (matchingStateKey) {
console.log(`Found state "${matchingStateKey}" with animation "${stateName}"`);
this.setAnimation(states[matchingStateKey]);
} else {
// Still not found, throw error
throw new Error(`Animation state "${stateName}" not found`);
}
}
}
// Get the current animation state
getAnimationState(): AnimationState | null {
return this.currentAnimationState;
}
// Update the current animation state
update(deltaTime: number): void {
if (this.currentAnimationState) {
this.currentAnimationState.update(deltaTime);
// Debug output every ~100 frames to avoid console spam
this.debugCounter++;
if (this.debugCounter >= 100) {
this.debugCounter = 0;
// Uncomment to enable frequent updates
// this.debugCurrentState();
}
}
}
// Display current state for debugging
debugCurrentState(): void {
if (this.currentAnimationState) {
console.log(
`%c[DEBUG] Current State: ${this.currentAnimationState.getStateName()} | ` +
`Animation: ${this.currentAnimationState.getAnimationName()}`,
'background: #9C27B0; color: white; padding: 2px 4px; border-radius: 2px;'
);
} else {
console.log('%c[DEBUG] No active animation state', 'background: #9C27B0; color: white; padding: 2px 4px; border-radius: 2px;');
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment