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 d3a88787c4
23 changed files with 4212 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,9 +5,18 @@ 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 - 🎉 Initial release of Owen Animation System
- ✨ Complete state machine implementation with Wait, React, Type, and Sleep states - ✨ Complete state machine implementation with Wait, React, Type, and Sleep states
- 🤖 Emotional response system for character animations - 🤖 Emotional response system for character animations
@ -21,7 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 📦 TypeScript type definitions - 📦 TypeScript type definitions
- 🛠️ Development tooling (ESLint, Vite, JSDoc) - 🛠️ Development tooling (ESLint, Vite, JSDoc)
[1.0.1]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.1
### Architecture ### Architecture
- **Core Classes:** - **Core Classes:**
- `OwenAnimationContext` - Main system controller - `OwenAnimationContext` - Main system controller
- `AnimationClip` - Individual animation management - `AnimationClip` - Individual animation management
@ -43,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `OwenSystemFactory` - Main system assembly factory - `OwenSystemFactory` - Main system assembly factory
### Features ### Features
- **Animation System:** - **Animation System:**
- Support for Loop (L), Quirk (Q), Transition (T), and Nested animations - Support for Loop (L), Quirk (Q), Transition (T), and Nested animations
- Automatic metadata parsing from animation names - Automatic metadata parsing from animation names
@ -63,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Vite development server setup - Vite development server setup
### Documentation ### Documentation
- Complete README with installation and usage instructions - Complete README with installation and usage instructions
- API documentation via JSDoc - API documentation via JSDoc
- Code examples for basic and advanced usage - Code examples for basic and advanced usage
@ -70,9 +84,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Troubleshooting section - Troubleshooting section
### Examples ### Examples
- Basic browser demo with Three.js integration - Basic browser demo with Three.js integration
- Simple Node.js example for testing - Simple Node.js example for testing
- Interactive controls for state transitions - Interactive controls for state transitions
- Mock model implementation for development - Mock model implementation for development
[1.0.0]: https://github.com/your-username/owen-animation-system/releases/tag/v1.0.0 [1.0.0]: https://gitea.kajkowalski.nl/kjanat/Owen/releases/tag/v1.0.0

View File

@ -181,7 +181,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 +227,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,8 +217,8 @@ 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;">
@ -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,15 +1,15 @@
<!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 {
@ -26,20 +26,20 @@
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

@ -22,8 +22,8 @@ export const ClipTypes = {
/** 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
@ -38,8 +38,8 @@ export const States = {
/** 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
@ -56,8 +56,8 @@ export const Emotions = {
/** Happy emotion */ /** Happy emotion */
HAPPY: 'ha', HAPPY: 'ha',
/** Sad emotion */ /** Sad emotion */
SAD: 'sa', SAD: 'sa'
}; }
/** /**
* Default configuration values * Default configuration values
@ -74,5 +74,5 @@ export const Config = {
/** 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,11 +3,11 @@
* @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
@ -22,24 +22,24 @@ 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(
@ -47,12 +47,12 @@ export class OwenSystemFactory {
mixer, mixer,
animationClipFactory, animationClipFactory,
stateFactory stateFactory
); )
// Initialize the system // Initialize the system
await owenContext.initialize(); await owenContext.initialize()
return owenContext; return owenContext
} }
/** /**
@ -60,11 +60,11 @@ export class OwenSystemFactory {
* @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)
} }
/** /**
@ -74,18 +74,18 @@ export class OwenSystemFactory {
* @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 // Reinitialize with custom states
system.initializeStates(); system.initializeStates()
return system; 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

@ -16,8 +16,8 @@ export class AnimationLoader {
* @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')
} }
} }
@ -31,20 +31,20 @@ 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()
} }
/** /**
@ -53,14 +53,14 @@ export class GLTFAnimationLoader extends AnimationLoader {
* @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 // In a real implementation, this would load from GLTF files
// For now, we'll assume animations are already loaded in the model // For now, we'll assume animations are already loaded in the model
throw new Error(`Animation '${name}' not found. Implement GLTF loading logic.`); throw new Error(`Animation '${name}' not found. Implement GLTF loading logic.`)
} }
/** /**
@ -68,10 +68,10 @@ export class GLTFAnimationLoader extends AnimationLoader {
* @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)
} }
} }
} }
@ -80,15 +80,15 @@ export class GLTFAnimationLoader extends AnimationLoader {
* 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
@ -16,14 +16,14 @@ 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
} }
/** /**
@ -32,15 +32,15 @@ export class ReactStateHandler 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 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
} }
} }
@ -50,25 +50,25 @@ export class ReactStateHandler 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 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 // Play emotional transition if available
let transitionName; let transitionName
if (emotion !== Emotions.NEUTRAL) { if (emotion !== Emotions.NEUTRAL) {
transitionName = `react_${this.emotion}2${toState}_${emotion}_T`; transitionName = `react_${this.emotion}2${toState}_${emotion}_T`
} else { } else {
transitionName = `react_2${toState}_T`; transitionName = `react_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)
} }
} }
@ -77,20 +77,20 @@ export class ReactStateHandler extends StateHandler {
* @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 emotionalReaction.play()
await this.waitForClipEnd(emotionalReaction); await this.waitForClipEnd(emotionalReaction)
} }
} }
} }
@ -101,8 +101,8 @@ export class ReactStateHandler extends StateHandler {
* @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 (
@ -111,7 +111,7 @@ export class ReactStateHandler extends StateHandler {
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
@ -122,7 +122,7 @@ export class ReactStateHandler extends StateHandler {
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
@ -133,7 +133,7 @@ export class ReactStateHandler extends StateHandler {
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
@ -143,17 +143,17 @@ export class ReactStateHandler extends StateHandler {
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
@ -16,32 +16,32 @@ 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
} }
/** /**
@ -50,19 +50,19 @@ export class WaitStateHandler 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 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 // Collect available quirks
this.quirks = this.context.getClipsByPattern('wait_*_Q'); this.quirks = this.context.getClipsByPattern('wait_*_Q')
this.quirkTimer = 0; this.quirkTimer = 0
} }
/** /**
@ -71,19 +71,19 @@ export class WaitStateHandler 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 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 // Play transition if available
const transitionName = `wait_2${toState}_T`; const transitionName = `wait_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)
} }
} }
@ -92,13 +92,13 @@ export class WaitStateHandler extends StateHandler {
* @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
} }
} }
@ -107,24 +107,24 @@ export class WaitStateHandler extends StateHandler {
* @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 // Play quirk
await quirk.play(); await quirk.play()
await this.waitForClipEnd(quirk); await this.waitForClipEnd(quirk)
// Return to idle // Return to idle
if (this.idleClip) { if (this.idleClip) {
await this.idleClip.play(); await this.idleClip.play()
this.currentClip = this.idleClip; this.currentClip = this.idleClip
} }
} }
@ -132,7 +132,7 @@ export class WaitStateHandler extends StateHandler {
* 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'
} }
} }
}); })