Refactor code for consistency and readability

- Updated import statements to use consistent formatting across files.
- Adjusted method definitions and class constructors for uniform spacing and style.
- Simplified promise handling and error messages in state handlers.
- Enhanced state transition logic in various state handlers.
- Improved quirk animation handling in WaitStateHandler.
- Streamlined animation loading and caching mechanisms in AnimationLoader.
- Updated Vite configuration for aliasing.
This commit is contained in:
2025-05-24 01:14:35 +02:00
parent 658e1e64b2
commit bb28337d94
23 changed files with 4216 additions and 1281 deletions

View File

@ -1,70 +0,0 @@
{
"extends": [
"eslint:recommended"
],
"env": {
"browser": true,
"es2022": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"no-console": [
"warn"
],
"max-len": [
"warn",
{
"code": 120
}
]
},
"overrides": [
{
"files": [
"examples/**/*.js"
],
"rules": {
"no-console": "off"
}
},
{
"files": [
"src/**/*.js"
],
"rules": {
"no-console": "off"
}
}
],
"ignorePatterns": [
"node_modules/",
"dist/",
"docs/"
]
}

41
.gitignore vendored
View File

@ -1,3 +1,30 @@
# Temporary files
*.tmp
*.temp
# Documentation output
/docs/
# Animation assets (if storing locally)
/assets/models/
/assets/animations/
# Example build outputs
/examples/dist/
# 3D Models (optional - remove if you want to commit models)
*.gltf
*.glb
*.fbx
*.obj
*.dae
# Three.js cache
.three-cache/
# Editor (optional - remove if you want to commit editor files)
.vscode/
# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node # Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node # Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node
@ -418,17 +445,3 @@ $RECYCLE.BIN/
*.lnk *.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node # End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,webstorm,vim,emacs,node
# Build outputs
docs/
# Temporary files
*.tmp
*.temp
# 3D Models (optional - remove if you want to commit models)
*.gltf
*.glb
*.fbx
*.obj
*.dae

View File

@ -5,74 +5,89 @@ All notable changes to the Owen Animation System will be documented in this file
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.1] - 2025-05-24
### Changed
- 🎨 Standardized code style throughout the codebase
- 🔧 Converted semicolons to non-semicolons according to JavaScript Standard Style
- 📝 Improved code consistency and readability
## [1.0.0] - 2025-05-23 ## [1.0.0] - 2025-05-23
### Added ### Added
- 🎉 Initial release of Owen Animation System
- ✨ Complete state machine implementation with Wait, React, Type, and Sleep states - 🎉 Initial release of Owen Animation System
- 🤖 Emotional response system for character animations - ✨ Complete state machine implementation with Wait, React, Type, and Sleep states
- 🏗️ Clean architecture with dependency injection and factory patterns - 🤖 Emotional response system for character animations
- 📝 Animation naming convention parser - 🏗️ Clean architecture with dependency injection and factory patterns
- 🔄 Smooth animation transitions with fade in/out support - 📝 Animation naming convention parser
- ⚡ Performance-optimized animation caching - 🔄 Smooth animation transitions with fade in/out support
- 🧩 Extensible design for custom states and emotions - ⚡ Performance-optimized animation caching
- 📊 Comprehensive JSDoc documentation - 🧩 Extensible design for custom states and emotions
- 🎮 Interactive demo with keyboard controls - 📊 Comprehensive JSDoc documentation
- 📦 TypeScript type definitions - 🎮 Interactive demo with keyboard controls
- 🛠️ Development tooling (ESLint, Vite, JSDoc) - 📦 TypeScript type definitions
- 🛠️ Development tooling (ESLint, Vite, JSDoc)
[1.0.1]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.1
### Architecture ### Architecture
- **Core Classes:**
- `OwenAnimationContext` - Main system controller
- `AnimationClip` - Individual animation management
- `AnimationClipFactory` - Animation creation with metadata parsing
- `StateHandler` - Abstract base for state implementations
- `StateFactory` - Dynamic state handler creation
- **State Handlers:** - **Core Classes:**
- `WaitStateHandler` - Idle state with quirk animations - `OwenAnimationContext` - Main system controller
- `ReactStateHandler` - User input response with emotion analysis - `AnimationClip` - Individual animation management
- `TypeStateHandler` - Typing state with emotional variations - `AnimationClipFactory` - Animation creation with metadata parsing
- `SleepStateHandler` - Inactive state management - `StateHandler` - Abstract base for state implementations
- `StateFactory` - Dynamic state handler creation
- **Animation Loaders:** - **State Handlers:**
- `AnimationLoader` - Abstract animation loading interface - `WaitStateHandler` - Idle state with quirk animations
- `GLTFAnimationLoader` - GLTF/GLB model animation loader - `ReactStateHandler` - User input response with emotion analysis
- `TypeStateHandler` - Typing state with emotional variations
- `SleepStateHandler` - Inactive state management
- **Factories:** - **Animation Loaders:**
- `OwenSystemFactory` - Main system assembly factory - `AnimationLoader` - Abstract animation loading interface
- `GLTFAnimationLoader` - GLTF/GLB model animation loader
- **Factories:**
- `OwenSystemFactory` - Main system assembly factory
### Features ### Features
- **Animation System:**
- Support for Loop (L), Quirk (Q), Transition (T), and Nested animations
- Automatic metadata parsing from animation names
- Efficient animation caching and resource management
- Smooth transitions between states and emotions
- **State Machine:** - **Animation System:**
- Four core states: Wait, React, Type, Sleep - Support for Loop (L), Quirk (Q), Transition (T), and Nested animations
- Emotional state transitions (Neutral, Angry, Shocked, Happy, Sad) - Automatic metadata parsing from animation names
- Automatic inactivity detection and sleep transitions - Efficient animation caching and resource management
- Message analysis for emotional response determination - Smooth transitions between states and emotions
- **Developer Experience:** - **State Machine:**
- Comprehensive TypeScript type definitions - Four core states: Wait, React, Type, Sleep
- JSDoc documentation for all public APIs - Emotional state transitions (Neutral, Angry, Shocked, Happy, Sad)
- Example implementations and demos - Automatic inactivity detection and sleep transitions
- ESLint configuration for code quality - Message analysis for emotional response determination
- Vite development server setup
- **Developer Experience:**
- Comprehensive TypeScript type definitions
- JSDoc documentation for all public APIs
- Example implementations and demos
- ESLint configuration for code quality
- Vite development server setup
### Documentation ### Documentation
- Complete README with installation and usage instructions
- API documentation via JSDoc - Complete README with installation and usage instructions
- Code examples for basic and advanced usage - API documentation via JSDoc
- Animation naming convention guide - Code examples for basic and advanced usage
- Troubleshooting section - Animation naming convention guide
- Troubleshooting section
### Examples ### Examples
- Basic browser demo with Three.js integration
- Simple Node.js example for testing
- Interactive controls for state transitions
- Mock model implementation for development
[1.0.0]: https://github.com/your-username/owen-animation-system/releases/tag/v1.0.0 - Basic browser demo with Three.js integration
- Simple Node.js example for testing
- Interactive controls for state transitions
- Mock model implementation for development
[1.0.0]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.0

View File

@ -2,6 +2,10 @@
A comprehensive Three.js animation system for character state management with clean architecture principles, dependency injection, and factory patterns. A comprehensive Three.js animation system for character state management with clean architecture principles, dependency injection, and factory patterns.
[![Gitea Issues](https://img.shields.io/gitea/issues/all/kjanat/Owen?gitea_url=https%3A%2F%2Fgitea.kajkowalski.nl%2F&style=social)](https://gitea.kajkowalski.nl/kjanat/Owen/issues)
[![Gitea Pull Requests](https://img.shields.io/gitea/pull-requests/all/kjanat/Owen?gitea_url=https%3A%2F%2Fgitea.kajkowalski.nl%2F&style=social)](https://gitea.kajkowalski.nl/kjanat/Owen/pulls)
[![Gitea Release](https://img.shields.io/gitea/v/release/kjanat/Owen?gitea_url=https%3A%2F%2Fgitea.kajkowalski.nl&include_prereleases&sort=semver&style=social)](https://gitea.kajkowalski.nl/kjanat/Owen/tags)
## 🎯 Overview ## 🎯 Overview
The Owen Animation System is a sophisticated character animation framework built for Three.js that manages complex state machines, emotional responses, and animation transitions. It's designed with clean architecture principles to be maintainable, extensible, and testable. The Owen Animation System is a sophisticated character animation framework built for Three.js that manages complex state machines, emotional responses, and animation transitions. It's designed with clean architecture principles to be maintainable, extensible, and testable.
@ -181,7 +185,6 @@ Owen/
│ └── basic-demo.js # Basic usage example │ └── basic-demo.js # Basic usage example
├── package.json ├── package.json
├── vite.config.js ├── vite.config.js
├── .eslintrc.json
├── jsdoc.config.json ├── jsdoc.config.json
└── README.md └── README.md
``` ```
@ -228,8 +231,8 @@ Documentation will be generated in the `docs/` directory.
- `npm run dev` - Start development server - `npm run dev` - Start development server
- `npm run build` - Build for production - `npm run build` - Build for production
- `npm run preview` - Preview production build - `npm run preview` - Preview production build
- `npm run lint` - Run ESLint - `npm run lint` - Run StandardJS linting
- `npm run lint:fix` - Fix ESLint issues - `npm run lint:fix` - Fix StandardJS issues
- `npm run docs` - Generate JSDoc documentation - `npm run docs` - Generate JSDoc documentation
## 🎮 Demo Controls ## 🎮 Demo Controls

View File

@ -1,332 +0,0 @@
/**
* @fileoverview Basic example of using the Owen Animation System
* @author Owen Animation System
*/
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OwenSystemFactory, States } from '../src/index.js';
/**
* Basic Owen Animation System demo
* @class
*/
class OwenDemo {
/**
* Create the demo
*/
constructor() {
/**
* The Three.js scene
* @type {THREE.Scene}
*/
this.scene = new THREE.Scene();
/**
* The Three.js camera
* @type {THREE.PerspectiveCamera}
*/
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
/**
* The Three.js renderer
* @type {THREE.WebGLRenderer}
*/
this.renderer = new THREE.WebGLRenderer({ antialias: true });
/**
* The Owen animation system
* @type {OwenAnimationContext|null}
*/
this.owenSystem = null;
/**
* Clock for tracking time
* @type {THREE.Clock}
*/
this.clock = new THREE.Clock();
}
/**
* Initialize the demo
* @returns {Promise<void>}
*/
async init() {
// Setup renderer
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setClearColor(0x1a1a1a);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(this.renderer.domElement);
// Setup camera
this.camera.position.set(0, 1.6, 3);
this.camera.lookAt(0, 1, 0);
// Add lighting
this.setupLighting();
// Load Owen model (replace with your model path)
await this.loadOwenModel();
// Setup event listeners
this.setupEventListeners();
// Start render loop
this.animate();
console.log('Owen Demo initialized');
}
/**
* Setup scene lighting
* @private
* @returns {void}
*/
setupLighting() {
// Ambient light
const ambientLight = new THREE.AmbientLight(0x404040, 0.4);
this.scene.add(ambientLight);
// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
this.scene.add(directionalLight);
// Fill light
const fillLight = new THREE.DirectionalLight(0x8bb7f0, 0.3);
fillLight.position.set(-5, 5, -5);
this.scene.add(fillLight);
}
/**
* Load the Owen character model
* @private
* @returns {Promise<void>}
*/
async loadOwenModel() {
try {
const loader = new GLTFLoader();
// Replace 'path/to/owen.gltf' with your actual model path
const gltf = await new Promise((resolve, reject) => {
loader.load(
'path/to/owen.gltf', // Update this path
resolve,
(progress) => console.log('Loading progress:', progress.loaded / progress.total * 100 + '%'),
reject
);
});
const model = gltf.scene;
model.position.set(0, 0, 0);
model.scale.setScalar(1);
// Enable shadows
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.scene.add(model);
// Create Owen animation system
this.owenSystem = await OwenSystemFactory.createOwenSystem(gltf, this.scene);
console.log('Owen model loaded and animation system created');
this.logSystemInfo();
} catch (error) {
console.error('Error loading Owen model:', error);
// Create a placeholder cube for demo purposes
this.createPlaceholderModel();
}
}
/**
* Create a placeholder model for demo purposes
* @private
* @returns {void}
*/
createPlaceholderModel() {
const geometry = new THREE.BoxGeometry(1, 2, 1);
const material = new THREE.MeshPhongMaterial({ color: 0x6699ff });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 1, 0);
cube.castShadow = true;
cube.receiveShadow = true;
this.scene.add(cube);
console.log('Created placeholder model (cube)');
}
/**
* Setup event listeners for user interaction
* @private
* @returns {void}
*/
setupEventListeners() {
// Keyboard controls
document.addEventListener('keydown', (event) => {
if (!this.owenSystem) return;
switch (event.key) {
case '1':
this.owenSystem.transitionTo(States.WAIT);
break;
case '2':
this.owenSystem.transitionTo(States.REACT);
break;
case '3':
this.owenSystem.transitionTo(States.TYPE);
break;
case '4':
this.owenSystem.transitionTo(States.SLEEP);
break;
case ' ':
this.sendTestMessage();
break;
}
});
// Mouse interaction
document.addEventListener('click', () => {
if (this.owenSystem) {
this.owenSystem.onUserActivity();
}
});
// Window resize
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
// Add instructions to the page
this.addInstructions();
}
/**
* Add on-screen instructions
* @private
* @returns {void}
*/
addInstructions() {
const instructions = document.createElement('div');
instructions.innerHTML = `
<div style="position: absolute; top: 10px; left: 10px; color: white; ` +
`font-family: monospace; font-size: 14px; line-height: 1.4;">
<h3>Owen Animation System Demo</h3>
<p><strong>Controls:</strong></p>
<p>1 - Wait State</p>
<p>2 - React State</p>
<p>3 - Type State</p>
<p>4 - Sleep State</p>
<p>Space - Send Test Message</p>
<p>Click - User Activity</p>
<br>
<p><strong>Current State:</strong> <span id="current-state">-</span></p>
<p><strong>Available Transitions:</strong> <span id="transitions">-</span></p>
</div>
`;
document.body.appendChild(instructions);
}
/**
* Send a test message to Owen
* @private
* @returns {void}
*/
sendTestMessage() {
if (!this.owenSystem) return;
const testMessages = [
'Hello Owen!',
'How are you doing?',
'This is urgent!',
'Great work!',
'Error in the system!',
'I\'m feeling sad today'
];
const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)];
console.log(`Sending message: "${randomMessage}"`);
this.owenSystem.handleUserMessage(randomMessage);
}
/**
* Log system information
* @private
* @returns {void}
*/
logSystemInfo() {
if (!this.owenSystem) return;
console.log('=== Owen System Info ===');
console.log('Available States:', this.owenSystem.getAvailableStates());
console.log('Available Clips:', this.owenSystem.getAvailableClips());
console.log('Current State:', this.owenSystem.getCurrentState());
console.log('========================');
}
/**
* Update UI with current system state
* @private
* @returns {void}
*/
updateUI() {
if (!this.owenSystem) return;
const currentStateElement = document.getElementById('current-state');
const transitionsElement = document.getElementById('transitions');
if (currentStateElement) {
currentStateElement.textContent = this.owenSystem.getCurrentState();
}
if (transitionsElement) {
transitionsElement.textContent = this.owenSystem.getAvailableTransitions().join(', ');
}
}
/**
* Main animation loop
* @private
* @returns {void}
*/
animate() {
requestAnimationFrame(() => this.animate());
const deltaTime = this.clock.getDelta() * 1000; // Convert to milliseconds
// Update Owen system
if (this.owenSystem) {
this.owenSystem.update(deltaTime);
}
// Update UI
this.updateUI();
// Render scene
this.renderer.render(this.scene, this.camera);
}
}
// Initialize the demo when the page loads
window.addEventListener('load', async () => {
const demo = new OwenDemo();
try {
await demo.init();
} catch (error) {
console.error('Failed to initialize Owen demo:', error);
}
});
export default OwenDemo;

View File

@ -3,9 +3,9 @@
* @author Owen Animation System * @author Owen Animation System
*/ */
import * as THREE from 'three'; import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OwenSystemFactory, States } from '../src/index.js'; import { OwenSystemFactory, States } from '../src/index.js'
/** /**
* Basic Owen Animation System demo * Basic Owen Animation System demo
@ -15,67 +15,67 @@ class OwenDemo {
/** /**
* Create the demo * Create the demo
*/ */
constructor() { constructor () {
/** /**
* The Three.js scene * The Three.js scene
* @type {THREE.Scene} * @type {THREE.Scene}
*/ */
this.scene = new THREE.Scene(); this.scene = new THREE.Scene()
/** /**
* The Three.js camera * The Three.js camera
* @type {THREE.PerspectiveCamera} * @type {THREE.PerspectiveCamera}
*/ */
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
/** /**
* The Three.js renderer * The Three.js renderer
* @type {THREE.WebGLRenderer} * @type {THREE.WebGLRenderer}
*/ */
this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer = new THREE.WebGLRenderer({ antialias: true })
/** /**
* The Owen animation system * The Owen animation system
* @type {OwenAnimationContext|null} * @type {OwenAnimationContext|null}
*/ */
this.owenSystem = null; this.owenSystem = null
/** /**
* Clock for tracking time * Clock for tracking time
* @type {THREE.Clock} * @type {THREE.Clock}
*/ */
this.clock = new THREE.Clock(); this.clock = new THREE.Clock()
} }
/** /**
* Initialize the demo * Initialize the demo
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async init() { async init () {
// Setup renderer // Setup renderer
this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setClearColor(0x1a1a1a); this.renderer.setClearColor(0x1a1a1a)
this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
document.body.appendChild(this.renderer.domElement); document.body.appendChild(this.renderer.domElement)
// Setup camera // Setup camera
this.camera.position.set(0, 1.6, 3); this.camera.position.set(0, 1.6, 3)
this.camera.lookAt(0, 1, 0); this.camera.lookAt(0, 1, 0)
// Add lighting // Add lighting
this.setupLighting(); this.setupLighting()
// Load Owen model (replace with your model path) // Load Owen model (replace with your model path)
await this.loadOwenModel(); await this.loadOwenModel()
// Setup event listeners // Setup event listeners
this.setupEventListeners(); this.setupEventListeners()
// Start render loop // Start render loop
this.animate(); this.animate()
console.log('Owen Demo initialized'); console.log('Owen Demo initialized')
} }
/** /**
@ -83,23 +83,23 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
setupLighting() { setupLighting () {
// Ambient light // Ambient light
const ambientLight = new THREE.AmbientLight(0x404040, 0.4); const ambientLight = new THREE.AmbientLight(0x404040, 0.4)
this.scene.add(ambientLight); this.scene.add(ambientLight)
// Directional light // Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(5, 10, 5); directionalLight.position.set(5, 10, 5)
directionalLight.castShadow = true; directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.width = 2048
directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.mapSize.height = 2048
this.scene.add(directionalLight); this.scene.add(directionalLight)
// Fill light // Fill light
const fillLight = new THREE.DirectionalLight(0x8bb7f0, 0.3); const fillLight = new THREE.DirectionalLight(0x8bb7f0, 0.3)
fillLight.position.set(-5, 5, -5); fillLight.position.set(-5, 5, -5)
this.scene.add(fillLight); this.scene.add(fillLight)
} }
/** /**
@ -107,9 +107,9 @@ class OwenDemo {
* @private * @private
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadOwenModel() { async loadOwenModel () {
try { try {
const loader = new GLTFLoader(); const loader = new GLTFLoader()
// Replace 'path/to/owen.gltf' with your actual model path // Replace 'path/to/owen.gltf' with your actual model path
const gltf = await new Promise((resolve, reject) => { const gltf = await new Promise((resolve, reject) => {
@ -118,34 +118,33 @@ class OwenDemo {
resolve, resolve,
(progress) => console.log('Loading progress:', progress.loaded / progress.total * 100 + '%'), (progress) => console.log('Loading progress:', progress.loaded / progress.total * 100 + '%'),
reject reject
); )
}); })
const model = gltf.scene; const model = gltf.scene
model.position.set(0, 0, 0); model.position.set(0, 0, 0)
model.scale.setScalar(1); model.scale.setScalar(1)
// Enable shadows // Enable shadows
model.traverse((child) => { model.traverse((child) => {
if (child.isMesh) { if (child.isMesh) {
child.castShadow = true; child.castShadow = true
child.receiveShadow = true; child.receiveShadow = true
} }
}); })
this.scene.add(model); this.scene.add(model)
// Create Owen animation system // Create Owen animation system
this.owenSystem = await OwenSystemFactory.createOwenSystem(gltf, this.scene); this.owenSystem = await OwenSystemFactory.createOwenSystem(gltf, this.scene)
console.log('Owen model loaded and animation system created');
this.logSystemInfo();
console.log('Owen model loaded and animation system created')
this.logSystemInfo()
} catch (error) { } catch (error) {
console.error('Error loading Owen model:', error); console.error('Error loading Owen model:', error)
// Create a placeholder cube for demo purposes // Create a placeholder cube for demo purposes
this.createPlaceholderModel(); this.createPlaceholderModel()
} }
} }
@ -154,16 +153,16 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
createPlaceholderModel() { createPlaceholderModel () {
const geometry = new THREE.BoxGeometry(1, 2, 1); const geometry = new THREE.BoxGeometry(1, 2, 1)
const material = new THREE.MeshPhongMaterial({ color: 0x6699ff }); const material = new THREE.MeshPhongMaterial({ color: 0x6699ff })
const cube = new THREE.Mesh(geometry, material); const cube = new THREE.Mesh(geometry, material)
cube.position.set(0, 1, 0); cube.position.set(0, 1, 0)
cube.castShadow = true; cube.castShadow = true
cube.receiveShadow = true; cube.receiveShadow = true
this.scene.add(cube); this.scene.add(cube)
console.log('Created placeholder model (cube)'); console.log('Created placeholder model (cube)')
} }
/** /**
@ -171,46 +170,46 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
setupEventListeners() { setupEventListeners () {
// Keyboard controls // Keyboard controls
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
if (!this.owenSystem) return; if (!this.owenSystem) return
switch (event.key) { switch (event.key) {
case '1': case '1':
this.owenSystem.transitionTo(States.WAIT); this.owenSystem.transitionTo(States.WAIT)
break; break
case '2': case '2':
this.owenSystem.transitionTo(States.REACT); this.owenSystem.transitionTo(States.REACT)
break; break
case '3': case '3':
this.owenSystem.transitionTo(States.TYPE); this.owenSystem.transitionTo(States.TYPE)
break; break
case '4': case '4':
this.owenSystem.transitionTo(States.SLEEP); this.owenSystem.transitionTo(States.SLEEP)
break; break
case ' ': case ' ':
this.sendTestMessage(); this.sendTestMessage()
break; break
} }
}); })
// Mouse interaction // Mouse interaction
document.addEventListener('click', () => { document.addEventListener('click', () => {
if (this.owenSystem) { if (this.owenSystem) {
this.owenSystem.onUserActivity(); this.owenSystem.onUserActivity()
} }
}); })
// Window resize // Window resize
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setSize(window.innerWidth, window.innerHeight)
}); })
// Add instructions to the page // Add instructions to the page
this.addInstructions(); this.addInstructions()
} }
/** /**
@ -218,11 +217,11 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
addInstructions() { addInstructions () {
const instructions = document.createElement('div'); const instructions = document.createElement('div')
instructions.innerHTML = ` instructions.innerHTML = `
<div style="position: absolute; top: 10px; left: 10px; color: white; ` + <div style="position: absolute; top: 10px; left: 10px; color: white; ` +
`font-family: monospace; font-size: 14px; line-height: 1.4;"> `font-family: monospace; font-size: 14px; line-height: 1.4;">
<h3>Owen Animation System Demo</h3> <h3>Owen Animation System Demo</h3>
<p><strong>Controls:</strong></p> <p><strong>Controls:</strong></p>
<p>1 - Wait State</p> <p>1 - Wait State</p>
@ -235,8 +234,8 @@ class OwenDemo {
<p><strong>Current State:</strong> <span id="current-state">-</span></p> <p><strong>Current State:</strong> <span id="current-state">-</span></p>
<p><strong>Available Transitions:</strong> <span id="transitions">-</span></p> <p><strong>Available Transitions:</strong> <span id="transitions">-</span></p>
</div> </div>
`; `
document.body.appendChild(instructions); document.body.appendChild(instructions)
} }
/** /**
@ -244,8 +243,8 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
sendTestMessage() { sendTestMessage () {
if (!this.owenSystem) return; if (!this.owenSystem) return
const testMessages = [ const testMessages = [
'Hello Owen!', 'Hello Owen!',
@ -254,11 +253,11 @@ class OwenDemo {
'Great work!', 'Great work!',
'Error in the system!', 'Error in the system!',
'I\'m feeling sad today' 'I\'m feeling sad today'
]; ]
const randomMessage = testMessages[ Math.floor(Math.random() * testMessages.length) ]; const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)]
console.log(`Sending message: "${randomMessage}"`); console.log(`Sending message: "${randomMessage}"`)
this.owenSystem.handleUserMessage(randomMessage); this.owenSystem.handleUserMessage(randomMessage)
} }
/** /**
@ -266,14 +265,14 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
logSystemInfo() { logSystemInfo () {
if (!this.owenSystem) return; if (!this.owenSystem) return
console.log('=== Owen System Info ==='); console.log('=== Owen System Info ===')
console.log('Available States:', this.owenSystem.getAvailableStates()); console.log('Available States:', this.owenSystem.getAvailableStates())
console.log('Available Clips:', this.owenSystem.getAvailableClips()); console.log('Available Clips:', this.owenSystem.getAvailableClips())
console.log('Current State:', this.owenSystem.getCurrentState()); console.log('Current State:', this.owenSystem.getCurrentState())
console.log('========================'); console.log('========================')
} }
/** /**
@ -281,18 +280,18 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
updateUI() { updateUI () {
if (!this.owenSystem) return; if (!this.owenSystem) return
const currentStateElement = document.getElementById('current-state'); const currentStateElement = document.getElementById('current-state')
const transitionsElement = document.getElementById('transitions'); const transitionsElement = document.getElementById('transitions')
if (currentStateElement) { if (currentStateElement) {
currentStateElement.textContent = this.owenSystem.getCurrentState(); currentStateElement.textContent = this.owenSystem.getCurrentState()
} }
if (transitionsElement) { if (transitionsElement) {
transitionsElement.textContent = this.owenSystem.getAvailableTransitions().join(', '); transitionsElement.textContent = this.owenSystem.getAvailableTransitions().join(', ')
} }
} }
@ -301,32 +300,32 @@ class OwenDemo {
* @private * @private
* @returns {void} * @returns {void}
*/ */
animate() { animate () {
requestAnimationFrame(() => this.animate()); requestAnimationFrame(() => this.animate())
const deltaTime = this.clock.getDelta() * 1000; // Convert to milliseconds const deltaTime = this.clock.getDelta() * 1000 // Convert to milliseconds
// Update Owen system // Update Owen system
if (this.owenSystem) { if (this.owenSystem) {
this.owenSystem.update(deltaTime); this.owenSystem.update(deltaTime)
} }
// Update UI // Update UI
this.updateUI(); this.updateUI()
// Render scene // Render scene
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera)
} }
} }
// Initialize the demo when the page loads // Initialize the demo when the page loads
window.addEventListener('load', async () => { window.addEventListener('load', async () => {
const demo = new OwenDemo(); const demo = new OwenDemo()
try { try {
await demo.init(); await demo.init()
} catch (error) { } catch (error) {
console.error('Failed to initialize Owen demo:', error); console.error('Failed to initialize Owen demo:', error)
} }
}); })
export default OwenDemo; export default OwenDemo

View File

@ -1,45 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Owen Animation System - Basic Demo</title> <title>Owen Animation System - Basic Demo</title>
<style> <style>
body { body {
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
background: #1a1a1a; background: #1a1a1a;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
} }
#loading { #loading {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
color: white; color: white;
font-size: 18px; font-size: 18px;
z-index: 1000; z-index: 1000;
} }
.hidden { .hidden {
display: none; display: none;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="loading">Loading Owen Animation System...</div> <div id="loading">Loading Owen Animation System...</div>
<script type="module"> <script type="module">
import './basic-demo.js'; import "./basic-demo.js";
// Hide loading screen after a short delay // Hide loading screen after a short delay
setTimeout(() => { setTimeout(() => {
const loading = document.getElementById('loading'); const loading = document.getElementById("loading");
if (loading) { if (loading) {
loading.classList.add('hidden'); loading.classList.add("hidden");
} }
}, 3000); }, 3000);
</script> </script>
</body> </body>
</html> </html>

View File

@ -3,39 +3,38 @@
* @author Owen Animation System * @author Owen Animation System
*/ */
import { OwenSystemFactory, States } from '../src/index.js'; import { OwenSystemFactory, States } from '../src/index.js'
/** /**
* Simple example of using Owen Animation System * Simple example of using Owen Animation System
* This example shows how to use the system without a browser environment * This example shows how to use the system without a browser environment
*/ */
class SimpleOwenExample { class SimpleOwenExample {
constructor() { constructor () {
this.owenSystem = null; this.owenSystem = null
} }
/** /**
* Initialize the Owen system with a mock model * Initialize the Owen system with a mock model
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async init() { async init () {
try { try {
// Create a mock GLTF model for demonstration // Create a mock GLTF model for demonstration
const mockModel = this.createMockModel(); const mockModel = this.createMockModel()
// Create the Owen system // Create the Owen system
this.owenSystem = await OwenSystemFactory.createBasicOwenSystem(mockModel); this.owenSystem = await OwenSystemFactory.createBasicOwenSystem(mockModel)
console.log('✅ Owen Animation System initialized successfully!'); console.log('✅ Owen Animation System initialized successfully!')
console.log('📊 System Info:'); console.log('📊 System Info:')
console.log(` Available States: ${this.owenSystem.getAvailableStates().join(', ')}`); console.log(` Available States: ${this.owenSystem.getAvailableStates().join(', ')}`)
console.log(` Current State: ${this.owenSystem.getCurrentState()}`); console.log(` Current State: ${this.owenSystem.getCurrentState()}`)
// Run some example interactions // Run some example interactions
await this.runExamples(); await this.runExamples()
} catch (error) { } catch (error) {
console.error('❌ Failed to initialize Owen system:', error.message); console.error('❌ Failed to initialize Owen system:', error.message)
} }
} }
@ -43,7 +42,7 @@ class SimpleOwenExample {
* Create a mock 3D model for demonstration purposes * Create a mock 3D model for demonstration purposes
* @returns {Object} Mock model object * @returns {Object} Mock model object
*/ */
createMockModel() { createMockModel () {
return { return {
animations: [ animations: [
{ name: 'wait_idle_L' }, { name: 'wait_idle_L' },
@ -63,46 +62,46 @@ class SimpleOwenExample {
], ],
scene: {}, scene: {},
userData: {} userData: {}
}; }
} }
/** /**
* Run example interactions with the Owen system * Run example interactions with the Owen system
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async runExamples() { async runExamples () {
console.log('\n🎬 Running example interactions...\n'); console.log('\n🎬 Running example interactions...\n')
// Example 1: Basic state transitions // Example 1: Basic state transitions
console.log('📝 Example 1: Manual state transitions'); console.log('📝 Example 1: Manual state transitions')
await this.demonstrateStateTransitions(); await this.demonstrateStateTransitions()
// Example 2: Message handling // Example 2: Message handling
console.log('\n📝 Example 2: Message handling with emotions'); console.log('\n📝 Example 2: Message handling with emotions')
await this.demonstrateMessageHandling(); await this.demonstrateMessageHandling()
// Example 3: System update loop // Example 3: System update loop
console.log('\n📝 Example 3: System update simulation'); console.log('\n📝 Example 3: System update simulation')
this.demonstrateUpdateLoop(); this.demonstrateUpdateLoop()
console.log('\n✨ All examples completed!'); console.log('\n✨ All examples completed!')
} }
/** /**
* Demonstrate manual state transitions * Demonstrate manual state transitions
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async demonstrateStateTransitions() { async demonstrateStateTransitions () {
const states = [ States.REACT, States.TYPE, States.WAIT, States.SLEEP ]; const states = [States.REACT, States.TYPE, States.WAIT, States.SLEEP]
for (const state of states) { for (const state of states) {
console.log(`🔄 Transitioning to ${state.toUpperCase()} state...`); console.log(`🔄 Transitioning to ${state.toUpperCase()} state...`)
await this.owenSystem.transitionTo(state); await this.owenSystem.transitionTo(state)
console.log(` ✓ Current state: ${this.owenSystem.getCurrentState()}`); console.log(` ✓ Current state: ${this.owenSystem.getCurrentState()}`)
console.log(` ✓ Available transitions: ${this.owenSystem.getAvailableTransitions().join(', ')}`); console.log(` ✓ Available transitions: ${this.owenSystem.getAvailableTransitions().join(', ')}`)
// Simulate some time passing // Simulate some time passing
await this.sleep(500); await this.sleep(500)
} }
} }
@ -110,23 +109,23 @@ class SimpleOwenExample {
* Demonstrate message handling with emotional responses * Demonstrate message handling with emotional responses
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async demonstrateMessageHandling() { async demonstrateMessageHandling () {
const messages = [ const messages = [
{ text: 'Hello Owen!', expected: 'neutral response' }, { text: 'Hello Owen!', expected: 'neutral response' },
{ text: 'This is urgent!', expected: 'angry/urgent response' }, { text: 'This is urgent!', expected: 'angry/urgent response' },
{ text: 'Great work!', expected: 'happy response' }, { text: 'Great work!', expected: 'happy response' },
{ text: 'There\'s an error in the system', expected: 'shocked response' }, { text: 'There\'s an error in the system', expected: 'shocked response' },
{ text: 'I\'m feeling sad today', expected: 'sad response' } { text: 'I\'m feeling sad today', expected: 'sad response' }
]; ]
for (const message of messages) { for (const message of messages) {
console.log(`💬 Sending message: "${message.text}"`); console.log(`💬 Sending message: "${message.text}"`)
console.log(` Expected: ${message.expected}`); console.log(` Expected: ${message.expected}`)
await this.owenSystem.handleUserMessage(message.text); await this.owenSystem.handleUserMessage(message.text)
console.log(` ✓ Current state after message: ${this.owenSystem.getCurrentState()}`); console.log(` ✓ Current state after message: ${this.owenSystem.getCurrentState()}`)
await this.sleep(300); await this.sleep(300)
} }
} }
@ -134,26 +133,26 @@ class SimpleOwenExample {
* Demonstrate the system update loop * Demonstrate the system update loop
* @returns {void} * @returns {void}
*/ */
demonstrateUpdateLoop() { demonstrateUpdateLoop () {
console.log('⏱️ Simulating update loop for 3 seconds...'); console.log('⏱️ Simulating update loop for 3 seconds...')
let iterations = 0; let iterations = 0
const startTime = Date.now(); const startTime = Date.now()
const updateLoop = () => { const updateLoop = () => {
const deltaTime = 16.67; // ~60 FPS const deltaTime = 16.67 // ~60 FPS
this.owenSystem.update(deltaTime); this.owenSystem.update(deltaTime)
iterations++; iterations++
if (Date.now() - startTime < 3000) { if (Date.now() - startTime < 3000) {
setTimeout(updateLoop, 16); setTimeout(updateLoop, 16)
} else { } else {
console.log(` ✓ Completed ${iterations} update iterations`); console.log(` ✓ Completed ${iterations} update iterations`)
console.log(` ✓ Final state: ${this.owenSystem.getCurrentState()}`); console.log(` ✓ Final state: ${this.owenSystem.getCurrentState()}`)
} }
}; }
updateLoop(); updateLoop()
} }
/** /**
@ -161,24 +160,24 @@ class SimpleOwenExample {
* @param {number} ms - Milliseconds to sleep * @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
sleep(ms) { sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms))
} }
} }
// Run the example if this file is executed directly // Run the example if this file is executed directly
if (import.meta.url === `file://${process.argv[ 1 ]}`) { if (import.meta.url === `file://${process.argv[1]}`) {
console.log('🚀 Starting Owen Animation System Example\n'); console.log('🚀 Starting Owen Animation System Example\n')
const example = new SimpleOwenExample(); const example = new SimpleOwenExample()
example.init() example.init()
.then(() => { .then(() => {
console.log('\n🎉 Example completed successfully!'); console.log('\n🎉 Example completed successfully!')
console.log('💡 Try modifying this example or check out the browser demo in examples/index.html'); console.log('💡 Try modifying this example or check out the browser demo in examples/index.html')
}) })
.catch(error => { .catch(error => {
console.error('\n💥 Example failed:', error); console.error('\n💥 Example failed:', error)
}); })
} }
export default SimpleOwenExample; export default SimpleOwenExample

3311
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
{ {
"name": "owen-animation-system", "name": "@kjanat/owen",
"version": "1.0.0", "version": "1.0.1",
"description": "A comprehensive Three.js animation system for character state management with clean architecture principles", "description": "A comprehensive Three.js animation system for character state management with clean architecture principles",
"main": "src/index.js", "main": "src/index.js",
"types": "src/index.d.ts", "types": "src/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src --ext .js", "lint": "standard",
"lint:fix": "eslint src --ext .js --fix", "lint:fix": "standard --fix",
"docs": "jsdoc -c jsdoc.config.json" "docs": "jsdoc -c jsdoc.config.json",
"format": "npx prettier --ignore-path --write '**/*.{html,css}' 'docs/**/*.{html,css}'"
}, },
"keywords": [ "keywords": [
"three.js", "three.js",
@ -21,17 +22,29 @@
"gltf", "gltf",
"3d" "3d"
], ],
"author": "Owen Animation System", "author": "Kaj \"@kjanat\" Kowalski",
"license": "AGPL-3.0-only OR LicenseRef-Commercial", "license": "AGPL-3.0-only OR LicenseRef-Commercial",
"dependencies": { "dependencies": {
"three": "^0.176.0" "three": "^0.176.0"
}, },
"devDependencies": { "devDependencies": {
"vite": "^6.3.5", "jsdoc": "^4.0.2",
"eslint": "^9.27.0", "pre-commit": "^1.2.2",
"jsdoc": "^4.0.2" "standard": "*",
"vite": "^6.3.5"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
} },
"standard": {
"globals": [
"requestAnimationFrame"
]
},
"pre-commit": [
"lint:fix",
"lint",
"docs",
"format"
]
} }

View File

@ -3,8 +3,8 @@
* @module animation * @module animation
*/ */
import * as THREE from 'three'; import * as THREE from 'three'
import { ClipTypes, Config } from '../constants.js'; import { ClipTypes, Config } from '../constants.js'
/** /**
* Represents a single animation clip with metadata and Three.js action * Represents a single animation clip with metadata and Three.js action
@ -17,36 +17,36 @@ export class AnimationClip {
* @param {THREE.AnimationClip} threeAnimation - The Three.js animation clip * @param {THREE.AnimationClip} threeAnimation - The Three.js animation clip
* @param {Object} metadata - Parsed metadata from animation name * @param {Object} metadata - Parsed metadata from animation name
*/ */
constructor(name, threeAnimation, metadata) { constructor (name, threeAnimation, metadata) {
/** /**
* The name of the animation clip * The name of the animation clip
* @type {string} * @type {string}
*/ */
this.name = name; this.name = name
/** /**
* The Three.js animation clip * The Three.js animation clip
* @type {THREE.AnimationClip} * @type {THREE.AnimationClip}
*/ */
this.animation = threeAnimation; this.animation = threeAnimation
/** /**
* Parsed metadata about the animation * Parsed metadata about the animation
* @type {Object} * @type {Object}
*/ */
this.metadata = metadata; this.metadata = metadata
/** /**
* The Three.js animation action * The Three.js animation action
* @type {THREE.AnimationAction|null} * @type {THREE.AnimationAction|null}
*/ */
this.action = null; this.action = null
/** /**
* The animation mixer * The animation mixer
* @type {THREE.AnimationMixer|null} * @type {THREE.AnimationMixer|null}
*/ */
this.mixer = null; this.mixer = null
} }
/** /**
@ -54,22 +54,22 @@ export class AnimationClip {
* @param {THREE.AnimationMixer} mixer - The animation mixer * @param {THREE.AnimationMixer} mixer - The animation mixer
* @returns {THREE.AnimationAction} The created action * @returns {THREE.AnimationAction} The created action
*/ */
createAction(mixer) { createAction (mixer) {
this.mixer = mixer; this.mixer = mixer
this.action = mixer.clipAction(this.animation); this.action = mixer.clipAction(this.animation)
// Configure based on type // Configure based on type
if ( if (
this.metadata.type === ClipTypes.LOOP || this.metadata.type === ClipTypes.LOOP ||
this.metadata.type === ClipTypes.NESTED_LOOP this.metadata.type === ClipTypes.NESTED_LOOP
) { ) {
this.action.setLoop(THREE.LoopRepeat, Infinity); this.action.setLoop(THREE.LoopRepeat, Infinity)
} else { } else {
this.action.setLoop(THREE.LoopOnce); this.action.setLoop(THREE.LoopOnce)
this.action.clampWhenFinished = true; this.action.clampWhenFinished = true
} }
return this.action; return this.action
} }
/** /**
@ -77,11 +77,11 @@ export class AnimationClip {
* @param {number} [fadeInDuration=0.3] - Fade in duration in seconds * @param {number} [fadeInDuration=0.3] - Fade in duration in seconds
* @returns {Promise<void>} Promise that resolves when fade in completes * @returns {Promise<void>} Promise that resolves when fade in completes
*/ */
play(fadeInDuration = Config.DEFAULT_FADE_IN) { play (fadeInDuration = Config.DEFAULT_FADE_IN) {
if (this.action) { if (this.action) {
this.action.reset(); this.action.reset()
this.action.fadeIn(fadeInDuration); this.action.fadeIn(fadeInDuration)
this.action.play(); this.action.play()
} }
} }
@ -90,14 +90,14 @@ export class AnimationClip {
* @param {number} [fadeOutDuration=0.3] - Fade out duration in seconds * @param {number} [fadeOutDuration=0.3] - Fade out duration in seconds
* @returns {Promise<void>} Promise that resolves when fade out completes * @returns {Promise<void>} Promise that resolves when fade out completes
*/ */
stop(fadeOutDuration = Config.DEFAULT_FADE_OUT) { stop (fadeOutDuration = Config.DEFAULT_FADE_OUT) {
if (this.action) { if (this.action) {
this.action.fadeOut(fadeOutDuration); this.action.fadeOut(fadeOutDuration)
setTimeout(() => { setTimeout(() => {
if (this.action) { if (this.action) {
this.action.stop(); this.action.stop()
} }
}, fadeOutDuration * 1000); }, fadeOutDuration * 1000)
} }
} }
@ -105,8 +105,8 @@ export class AnimationClip {
* Check if the animation is currently playing * Check if the animation is currently playing
* @returns {boolean} True if playing, false otherwise * @returns {boolean} True if playing, false otherwise
*/ */
isPlaying() { isPlaying () {
return this.action?.isRunning() || false; return this.action?.isRunning() || false
} }
} }
@ -119,18 +119,18 @@ export class AnimationClipFactory {
* Create an animation clip factory * Create an animation clip factory
* @param {AnimationLoader} animationLoader - The animation loader instance * @param {AnimationLoader} animationLoader - The animation loader instance
*/ */
constructor(animationLoader) { constructor (animationLoader) {
/** /**
* The animation loader for loading animation data * The animation loader for loading animation data
* @type {AnimationLoader} * @type {AnimationLoader}
*/ */
this.animationLoader = animationLoader; this.animationLoader = animationLoader
/** /**
* Cache for created animation clips * Cache for created animation clips
* @type {Map<string, AnimationClip>} * @type {Map<string, AnimationClip>}
*/ */
this.clipCache = new Map(); this.clipCache = new Map()
} }
/** /**
@ -139,67 +139,67 @@ export class AnimationClipFactory {
* @param {string} name - The animation name to parse * @param {string} name - The animation name to parse
* @returns {Object} Parsed metadata object * @returns {Object} Parsed metadata object
*/ */
parseAnimationName(name) { parseAnimationName (name) {
const parts = name.split('_'); const parts = name.split('_')
const state = parts[ 0 ]; const state = parts[0]
const action = parts[ 1 ]; const action = parts[1]
// Handle transitions with emotions // Handle transitions with emotions
if (parts[ 2 ]?.includes('2') && parts[ 3 ] === ClipTypes.TRANSITION) { if (parts[2]?.includes('2') && parts[3] === ClipTypes.TRANSITION) {
const [ , toState ] = parts[ 2 ].split('2'); const [, toState] = parts[2].split('2')
return { return {
state, state,
action, action,
toState, toState,
emotion: parts[ 2 ] || '', emotion: parts[2] || '',
type: ClipTypes.TRANSITION, type: ClipTypes.TRANSITION,
isTransition: true, isTransition: true,
hasEmotion: true, hasEmotion: true
}; }
} }
// Handle regular transitions // Handle regular transitions
if (parts[ 2 ] === ClipTypes.TRANSITION) { if (parts[2] === ClipTypes.TRANSITION) {
return { return {
state, state,
action, action,
type: ClipTypes.TRANSITION, type: ClipTypes.TRANSITION,
isTransition: true, isTransition: true
}; }
} }
// Handle nested animations // Handle nested animations
if (parts[ 2 ] === ClipTypes.NESTED_IN || parts[ 2 ] === ClipTypes.NESTED_OUT) { if (parts[2] === ClipTypes.NESTED_IN || parts[2] === ClipTypes.NESTED_OUT) {
return { return {
state, state,
action, action,
type: parts[ 2 ], type: parts[2],
nestedType: parts[ 3 ], nestedType: parts[3],
isNested: true, isNested: true
}; }
} }
// Handle nested loops and quirks // Handle nested loops and quirks
if ( if (
parts[ 3 ] === ClipTypes.NESTED_LOOP || parts[3] === ClipTypes.NESTED_LOOP ||
parts[ 3 ] === ClipTypes.NESTED_QUIRK parts[3] === ClipTypes.NESTED_QUIRK
) { ) {
return { return {
state, state,
action, action,
subAction: parts[ 2 ], subAction: parts[2],
type: parts[ 3 ], type: parts[3],
isNested: true, isNested: true
}; }
} }
// Handle standard loops and quirks // Handle standard loops and quirks
return { return {
state, state,
action, action,
type: parts[ 2 ], type: parts[2],
isStandard: true, isStandard: true
}; }
} }
/** /**
@ -207,18 +207,18 @@ export class AnimationClipFactory {
* @param {string} name - The animation name * @param {string} name - The animation name
* @returns {Promise<AnimationClip>} The created animation clip * @returns {Promise<AnimationClip>} The created animation clip
*/ */
async createClip(name) { async createClip (name) {
if (this.clipCache.has(name)) { if (this.clipCache.has(name)) {
return this.clipCache.get(name); return this.clipCache.get(name)
} }
const metadata = this.parseAnimationName(name); const metadata = this.parseAnimationName(name)
const animation = await this.animationLoader.loadAnimation(name); const animation = await this.animationLoader.loadAnimation(name)
const clip = new AnimationClip(name, animation, metadata); const clip = new AnimationClip(name, animation, metadata)
this.clipCache.set(name, clip); this.clipCache.set(name, clip)
return clip; return clip
} }
/** /**
@ -226,24 +226,24 @@ export class AnimationClipFactory {
* @param {THREE.Object3D} model - The 3D model containing animations * @param {THREE.Object3D} model - The 3D model containing animations
* @returns {Promise<Map<string, AnimationClip>>} Map of animation name to clip * @returns {Promise<Map<string, AnimationClip>>} Map of animation name to clip
*/ */
async createClipsFromModel(model) { async createClipsFromModel (model) {
const clips = new Map(); const clips = new Map()
const animations = model.animations || []; const animations = model.animations || []
for (const animation of animations) { for (const animation of animations) {
const clip = await this.createClip(animation.name, model); const clip = await this.createClip(animation.name, model)
clips.set(animation.name, clip); clips.set(animation.name, clip)
} }
return clips; return clips
} }
/** /**
* Clear the clip cache * Clear the clip cache
* @returns {void} * @returns {void}
*/ */
clearCache() { clearCache () {
this.clipCache.clear(); this.clipCache.clear()
} }
/** /**
@ -251,7 +251,7 @@ export class AnimationClipFactory {
* @param {string} name - The animation name * @param {string} name - The animation name
* @returns {AnimationClip|undefined} The cached clip or undefined * @returns {AnimationClip|undefined} The cached clip or undefined
*/ */
getCachedClip(name) { getCachedClip (name) {
return this.clipCache.get(name); return this.clipCache.get(name)
} }
} }

View File

@ -9,21 +9,21 @@
* @enum {string} * @enum {string}
*/ */
export const ClipTypes = { export const ClipTypes = {
/** Loop animation */ /** Loop animation */
LOOP: 'L', LOOP: 'L',
/** Quirk animation */ /** Quirk animation */
QUIRK: 'Q', QUIRK: 'Q',
/** Nested loop animation */ /** Nested loop animation */
NESTED_LOOP: 'NL', NESTED_LOOP: 'NL',
/** Nested quirk animation */ /** Nested quirk animation */
NESTED_QUIRK: 'NQ', NESTED_QUIRK: 'NQ',
/** Nested in transition */ /** Nested in transition */
NESTED_IN: 'IN_NT', NESTED_IN: 'IN_NT',
/** Nested out transition */ /** Nested out transition */
NESTED_OUT: 'OUT_NT', NESTED_OUT: 'OUT_NT',
/** Transition animation */ /** Transition animation */
TRANSITION: 'T', TRANSITION: 'T'
}; }
/** /**
* Character animation states * Character animation states
@ -31,15 +31,15 @@ export const ClipTypes = {
* @enum {string} * @enum {string}
*/ */
export const States = { export const States = {
/** Waiting/idle state */ /** Waiting/idle state */
WAIT: 'wait', WAIT: 'wait',
/** Reacting to input state */ /** Reacting to input state */
REACT: 'react', REACT: 'react',
/** Typing response state */ /** Typing response state */
TYPE: 'type', TYPE: 'type',
/** Sleep/inactive state */ /** Sleep/inactive state */
SLEEP: 'sleep', SLEEP: 'sleep'
}; }
/** /**
* Character emotional states * Character emotional states
@ -47,17 +47,17 @@ export const States = {
* @enum {string} * @enum {string}
*/ */
export const Emotions = { export const Emotions = {
/** Neutral emotion */ /** Neutral emotion */
NEUTRAL: '', NEUTRAL: '',
/** Angry emotion */ /** Angry emotion */
ANGRY: 'an', ANGRY: 'an',
/** Shocked emotion */ /** Shocked emotion */
SHOCKED: 'sh', SHOCKED: 'sh',
/** Happy emotion */ /** Happy emotion */
HAPPY: 'ha', HAPPY: 'ha',
/** Sad emotion */ /** Sad emotion */
SAD: 'sa', SAD: 'sa'
}; }
/** /**
* Default configuration values * Default configuration values
@ -65,14 +65,14 @@ export const Emotions = {
* @type {Object} * @type {Object}
*/ */
export const Config = { export const Config = {
/** Default fade in duration for animations (ms) */ /** Default fade in duration for animations (ms) */
DEFAULT_FADE_IN: 0.3, DEFAULT_FADE_IN: 0.3,
/** Default fade out duration for animations (ms) */ /** Default fade out duration for animations (ms) */
DEFAULT_FADE_OUT: 0.3, DEFAULT_FADE_OUT: 0.3,
/** Default quirk interval (ms) */ /** Default quirk interval (ms) */
QUIRK_INTERVAL: 5000, QUIRK_INTERVAL: 5000,
/** Default inactivity timeout (ms) */ /** Default inactivity timeout (ms) */
INACTIVITY_TIMEOUT: 60000, INACTIVITY_TIMEOUT: 60000,
/** Quirk probability threshold */ /** Quirk probability threshold */
QUIRK_PROBABILITY: 0.3, QUIRK_PROBABILITY: 0.3
}; }

View File

@ -3,7 +3,7 @@
* @module core * @module core
*/ */
import { States, Emotions, Config } from '../constants.js'; import { States, Emotions, Config } from '../constants.js'
/** /**
* Main controller for the Owen animation system * Main controller for the Owen animation system
@ -18,97 +18,97 @@ export class OwenAnimationContext {
* @param {AnimationClipFactory} animationClipFactory - Factory for creating clips * @param {AnimationClipFactory} animationClipFactory - Factory for creating clips
* @param {StateFactory} stateFactory - Factory for creating state handlers * @param {StateFactory} stateFactory - Factory for creating state handlers
*/ */
constructor(model, mixer, animationClipFactory, stateFactory) { constructor (model, mixer, animationClipFactory, stateFactory) {
/** /**
* The 3D character model * The 3D character model
* @type {THREE.Object3D} * @type {THREE.Object3D}
*/ */
this.model = model; this.model = model
/** /**
* The Three.js animation mixer * The Three.js animation mixer
* @type {THREE.AnimationMixer} * @type {THREE.AnimationMixer}
*/ */
this.mixer = mixer; this.mixer = mixer
/** /**
* Factory for creating animation clips * Factory for creating animation clips
* @type {AnimationClipFactory} * @type {AnimationClipFactory}
*/ */
this.animationClipFactory = animationClipFactory; this.animationClipFactory = animationClipFactory
/** /**
* Factory for creating state handlers * Factory for creating state handlers
* @type {StateFactory} * @type {StateFactory}
*/ */
this.stateFactory = stateFactory; this.stateFactory = stateFactory
/** /**
* Map of animation clips by name * Map of animation clips by name
* @type {Map<string, AnimationClip>} * @type {Map<string, AnimationClip>}
*/ */
this.clips = new Map(); this.clips = new Map()
/** /**
* Map of state handlers by name * Map of state handlers by name
* @type {Map<string, StateHandler>} * @type {Map<string, StateHandler>}
*/ */
this.states = new Map(); this.states = new Map()
/** /**
* Current active state * Current active state
* @type {string} * @type {string}
*/ */
this.currentState = States.WAIT; this.currentState = States.WAIT
/** /**
* Current active state handler * Current active state handler
* @type {StateHandler|null} * @type {StateHandler|null}
*/ */
this.currentStateHandler = null; this.currentStateHandler = null
/** /**
* Timer for inactivity detection * Timer for inactivity detection
* @type {number} * @type {number}
*/ */
this.inactivityTimer = 0; this.inactivityTimer = 0
/** /**
* Inactivity timeout in milliseconds * Inactivity timeout in milliseconds
* @type {number} * @type {number}
*/ */
this.inactivityTimeout = Config.INACTIVITY_TIMEOUT; this.inactivityTimeout = Config.INACTIVITY_TIMEOUT
/** /**
* Whether the system is initialized * Whether the system is initialized
* @type {boolean} * @type {boolean}
*/ */
this.initialized = false; this.initialized = false
} }
/** /**
* Initialize the animation system * Initialize the animation system
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async initialize() { async initialize () {
if (this.initialized) return; if (this.initialized) return
// Create animation clips from model // Create animation clips from model
this.clips = await this.animationClipFactory.createClipsFromModel(this.model); this.clips = await this.animationClipFactory.createClipsFromModel(this.model)
// Create actions for all clips // Create actions for all clips
for (const [ , clip ] of this.clips) { for (const [, clip] of this.clips) {
clip.createAction(this.mixer); clip.createAction(this.mixer)
} }
// Initialize state handlers // Initialize state handlers
this.initializeStates(); this.initializeStates()
// Start in wait state // Start in wait state
await this.transitionTo(States.WAIT); await this.transitionTo(States.WAIT)
this.initialized = true; this.initialized = true
console.log('Owen Animation System initialized'); console.log('Owen Animation System initialized')
} }
/** /**
@ -116,12 +116,12 @@ export class OwenAnimationContext {
* @private * @private
* @returns {void} * @returns {void}
*/ */
initializeStates() { initializeStates () {
const stateNames = this.stateFactory.getAvailableStates(); const stateNames = this.stateFactory.getAvailableStates()
for (const stateName of stateNames) { for (const stateName of stateNames) {
const handler = this.stateFactory.createStateHandler(stateName, this); const handler = this.stateFactory.createStateHandler(stateName, this)
this.states.set(stateName, handler); this.states.set(stateName, handler)
} }
} }
@ -132,28 +132,28 @@ export class OwenAnimationContext {
* @returns {Promise<void>} * @returns {Promise<void>}
* @throws {Error} If state is not found or transition is invalid * @throws {Error} If state is not found or transition is invalid
*/ */
async transitionTo(newStateName, emotion = Emotions.NEUTRAL) { async transitionTo (newStateName, emotion = Emotions.NEUTRAL) {
if (!this.states.has(newStateName)) { if (!this.states.has(newStateName)) {
throw new Error(`State '${newStateName}' not found`); throw new Error(`State '${newStateName}' not found`)
} }
const oldState = this.currentState; const oldState = this.currentState
const newStateHandler = this.states.get(newStateName); const newStateHandler = this.states.get(newStateName)
console.log(`Transitioning from ${oldState} to ${newStateName}`); console.log(`Transitioning from ${oldState} to ${newStateName}`)
// Exit current state // Exit current state
if (this.currentStateHandler) { if (this.currentStateHandler) {
await this.currentStateHandler.exit(newStateName, emotion); await this.currentStateHandler.exit(newStateName, emotion)
} }
// Enter new state // Enter new state
this.currentState = newStateName; this.currentState = newStateName
this.currentStateHandler = newStateHandler; this.currentStateHandler = newStateHandler
await this.currentStateHandler.enter(oldState, emotion); await this.currentStateHandler.enter(oldState, emotion)
// Reset inactivity timer // Reset inactivity timer
this.resetActivityTimer(); this.resetActivityTimer()
} }
/** /**
@ -161,26 +161,26 @@ export class OwenAnimationContext {
* @param {string} message - The user message * @param {string} message - The user message
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async handleUserMessage(message) { async handleUserMessage (message) {
console.log(`Handling user message: "${message}"`); console.log(`Handling user message: "${message}"`)
this.onUserActivity(); this.onUserActivity()
// If sleeping, wake up first // If sleeping, wake up first
if (this.currentState === States.SLEEP) { if (this.currentState === States.SLEEP) {
await this.transitionTo(States.REACT); await this.transitionTo(States.REACT)
} }
// Let current state handle the message // Let current state handle the message
if (this.currentStateHandler) { if (this.currentStateHandler) {
await this.currentStateHandler.handleMessage(message); await this.currentStateHandler.handleMessage(message)
} }
// Transition to appropriate next state based on current state // Transition to appropriate next state based on current state
if (this.currentState === States.WAIT) { if (this.currentState === States.WAIT) {
await this.transitionTo(States.REACT); await this.transitionTo(States.REACT)
} else if (this.currentState === States.REACT) { } else if (this.currentState === States.REACT) {
await this.transitionTo(States.TYPE); await this.transitionTo(States.TYPE)
} }
} }
@ -188,12 +188,12 @@ export class OwenAnimationContext {
* Called when user activity is detected * Called when user activity is detected
* @returns {void} * @returns {void}
*/ */
onUserActivity() { onUserActivity () {
this.resetActivityTimer(); this.resetActivityTimer()
// Wake up if sleeping // Wake up if sleeping
if (this.currentState === States.SLEEP) { if (this.currentState === States.SLEEP) {
this.transitionTo(States.WAIT); this.transitionTo(States.WAIT)
} }
} }
@ -202,8 +202,8 @@ export class OwenAnimationContext {
* @private * @private
* @returns {void} * @returns {void}
*/ */
resetActivityTimer() { resetActivityTimer () {
this.inactivityTimer = 0; this.inactivityTimer = 0
} }
/** /**
@ -211,9 +211,9 @@ export class OwenAnimationContext {
* @private * @private
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async handleInactivity() { async handleInactivity () {
console.log('Inactivity detected, transitioning to sleep'); console.log('Inactivity detected, transitioning to sleep')
await this.transitionTo(States.SLEEP); await this.transitionTo(States.SLEEP)
} }
/** /**
@ -221,21 +221,21 @@ export class OwenAnimationContext {
* @param {number} deltaTime - Time elapsed since last update (ms) * @param {number} deltaTime - Time elapsed since last update (ms)
* @returns {void} * @returns {void}
*/ */
update(deltaTime) { update (deltaTime) {
if (!this.initialized) return; if (!this.initialized) return
// Update Three.js mixer // Update Three.js mixer
this.mixer.update(deltaTime / 1000); // Convert to seconds this.mixer.update(deltaTime / 1000) // Convert to seconds
// Update current state // Update current state
if (this.currentStateHandler) { if (this.currentStateHandler) {
this.currentStateHandler.update(deltaTime); this.currentStateHandler.update(deltaTime)
} }
// Update inactivity timer // Update inactivity timer
this.inactivityTimer += deltaTime; this.inactivityTimer += deltaTime
if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEP) { if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEP) {
this.handleInactivity(); this.handleInactivity()
} }
} }
@ -244,8 +244,8 @@ export class OwenAnimationContext {
* @param {string} name - The animation clip name * @param {string} name - The animation clip name
* @returns {AnimationClip|undefined} The animation clip or undefined if not found * @returns {AnimationClip|undefined} The animation clip or undefined if not found
*/ */
getClip(name) { getClip (name) {
return this.clips.get(name); return this.clips.get(name)
} }
/** /**
@ -253,80 +253,80 @@ export class OwenAnimationContext {
* @param {string} pattern - Pattern to match (supports * wildcards) * @param {string} pattern - Pattern to match (supports * wildcards)
* @returns {AnimationClip[]} Array of matching clips * @returns {AnimationClip[]} Array of matching clips
*/ */
getClipsByPattern(pattern) { getClipsByPattern (pattern) {
const regex = new RegExp(pattern.replace(/\*/g, '.*')); const regex = new RegExp(pattern.replace(/\*/g, '.*'))
const matches = []; const matches = []
for (const [ name, clip ] of this.clips) { for (const [name, clip] of this.clips) {
if (regex.test(name)) { if (regex.test(name)) {
matches.push(clip); matches.push(clip)
} }
} }
return matches; return matches
} }
/** /**
* Get the current state name * Get the current state name
* @returns {string} The current state name * @returns {string} The current state name
*/ */
getCurrentState() { getCurrentState () {
return this.currentState; return this.currentState
} }
/** /**
* Get the current state handler * Get the current state handler
* @returns {StateHandler|null} The current state handler * @returns {StateHandler|null} The current state handler
*/ */
getCurrentStateHandler() { getCurrentStateHandler () {
return this.currentStateHandler; return this.currentStateHandler
} }
/** /**
* Get available transitions from current state * Get available transitions from current state
* @returns {string[]} Array of available state transitions * @returns {string[]} Array of available state transitions
*/ */
getAvailableTransitions() { getAvailableTransitions () {
if (this.currentStateHandler) { if (this.currentStateHandler) {
return this.currentStateHandler.getAvailableTransitions(); return this.currentStateHandler.getAvailableTransitions()
} }
return []; return []
} }
/** /**
* Get all available animation clip names * Get all available animation clip names
* @returns {string[]} Array of clip names * @returns {string[]} Array of clip names
*/ */
getAvailableClips() { getAvailableClips () {
return Array.from(this.clips.keys()); return Array.from(this.clips.keys())
} }
/** /**
* Get all available state names * Get all available state names
* @returns {string[]} Array of state names * @returns {string[]} Array of state names
*/ */
getAvailableStates() { getAvailableStates () {
return Array.from(this.states.keys()); return Array.from(this.states.keys())
} }
/** /**
* Dispose of the animation system and clean up resources * Dispose of the animation system and clean up resources
* @returns {void} * @returns {void}
*/ */
dispose() { dispose () {
// Stop all animations // Stop all animations
for (const [ , clip ] of this.clips) { for (const [, clip] of this.clips) {
if (clip.action) { if (clip.action) {
clip.action.stop(); clip.action.stop()
} }
} }
// Clear caches // Clear caches
this.clips.clear(); this.clips.clear()
this.states.clear(); this.states.clear()
this.animationClipFactory.clearCache(); this.animationClipFactory.clearCache()
this.initialized = false; this.initialized = false
console.log('Owen Animation System disposed'); console.log('Owen Animation System disposed')
} }
} }

View File

@ -3,18 +3,18 @@
* @module factories * @module factories
*/ */
import * as THREE from 'three'; import * as THREE from 'three'
import { OwenAnimationContext } from '../core/OwenAnimationContext.js'; import { OwenAnimationContext } from '../core/OwenAnimationContext.js'
import { AnimationClipFactory } from '../animation/AnimationClip.js'; import { AnimationClipFactory } from '../animation/AnimationClip.js'
import { GLTFAnimationLoader } from '../loaders/AnimationLoader.js'; import { GLTFAnimationLoader } from '../loaders/AnimationLoader.js'
import { StateFactory } from '../states/StateFactory.js'; import { StateFactory } from '../states/StateFactory.js'
/** /**
* Main factory for creating the complete Owen animation system * Main factory for creating the complete Owen animation system
* @class * @class
*/ */
export class OwenSystemFactory { export class OwenSystemFactory {
/** /**
* Create a complete Owen animation system * Create a complete Owen animation system
* @param {THREE.Object3D} gltfModel - The loaded GLTF model * @param {THREE.Object3D} gltfModel - The loaded GLTF model
* @param {THREE.Scene} scene - The Three.js scene * @param {THREE.Scene} scene - The Three.js scene
@ -22,70 +22,70 @@ export class OwenSystemFactory {
* @param {THREE.GLTFLoader} [options.gltfLoader] - Custom GLTF loader * @param {THREE.GLTFLoader} [options.gltfLoader] - Custom GLTF loader
* @returns {Promise<OwenAnimationContext>} The configured Owen system * @returns {Promise<OwenAnimationContext>} The configured Owen system
*/ */
static async createOwenSystem(gltfModel, scene, options = {}) { static async createOwenSystem (gltfModel, scene, options = {}) {
// Create Three.js animation mixer // Create Three.js animation mixer
const mixer = new THREE.AnimationMixer(gltfModel); const mixer = new THREE.AnimationMixer(gltfModel)
// Create GLTF loader if not provided // Create GLTF loader if not provided
const gltfLoader = options.gltfLoader || new THREE.GLTFLoader(); const gltfLoader = options.gltfLoader || new THREE.GLTFLoader()
// Create animation loader // Create animation loader
const animationLoader = new GLTFAnimationLoader(gltfLoader); const animationLoader = new GLTFAnimationLoader(gltfLoader)
// Preload animations from the model // Preload animations from the model
await animationLoader.preloadAnimations(gltfModel); await animationLoader.preloadAnimations(gltfModel)
// Create animation clip factory // Create animation clip factory
const animationClipFactory = new AnimationClipFactory(animationLoader); const animationClipFactory = new AnimationClipFactory(animationLoader)
// Create state factory // Create state factory
const stateFactory = new StateFactory(); const stateFactory = new StateFactory()
// Create the main Owen context // Create the main Owen context
const owenContext = new OwenAnimationContext( const owenContext = new OwenAnimationContext(
gltfModel, gltfModel,
mixer, mixer,
animationClipFactory, animationClipFactory,
stateFactory stateFactory
); )
// Initialize the system // Initialize the system
await owenContext.initialize(); await owenContext.initialize()
return owenContext; return owenContext
} }
/** /**
* Create a basic Owen system with minimal configuration * Create a basic Owen system with minimal configuration
* @param {THREE.Object3D} model - The 3D model * @param {THREE.Object3D} model - The 3D model
* @returns {Promise<OwenAnimationContext>} The configured Owen system * @returns {Promise<OwenAnimationContext>} The configured Owen system
*/ */
static async createBasicOwenSystem(model) { static async createBasicOwenSystem (model) {
const scene = new THREE.Scene(); const scene = new THREE.Scene()
scene.add(model); scene.add(model)
return await OwenSystemFactory.createOwenSystem(model, scene); return await OwenSystemFactory.createOwenSystem(model, scene)
} }
/** /**
* Create an Owen system with custom state handlers * Create an Owen system with custom state handlers
* @param {THREE.Object3D} gltfModel - The loaded GLTF model * @param {THREE.Object3D} gltfModel - The loaded GLTF model
* @param {THREE.Scene} scene - The Three.js scene * @param {THREE.Scene} scene - The Three.js scene
* @param {Map<string, Function>} customStates - Map of state name to handler class * @param {Map<string, Function>} customStates - Map of state name to handler class
* @returns {Promise<OwenAnimationContext>} The configured Owen system * @returns {Promise<OwenAnimationContext>} The configured Owen system
*/ */
static async createCustomOwenSystem(gltfModel, scene, customStates) { static async createCustomOwenSystem (gltfModel, scene, customStates) {
const system = await OwenSystemFactory.createOwenSystem(gltfModel, scene); const system = await OwenSystemFactory.createOwenSystem(gltfModel, scene)
// Register custom state handlers // Register custom state handlers
const stateFactory = system.stateFactory; const stateFactory = system.stateFactory
for (const [ stateName, handlerClass ] of customStates) { for (const [stateName, handlerClass] of customStates) {
stateFactory.registerStateHandler(stateName, handlerClass); stateFactory.registerStateHandler(stateName, handlerClass)
}
// Reinitialize with custom states
system.initializeStates();
return system;
} }
// Reinitialize with custom states
system.initializeStates()
return system
}
} }

View File

@ -4,32 +4,32 @@
*/ */
// Core exports // Core exports
export { OwenAnimationContext } from './core/OwenAnimationContext.js'; // Import for default export
import { OwenSystemFactory } from './factories/OwenSystemFactory.js'
import { OwenAnimationContext } from './core/OwenAnimationContext.js'
import { States, Emotions, ClipTypes, Config } from './constants.js'
export { OwenAnimationContext } from './core/OwenAnimationContext.js'
// Animation system exports // Animation system exports
export { AnimationClip, AnimationClipFactory } from './animation/AnimationClip.js'; export { AnimationClip, AnimationClipFactory } from './animation/AnimationClip.js'
// Loader exports // Loader exports
export { AnimationLoader, GLTFAnimationLoader } from './loaders/AnimationLoader.js'; export { AnimationLoader, GLTFAnimationLoader } from './loaders/AnimationLoader.js'
// State system exports // State system exports
export { StateHandler } from './states/StateHandler.js'; export { StateHandler } from './states/StateHandler.js'
export { WaitStateHandler } from './states/WaitStateHandler.js'; export { WaitStateHandler } from './states/WaitStateHandler.js'
export { ReactStateHandler } from './states/ReactStateHandler.js'; export { ReactStateHandler } from './states/ReactStateHandler.js'
export { TypeStateHandler } from './states/TypeStateHandler.js'; export { TypeStateHandler } from './states/TypeStateHandler.js'
export { SleepStateHandler } from './states/SleepStateHandler.js'; export { SleepStateHandler } from './states/SleepStateHandler.js'
export { StateFactory } from './states/StateFactory.js'; export { StateFactory } from './states/StateFactory.js'
// Factory exports // Factory exports
export { OwenSystemFactory } from './factories/OwenSystemFactory.js'; export { OwenSystemFactory } from './factories/OwenSystemFactory.js'
// Constants exports // Constants exports
export { ClipTypes, States, Emotions, Config } from './constants.js'; export { ClipTypes, States, Emotions, Config } from './constants.js'
// Import for default export
import { OwenSystemFactory } from './factories/OwenSystemFactory.js';
import { OwenAnimationContext } from './core/OwenAnimationContext.js';
import { States, Emotions, ClipTypes, Config } from './constants.js';
/** /**
* Default export - the main factory for easy usage * Default export - the main factory for easy usage
@ -41,4 +41,4 @@ export default {
Emotions, Emotions,
ClipTypes, ClipTypes,
Config Config
}; }

View File

@ -9,16 +9,16 @@
* @class * @class
*/ */
export class AnimationLoader { export class AnimationLoader {
/** /**
* Load an animation by name * Load an animation by name
* @abstract * @abstract
* @param {string} _name - The animation name to load (unused in base class) * @param {string} _name - The animation name to load (unused in base class)
* @returns {Promise<THREE.AnimationClip>} The loaded animation clip * @returns {Promise<THREE.AnimationClip>} The loaded animation clip
* @throws {Error} Must be implemented by subclasses * @throws {Error} Must be implemented by subclasses
*/ */
async loadAnimation(_name) { async loadAnimation (_name) {
throw new Error('loadAnimation method must be implemented by subclasses'); throw new Error('loadAnimation method must be implemented by subclasses')
} }
} }
/** /**
@ -27,68 +27,68 @@ export class AnimationLoader {
* @extends AnimationLoader * @extends AnimationLoader
*/ */
export class GLTFAnimationLoader extends AnimationLoader { export class GLTFAnimationLoader extends AnimationLoader {
/** /**
* Create a GLTF animation loader * Create a GLTF animation loader
* @param {THREE.GLTFLoader} gltfLoader - The Three.js GLTF loader instance * @param {THREE.GLTFLoader} gltfLoader - The Three.js GLTF loader instance
*/ */
constructor(gltfLoader) { constructor (gltfLoader) {
super(); super()
/** /**
* The Three.js GLTF loader * The Three.js GLTF loader
* @type {THREE.GLTFLoader} * @type {THREE.GLTFLoader}
*/ */
this.gltfLoader = gltfLoader; this.gltfLoader = gltfLoader
/** /**
* Cache for loaded animations * Cache for loaded animations
* @type {Map<string, THREE.AnimationClip>} * @type {Map<string, THREE.AnimationClip>}
*/ */
this.animationCache = new Map(); this.animationCache = new Map()
} }
/** /**
* Load an animation from GLTF by name * Load an animation from GLTF by name
* @param {string} name - The animation name to load * @param {string} name - The animation name to load
* @returns {Promise<THREE.AnimationClip>} The loaded animation clip * @returns {Promise<THREE.AnimationClip>} The loaded animation clip
* @throws {Error} If animation is not found * @throws {Error} If animation is not found
*/ */
async loadAnimation(name) { async loadAnimation (name) {
if (this.animationCache.has(name)) { if (this.animationCache.has(name)) {
return this.animationCache.get(name); return this.animationCache.get(name)
}
// In a real implementation, this would load from GLTF files
// For now, we'll assume animations are already loaded in the model
throw new Error(`Animation '${name}' not found. Implement GLTF loading logic.`);
} }
/** // In a real implementation, this would load from GLTF files
// For now, we'll assume animations are already loaded in the model
throw new Error(`Animation '${name}' not found. Implement GLTF loading logic.`)
}
/**
* Preload animations from a GLTF model * Preload animations from a GLTF model
* @param {Object} gltfModel - The loaded GLTF model * @param {Object} gltfModel - The loaded GLTF model
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async preloadAnimations(gltfModel) { async preloadAnimations (gltfModel) {
if (gltfModel.animations) { if (gltfModel.animations) {
for (const animation of gltfModel.animations) { for (const animation of gltfModel.animations) {
this.animationCache.set(animation.name, animation); this.animationCache.set(animation.name, animation)
} }
}
} }
}
/** /**
* Clear the animation cache * Clear the animation cache
* @returns {void} * @returns {void}
*/ */
clearCache() { clearCache () {
this.animationCache.clear(); this.animationCache.clear()
} }
/** /**
* Get all cached animation names * Get all cached animation names
* @returns {string[]} Array of cached animation names * @returns {string[]} Array of cached animation names
*/ */
getCachedAnimationNames() { getCachedAnimationNames () {
return Array.from(this.animationCache.keys()); return Array.from(this.animationCache.keys())
} }
} }

View File

@ -3,8 +3,8 @@
* @module states * @module states
*/ */
import { StateHandler } from './StateHandler.js'; import { StateHandler } from './StateHandler.js'
import { States, Emotions } from '../constants.js'; import { States, Emotions } from '../constants.js'
/** /**
* Handler for the React state * Handler for the React state
@ -12,148 +12,148 @@ import { States, Emotions } from '../constants.js';
* @extends StateHandler * @extends StateHandler
*/ */
export class ReactStateHandler extends StateHandler { export class ReactStateHandler extends StateHandler {
/** /**
* Create a react state handler * Create a react state handler
* @param {OwenAnimationContext} context - The animation context * @param {OwenAnimationContext} context - The animation context
*/ */
constructor(context) { constructor (context) {
super(States.REACT, context); super(States.REACT, context)
/** /**
* Current emotional state * Current emotional state
* @type {string} * @type {string}
*/ */
this.emotion = Emotions.NEUTRAL; this.emotion = Emotions.NEUTRAL
} }
/** /**
* Enter the react state * Enter the react state
* @param {string|null} [_fromState=null] - The previous state (unused) * @param {string|null} [_fromState=null] - The previous state (unused)
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with * @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async enter(_fromState = null, emotion = Emotions.NEUTRAL) { async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
console.log(`Entering REACT state with emotion: ${emotion}`); console.log(`Entering REACT state with emotion: ${emotion}`)
this.emotion = emotion; this.emotion = emotion
// Play appropriate reaction // Play appropriate reaction
const reactionClip = this.context.getClip('react_idle_L'); const reactionClip = this.context.getClip('react_idle_L')
if (reactionClip) { if (reactionClip) {
await reactionClip.play(); await reactionClip.play()
this.currentClip = reactionClip; this.currentClip = reactionClip
}
} }
}
/** /**
* Exit the react state * Exit the react state
* @param {string|null} [toState=null] - The next state * @param {string|null} [toState=null] - The next state
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with * @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async exit(toState = null, emotion = Emotions.NEUTRAL) { async exit (toState = null, emotion = Emotions.NEUTRAL) {
console.log(`Exiting REACT state to ${toState} with emotion: ${emotion}`); console.log(`Exiting REACT state to ${toState} with emotion: ${emotion}`)
if (this.currentClip) { if (this.currentClip) {
await this.stopCurrentClip(); await this.stopCurrentClip()
}
// Play emotional transition if available
let transitionName;
if (emotion !== Emotions.NEUTRAL) {
transitionName = `react_${this.emotion}2${toState}_${emotion}_T`;
} else {
transitionName = `react_2${toState}_T`;
}
const transition = this.context.getClip(transitionName);
if (transition) {
await transition.play();
await this.waitForClipEnd(transition);
}
} }
/** // Play emotional transition if available
let transitionName
if (emotion !== Emotions.NEUTRAL) {
transitionName = `react_${this.emotion}2${toState}_${emotion}_T`
} else {
transitionName = `react_2${toState}_T`
}
const transition = this.context.getClip(transitionName)
if (transition) {
await transition.play()
await this.waitForClipEnd(transition)
}
}
/**
* Handle a user message in react state * Handle a user message in react state
* @param {string} message - The user message * @param {string} message - The user message
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async handleMessage(message) { async handleMessage (message) {
// Analyze message sentiment to determine emotion // Analyze message sentiment to determine emotion
const emotion = this.analyzeMessageEmotion(message); const emotion = this.analyzeMessageEmotion(message)
this.emotion = emotion; this.emotion = emotion
// Play emotional reaction if needed // Play emotional reaction if needed
if (emotion !== Emotions.NEUTRAL) { if (emotion !== Emotions.NEUTRAL) {
const emotionalReaction = this.context.getClip(`react_${emotion}_Q`); const emotionalReaction = this.context.getClip(`react_${emotion}_Q`)
if (emotionalReaction) { if (emotionalReaction) {
if (this.currentClip) { if (this.currentClip) {
await this.stopCurrentClip(0.2); await this.stopCurrentClip(0.2)
}
await emotionalReaction.play();
await this.waitForClipEnd(emotionalReaction);
}
} }
await emotionalReaction.play()
await this.waitForClipEnd(emotionalReaction)
}
} }
}
/** /**
* Analyze message to determine emotional response * Analyze message to determine emotional response
* @private * @private
* @param {string} message - The message to analyze * @param {string} message - The message to analyze
* @returns {string} The determined emotion * @returns {string} The determined emotion
*/ */
analyzeMessageEmotion(message) { analyzeMessageEmotion (message) {
const text = message.toLowerCase(); const text = message.toLowerCase()
// Check for urgent/angry indicators // Check for urgent/angry indicators
if ( if (
text.includes('!') || text.includes('!') ||
text.includes('urgent') || text.includes('urgent') ||
text.includes('asap') || text.includes('asap') ||
text.includes('hurry') text.includes('hurry')
) { ) {
return Emotions.ANGRY; return Emotions.ANGRY
} }
// Check for error/shocked indicators // Check for error/shocked indicators
if ( if (
text.includes('error') || text.includes('error') ||
text.includes('problem') || text.includes('problem') ||
text.includes('issue') || text.includes('issue') ||
text.includes('bug') || text.includes('bug') ||
text.includes('broken') text.includes('broken')
) { ) {
return Emotions.SHOCKED; return Emotions.SHOCKED
} }
// Check for positive/happy indicators // Check for positive/happy indicators
if ( if (
text.includes('great') || text.includes('great') ||
text.includes('awesome') || text.includes('awesome') ||
text.includes('good') || text.includes('good') ||
text.includes('excellent') || text.includes('excellent') ||
text.includes('perfect') text.includes('perfect')
) { ) {
return Emotions.HAPPY; return Emotions.HAPPY
} }
// Check for sad indicators // Check for sad indicators
if ( if (
text.includes('sad') || text.includes('sad') ||
text.includes('disappointed') || text.includes('disappointed') ||
text.includes('failed') || text.includes('failed') ||
text.includes('wrong') text.includes('wrong')
) { ) {
return Emotions.SAD; return Emotions.SAD
}
return Emotions.NEUTRAL;
} }
/** return Emotions.NEUTRAL
}
/**
* Get available transitions from react state * Get available transitions from react state
* @returns {string[]} Array of available state transitions * @returns {string[]} Array of available state transitions
*/ */
getAvailableTransitions() { getAvailableTransitions () {
return [ States.TYPE, States.WAIT ]; return [States.TYPE, States.WAIT]
} }
} }

View File

@ -3,8 +3,8 @@
* @module states * @module states
*/ */
import { StateHandler } from './StateHandler.js'; import { StateHandler } from './StateHandler.js'
import { States, Emotions } from '../constants.js'; import { States, Emotions } from '../constants.js'
/** /**
* Handler for the Sleep state * Handler for the Sleep state
@ -16,20 +16,20 @@ export class SleepStateHandler extends StateHandler {
* Create a sleep state handler * Create a sleep state handler
* @param {OwenAnimationContext} context - The animation context * @param {OwenAnimationContext} context - The animation context
*/ */
constructor(context) { constructor (context) {
super(States.SLEEP, context); super(States.SLEEP, context)
/** /**
* Sleep animation clip * Sleep animation clip
* @type {AnimationClip|null} * @type {AnimationClip|null}
*/ */
this.sleepClip = null; this.sleepClip = null
/** /**
* Whether the character is in deep sleep * Whether the character is in deep sleep
* @type {boolean} * @type {boolean}
*/ */
this.isDeepSleep = false; this.isDeepSleep = false
} }
/** /**
@ -38,24 +38,24 @@ export class SleepStateHandler extends StateHandler {
* @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to enter with (unused) * @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to enter with (unused)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async enter(fromState = null, _emotion = Emotions.NEUTRAL) { async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Entering SLEEP state from ${fromState}`); console.log(`Entering SLEEP state from ${fromState}`)
// Play sleep transition if available // Play sleep transition if available
const sleepTransition = this.context.getClip('wait_2sleep_T'); const sleepTransition = this.context.getClip('wait_2sleep_T')
if (sleepTransition) { if (sleepTransition) {
await sleepTransition.play(); await sleepTransition.play()
await this.waitForClipEnd(sleepTransition); await this.waitForClipEnd(sleepTransition)
} }
// Start sleep loop // Start sleep loop
this.sleepClip = this.context.getClip('sleep_idle_L'); this.sleepClip = this.context.getClip('sleep_idle_L')
if (this.sleepClip) { if (this.sleepClip) {
await this.sleepClip.play(); await this.sleepClip.play()
this.currentClip = this.sleepClip; this.currentClip = this.sleepClip
} }
this.isDeepSleep = true; this.isDeepSleep = true
} }
/** /**
@ -64,27 +64,27 @@ export class SleepStateHandler extends StateHandler {
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with * @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async exit(toState = null, _emotion = Emotions.NEUTRAL) { async exit (toState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Exiting SLEEP state to ${toState}`); console.log(`Exiting SLEEP state to ${toState}`)
this.isDeepSleep = false; this.isDeepSleep = false
if (this.currentClip) { if (this.currentClip) {
await this.stopCurrentClip(); await this.stopCurrentClip()
} }
// Play wake up animation // Play wake up animation
const wakeUpClip = this.context.getClip('sleep_wakeup_T'); const wakeUpClip = this.context.getClip('sleep_wakeup_T')
if (wakeUpClip) { if (wakeUpClip) {
await wakeUpClip.play(); await wakeUpClip.play()
await this.waitForClipEnd(wakeUpClip); await this.waitForClipEnd(wakeUpClip)
} }
// Play transition to next state if available // Play transition to next state if available
const transitionName = `sleep_2${toState}_T`; const transitionName = `sleep_2${toState}_T`
const transition = this.context.getClip(transitionName); const transition = this.context.getClip(transitionName)
if (transition) { if (transition) {
await transition.play(); await transition.play()
await this.waitForClipEnd(transition); await this.waitForClipEnd(transition)
} }
} }
@ -93,7 +93,7 @@ export class SleepStateHandler extends StateHandler {
* @param {number} _deltaTime - Time elapsed since last update (ms, unused) * @param {number} _deltaTime - Time elapsed since last update (ms, unused)
* @returns {void} * @returns {void}
*/ */
update(_deltaTime) { update (_deltaTime) {
// Sleep state doesn't need regular updates // Sleep state doesn't need regular updates
// Character remains asleep until external stimulus // Character remains asleep until external stimulus
} }
@ -103,12 +103,12 @@ export class SleepStateHandler extends StateHandler {
* @param {string} _message - The user message (unused, just triggers wake up) * @param {string} _message - The user message (unused, just triggers wake up)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async handleMessage(_message) { async handleMessage (_message) {
// Any message should wake up the character // Any message should wake up the character
if (this.isDeepSleep) { if (this.isDeepSleep) {
console.log('Waking up due to user message'); console.log('Waking up due to user message')
// This will trigger a state transition to REACT // This will trigger a state transition to REACT
await this.context.transitionTo(States.REACT); await this.context.transitionTo(States.REACT)
} }
} }
@ -116,25 +116,25 @@ export class SleepStateHandler extends StateHandler {
* Get available transitions from sleep state * Get available transitions from sleep state
* @returns {string[]} Array of available state transitions * @returns {string[]} Array of available state transitions
*/ */
getAvailableTransitions() { getAvailableTransitions () {
return [ States.WAIT, States.REACT ]; return [States.WAIT, States.REACT]
} }
/** /**
* Check if in deep sleep * Check if in deep sleep
* @returns {boolean} True if in deep sleep, false otherwise * @returns {boolean} True if in deep sleep, false otherwise
*/ */
isInDeepSleep() { isInDeepSleep () {
return this.isDeepSleep; return this.isDeepSleep
} }
/** /**
* Force wake up from sleep * Force wake up from sleep
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async wakeUp() { async wakeUp () {
if (this.isDeepSleep) { if (this.isDeepSleep) {
await this.context.transitionTo(States.WAIT); await this.context.transitionTo(States.WAIT)
} }
} }
} }

View File

@ -3,11 +3,11 @@
* @module states * @module states
*/ */
import { WaitStateHandler } from './WaitStateHandler.js'; import { WaitStateHandler } from './WaitStateHandler.js'
import { ReactStateHandler } from './ReactStateHandler.js'; import { ReactStateHandler } from './ReactStateHandler.js'
import { TypeStateHandler } from './TypeStateHandler.js'; import { TypeStateHandler } from './TypeStateHandler.js'
import { SleepStateHandler } from './SleepStateHandler.js'; import { SleepStateHandler } from './SleepStateHandler.js'
import { States } from '../constants.js'; import { States } from '../constants.js'
/** /**
* Factory for creating state handlers using dependency injection * Factory for creating state handlers using dependency injection
@ -17,19 +17,19 @@ export class StateFactory {
/** /**
* Create a state factory * Create a state factory
*/ */
constructor() { constructor () {
/** /**
* Registry of state handler classes * Registry of state handler classes
* @type {Map<string, Function>} * @type {Map<string, Function>}
* @private * @private
*/ */
this.stateHandlers = new Map(); this.stateHandlers = new Map()
// Register default state handlers // Register default state handlers
this.registerStateHandler(States.WAIT, WaitStateHandler); this.registerStateHandler(States.WAIT, WaitStateHandler)
this.registerStateHandler(States.REACT, ReactStateHandler); this.registerStateHandler(States.REACT, ReactStateHandler)
this.registerStateHandler(States.TYPE, TypeStateHandler); this.registerStateHandler(States.TYPE, TypeStateHandler)
this.registerStateHandler(States.SLEEP, SleepStateHandler); this.registerStateHandler(States.SLEEP, SleepStateHandler)
} }
/** /**
@ -38,8 +38,8 @@ export class StateFactory {
* @param {Function} handlerClass - The handler class constructor * @param {Function} handlerClass - The handler class constructor
* @returns {void} * @returns {void}
*/ */
registerStateHandler(stateName, handlerClass) { registerStateHandler (stateName, handlerClass) {
this.stateHandlers.set(stateName, handlerClass); this.stateHandlers.set(stateName, handlerClass)
} }
/** /**
@ -49,21 +49,21 @@ export class StateFactory {
* @returns {StateHandler} The created state handler * @returns {StateHandler} The created state handler
* @throws {Error} If state handler is not registered * @throws {Error} If state handler is not registered
*/ */
createStateHandler(stateName, context) { createStateHandler (stateName, context) {
const HandlerClass = this.stateHandlers.get(stateName); const HandlerClass = this.stateHandlers.get(stateName)
if (!HandlerClass) { if (!HandlerClass) {
throw new Error(`No handler registered for state: ${stateName}`); throw new Error(`No handler registered for state: ${stateName}`)
} }
return new HandlerClass(context); return new HandlerClass(context)
} }
/** /**
* Get all available state names * Get all available state names
* @returns {string[]} Array of registered state names * @returns {string[]} Array of registered state names
*/ */
getAvailableStates() { getAvailableStates () {
return Array.from(this.stateHandlers.keys()); return Array.from(this.stateHandlers.keys())
} }
/** /**
@ -71,8 +71,8 @@ export class StateFactory {
* @param {string} stateName - The state name to check * @param {string} stateName - The state name to check
* @returns {boolean} True if registered, false otherwise * @returns {boolean} True if registered, false otherwise
*/ */
isStateRegistered(stateName) { isStateRegistered (stateName) {
return this.stateHandlers.has(stateName); return this.stateHandlers.has(stateName)
} }
/** /**
@ -80,7 +80,7 @@ export class StateFactory {
* @param {string} stateName - The state name to unregister * @param {string} stateName - The state name to unregister
* @returns {boolean} True if removed, false if not found * @returns {boolean} True if removed, false if not found
*/ */
unregisterStateHandler(stateName) { unregisterStateHandler (stateName) {
return this.stateHandlers.delete(stateName); return this.stateHandlers.delete(stateName)
} }
} }

View File

@ -3,7 +3,7 @@
* @module StateHandler * @module StateHandler
*/ */
import { Emotions, Config } from '../constants.js'; import { Emotions, Config } from '../constants.js'
/** /**
* Abstract base class for state handlers * Abstract base class for state handlers
@ -16,30 +16,30 @@ export class StateHandler {
* @param {string} stateName - The name of the state * @param {string} stateName - The name of the state
* @param {OwenAnimationContext} context - The animation context * @param {OwenAnimationContext} context - The animation context
*/ */
constructor(stateName, context) { constructor (stateName, context) {
/** /**
* The name of this state * The name of this state
* @type {string} * @type {string}
*/ */
this.stateName = stateName; this.stateName = stateName
/** /**
* The animation context * The animation context
* @type {OwenAnimationContext} * @type {OwenAnimationContext}
*/ */
this.context = context; this.context = context
/** /**
* Currently playing animation clip * Currently playing animation clip
* @type {AnimationClip|null} * @type {AnimationClip|null}
*/ */
this.currentClip = null; this.currentClip = null
/** /**
* Nested state information * Nested state information
* @type {Object|null} * @type {Object|null}
*/ */
this.nestedState = null; this.nestedState = null
} }
/** /**
@ -50,8 +50,8 @@ export class StateHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
* @throws {Error} Must be implemented by subclasses * @throws {Error} Must be implemented by subclasses
*/ */
async enter(_fromState = null, _emotion = Emotions.NEUTRAL) { async enter (_fromState = null, _emotion = Emotions.NEUTRAL) {
throw new Error('enter method must be implemented by subclasses'); throw new Error('enter method must be implemented by subclasses')
} }
/** /**
@ -62,8 +62,8 @@ export class StateHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
* @throws {Error} Must be implemented by subclasses * @throws {Error} Must be implemented by subclasses
*/ */
async exit(_toState = null, _emotion = Emotions.NEUTRAL) { async exit (_toState = null, _emotion = Emotions.NEUTRAL) {
throw new Error('exit method must be implemented by subclasses'); throw new Error('exit method must be implemented by subclasses')
} }
/** /**
@ -71,7 +71,7 @@ export class StateHandler {
* @param {number} _deltaTime - Time elapsed since last update (ms, unused in base class) * @param {number} _deltaTime - Time elapsed since last update (ms, unused in base class)
* @returns {void} * @returns {void}
*/ */
update(_deltaTime) { update (_deltaTime) {
// Override in subclasses if needed // Override in subclasses if needed
} }
@ -80,7 +80,7 @@ export class StateHandler {
* @param {string} _message - The user message (unused in base class) * @param {string} _message - The user message (unused in base class)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async handleMessage(_message) { async handleMessage (_message) {
// Override in subclasses if needed // Override in subclasses if needed
} }
@ -88,8 +88,8 @@ export class StateHandler {
* Get available transitions from this state * Get available transitions from this state
* @returns {string[]} Array of state names that can be transitioned to * @returns {string[]} Array of state names that can be transitioned to
*/ */
getAvailableTransitions() { getAvailableTransitions () {
return []; return []
} }
/** /**
@ -98,17 +98,17 @@ export class StateHandler {
* @param {AnimationClip} clip - The animation clip to wait for * @param {AnimationClip} clip - The animation clip to wait for
* @returns {Promise<void>} Promise that resolves when the clip finishes * @returns {Promise<void>} Promise that resolves when the clip finishes
*/ */
async waitForClipEnd(clip) { async waitForClipEnd (clip) {
return new Promise((resolve) => { return new Promise((resolve) => {
const checkFinished = () => { const checkFinished = () => {
if (!clip.isPlaying()) { if (!clip.isPlaying()) {
resolve(); resolve()
} else { } else {
requestAnimationFrame(checkFinished); requestAnimationFrame(checkFinished)
} }
}; }
checkFinished(); checkFinished()
}); })
} }
/** /**
@ -117,10 +117,10 @@ export class StateHandler {
* @param {number} [fadeOutDuration] - Fade out duration * @param {number} [fadeOutDuration] - Fade out duration
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async stopCurrentClip(fadeOutDuration = Config.DEFAULT_FADE_OUT) { async stopCurrentClip (fadeOutDuration = Config.DEFAULT_FADE_OUT) {
if (this.currentClip) { if (this.currentClip) {
await this.currentClip.stop(fadeOutDuration); await this.currentClip.stop(fadeOutDuration)
this.currentClip = null; this.currentClip = null
} }
} }
} }

View File

@ -3,8 +3,8 @@
* @module states * @module states
*/ */
import { StateHandler } from './StateHandler.js'; import { StateHandler } from './StateHandler.js'
import { States, Emotions } from '../constants.js'; import { States, Emotions } from '../constants.js'
/** /**
* Handler for the Type state * Handler for the Type state
@ -16,20 +16,20 @@ export class TypeStateHandler extends StateHandler {
* Create a type state handler * Create a type state handler
* @param {OwenAnimationContext} context - The animation context * @param {OwenAnimationContext} context - The animation context
*/ */
constructor(context) { constructor (context) {
super(States.TYPE, context); super(States.TYPE, context)
/** /**
* Current emotional state * Current emotional state
* @type {string} * @type {string}
*/ */
this.emotion = Emotions.NEUTRAL; this.emotion = Emotions.NEUTRAL
/** /**
* Whether currently typing * Whether currently typing
* @type {boolean} * @type {boolean}
*/ */
this.isTyping = false; this.isTyping = false
} }
/** /**
@ -38,21 +38,21 @@ export class TypeStateHandler extends StateHandler {
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with * @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async enter(_fromState = null, emotion = Emotions.NEUTRAL) { async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
console.log(`Entering TYPE state with emotion: ${emotion}`); console.log(`Entering TYPE state with emotion: ${emotion}`)
this.emotion = emotion; this.emotion = emotion
this.isTyping = true; this.isTyping = true
// Play appropriate typing animation // Play appropriate typing animation
let typingClipName = 'type_idle_L'; let typingClipName = 'type_idle_L'
if (emotion !== Emotions.NEUTRAL) { if (emotion !== Emotions.NEUTRAL) {
typingClipName = `type_${emotion}_L`; typingClipName = `type_${emotion}_L`
} }
const typingClip = this.context.getClip(typingClipName); const typingClip = this.context.getClip(typingClipName)
if (typingClip) { if (typingClip) {
await typingClip.play(); await typingClip.play()
this.currentClip = typingClip; this.currentClip = typingClip
} }
} }
@ -62,24 +62,24 @@ export class TypeStateHandler extends StateHandler {
* @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to exit with (unused) * @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to exit with (unused)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async exit(toState = null, _emotion = Emotions.NEUTRAL) { async exit (toState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Exiting TYPE state to ${toState}`); console.log(`Exiting TYPE state to ${toState}`)
this.isTyping = false; this.isTyping = false
if (this.currentClip) { if (this.currentClip) {
await this.stopCurrentClip(); await this.stopCurrentClip()
} }
// Play transition if available // Play transition if available
let transitionName = `type_2${toState}_T`; let transitionName = `type_2${toState}_T`
if (this.emotion !== Emotions.NEUTRAL) { if (this.emotion !== Emotions.NEUTRAL) {
transitionName = `type_${this.emotion}2${toState}_T`; transitionName = `type_${this.emotion}2${toState}_T`
} }
const transition = this.context.getClip(transitionName); const transition = this.context.getClip(transitionName)
if (transition) { if (transition) {
await transition.play(); await transition.play()
await this.waitForClipEnd(transition); await this.waitForClipEnd(transition)
} }
} }
@ -87,34 +87,34 @@ export class TypeStateHandler extends StateHandler {
* Finish typing and prepare to transition * Finish typing and prepare to transition
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async finishTyping() { async finishTyping () {
if (!this.isTyping) return; if (!this.isTyping) return
// Play typing finish animation if available // Play typing finish animation if available
const finishClip = this.context.getClip('type_finish_Q'); const finishClip = this.context.getClip('type_finish_Q')
if (finishClip && this.currentClip) { if (finishClip && this.currentClip) {
await this.stopCurrentClip(0.2); await this.stopCurrentClip(0.2)
await finishClip.play(); await finishClip.play()
await this.waitForClipEnd(finishClip); await this.waitForClipEnd(finishClip)
} }
this.isTyping = false; this.isTyping = false
} }
/** /**
* Get available transitions from type state * Get available transitions from type state
* @returns {string[]} Array of available state transitions * @returns {string[]} Array of available state transitions
*/ */
getAvailableTransitions() { getAvailableTransitions () {
return [ States.WAIT, States.REACT ]; return [States.WAIT, States.REACT]
} }
/** /**
* Check if currently typing * Check if currently typing
* @returns {boolean} True if typing, false otherwise * @returns {boolean} True if typing, false otherwise
*/ */
getIsTyping() { getIsTyping () {
return this.isTyping; return this.isTyping
} }
/** /**
@ -122,7 +122,7 @@ export class TypeStateHandler extends StateHandler {
* @param {boolean} typing - Whether currently typing * @param {boolean} typing - Whether currently typing
* @returns {void} * @returns {void}
*/ */
setTyping(typing) { setTyping (typing) {
this.isTyping = typing; this.isTyping = typing
} }
} }

View File

@ -3,8 +3,8 @@
* @module states * @module states
*/ */
import { StateHandler } from './StateHandler.js'; import { StateHandler } from './StateHandler.js'
import { States, Emotions, Config } from '../constants.js'; import { States, Emotions, Config } from '../constants.js'
/** /**
* Handler for the Wait/Idle state * Handler for the Wait/Idle state
@ -12,127 +12,127 @@ import { States, Emotions, Config } from '../constants.js';
* @extends StateHandler * @extends StateHandler
*/ */
export class WaitStateHandler extends StateHandler { export class WaitStateHandler extends StateHandler {
/** /**
* Create a wait state handler * Create a wait state handler
* @param {OwenAnimationContext} context - The animation context * @param {OwenAnimationContext} context - The animation context
*/ */
constructor(context) { constructor (context) {
super(States.WAIT, context); super(States.WAIT, context)
/** /**
* The main idle animation clip * The main idle animation clip
* @type {AnimationClip|null} * @type {AnimationClip|null}
*/ */
this.idleClip = null; this.idleClip = null
/** /**
* Available quirk animations * Available quirk animations
* @type {AnimationClip[]} * @type {AnimationClip[]}
*/ */
this.quirks = []; this.quirks = []
/** /**
* Timer for quirk animations * Timer for quirk animations
* @type {number} * @type {number}
*/ */
this.quirkTimer = 0; this.quirkTimer = 0
/** /**
* Interval between quirk attempts (ms) * Interval between quirk attempts (ms)
* @type {number} * @type {number}
*/ */
this.quirkInterval = Config.QUIRK_INTERVAL; this.quirkInterval = Config.QUIRK_INTERVAL
} }
/** /**
* Enter the wait state * Enter the wait state
* @param {string|null} [fromState=null] - The previous state * @param {string|null} [fromState=null] - The previous state
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with * @param {string} [emotion=Emotions.NEUTRAL] - The emotion to enter with
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async enter(fromState = null, _emotion = Emotions.NEUTRAL) { async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Entering WAIT state from ${fromState}`); console.log(`Entering WAIT state from ${fromState}`)
// Play idle loop // Play idle loop
this.idleClip = this.context.getClip('wait_idle_L'); this.idleClip = this.context.getClip('wait_idle_L')
if (this.idleClip) { if (this.idleClip) {
await this.idleClip.play(); await this.idleClip.play()
this.currentClip = this.idleClip; this.currentClip = this.idleClip
}
// Collect available quirks
this.quirks = this.context.getClipsByPattern('wait_*_Q');
this.quirkTimer = 0;
} }
/** // Collect available quirks
this.quirks = this.context.getClipsByPattern('wait_*_Q')
this.quirkTimer = 0
}
/**
* Exit the wait state * Exit the wait state
* @param {string|null} [toState=null] - The next state * @param {string|null} [toState=null] - The next state
* @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with * @param {string} [emotion=Emotions.NEUTRAL] - The emotion to exit with
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async exit(toState = null, _emotion = Emotions.NEUTRAL) { async exit (toState = null, _emotion = Emotions.NEUTRAL) {
console.log(`Exiting WAIT state to ${toState}`); console.log(`Exiting WAIT state to ${toState}`)
if (this.currentClip) { if (this.currentClip) {
await this.stopCurrentClip(); await this.stopCurrentClip()
}
// Play transition if available
const transitionName = `wait_2${toState}_T`;
const transition = this.context.getClip(transitionName);
if (transition) {
await transition.play();
await this.waitForClipEnd(transition);
}
} }
/** // Play transition if available
const transitionName = `wait_2${toState}_T`
const transition = this.context.getClip(transitionName)
if (transition) {
await transition.play()
await this.waitForClipEnd(transition)
}
}
/**
* Update the wait state * Update the wait state
* @param {number} deltaTime - Time elapsed since last update (ms) * @param {number} deltaTime - Time elapsed since last update (ms)
* @returns {void} * @returns {void}
*/ */
update(deltaTime) { update (deltaTime) {
this.quirkTimer += deltaTime; this.quirkTimer += deltaTime
// Randomly play quirks // Randomly play quirks
if (this.quirkTimer > this.quirkInterval && Math.random() < Config.QUIRK_PROBABILITY) { if (this.quirkTimer > this.quirkInterval && Math.random() < Config.QUIRK_PROBABILITY) {
this.playRandomQuirk(); this.playRandomQuirk()
this.quirkTimer = 0; this.quirkTimer = 0
}
} }
}
/** /**
* Play a random quirk animation * Play a random quirk animation
* @private * @private
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async playRandomQuirk() { async playRandomQuirk () {
if (this.quirks.length === 0) return; if (this.quirks.length === 0) return
const quirk = this.quirks[ Math.floor(Math.random() * this.quirks.length) ]; const quirk = this.quirks[Math.floor(Math.random() * this.quirks.length)]
// Fade out idle // Fade out idle
if (this.idleClip) { if (this.idleClip) {
await this.idleClip.stop(0.2); await this.idleClip.stop(0.2)
}
// Play quirk
await quirk.play();
await this.waitForClipEnd(quirk);
// Return to idle
if (this.idleClip) {
await this.idleClip.play();
this.currentClip = this.idleClip;
}
} }
/** // Play quirk
await quirk.play()
await this.waitForClipEnd(quirk)
// Return to idle
if (this.idleClip) {
await this.idleClip.play()
this.currentClip = this.idleClip
}
}
/**
* Get available transitions from wait state * Get available transitions from wait state
* @returns {string[]} Array of available state transitions * @returns {string[]} Array of available state transitions
*/ */
getAvailableTransitions() { getAvailableTransitions () {
return [ States.REACT, States.SLEEP ]; return [States.REACT, States.SLEEP]
} }
} }

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
root: 'examples', root: 'examples',
@ -17,7 +17,7 @@ export default defineConfig({
}, },
resolve: { resolve: {
alias: { alias: {
'owen': '/src/index.js' owen: '/src/index.js'
} }
} }
}); })