diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 9fd1e03..732623e 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -278,7 +278,7 @@ jobs: # Check if there are changes to commit if git diff --quiet VERSION portainer.yml; then - echo "âš ī¸ No changes to commit (files already up to date)" + echo "â„šī¸ No changes to commit (files already up to date)" else # Stage and commit with [skip ci] to prevent infinite loop # Note: Forgejo Actions should respect [skip ci] in commit messages diff --git a/Dockerfile b/Dockerfile index 4da0eb6..59de9f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,12 +7,12 @@ ARG BUILD_DATE=unknown # Create version.json file with build information RUN printf '{"version":"%s","buildDate":"%s"}\n' "${VERSION}" "${BUILD_DATE}" > /usr/share/nginx/html/version.json -# Copy HTML and source files -COPY index.html /usr/share/nginx/html/ -COPY src/ /usr/share/nginx/html/src/ +# Copy the HTML file +COPY index.html /usr/share/nginx/html/index.html # Copy nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf # Expose port 80 EXPOSE 80 + diff --git a/README.md b/README.md index 4837589..a26a482 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,15 @@ # 3D Coin Collector Game -A fun and engaging 3D game built with **Three.js** and **Entity Component System (ECS)** architecture. Test your reflexes as you collect coins while avoiding obstacles! +A fun and engaging 3D game built with Three.js where you control a player to collect coins while avoiding obstacles. Test your reflexes and see how high you can score! ## 🎮 Game Overview Navigate your character (a blue cube) through a 3D arena to collect golden coins while avoiding dangerous red obstacles. Each coin collected increases your score, but colliding with obstacles reduces your health. The game ends when your health reaches zero. -## đŸ—ī¸ Architecture - -This game is built using the **Entity Component System (ECS)** pattern, making it highly scalable and maintainable. See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed documentation. - -### Why ECS? - -- **Scalable**: Easily add new features without modifying existing code -- **Performant**: Data-oriented design for better cache utilization -- **Flexible**: Mix and match components to create new entity types -- **Maintainable**: Clean separation of data (components) and logic (systems) -- **Testable**: Systems can be tested independently - ## ✨ Features -### Game Features - **3D Graphics**: Beautiful 3D environment with shadows, lighting, and fog effects -- **Smooth Controls**: Responsive keyboard and touch controls with easing +- **Smooth Controls**: Responsive keyboard and touch controls - **Dynamic Gameplay**: Moving obstacles that bounce off boundaries - **Animated Coins**: Coins rotate and float with smooth animations - **Health System**: Start with 100 health, lose 1 point per obstacle collision @@ -31,14 +18,6 @@ This game is built using the **Entity Component System (ECS)** pattern, making i - **Mobile Support**: Full touch control support for smartphones and tablets - **Responsive Design**: Adapts to different screen sizes -### Technical Features -- **ECS Architecture**: Scalable entity component system -- **Zero Dependencies**: No npm packages, no build step, no bullshit -- **Native ES6 Modules**: Modern JavaScript, works directly in browsers -- **CDN-based**: Three.js loaded from CDN -- **Docker Support**: Simple nginx deployment -- **CI/CD**: Automated versioning and deployment pipeline - ## đŸŽ¯ How to Play 1. **Objective**: Collect as many yellow coins as possible while avoiding red obstacles @@ -85,41 +64,23 @@ This game is built using the **Entity Component System (ECS)** pattern, making i ## đŸ› ī¸ Technical Details ### Built With -- **Three.js**: 3D graphics library (from CDN) -- **Native ES6 Modules**: No bundler needed +- **Three.js r128**: 3D graphics library +- **Vanilla JavaScript**: No frameworks required - **HTML5 Canvas**: WebGL rendering - **CSS3**: Modern styling and responsive design -- **Pure JavaScript**: No frameworks, no dependencies -### ECS Architecture - -``` -├── src/ -│ ├── ecs/ # Core ECS implementation -│ ├── components/ # Data containers (Transform, Velocity, etc.) -│ ├── systems/ # Logic processors (Movement, Collision, etc.) -│ ├── game/ # Game-specific code -│ └── main.js # Entry point -``` - -**Components (Data)**: -- `Transform`: Position, rotation, scale -- `Velocity`: Movement speed with easing -- `MeshComponent`: Three.js mesh reference -- `Collidable`: Collision detection data -- `Health`: Hit points -- `Tags`: Entity type markers (Player, Coin, Obstacle) - -**Systems (Logic)**: -- `InputSystem`: Keyboard and touch input -- `PlayerControlSystem`: Player movement with easing -- `MovementSystem`: Apply velocity to position -- `CollisionSystem`: Detect and handle collisions -- `CoinSystem`: Coin rotation and bobbing -- `ObstacleSystem`: Obstacle movement and bouncing -- `BoundarySystem`: Keep entities in bounds -- `RenderSystem`: Sync Three.js with ECS +### Game Architecture +- **Object-Oriented Design**: Uses ES6 classes with inheritance +- **GameObject Base Class**: Common functionality for all game entities +- **Modular Classes**: Player, Coin, and Obstacle extend GameObject +- **Game Loop**: Smooth 60 FPS animation using requestAnimationFrame +### Features Implementation +- **Shadow Mapping**: PCF soft shadows for realistic lighting +- **Fog Effect**: Atmospheric depth with distance fog +- **Collision Detection**: Distance-based collision system +- **Boundary Constraints**: Prevents player and obstacles from leaving the arena +- **Touch Event Handling**: Full support for multi-touch devices ## 📋 Requirements @@ -127,53 +88,27 @@ This game is built using the **Entity Component System (ECS)** pattern, making i - JavaScript enabled - Internet connection (for Three.js CDN) - ## 🚀 Getting Started -### Development +1. **Clone or Download** this repository +2. **Open** `index.html` in your web browser +3. **Start Playing** immediately - no installation required! -1. **Clone the repository** -```bash -git clone -cd threejs-test -``` - -2. **Open in browser** - -Uuse a simple HTTP server: +### Local Server (Optional) +For best performance, you can run a local server: ```bash -# Option 1: Python -python -m http.server 3000 +# Using Python 3 +python -m http.server 8000 -# Option 2: Node (if you have it) -npx serve . +# Using Python 2 +python -m SimpleHTTPServer 8000 -# Option 3: PHP -php -S localhost:3000 +# Using Node.js (with http-server) +npx http-server ``` -Then navigate to `http://localhost:3000` - -**That's it!** The game uses: -- Native ES6 modules (no bundler needed) -- Three.js from CDN (no npm install) - -### Docker - -Build and run with Docker: -```bash -# Build -docker build -t threejs-game . - -# Run -docker run -p 80:80 threejs-game -``` - -Or use docker-compose: -```bash -docker-compose up -``` +Then open `http://localhost:8000` in your browser. ## 📱 Mobile Compatibility diff --git a/index.html b/index.html index a285ca5..c509cbe 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,9 @@ font-family: 'Arial', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } + #gameCanvas { + display: block; + } #ui { position: absolute; top: 20px; @@ -79,26 +82,6 @@ z-index: 100; font-family: 'Courier New', monospace; } - #perfMonitor { - position: absolute; - top: 60px; - right: 20px; - background: rgba(0, 0, 0, 0.7); - padding: 10px; - border-radius: 5px; - color: #00ff00; - font-size: 12px; - font-family: 'Courier New', monospace; - z-index: 100; - display: none; - min-width: 120px; - } - #perfMonitor.visible { - display: block; - } - #perfMonitor .label { - color: #888; - } @media (max-width: 768px) { #instructions { font-size: 14px; @@ -108,11 +91,6 @@ top: 10px; right: 10px; } - #perfMonitor { - top: 40px; - right: 10px; - font-size: 11px; - } } @@ -124,12 +102,6 @@
v-
-
-
FPS: 60
-
Frame: 16.7ms
-
Entities: 0
-
-

Game Over!

Final Score: 0

@@ -138,22 +110,623 @@

Controls: WASD or Arrow Keys to move | Touch and drag to move (mobile) | Collect yellow coins | Avoid red obstacles!

-

Press "T" or shake device to toggle performance monitor

- + - - + \ No newline at end of file diff --git a/src/components/Collidable.js b/src/components/Collidable.js deleted file mode 100644 index 7c45c2d..0000000 --- a/src/components/Collidable.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Collidable component - defines collision properties for an entity - */ -export class Collidable { - /** - * @param {number} [radius=1.0] - Collision radius - * @param {string} [layer='default'] - Collision layer (e.g., 'player', 'coin', 'obstacle') - */ - constructor(radius = 1.0, layer = 'default') { - /** @type {number} Collision detection radius */ - this.radius = radius; - - /** @type {string} Collision layer identifier */ - this.layer = layer; - - /** @type {string[]} Array of layer names this entity can collide with */ - this.collidesWith = []; - } -} - diff --git a/src/components/Health.js b/src/components/Health.js deleted file mode 100644 index 304acb6..0000000 --- a/src/components/Health.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Health component - health points and damage/healing logic for entities - */ -export class Health { - /** - * @param {number} [maxHealth=100] - Maximum health points - */ - constructor(maxHealth = 100) { - /** @type {number} Maximum health value */ - this.maxHealth = maxHealth; - - /** @type {number} Current health value */ - this.currentHealth = maxHealth; - } - - /** - * Apply damage to the entity - * @param {number} amount - Damage amount - * @returns {boolean} True if entity is dead (health <= 0) - */ - damage(amount) { - this.currentHealth = Math.max(0, this.currentHealth - amount); - return this.currentHealth <= 0; - } - - /** - * Heal the entity - * @param {number} amount - Heal amount - */ - heal(amount) { - this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount); - } - - /** - * Check if entity is alive - * @returns {boolean} True if health > 0 - */ - isAlive() { - return this.currentHealth > 0; - } - - /** - * Get health as a percentage - * @returns {number} Health percentage (0-100) - */ - getPercentage() { - return (this.currentHealth / this.maxHealth) * 100; - } -} - diff --git a/src/components/MeshComponent.js b/src/components/MeshComponent.js deleted file mode 100644 index 6c4247b..0000000 --- a/src/components/MeshComponent.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Mesh component - holds reference to Three.js mesh for rendering - */ -export class MeshComponent { - /** - * @param {THREE.Mesh} mesh - The Three.js mesh to render - */ - constructor(mesh) { - /** @type {THREE.Mesh} The Three.js mesh object */ - this.mesh = mesh; - - /** @type {boolean} Whether this mesh casts shadows */ - this.castShadow = true; - - /** @type {boolean} Whether this mesh receives shadows */ - this.receiveShadow = true; - } -} - diff --git a/src/components/Tags.js b/src/components/Tags.js deleted file mode 100644 index e26a33a..0000000 --- a/src/components/Tags.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Tag components - simple markers and metadata for entity types. - * These are lightweight components used for filtering and entity-specific data. - */ - -/** - * PlayerTag - marks the player entity - */ -export class PlayerTag {} - -/** - * CoinTag - marks collectible coin entities with animation properties - */ -export class CoinTag { - /** - * @param {number} [index=0] - Unique index for this coin - */ - constructor(index = 0) { - /** @type {number} Unique identifier for animation offset */ - this.index = index; - - /** @type {number} Rotation speed for coin spin */ - this.rotationSpeed = 0.02; - - /** @type {number} Bobbing animation speed */ - this.bobSpeed = 0.003; - - /** @type {number} Bobbing animation height */ - this.bobAmount = 0.2; - } -} - -/** - * ObstacleTag - marks obstacle entities with movement properties - */ -export class ObstacleTag { - constructor() { - /** @type {number} Movement speed for obstacle */ - this.speed = 0.05; - } -} - -/** - * BoundaryConstrained - marks entities that should stay within boundaries - */ -export class BoundaryConstrained { - /** - * @param {number} [boundarySize=30] - Total size of the boundary area - */ - constructor(boundarySize = 30) { - /** @type {number} Total boundary size */ - this.boundarySize = boundarySize; - - /** @type {number} Safety margin from edges */ - this.margin = 0.5; - } - - /** - * Get the effective boundary accounting for margin - * @returns {number} Usable boundary from center - */ - getBoundary() { - return this.boundarySize / 2 - this.margin; - } -} - diff --git a/src/components/Transform.js b/src/components/Transform.js deleted file mode 100644 index 8147fd4..0000000 --- a/src/components/Transform.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Transform component - position, rotation, and scale in 3D space - */ -export class Transform { - /** - * @param {number} [x=0] - X position - * @param {number} [y=0] - Y position - * @param {number} [z=0] - Z position - */ - constructor(x = 0, y = 0, z = 0) { - /** @type {THREE.Vector3} */ - this.position = new window.THREE.Vector3(x, y, z); - - /** @type {THREE.Euler} */ - this.rotation = new window.THREE.Euler(0, 0, 0); - - /** @type {THREE.Vector3} */ - this.scale = new window.THREE.Vector3(1, 1, 1); - } - - /** - * Set the position - * @param {number} x - X position - * @param {number} y - Y position - * @param {number} z - Z position - */ - setPosition(x, y, z) { - this.position.set(x, y, z); - } - - /** - * Get a clone of the position - * @returns {THREE.Vector3} Cloned position vector - */ - getPosition() { - return this.position.clone(); - } -} - diff --git a/src/components/Velocity.js b/src/components/Velocity.js deleted file mode 100644 index 1ed9301..0000000 --- a/src/components/Velocity.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Velocity component - movement speed, direction, and physics properties - */ -export class Velocity { - /** - * @param {number} [x=0] - Initial X velocity - * @param {number} [y=0] - Initial Y velocity - * @param {number} [z=0] - Initial Z velocity - */ - constructor(x = 0, y = 0, z = 0) { - /** @type {THREE.Vector3} */ - this.velocity = new window.THREE.Vector3(x, y, z); - - /** @type {number} Maximum speed magnitude */ - this.maxSpeed = 0.15; - - /** @type {number} Acceleration rate */ - this.acceleration = 0.08; - - /** @type {number} Deceleration rate */ - this.deceleration = 0.12; - } - - /** - * Set velocity directly - * @param {number} x - X velocity - * @param {number} y - Y velocity - * @param {number} z - Z velocity - */ - set(x, y, z) { - this.velocity.set(x, y, z); - } - - /** - * Add to current velocity - * @param {number} x - X velocity to add - * @param {number} y - Y velocity to add - * @param {number} z - Z velocity to add - */ - add(x, y, z) { - this.velocity.add(new window.THREE.Vector3(x, y, z)); - } - - /** - * Multiply velocity by scalar - * @param {number} scalar - Multiplier - */ - multiplyScalar(scalar) { - this.velocity.multiplyScalar(scalar); - } - - /** - * Get the magnitude of velocity - * @returns {number} Velocity magnitude - */ - length() { - return this.velocity.length(); - } - - /** - * Reset velocity to zero - */ - reset() { - this.velocity.set(0, 0, 0); - } -} - diff --git a/src/ecs/System.js b/src/ecs/System.js deleted file mode 100644 index 90b2959..0000000 --- a/src/ecs/System.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Base class for all systems. - * Systems contain logic that operates on entities with specific components. - * - * @typedef {import('./World.js').EntityId} EntityId - * @typedef {import('./World.js').ComponentClass} ComponentClass - */ -export class System { - constructor() { - /** @type {import('./World.js').World|null} */ - this.world = null; - - /** @type {boolean} */ - this.enabled = true; - } - - /** - * Called once when the system is added to the world - */ - init() {} - - /** - * Called every frame - * @param {number} deltaTime - Time since last frame in seconds - */ - update(_deltaTime) {} - - /** - * Called when the system is removed or world is cleaned up - */ - cleanup() {} - - /** - * Helper to get entities with specific components - * @param {...ComponentClass} componentClasses - The component classes to filter by - * @returns {EntityId[]} Array of entity IDs - */ - getEntities(...componentClasses) { - return this.world.getEntitiesWithComponents(...componentClasses); - } - - /** - * Helper to get a component from an entity - * @template T - * @param {EntityId} entityId - The entity ID - * @param {new (...args: any[]) => T} componentClass - The component class - * @returns {T|undefined} The component instance or undefined - */ - getComponent(entityId, componentClass) { - return this.world.getComponent(entityId, componentClass); - } -} - diff --git a/src/ecs/World.js b/src/ecs/World.js deleted file mode 100644 index eaf2141..0000000 --- a/src/ecs/World.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * World manages all entities, components, and systems in the ECS architecture. - * - * @typedef {number} EntityId - Unique identifier for an entity - * @typedef {Function} ComponentClass - A component class constructor - * @typedef {Object} Component - A component instance (data only) - */ -export class World { - constructor() { - /** @type {Map>} */ - this.entities = new Map(); // entityId -> Set of component classes - - /** @type {Map>} */ - this.components = new Map(); // componentClass -> Map(entityId -> component) - - /** @type {import('./System.js').System[]} */ - this.systems = []; - - /** @type {EntityId} */ - this.nextEntityId = 1; - } - - /** - * Create a new entity and return its ID - * @returns {EntityId} The newly created entity ID - */ - createEntity() { - const id = this.nextEntityId++; - this.entities.set(id, new Set()); - return id; - } - - /** - * Remove an entity and all its components - * @param {EntityId} entityId - The entity to remove - */ - removeEntity(entityId) { - if (!this.entities.has(entityId)) return; - - // Remove all components for this entity - const componentClasses = this.entities.get(entityId); - for (const componentClass of componentClasses) { - const componentMap = this.components.get(componentClass); - if (componentMap) { - componentMap.delete(entityId); - } - } - - this.entities.delete(entityId); - } - - /** - * Add a component instance to an entity. Component class is inferred from the instance. - * @param {EntityId} entityId - The entity to add the component to - * @param {Component} component - The component instance (must have a constructor) - * @throws {Error} If entity doesn't exist - */ - addComponent(entityId, component) { - if (!this.entities.has(entityId)) { - throw new Error(`Entity ${entityId} does not exist`); - } - - const componentClass = component.constructor; - - // Track that this entity has this component class - this.entities.get(entityId).add(componentClass); - - // Store the component instance - if (!this.components.has(componentClass)) { - this.components.set(componentClass, new Map()); - } - this.components.get(componentClass).set(entityId, component); - } - - /** - * Get a component from an entity - * @template T - * @param {EntityId} entityId - The entity ID - * @param {new (...args: any[]) => T} componentClass - The component class - * @returns {T|undefined} The component instance or undefined - */ - getComponent(entityId, componentClass) { - const componentMap = this.components.get(componentClass); - return componentMap ? componentMap.get(entityId) : undefined; - } - - /** - * Check if an entity has a component - * @param {EntityId} entityId - The entity ID - * @param {ComponentClass} componentClass - The component class - * @returns {boolean} True if the entity has the component - */ - hasComponent(entityId, componentClass) { - return this.entities.get(entityId)?.has(componentClass) || false; - } - - /** - * Remove a component from an entity - * @param {EntityId} entityId - The entity ID - * @param {ComponentClass} componentClass - The component class to remove - */ - removeComponent(entityId, componentClass) { - const entityComponents = this.entities.get(entityId); - if (entityComponents) { - entityComponents.delete(componentClass); - } - - const componentMap = this.components.get(componentClass); - if (componentMap) { - componentMap.delete(entityId); - } - } - - /** - * Get all entities that have ALL of the specified component classes - * @param {...ComponentClass} componentClasses - The component classes to filter by - * @returns {EntityId[]} Array of entity IDs that have all specified components - */ - getEntitiesWithComponents(...componentClasses) { - const result = []; - - for (const [entityId, entityComponentClasses] of this.entities.entries()) { - const hasAll = componentClasses.every(cls => entityComponentClasses.has(cls)); - if (hasAll) { - result.push(entityId); - } - } - - return result; - } - - /** - * Add a system to the world - * @param {import('./System.js').System} system - The system to add - */ - addSystem(system) { - system.world = this; - this.systems.push(system); - if (system.init) { - system.init(); - } - } - - /** - * Update all systems with delta time - * @param {number} deltaTime - Time elapsed since last frame (in seconds) - */ - update(deltaTime) { - for (const system of this.systems) { - if (system.enabled !== false) { - system.update(deltaTime); - } - } - } - - /** - * Cleanup all systems and clear all entities/components - */ - cleanup() { - for (const system of this.systems) { - if (system.cleanup) { - system.cleanup(); - } - } - this.entities.clear(); - this.components.clear(); - this.systems = []; - } -} - diff --git a/src/game/EntityFactory.js b/src/game/EntityFactory.js deleted file mode 100644 index 31bbe19..0000000 --- a/src/game/EntityFactory.js +++ /dev/null @@ -1,157 +0,0 @@ -import { Transform } from '../components/Transform.js'; -import { Velocity } from '../components/Velocity.js'; -import { MeshComponent } from '../components/MeshComponent.js'; -import { Collidable } from '../components/Collidable.js'; -import { Health } from '../components/Health.js'; -import { PlayerTag, CoinTag, ObstacleTag, BoundaryConstrained } from '../components/Tags.js'; - -/** - * EntityFactory - creates pre-configured game entities with appropriate components. - * Centralizes entity creation logic for consistency. - * - * @typedef {import('../ecs/World.js').EntityId} EntityId - */ -export class EntityFactory { - /** - * @param {import('../ecs/World.js').World} world - The ECS world - * @param {THREE.Scene} scene - The Three.js scene - */ - constructor(world, scene) { - /** @type {import('../ecs/World.js').World} */ - this.world = world; - - /** @type {THREE.Scene} */ - this.scene = scene; - - /** @type {number} Size of the game ground/play area */ - this.groundSize = 30; - } - - /** - * Create the player entity - * @returns {EntityId} The player entity ID - */ - createPlayer() { - const entity = this.world.createEntity(); - - // Create mesh - const geometry = new window.THREE.BoxGeometry(1, 1, 1); - const material = new window.THREE.MeshStandardMaterial({ - color: 0x4169E1, - metalness: 0.3, - roughness: 0.4 - }); - const mesh = new window.THREE.Mesh(geometry, material); - mesh.castShadow = true; - mesh.receiveShadow = true; - this.scene.add(mesh); - - // Add components - this.world.addComponent(entity, new Transform(0, 0.5, 0)); - this.world.addComponent(entity, new Velocity()); - this.world.addComponent(entity, new MeshComponent(mesh)); - this.world.addComponent(entity, new Collidable(0, 'player')); // Player center point (original behavior) - this.world.addComponent(entity, new Health(100)); - this.world.addComponent(entity, new PlayerTag()); - this.world.addComponent(entity, new BoundaryConstrained(this.groundSize)); - - return entity; - } - - /** - * Create a collectible coin entity - * @param {number} [index=0] - Unique index for animation offset - * @returns {EntityId} The coin entity ID - */ - createCoin(index = 0) { - const entity = this.world.createEntity(); - - // Create mesh - const geometry = new window.THREE.SphereGeometry(0.3, 16, 16); - const material = new window.THREE.MeshStandardMaterial({ - color: 0xFFD700, - metalness: 0.8, - roughness: 0.2, - emissive: 0xFFD700, - emissiveIntensity: 0.3 - }); - const mesh = new window.THREE.Mesh(geometry, material); - mesh.castShadow = true; - mesh.receiveShadow = true; - this.scene.add(mesh); - - // Random position - const x = (Math.random() - 0.5) * (this.groundSize - 4); - const z = (Math.random() - 0.5) * (this.groundSize - 4); - - // Add components - this.world.addComponent(entity, new Transform(x, 0.5, z)); - this.world.addComponent(entity, new MeshComponent(mesh)); - this.world.addComponent(entity, new Collidable(0.8, 'coin')); - this.world.addComponent(entity, new CoinTag(index)); - - return entity; - } - - /** - * Create an obstacle entity - * @returns {EntityId} The obstacle entity ID - */ - createObstacle() { - const entity = this.world.createEntity(); - - // Create mesh - const geometry = new window.THREE.BoxGeometry(1.5, 2, 1.5); - const material = new window.THREE.MeshStandardMaterial({ - color: 0xFF4500, - metalness: 0.3, - roughness: 0.7 - }); - const mesh = new window.THREE.Mesh(geometry, material); - mesh.castShadow = true; - mesh.receiveShadow = true; - this.scene.add(mesh); - - // Random position (away from center) - let posX, posZ; - do { - posX = (Math.random() - 0.5) * (this.groundSize - 4); - posZ = (Math.random() - 0.5) * (this.groundSize - 4); - } while (Math.abs(posX) < 3 && Math.abs(posZ) < 3); - - // Random velocity - const velocity = new Velocity( - (Math.random() - 0.5) * 0.05, - 0, - (Math.random() - 0.5) * 0.05 - ); - - // Add components - this.world.addComponent(entity, new Transform(posX, 1, posZ)); - this.world.addComponent(entity, velocity); - this.world.addComponent(entity, new MeshComponent(mesh)); - this.world.addComponent(entity, new Collidable(1.5, 'obstacle')); - this.world.addComponent(entity, new ObstacleTag()); - this.world.addComponent(entity, new BoundaryConstrained(this.groundSize)); - - return entity; - } - - /** - * Remove entity and its mesh from scene - * @param {EntityId} entityId - The entity to destroy - */ - destroyEntity(entityId) { - // Remove mesh from scene if it exists - const meshComp = this.world.getComponent(entityId, MeshComponent); - if (meshComp) { - this.scene.remove(meshComp.mesh); - meshComp.mesh.geometry.dispose(); - meshComp.mesh.material.dispose(); - } - - // Remove entity from world - this.world.removeEntity(entityId); - } -} - diff --git a/src/game/Game.js b/src/game/Game.js deleted file mode 100644 index 3dd9dad..0000000 --- a/src/game/Game.js +++ /dev/null @@ -1,486 +0,0 @@ -import { World } from '../ecs/World.js'; -import { EntityFactory } from './EntityFactory.js'; - -// Systems -import { InputSystem } from '../systems/InputSystem.js'; -import { PlayerControlSystem } from '../systems/PlayerControlSystem.js'; -import { MovementSystem } from '../systems/MovementSystem.js'; -import { BoundarySystem } from '../systems/BoundarySystem.js'; -import { CoinSystem } from '../systems/CoinSystem.js'; -import { ObstacleSystem } from '../systems/ObstacleSystem.js'; -import { CollisionSystem } from '../systems/CollisionSystem.js'; -import { RenderSystem } from '../systems/RenderSystem.js'; - -// Components -import { Transform } from '../components/Transform.js'; -import { Health } from '../components/Health.js'; - -/** - * Main Game class - manages the game loop and coordinates all systems. - * Orchestrates the ECS architecture and Three.js rendering. - * - * @typedef {import('../ecs/World.js').EntityId} EntityId - */ -export class Game { - constructor() { - /** @type {number} Size of the game play area */ - this.groundSize = 30; - - /** @type {number} Current game score */ - this.score = 0; - - /** @type {boolean} Whether the game is currently active */ - this.gameActive = true; - - /** @type {EntityId|null} The player entity ID */ - this.playerEntity = null; - - /** @type {EntityId[]} Array of coin entity IDs */ - this.coins = []; - - /** @type {EntityId[]} Array of obstacle entity IDs */ - this.obstacles = []; - - /** @type {number} Last frame timestamp for deltaTime calculation */ - this.lastTime = performance.now(); - - /** @type {number} Maximum deltaTime cap (in seconds) to prevent huge jumps */ - this.maxDeltaTime = 0.1; // 100ms cap - - /** @type {number} Smoothed FPS for display */ - this.smoothedFPS = 60; - - /** @type {number} Last time performance monitor was updated */ - this.lastPerfUpdate = performance.now(); - - /** @type {boolean} Whether the game is paused (e.g., tab not visible) */ - this.isPaused = false; - - /** @type {boolean} Whether performance monitor is visible */ - this.perfMonitorVisible = false; - - /** @type {Object} Shake detection state */ - this.shakeDetection = { - lastX: 0, - lastY: 0, - lastZ: 0, - shakeThreshold: 15, - shakeCount: 0, - lastShakeTime: 0 - }; - - this.init(); - this.setupEventListeners(); - this.animate(); - } - - /** - * Initialize the game (ECS, Three.js, entities) - */ - init() { - // Initialize ECS - this.world = new World(); - - // Setup Three.js - this.setupScene(); - this.setupCamera(); - this.setupRenderer(); - this.setupLights(); - this.setupGround(); - - // Create entity factory - this.entityFactory = new EntityFactory(this.world, this.scene); - - // Initialize systems - this.setupSystems(); - - // Create game entities - this.createGameEntities(); - } - - setupScene() { - this.scene = new window.THREE.Scene(); - this.scene.background = new window.THREE.Color(0x87CEEB); - this.scene.fog = new window.THREE.Fog(0x87CEEB, 0, 50); - } - - setupCamera() { - this.camera = new window.THREE.PerspectiveCamera( - 75, - window.innerWidth / window.innerHeight, - 0.1, - 1000 - ); - this.camera.position.set(0, 10, 15); - this.camera.lookAt(0, 0, 0); - } - - setupRenderer() { - this.renderer = new window.THREE.WebGLRenderer({ antialias: true }); - this.renderer.setSize(window.innerWidth, window.innerHeight); - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = window.THREE.PCFSoftShadowMap; - - document.body.appendChild(this.renderer.domElement); - } - - setupLights() { - // Increased ambient light for brighter scene (was 0.6) - const ambientLight = new window.THREE.AmbientLight(0xffffff, 0.6); - this.scene.add(ambientLight); - - // Increased directional light for better clarity (was 0.8) - const directionalLight = new window.THREE.DirectionalLight(0xffffff, 3.0); - directionalLight.position.set(10, 20, 10); - directionalLight.castShadow = true; - directionalLight.shadow.mapSize.width = 2048; - directionalLight.shadow.mapSize.height = 2048; - directionalLight.shadow.camera.left = -20; - directionalLight.shadow.camera.right = 20; - directionalLight.shadow.camera.top = 20; - directionalLight.shadow.camera.bottom = -20; - directionalLight.shadow.camera.near = 0.5; - directionalLight.shadow.camera.far = 50; - this.scene.add(directionalLight); - } - - setupGround() { - const groundGeometry = new window.THREE.PlaneGeometry(this.groundSize, this.groundSize); - const groundMaterial = new window.THREE.MeshStandardMaterial({ - color: 0x90EE90, - roughness: 0.8 - }); - const ground = new window.THREE.Mesh(groundGeometry, groundMaterial); - ground.rotation.x = -Math.PI / 2; - ground.receiveShadow = true; - this.scene.add(ground); - - const gridHelper = new window.THREE.GridHelper(this.groundSize, 20, 0x000000, 0x000000); - gridHelper.material.opacity = 0.2; - gridHelper.material.transparent = true; - this.scene.add(gridHelper); - } - - setupSystems() { - // Input system (must be first) - this.inputSystem = new InputSystem(); - this.world.addSystem(this.inputSystem); - - // Player control - this.world.addSystem(new PlayerControlSystem(this.inputSystem)); - - // Movement and physics - this.world.addSystem(new MovementSystem()); - this.world.addSystem(new BoundarySystem()); - this.world.addSystem(new ObstacleSystem()); - - // Game-specific behavior - this.world.addSystem(new CoinSystem()); - - // Collision detection - this.collisionSystem = new CollisionSystem(); - this.collisionSystem.onCollision((entity1, entity2, layer1, layer2) => { - this.handleCollision(entity1, entity2, layer1, layer2); - }); - this.world.addSystem(this.collisionSystem); - - // Rendering (must be last to sync transforms) - this.world.addSystem(new RenderSystem(this.scene)); - } - - createGameEntities() { - // Create player - this.playerEntity = this.entityFactory.createPlayer(); - - // Create coins - for (let i = 0; i < 10; i++) { - const coin = this.entityFactory.createCoin(this.coins.length); - this.coins.push(coin); - } - - // Create obstacles - for (let i = 0; i < 8; i++) { - const obstacle = this.entityFactory.createObstacle(); - this.obstacles.push(obstacle); - } - } - - handleCollision(entity1, entity2, layer1, layer2) { - if (!this.gameActive) return; - - // Player-Coin collision - if ((layer1 === 'player' && layer2 === 'coin') || (layer1 === 'coin' && layer2 === 'player')) { - const coinEntity = layer1 === 'coin' ? entity1 : entity2; - this.collectCoin(coinEntity); - } - - // Player-Obstacle collision - if ((layer1 === 'player' && layer2 === 'obstacle') || (layer1 === 'obstacle' && layer2 === 'player')) { - const playerEntity = layer1 === 'player' ? entity1 : entity2; - const obstacleEntity = layer1 === 'obstacle' ? entity1 : entity2; - this.handleObstacleCollision(playerEntity, obstacleEntity); - } - } - - collectCoin(coinEntity) { - // Remove coin - this.entityFactory.destroyEntity(coinEntity); - const index = this.coins.indexOf(coinEntity); - if (index > -1) { - this.coins.splice(index, 1); - } - - // Update score - this.score += 10; - this.updateUI(); - - // Spawn new coin - const newCoin = this.entityFactory.createCoin(this.coins.length); - this.coins.push(newCoin); - } - - handleObstacleCollision(playerEntity, obstacleEntity) { - const health = this.world.getComponent(playerEntity, Health); - const playerTransform = this.world.getComponent(playerEntity, Transform); - const obstacleTransform = this.world.getComponent(obstacleEntity, Transform); - - // Damage player - const isDead = health.damage(1); - - // Push player back - const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position); - pushDirection.y = 0; - pushDirection.normalize(); - playerTransform.position.add(pushDirection.multiplyScalar(0.3)); - playerTransform.position.y = 0.5; - - this.updateUI(); - - if (isDead) { - this.gameOver(); - } - } - - updateCamera() { - if (!this.playerEntity) return; - - const playerTransform = this.world.getComponent(this.playerEntity, Transform); - if (playerTransform) { - this.camera.position.x = playerTransform.position.x; - this.camera.position.z = playerTransform.position.z + 15; - this.camera.lookAt(playerTransform.position); - } - } - - updateUI() { - document.getElementById('score').textContent = this.score; - - const health = this.world.getComponent(this.playerEntity, Health); - if (health) { - document.getElementById('health').textContent = Math.max(0, health.currentHealth); - } - } - - gameOver() { - this.gameActive = false; - document.getElementById('finalScore').textContent = this.score; - document.getElementById('gameOver').style.display = 'block'; - } - - restart() { - // Clean up old entities - [...this.coins].forEach(coin => this.entityFactory.destroyEntity(coin)); - [...this.obstacles].forEach(obstacle => this.entityFactory.destroyEntity(obstacle)); - if (this.playerEntity) { - this.entityFactory.destroyEntity(this.playerEntity); - } - - this.coins = []; - this.obstacles = []; - - // Reset game state - this.score = 0; - this.gameActive = true; - this.lastTime = performance.now(); // Reset timer to prevent deltaTime spike - - // Recreate entities - this.createGameEntities(); - - // Hide game over screen - document.getElementById('gameOver').style.display = 'none'; - this.updateUI(); - } - - setupEventListeners() { - window.addEventListener('resize', () => this.onWindowResize()); - document.getElementById('restartBtn').addEventListener('click', () => this.restart()); - - // Toggle performance monitor with 'T' key - window.addEventListener('keydown', (e) => { - if (e.key.toLowerCase() === 't') { - this.togglePerformanceMonitor(); - } - }); - - // Shake detection for mobile - if (window.DeviceMotionEvent) { - window.addEventListener('devicemotion', (e) => this.handleDeviceMotion(e), false); - } - - // Pause game when tab loses focus - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - this.isPaused = true; - console.log('Game paused (tab hidden)'); - } else { - this.isPaused = false; - // Reset timer to prevent deltaTime spike - this.lastTime = performance.now(); - console.log('Game resumed'); - } - }); - - // Also handle window blur/focus as fallback - window.addEventListener('blur', () => { - this.isPaused = true; - }); - - window.addEventListener('focus', () => { - if (!document.hidden) { - this.isPaused = false; - this.lastTime = performance.now(); - } - }); - - // Load version - this.loadVersion(); - } - - /** - * Toggle performance monitor visibility - */ - togglePerformanceMonitor() { - this.perfMonitorVisible = !this.perfMonitorVisible; - const monitor = document.getElementById('perfMonitor'); - if (this.perfMonitorVisible) { - monitor.classList.add('visible'); - console.log('Performance monitor enabled'); - } else { - monitor.classList.remove('visible'); - console.log('Performance monitor disabled'); - } - } - - /** - * Handle device motion for shake detection - * @param {DeviceMotionEvent} event - */ - handleDeviceMotion(event) { - const acceleration = event.accelerationIncludingGravity; - if (!acceleration) return; - - const currentTime = Date.now(); - const timeDiff = currentTime - this.shakeDetection.lastShakeTime; - - if (timeDiff > 100) { // Check every 100ms - const { x = 0, y = 0, z = 0 } = acceleration; - - const deltaX = Math.abs(x - this.shakeDetection.lastX); - const deltaY = Math.abs(y - this.shakeDetection.lastY); - const deltaZ = Math.abs(z - this.shakeDetection.lastZ); - - if (deltaX + deltaY + deltaZ > this.shakeDetection.shakeThreshold) { - this.shakeDetection.shakeCount++; - - // Toggle after 2 shakes within 500ms - if (this.shakeDetection.shakeCount >= 2) { - this.togglePerformanceMonitor(); - this.shakeDetection.shakeCount = 0; - } - } else { - this.shakeDetection.shakeCount = 0; - } - - this.shakeDetection.lastX = x; - this.shakeDetection.lastY = y; - this.shakeDetection.lastZ = z; - this.shakeDetection.lastShakeTime = currentTime; - } - } - - loadVersion() { - fetch('/version.json') - .then(response => { - if (response.ok) { - return response.json(); - } - throw new Error('Version file not found'); - }) - .then(data => { - const versionElement = document.getElementById('versionNumber'); - if (versionElement && data.version) { - versionElement.textContent = data.version; - } - }) - .catch(error => { - console.debug('Version information not available:', error.message); - }); - } - - onWindowResize() { - this.camera.aspect = window.innerWidth / window.innerHeight; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(window.innerWidth, window.innerHeight); - } - - /** - * Main game loop - calculates deltaTime and updates all systems - * @param {number} [currentTime] - Current timestamp from requestAnimationFrame - */ - animate(currentTime = performance.now()) { - requestAnimationFrame((time) => this.animate(time)); - - // If paused, skip updates but keep rendering - if (this.isPaused) { - this.renderer.render(this.scene, this.camera); - return; - } - - // Calculate deltaTime in seconds - const deltaTime = Math.min((currentTime - this.lastTime) / 1000, this.maxDeltaTime); - this.lastTime = currentTime; - - // Update performance monitor with smoothed values - if (this.perfMonitorVisible) { - // Calculate instant FPS from deltaTime - const instantFPS = 1 / deltaTime; - - // Smooth FPS using exponential moving average for stability - this.smoothedFPS = this.smoothedFPS * 0.9 + instantFPS * 0.1; - - // Update display every 100ms for real-time feel without flickering - if (currentTime - this.lastPerfUpdate >= 100) { - const frameTime = (deltaTime * 1000).toFixed(1); - const entityCount = this.world.entities.size; - - document.getElementById('fps').textContent = Math.round(this.smoothedFPS); - document.getElementById('frameTime').textContent = frameTime; - document.getElementById('entityCount').textContent = entityCount; - - this.lastPerfUpdate = currentTime; - } - } - - if (this.gameActive) { - // Update ECS world with actual deltaTime - this.world.update(deltaTime); - - // Update camera - this.updateCamera(); - } - - // Render scene - this.renderer.render(this.scene, this.camera); - } -} - diff --git a/src/main.js b/src/main.js deleted file mode 100644 index a17285c..0000000 --- a/src/main.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Game } from './game/Game.js'; - -// Start the game immediately (script is at end of body, DOM is ready) -console.log('Starting 3D Coin Collector Game with ECS Architecture'); -console.log('Press "T" to toggle performance monitor (or shake device on mobile)'); - -// Wait a tick to ensure everything is loaded -setTimeout(() => { - try { - const game = new Game(); - - // Make game accessible from console for debugging - window.game = game; - } catch (error) { - console.error('Game initialization failed:', error); - } -}, 0); - diff --git a/src/systems/BoundarySystem.js b/src/systems/BoundarySystem.js deleted file mode 100644 index 4ee336e..0000000 --- a/src/systems/BoundarySystem.js +++ /dev/null @@ -1,43 +0,0 @@ -import { System } from '../ecs/System.js'; -import { BoundaryConstrained } from '../components/Tags.js'; -import { Transform } from '../components/Transform.js'; -import { Velocity } from '../components/Velocity.js'; - -/** - * BoundarySystem - keeps entities within defined boundaries - */ -export class BoundarySystem extends System { - update(_deltaTime) { - const entities = this.getEntities(BoundaryConstrained, Transform); - - for (const entityId of entities) { - const boundary = this.getComponent(entityId, BoundaryConstrained); - const transform = this.getComponent(entityId, Transform); - const velocity = this.getComponent(entityId, Velocity); - - const limit = boundary.getBoundary(); - - // Clamp position to boundaries - const clamped = { - x: Math.max(-limit, Math.min(limit, transform.position.x)), - z: Math.max(-limit, Math.min(limit, transform.position.z)) - }; - - // If clamped, update position and stop velocity - if (clamped.x !== transform.position.x) { - transform.position.x = clamped.x; - if (velocity) { - velocity.velocity.x = 0; - } - } - - if (clamped.z !== transform.position.z) { - transform.position.z = clamped.z; - if (velocity) { - velocity.velocity.z = 0; - } - } - } - } -} - diff --git a/src/systems/CoinSystem.js b/src/systems/CoinSystem.js deleted file mode 100644 index c697cf9..0000000 --- a/src/systems/CoinSystem.js +++ /dev/null @@ -1,33 +0,0 @@ -import { System } from '../ecs/System.js'; -import { CoinTag } from '../components/Tags.js'; -import { Transform } from '../components/Transform.js'; - -/** - * CoinSystem - handles coin-specific behavior (rotation and bobbing) - */ -export class CoinSystem extends System { - constructor() { - super(); - this.elapsedTime = 0; - } - - update(deltaTime) { - this.elapsedTime += deltaTime; - const coins = this.getEntities(CoinTag, Transform); - - for (const entityId of coins) { - const coinTag = this.getComponent(entityId, CoinTag); - const transform = this.getComponent(entityId, Transform); - - // Rotate coin (frame-rate independent) - // rotationSpeed is radians per second - transform.rotation.y += coinTag.rotationSpeed * 60 * deltaTime; - - // Bob up and down using accumulated time - const baseY = 0.5; - const timeScale = 3; // Speed multiplier (3 matches original 0.003 * 1000ms) - transform.position.y = baseY + Math.sin(this.elapsedTime * timeScale + coinTag.index) * coinTag.bobAmount; - } - } -} - diff --git a/src/systems/CollisionSystem.js b/src/systems/CollisionSystem.js deleted file mode 100644 index 15441c4..0000000 --- a/src/systems/CollisionSystem.js +++ /dev/null @@ -1,80 +0,0 @@ -import { System } from '../ecs/System.js'; -import { Transform } from '../components/Transform.js'; -import { Collidable } from '../components/Collidable.js'; - -/** - * CollisionSystem - detects and reports collisions - */ -export class CollisionSystem extends System { - constructor() { - super(); - this.collisionCallbacks = []; - } - - /** - * Register a callback for collision events - * @param {Function} callback - (entity1Id, entity2Id, layer1, layer2) => void - */ - onCollision(callback) { - this.collisionCallbacks.push(callback); - } - - /** - * Update collision detection - * Note: Entity list is captured at the start of update, but entities may be - * destroyed during collision callbacks, so we need defensive null checks. - * @param {number} _deltaTime - Time since last frame (unused - collision is instantaneous) - */ - update(_deltaTime) { - const entities = this.getEntities(Transform, Collidable); - - // Track checked pairs to avoid duplicate collision callbacks this frame - const checkedPairs = new Set(); - - // Check all pairs of collidable entities - for (let i = 0; i < entities.length; i++) { - for (let j = i + 1; j < entities.length; j++) { - const entity1 = entities[i]; - const entity2 = entities[j]; - - // Create unique pair ID - const pairId = `${Math.min(entity1, entity2)}-${Math.max(entity1, entity2)}`; - if (checkedPairs.has(pairId)) continue; - checkedPairs.add(pairId); - - const transform1 = this.getComponent(entity1, Transform); - const transform2 = this.getComponent(entity2, Transform); - const collidable1 = this.getComponent(entity1, Collidable); - const collidable2 = this.getComponent(entity2, Collidable); - - // Skip if entity was destroyed during collision handling this frame - if (!transform1 || !transform2 || !collidable1 || !collidable2) { - continue; - } - - // Calculate distance between entities - const distance = transform1.position.distanceTo(transform2.position); - - // Determine which radius to use (use non-player radius, or sum if both non-player) - let collisionRadius; - if (collidable1.layer === 'player') { - collisionRadius = collidable2.radius; // Use other entity's radius - } else if (collidable2.layer === 'player') { - collisionRadius = collidable1.radius; // Use other entity's radius - } else { - // Both are non-player, use sum of radii - collisionRadius = collidable1.radius + collidable2.radius; - } - - // Check if colliding - if (distance < collisionRadius) { - // Notify all collision callbacks - for (const callback of this.collisionCallbacks) { - callback(entity1, entity2, collidable1.layer, collidable2.layer); - } - } - } - } - } -} - diff --git a/src/systems/InputSystem.js b/src/systems/InputSystem.js deleted file mode 100644 index b2511e8..0000000 --- a/src/systems/InputSystem.js +++ /dev/null @@ -1,105 +0,0 @@ -import { System } from '../ecs/System.js'; - -/** - * InputSystem - manages keyboard and touch input - */ -export class InputSystem extends System { - constructor() { - super(); - this.keys = {}; - this.touch = { - active: false, - startX: 0, - startY: 0, - currentX: 0, - currentY: 0, - id: null - }; - } - - init() { - // Keyboard events - window.addEventListener('keydown', (e) => { - this.keys[e.key.toLowerCase()] = true; - }); - - window.addEventListener('keyup', (e) => { - this.keys[e.key.toLowerCase()] = false; - }); - - // Touch events - const canvas = document.querySelector('canvas'); - if (canvas) { - canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: false }); - canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e), { passive: false }); - canvas.addEventListener('touchend', (e) => this.handleTouchEnd(e), { passive: false }); - canvas.addEventListener('touchcancel', (e) => this.handleTouchEnd(e), { passive: false }); - } - } - - handleTouchStart(e) { - e.preventDefault(); - if (this.touch.active) return; - - const touch = e.touches[0]; - const rect = e.target.getBoundingClientRect(); - - this.touch.startX = rect.left + rect.width / 2; - this.touch.startY = rect.top + rect.height / 2; - this.touch.currentX = touch.clientX; - this.touch.currentY = touch.clientY; - this.touch.id = touch.identifier; - this.touch.active = true; - } - - handleTouchMove(e) { - e.preventDefault(); - if (!this.touch.active) return; - - const touch = Array.from(e.touches).find(t => t.identifier === this.touch.id); - if (!touch) return; - - this.touch.currentX = touch.clientX; - this.touch.currentY = touch.clientY; - } - - handleTouchEnd(e) { - e.preventDefault(); - if (!this.touch.active) return; - - const touch = Array.from(e.changedTouches).find(t => t.identifier === this.touch.id); - if (!touch) return; - - this.touch.active = false; - this.touch.id = null; - } - - isKeyPressed(key) { - return this.keys[key] || this.keys['arrow' + key] || false; - } - - getTouchDirection() { - if (!this.touch.active) { - return { x: 0, y: 0 }; - } - - const deltaX = this.touch.currentX - this.touch.startX; - const deltaY = this.touch.currentY - this.touch.startY; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - const threshold = 10; - if (distance < threshold) { - return { x: 0, y: 0 }; - } - - const normalizedX = deltaX / distance; - const normalizedY = deltaY / distance; - - return { x: normalizedX, y: normalizedY }; - } - - update(_deltaTime) { - // Input is passive, just stores state for other systems to query - } -} - diff --git a/src/systems/MovementSystem.js b/src/systems/MovementSystem.js deleted file mode 100644 index 2e53d16..0000000 --- a/src/systems/MovementSystem.js +++ /dev/null @@ -1,23 +0,0 @@ -import { System } from '../ecs/System.js'; -import { Transform } from '../components/Transform.js'; -import { Velocity } from '../components/Velocity.js'; - -/** - * MovementSystem - applies velocity to transform (frame-rate independent) - */ -export class MovementSystem extends System { - update(deltaTime) { - const entities = this.getEntities(Transform, Velocity); - - for (const entityId of entities) { - const transform = this.getComponent(entityId, Transform); - const velocity = this.getComponent(entityId, Velocity); - - // Apply velocity scaled by deltaTime for frame-rate independence - // Velocity is in units per second, deltaTime converts to units per frame - const displacement = velocity.velocity.clone().multiplyScalar(deltaTime * 60); - transform.position.add(displacement); - } - } -} - diff --git a/src/systems/ObstacleSystem.js b/src/systems/ObstacleSystem.js deleted file mode 100644 index 92826a6..0000000 --- a/src/systems/ObstacleSystem.js +++ /dev/null @@ -1,34 +0,0 @@ -import { System } from '../ecs/System.js'; -import { ObstacleTag, BoundaryConstrained } from '../components/Tags.js'; -import { Transform } from '../components/Transform.js'; -import { Velocity } from '../components/Velocity.js'; - -/** - * ObstacleSystem - handles obstacle-specific behavior - */ -export class ObstacleSystem extends System { - update(_deltaTime) { - const obstacles = this.getEntities(ObstacleTag, Transform, Velocity, BoundaryConstrained); - - for (const entityId of obstacles) { - const transform = this.getComponent(entityId, Transform); - const velocity = this.getComponent(entityId, Velocity); - const boundary = this.getComponent(entityId, BoundaryConstrained); - - const boundaryLimit = boundary.getBoundary() - 1; - - // Bounce off boundaries - if (Math.abs(transform.position.x) > boundaryLimit) { - velocity.velocity.x *= -1; - // Clamp position - transform.position.x = Math.sign(transform.position.x) * boundaryLimit; - } - if (Math.abs(transform.position.z) > boundaryLimit) { - velocity.velocity.z *= -1; - // Clamp position - transform.position.z = Math.sign(transform.position.z) * boundaryLimit; - } - } - } -} - diff --git a/src/systems/PlayerControlSystem.js b/src/systems/PlayerControlSystem.js deleted file mode 100644 index 5c71304..0000000 --- a/src/systems/PlayerControlSystem.js +++ /dev/null @@ -1,76 +0,0 @@ -import { System } from '../ecs/System.js'; -import { PlayerTag } from '../components/Tags.js'; -import { Velocity } from '../components/Velocity.js'; -import { Transform } from '../components/Transform.js'; - -/** - * PlayerControlSystem - handles player input and applies to velocity - */ -export class PlayerControlSystem extends System { - constructor(inputSystem) { - super(); - this.inputSystem = inputSystem; - } - - update(deltaTime) { - const players = this.getEntities(PlayerTag, Velocity, Transform); - - for (const entityId of players) { - const velocity = this.getComponent(entityId, Velocity); - const transform = this.getComponent(entityId, Transform); - - // Calculate target velocity from input - const targetVelocity = new window.THREE.Vector3(0, 0, 0); - - // Keyboard input - if (this.inputSystem.isKeyPressed('w') || this.inputSystem.isKeyPressed('up')) { - targetVelocity.z -= velocity.maxSpeed; - } - if (this.inputSystem.isKeyPressed('s') || this.inputSystem.isKeyPressed('down')) { - targetVelocity.z += velocity.maxSpeed; - } - if (this.inputSystem.isKeyPressed('a') || this.inputSystem.isKeyPressed('left')) { - targetVelocity.x -= velocity.maxSpeed; - } - if (this.inputSystem.isKeyPressed('d') || this.inputSystem.isKeyPressed('right')) { - targetVelocity.x += velocity.maxSpeed; - } - - // Touch input - const touch = this.inputSystem.getTouchDirection(); - if (Math.abs(touch.x) > 0.3) { - targetVelocity.x = touch.x * velocity.maxSpeed; - } - if (Math.abs(touch.y) > 0.3) { - targetVelocity.z = touch.y * velocity.maxSpeed; - } - - // Apply smooth acceleration/deceleration - const isMoving = targetVelocity.length() > 0; - const accelRate = isMoving ? velocity.acceleration : velocity.deceleration; - - // Smooth interpolation for each axis - const velDiffX = targetVelocity.x - velocity.velocity.x; - const velDiffZ = targetVelocity.z - velocity.velocity.z; - - if (Math.abs(velDiffX) > 0.001) { - velocity.velocity.x += velDiffX * accelRate; - } else { - velocity.velocity.x = targetVelocity.x; - } - - if (Math.abs(velDiffZ) > 0.001) { - velocity.velocity.z += velDiffZ * accelRate; - } else { - velocity.velocity.z = targetVelocity.z; - } - - // Rotate player when moving (frame-rate independent) - // Original was 0.1 per frame at ~60fps = 6 rad/s - if (velocity.velocity.length() > 0.01) { - transform.rotation.y += 6 * deltaTime; - } - } - } -} - diff --git a/src/systems/RenderSystem.js b/src/systems/RenderSystem.js deleted file mode 100644 index c3f89d1..0000000 --- a/src/systems/RenderSystem.js +++ /dev/null @@ -1,28 +0,0 @@ -import { System } from '../ecs/System.js'; -import { Transform } from '../components/Transform.js'; -import { MeshComponent } from '../components/MeshComponent.js'; - -/** - * RenderSystem - syncs Three.js mesh positions with Transform components - */ -export class RenderSystem extends System { - constructor(scene) { - super(); - this.scene = scene; - } - - update(_deltaTime) { - const entities = this.getEntities(Transform, MeshComponent); - - for (const entityId of entities) { - const transform = this.getComponent(entityId, Transform); - const meshComp = this.getComponent(entityId, MeshComponent); - - // Sync mesh transform with component - meshComp.mesh.position.copy(transform.position); - meshComp.mesh.rotation.copy(transform.rotation); - meshComp.mesh.scale.copy(transform.scale); - } - } -} -