Created
March 16, 2025 16:22
-
-
Save lukevanin/67f0555271b9223bce7dc4c785c32f14 to your computer and use it in GitHub Desktop.
Vibe coded animation state machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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