diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a9bf588
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+dist/
+.DS_Store
+*.log
+.vite/
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6fe8061
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+# Build stage
+FROM node:20-alpine AS build
+
+WORKDIR /app
+
+# Copy package files and install dependencies
+COPY package*.json ./
+RUN npm install
+
+# Copy source code and build
+COPY . .
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+
+# Copy built assets from build stage
+COPY --from=build /app/dist /usr/share/nginx/html
+
+# Copy custom nginx config if needed (optional)
+# COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2eff69f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,132 @@
+# Slime Genesis - ECS RPG PoC
+
+A complete proof of concept for **Slime Genesis: The Awakening of the Entity** built with vanilla JavaScript using an Entity Component System (ECS) architecture.
+
+## Quick Start
+
+1. **Install dependencies:**
+ ```bash
+ npm install
+ ```
+
+2. **Run the development server:**
+ ```bash
+ npm run dev
+ ```
+
+3. **Open your browser** to the URL shown (usually `http://localhost:5173`)
+
+## Controls
+
+- **WASD** or **Arrow Keys**: Move your slime
+- **Mouse**: Aim/face direction
+- **Left Click** or **Space**: Attack
+- **Shift**: Toggle stealth mode
+- **1-9**: Activate skills (when you have them)
+
+## Features
+
+### Core Systems
+- ✅ **ECS Architecture**: Entity Component System for flexible game design
+- ✅ **Real-time Combat**: Fast-paced action combat with form-specific styles
+- ✅ **Evolution System**: Three paths (Human, Beast, Slime) that change based on absorption
+- ✅ **Skill Absorption**: Defeat creatures to gain their abilities
+- ✅ **Stealth System**: Form-specific stealth mechanics (Social, Hunting, Structural)
+- ✅ **RPG Systems**: Stats, leveling, XP, inventory, equipment
+- ✅ **AI System**: Intelligent creature behaviors (wander, chase, combat, flee)
+- ✅ **Projectile System**: Skills can create projectiles (Water Gun, etc.)
+
+### Graphics & Polish
+- ✅ **Animated Slime**: Smooth morphing blob with jiggle physics
+- ✅ **Combat Effects**: Damage numbers, attack indicators, particle effects
+- ✅ **Absorption Visuals**: Swirling particles and color transitions
+- ✅ **Stealth Indicators**: Visibility meters and detection warnings
+- ✅ **Polished UI**: Health bars, XP bars, skill hotbar, stat displays
+
+### Skills
+- **Water Gun**: Shoot a jet of water at enemies
+- **Fire Breath**: Breathe fire in a cone
+- **Pounce**: Leap forward and damage enemies
+- **Stealth Mode**: Enter stealth mode (form-specific)
+
+## Architecture
+
+This game uses a pure ECS (Entity Component System) architecture:
+
+- **Entities**: Game objects (player, creatures, projectiles)
+- **Components**: Data containers (Position, Health, Stats, Evolution, etc.)
+- **Systems**: Logic processors (RenderSystem, CombatSystem, AbsorptionSystem, etc.)
+
+This architecture makes it easy to:
+- Add new skills and abilities
+- Create mutations and combinations
+- Extend creature behaviors
+- Add new game mechanics
+
+## Project Structure
+
+```
+src/
+├── core/ # ECS framework
+│ ├── Engine.js # Main game loop
+│ ├── Entity.js # Entity manager
+│ ├── Component.js # Base component
+│ └── System.js # Base system
+├── components/ # All game components
+│ ├── Position.js
+│ ├── Health.js
+│ ├── Stats.js
+│ ├── Evolution.js
+│ └── ...
+├── systems/ # All game systems
+│ ├── RenderSystem.js
+│ ├── CombatSystem.js
+│ ├── AbsorptionSystem.js
+│ └── ...
+├── skills/ # Skill system
+│ ├── Skill.js
+│ ├── SkillRegistry.js
+│ └── skills/ # Individual skills
+├── items/ # Item system
+│ ├── Item.js
+│ └── ItemRegistry.js
+├── world/ # World management
+│ └── World.js
+└── main.js # Entry point
+```
+
+## Gameplay Loop
+
+1. **Explore**: Move around the cave, discover creatures
+2. **Fight**: Engage in real-time combat with creatures
+3. **Absorb**: Defeat creatures to gain evolution points and skills
+4. **Evolve**: Your form changes based on what you absorb
+5. **Level Up**: Gain XP, increase stats, unlock new possibilities
+6. **Stealth**: Use form-specific stealth to avoid or ambush enemies
+
+## Evolution Paths
+
+- **Human Path**: Absorb humanoids to gain human traits, access to social areas
+- **Beast Path**: Absorb beasts to become a predator, gain physical power
+- **Slime Path**: Maintain your original form, gain unique abilities
+
+## Technical Details
+
+- **No External Dependencies**: Pure vanilla JavaScript (except Vite for dev server)
+- **Canvas 2D**: High-performance rendering with Canvas API
+- **Modular Design**: Easy to extend and modify
+- **ECS Pattern**: Scalable architecture for complex game mechanics
+
+## Future Enhancements
+
+- More skills and mutations
+- Multiple areas/zones
+- NPCs with dialogue
+- Quest system
+- Equipment system expansion
+- Save/load functionality
+- More creature types and behaviors
+
+## License
+
+This is a proof of concept. Use as you see fit for your game development.
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c4d8345
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ game:
+ build: .
+ ports:
+ - "8080:80"
+ volumes:
+ - .:/app
+ environment:
+ - NODE_ENV=production
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..1462103
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,26 @@
+import js from "@eslint/js";
+import globals from "globals";
+
+export default [
+ js.configs.recommended,
+ {
+ languageOptions: {
+ ecmaVersion: 2022,
+ sourceType: "module",
+ globals: {
+ ...globals.browser,
+ ...globals.node,
+ performance: "readonly",
+ requestAnimationFrame: "readonly",
+ },
+ },
+ rules: {
+ "no-unused-vars": ["error", {
+ "argsIgnorePattern": "^_",
+ "varsIgnorePattern": "^_"
+ }],
+ "no-console": "off",
+ "indent": ["error", 2],
+ },
+ },
+];
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..21eb741
--- /dev/null
+++ b/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Slime Genesis - PoC
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..9cce95d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2281 @@
+{
+ "name": "slime-genesis-poc",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "slime-genesis-poc",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "eslint": "^9.39.2",
+ "globals": "^17.0.0",
+ "terser": "^5.44.1",
+ "vite": "^7.3.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz",
+ "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.44.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
+ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
+ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..36a494f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "slime-genesis-poc",
+ "version": "0.1.0",
+ "description": "Proof of Concept for Slime Genesis: The Awakening of the Entity",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint src",
+ "lint:fix": "eslint src --fix"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "eslint": "^9.39.2",
+ "globals": "^17.0.0",
+ "terser": "^5.44.1",
+ "vite": "^7.3.0"
+ }
+}
diff --git a/src/GameConfig.js b/src/GameConfig.js
new file mode 100644
index 0000000..4995f4f
--- /dev/null
+++ b/src/GameConfig.js
@@ -0,0 +1,38 @@
+/**
+ * Centralized Game Configuration
+ * Thresholds, rates, and balancing constants
+ */
+export const GameConfig = {
+ Evolution: {
+ totalTarget: 150,
+ thresholds: {
+ hardenedShell: { constitution: 25 },
+ electricSkin: { intelligence: 25 },
+ glowingBody: { human: 50 }
+ }
+ },
+
+ Absorption: {
+ range: 80,
+ healPercentMin: 0.1,
+ healPercentMax: 0.2,
+ skillAbsorptionChance: 0.3,
+ mutationChance: 0.1,
+ removalDelay: 3.0, // Seconds after death
+ },
+
+ Combat: {
+ knockbackPower: 150,
+ defaultAttackArc: 0.5,
+ damageReflectionPercent: 0.2,
+ hardenedShellReduction: 0.7
+ },
+
+ AI: {
+ detectionAwarenessThreshold: 0.7,
+ passiveAwarenessThreshold: 0.95,
+ fleeAwarenessThreshold: 0.5,
+ awarenessLossRate: 0.5,
+ awarenessGainMultiplier: 2.0
+ }
+};
diff --git a/src/components/AI.js b/src/components/AI.js
new file mode 100644
index 0000000..dbe67f1
--- /dev/null
+++ b/src/components/AI.js
@@ -0,0 +1,52 @@
+import { Component } from '../core/Component.js';
+
+export class AI extends Component {
+ constructor(behaviorType = 'wander') {
+ super('AI');
+ this.behaviorType = behaviorType; // 'wander', 'patrol', 'chase', 'flee', 'combat'
+ this.state = 'idle'; // 'idle', 'moving', 'attacking', 'fleeing'
+ this.target = null; // Entity ID to target
+ this.awareness = 0; // 0-1, how aware of player
+ this.alertRadius = 150;
+ this.chaseRadius = 300;
+ this.fleeRadius = 100;
+
+ // Behavior parameters
+ this.wanderSpeed = 50;
+ this.wanderDirection = Math.random() * Math.PI * 2;
+ this.wanderChangeTime = 0;
+ this.wanderChangeInterval = 2.0; // seconds
+ }
+
+ /**
+ * Set behavior type
+ */
+ setBehavior(type) {
+ this.behaviorType = type;
+ }
+
+ /**
+ * Set target entity
+ */
+ setTarget(entityId) {
+ this.target = entityId;
+ }
+
+ /**
+ * Clear target
+ */
+ clearTarget() {
+ this.target = null;
+ }
+
+ /**
+ * Update awareness
+ */
+ updateAwareness(delta, maxAwareness = 1.0) {
+ this.awareness = Math.min(maxAwareness, this.awareness + delta);
+ if (this.awareness <= 0) {
+ this.awareness = 0;
+ }
+ }
+}
+
diff --git a/src/components/Absorbable.js b/src/components/Absorbable.js
new file mode 100644
index 0000000..ef4b6f0
--- /dev/null
+++ b/src/components/Absorbable.js
@@ -0,0 +1,54 @@
+import { Component } from '../core/Component.js';
+
+export class Absorbable extends Component {
+ constructor() {
+ super('Absorbable');
+ this.evolutionData = {
+ human: 0,
+ beast: 0,
+ slime: 0
+ };
+ this.skillsGranted = []; // Array of skill IDs that can be absorbed
+ this.skillAbsorptionChance = 0.3; // 30% chance to absorb a skill
+ this.mutationChance = 0.1; // 10% chance for mutation
+ this.absorbed = false;
+ }
+
+ /**
+ * Set evolution data
+ */
+ setEvolutionData(human, beast, slime) {
+ this.evolutionData = { human, beast, slime };
+ }
+
+ /**
+ * Add a skill that can be absorbed
+ */
+ addSkill(skillId, chance = null) {
+ this.skillsGranted.push({
+ id: skillId,
+ chance: chance || this.skillAbsorptionChance
+ });
+ }
+
+ /**
+ * Get skills that were successfully absorbed (rolls for each)
+ */
+ getAbsorbedSkills() {
+ const absorbed = [];
+ for (const skill of this.skillsGranted) {
+ if (Math.random() < skill.chance) {
+ absorbed.push(skill.id);
+ }
+ }
+ return absorbed;
+ }
+
+ /**
+ * Check if should mutate
+ */
+ shouldMutate() {
+ return Math.random() < this.mutationChance;
+ }
+}
+
diff --git a/src/components/Combat.js b/src/components/Combat.js
new file mode 100644
index 0000000..516caa0
--- /dev/null
+++ b/src/components/Combat.js
@@ -0,0 +1,52 @@
+import { Component } from '../core/Component.js';
+
+export class Combat extends Component {
+ constructor() {
+ super('Combat');
+ this.attackDamage = 10;
+ this.defense = 5;
+ this.attackSpeed = 1.0; // Attacks per second
+ this.attackRange = 50;
+ this.lastAttackTime = 0;
+ this.attackCooldown = 0;
+
+ // Combat state
+ this.isAttacking = false;
+ this.attackDirection = 0; // Angle in radians
+ this.knockbackResistance = 0.5;
+ }
+
+ /**
+ * Check if can attack
+ */
+ canAttack(currentTime) {
+ return (currentTime - this.lastAttackTime) >= (1.0 / this.attackSpeed);
+ }
+
+ /**
+ * Perform attack
+ */
+ attack(currentTime, direction) {
+ if (!this.canAttack(currentTime)) return false;
+
+ this.lastAttackTime = currentTime;
+ this.isAttacking = true;
+ this.attackDirection = direction;
+ this.attackCooldown = 0.3; // Attack animation duration
+
+ return true;
+ }
+
+ /**
+ * Update attack state
+ */
+ update(deltaTime) {
+ if (this.attackCooldown > 0) {
+ this.attackCooldown -= deltaTime;
+ if (this.attackCooldown <= 0) {
+ this.isAttacking = false;
+ }
+ }
+ }
+}
+
diff --git a/src/components/Evolution.js b/src/components/Evolution.js
new file mode 100644
index 0000000..e3a9c63
--- /dev/null
+++ b/src/components/Evolution.js
@@ -0,0 +1,105 @@
+import { Component } from '../core/Component.js';
+import { GameConfig } from '../GameConfig.js';
+import { Events } from '../core/EventBus.js';
+
+export class Evolution extends Component {
+ constructor() {
+ super('Evolution');
+ this.human = 0;
+ this.beast = 0;
+ this.slime = 100; // Start as pure slime
+
+ // Mutation tracking
+ this.mutations = new Set(); // Use Set for unique functional mutations
+ this.mutationEffects = {
+ electricSkin: false,
+ glowingBody: false,
+ hardenedShell: false
+ };
+ }
+
+ /**
+ * Check and apply functional mutations
+ */
+ checkMutations(stats, engine = null) {
+ if (!stats) return;
+
+ const config = GameConfig.Evolution.thresholds;
+
+ // Threshold-based functional mutations
+ if (stats.constitution > config.hardenedShell.constitution && !this.mutationEffects.hardenedShell) {
+ this.mutationEffects.hardenedShell = true;
+ this.mutations.add('Hardened Shell');
+ if (engine) engine.emit(Events.MUTATION_GAINED, { name: 'Hardened Shell', description: 'Defense Up' });
+ console.log("Mutation Gained: Hardened Shell (Defense Up)");
+ }
+
+ if (stats.intelligence > config.electricSkin.intelligence && !this.mutationEffects.electricSkin) {
+ this.mutationEffects.electricSkin = true;
+ this.mutations.add('Electric Skin');
+ if (engine) engine.emit(Events.MUTATION_GAINED, { name: 'Electric Skin', description: 'Damage Reflection' });
+ console.log("Mutation Gained: Electric Skin (Damage Reflection)");
+ }
+
+ if (this.human > config.glowingBody.human && !this.mutationEffects.glowingBody) {
+ this.mutationEffects.glowingBody = true;
+ this.mutations.add('Bioluminescence');
+ if (engine) engine.emit(Events.MUTATION_GAINED, { name: 'Bioluminescence', description: 'Light Source' });
+ console.log("Mutation Gained: Bioluminescence (Light Source)");
+ }
+ }
+
+ /**
+ * Add evolution points
+ */
+ addEvolution(human = 0, beast = 0, slime = 0) {
+ this.human += human;
+ this.beast += beast;
+ this.slime += slime;
+
+ // Normalize to keep total around target
+ const target = GameConfig.Evolution.totalTarget;
+ const total = this.human + this.beast + this.slime;
+ if (total > target) {
+ const factor = target / total;
+ this.human *= factor;
+ this.beast *= factor;
+ this.slime *= factor;
+ }
+
+ // Ensure no negative values
+ this.human = Math.max(0, this.human);
+ this.beast = Math.max(0, this.beast);
+ this.slime = Math.max(0, this.slime);
+ }
+
+ /**
+ * Get dominant form
+ */
+ getDominantForm() {
+ if (this.human > this.beast && this.human > this.slime) {
+ return 'human';
+ } else if (this.beast > this.human && this.beast > this.slime) {
+ return 'beast';
+ } else {
+ return 'slime';
+ }
+ }
+
+ /**
+ * Get form percentage (0-1)
+ */
+ getFormPercentage(form) {
+ const total = this.human + this.beast + this.slime;
+ if (total === 0) return 0;
+ return this[form] / total;
+ }
+
+ /**
+ * Add a mutation
+ */
+ addMutation(mutation) {
+ this.mutations.push(mutation);
+ }
+}
+
diff --git a/src/components/Health.js b/src/components/Health.js
new file mode 100644
index 0000000..063f0d0
--- /dev/null
+++ b/src/components/Health.js
@@ -0,0 +1,29 @@
+import { Component } from '../core/Component.js';
+
+export class Health extends Component {
+ constructor(maxHp = 100, hp = null) {
+ super('Health');
+ this.maxHp = maxHp;
+ this.hp = hp !== null ? hp : maxHp;
+ this.regeneration = 2; // HP per second (slime regenerates)
+ this.lastDamageTime = 0;
+ this.invulnerable = false;
+ this.invulnerabilityDuration = 0;
+ }
+
+ takeDamage(amount) {
+ if (this.invulnerable) return 0;
+ this.hp = Math.max(0, this.hp - amount);
+ this.lastDamageTime = Date.now();
+ return amount;
+ }
+
+ heal(amount) {
+ this.hp = Math.min(this.maxHp, this.hp + amount);
+ }
+
+ isDead() {
+ return this.hp <= 0;
+ }
+}
+
diff --git a/src/components/Inventory.js b/src/components/Inventory.js
new file mode 100644
index 0000000..7b619d7
--- /dev/null
+++ b/src/components/Inventory.js
@@ -0,0 +1,71 @@
+import { Component } from '../core/Component.js';
+
+export class Inventory extends Component {
+ constructor() {
+ super('Inventory');
+ this.items = []; // Array of item objects
+ this.maxSize = 20;
+ this.equipped = {
+ weapon: null,
+ armor: null,
+ accessory: null
+ };
+ }
+
+ /**
+ * Add an item to inventory
+ */
+ addItem(item) {
+ if (this.items.length < this.maxSize) {
+ this.items.push(item);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Remove an item
+ */
+ removeItem(itemId) {
+ const index = this.items.findIndex(item => item.id === itemId);
+ if (index > -1) {
+ return this.items.splice(index, 1)[0];
+ }
+ return null;
+ }
+
+ /**
+ * Equip an item
+ */
+ equipItem(itemId, slot) {
+ const item = this.items.find(i => i.id === itemId);
+ if (!item) return false;
+
+ // Unequip current item in slot
+ if (this.equipped[slot]) {
+ this.items.push(this.equipped[slot]);
+ }
+
+ // Equip new item
+ this.equipped[slot] = item;
+ const index = this.items.indexOf(item);
+ if (index > -1) {
+ this.items.splice(index, 1);
+ }
+
+ return true;
+ }
+
+ /**
+ * Unequip an item
+ */
+ unequipItem(slot) {
+ if (this.equipped[slot]) {
+ this.items.push(this.equipped[slot]);
+ this.equipped[slot] = null;
+ return true;
+ }
+ return false;
+ }
+}
+
diff --git a/src/components/Position.js b/src/components/Position.js
new file mode 100644
index 0000000..5713b4b
--- /dev/null
+++ b/src/components/Position.js
@@ -0,0 +1,11 @@
+import { Component } from '../core/Component.js';
+
+export class Position extends Component {
+ constructor(x = 0, y = 0, rotation = 0) {
+ super('Position');
+ this.x = x;
+ this.y = y;
+ this.rotation = rotation;
+ }
+}
+
diff --git a/src/components/SkillProgress.js b/src/components/SkillProgress.js
new file mode 100644
index 0000000..cb27042
--- /dev/null
+++ b/src/components/SkillProgress.js
@@ -0,0 +1,45 @@
+import { Component } from '../core/Component.js';
+
+/**
+ * Tracks progress toward learning skills
+ * Need to absorb multiple enemies with the same skill to learn it
+ */
+export class SkillProgress extends Component {
+ constructor() {
+ super('SkillProgress');
+ this.skillProgress = new Map(); // skillId -> count (how many times absorbed)
+ this.requiredAbsorptions = 5; // Need to absorb 5 enemies with a skill to learn it
+ }
+
+ /**
+ * Add progress toward learning a skill
+ */
+ addSkillProgress(skillId) {
+ const current = this.skillProgress.get(skillId) || 0;
+ this.skillProgress.set(skillId, current + 1);
+ return this.skillProgress.get(skillId);
+ }
+
+ /**
+ * Check if skill can be learned
+ */
+ canLearnSkill(skillId) {
+ const progress = this.skillProgress.get(skillId) || 0;
+ return progress >= this.requiredAbsorptions;
+ }
+
+ /**
+ * Get progress for a skill
+ */
+ getSkillProgress(skillId) {
+ return this.skillProgress.get(skillId) || 0;
+ }
+
+ /**
+ * Get all skill progress
+ */
+ getAllProgress() {
+ return this.skillProgress;
+ }
+}
+
diff --git a/src/components/Skills.js b/src/components/Skills.js
new file mode 100644
index 0000000..6c1869f
--- /dev/null
+++ b/src/components/Skills.js
@@ -0,0 +1,69 @@
+import { Component } from '../core/Component.js';
+
+export class Skills extends Component {
+ constructor() {
+ super('Skills');
+ this.activeSkills = []; // Array of skill IDs
+ this.passiveSkills = []; // Array of passive skill IDs
+ this.skillCooldowns = new Map(); // skillId -> remaining cooldown time
+ }
+
+ /**
+ * Add a skill
+ */
+ addSkill(skillId, isPassive = false) {
+ if (isPassive) {
+ if (!this.passiveSkills.includes(skillId)) {
+ this.passiveSkills.push(skillId);
+ }
+ } else {
+ if (!this.activeSkills.includes(skillId)) {
+ this.activeSkills.push(skillId);
+ }
+ }
+ }
+
+ /**
+ * Check if entity has a skill
+ */
+ hasSkill(skillId) {
+ return this.activeSkills.includes(skillId) ||
+ this.passiveSkills.includes(skillId);
+ }
+
+ /**
+ * Set skill cooldown
+ */
+ setCooldown(skillId, duration) {
+ this.skillCooldowns.set(skillId, duration);
+ }
+
+ /**
+ * Update cooldowns
+ */
+ updateCooldowns(deltaTime) {
+ for (const [skillId, cooldown] of this.skillCooldowns.entries()) {
+ const newCooldown = cooldown - deltaTime;
+ if (newCooldown <= 0) {
+ this.skillCooldowns.delete(skillId);
+ } else {
+ this.skillCooldowns.set(skillId, newCooldown);
+ }
+ }
+ }
+
+ /**
+ * Check if skill is on cooldown
+ */
+ isOnCooldown(skillId) {
+ return this.skillCooldowns.has(skillId);
+ }
+
+ /**
+ * Get remaining cooldown
+ */
+ getCooldown(skillId) {
+ return this.skillCooldowns.get(skillId) || 0;
+ }
+}
+
diff --git a/src/components/Sprite.js b/src/components/Sprite.js
new file mode 100644
index 0000000..94ab18c
--- /dev/null
+++ b/src/components/Sprite.js
@@ -0,0 +1,18 @@
+import { Component } from '../core/Component.js';
+
+export class Sprite extends Component {
+ constructor(color = '#00ff96', width = 30, height = 30, shape = 'circle') {
+ super('Sprite');
+ this.color = color;
+ this.width = width;
+ this.height = height;
+ this.shape = shape; // 'circle', 'rect', 'slime'
+ this.alpha = 1.0;
+ this.scale = 1.0;
+
+ // Animation properties
+ this.animationTime = 0;
+ this.morphAmount = 0; // For slime morphing
+ }
+}
+
diff --git a/src/components/Stats.js b/src/components/Stats.js
new file mode 100644
index 0000000..9c22ee0
--- /dev/null
+++ b/src/components/Stats.js
@@ -0,0 +1,55 @@
+import { Component } from '../core/Component.js';
+
+export class Stats extends Component {
+ constructor() {
+ super('Stats');
+ this.strength = 10; // Physical damage
+ this.agility = 10; // Movement speed, attack speed
+ this.intelligence = 10; // Magic damage, skill effectiveness
+ this.constitution = 10; // Max HP, defense
+ this.perception = 10; // Detection range, stealth detection
+
+ // Derived stats
+ this.level = 1;
+ this.experience = 0;
+ this.experienceToNext = 100;
+ }
+
+ /**
+ * Add experience and handle level ups
+ */
+ addExperience(amount) {
+ this.experience += amount;
+ let leveledUp = false;
+
+ while (this.experience >= this.experienceToNext) {
+ this.experience -= this.experienceToNext;
+ this.levelUp();
+ leveledUp = true;
+ }
+
+ return leveledUp;
+ }
+
+ /**
+ * Level up - increase stats
+ */
+ levelUp() {
+ this.level++;
+ this.strength += 2;
+ this.agility += 2;
+ this.intelligence += 2;
+ this.constitution += 2;
+ this.perception += 2;
+ this.experienceToNext = Math.floor(this.experienceToNext * 1.5);
+ }
+
+ /**
+ * Get total stat points
+ */
+ getTotalStats() {
+ return this.strength + this.agility + this.intelligence +
+ this.constitution + this.perception;
+ }
+}
+
diff --git a/src/components/Stealth.js b/src/components/Stealth.js
new file mode 100644
index 0000000..f1de5ad
--- /dev/null
+++ b/src/components/Stealth.js
@@ -0,0 +1,48 @@
+import { Component } from '../core/Component.js';
+
+export class Stealth extends Component {
+ constructor() {
+ super('Stealth');
+ this.visibility = 1.0; // 0 = fully hidden, 1 = fully visible
+ this.stealthType = 'slime'; // 'slime', 'beast', 'human'
+ this.isStealthed = false;
+ this.stealthLevel = 0; // 0-100
+ this.detectionRadius = 100; // How far others can detect this entity
+ }
+
+ /**
+ * Enter stealth mode
+ */
+ enterStealth(type) {
+ this.stealthType = type;
+ this.isStealthed = true;
+ this.visibility = 0.3;
+ }
+
+ /**
+ * Exit stealth mode
+ */
+ exitStealth() {
+ this.isStealthed = false;
+ this.visibility = 1.0;
+ }
+
+ /**
+ * Update stealth based on movement and actions
+ */
+ updateStealth(isMoving, isInCombat) {
+ if (isInCombat) {
+ this.exitStealth();
+ return;
+ }
+
+ if (this.isStealthed) {
+ if (isMoving) {
+ this.visibility = Math.min(1.0, this.visibility + 0.1);
+ } else {
+ this.visibility = Math.max(0.1, this.visibility - 0.05);
+ }
+ }
+ }
+}
+
diff --git a/src/components/Velocity.js b/src/components/Velocity.js
new file mode 100644
index 0000000..d6ebd46
--- /dev/null
+++ b/src/components/Velocity.js
@@ -0,0 +1,11 @@
+import { Component } from '../core/Component.js';
+
+export class Velocity extends Component {
+ constructor(vx = 0, vy = 0) {
+ super('Velocity');
+ this.vx = vx;
+ this.vy = vy;
+ this.maxSpeed = 200;
+ }
+}
+
diff --git a/src/core/Component.js b/src/core/Component.js
new file mode 100644
index 0000000..b44eac9
--- /dev/null
+++ b/src/core/Component.js
@@ -0,0 +1,14 @@
+/**
+ * Base Component class for ECS architecture
+ * Components are pure data containers
+ */
+export class Component {
+ constructor(type) {
+ this.type = type;
+ }
+
+ static getType() {
+ return this.name;
+ }
+}
+
diff --git a/src/core/Engine.js b/src/core/Engine.js
new file mode 100644
index 0000000..f9720cc
--- /dev/null
+++ b/src/core/Engine.js
@@ -0,0 +1,139 @@
+import { System } from './System.js';
+import { Entity } from './Entity.js';
+import { EventBus } from './EventBus.js';
+
+/**
+ * Main game engine - manages ECS, game loop, and systems
+ */
+export class Engine {
+ constructor(canvas) {
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d');
+ this.entities = [];
+ this.systems = [];
+ this.events = new EventBus();
+ this.running = false;
+ this.lastTime = 0;
+
+ // Set canvas size
+ this.canvas.width = 1024;
+ this.canvas.height = 768;
+
+ // Game state
+ this.deltaTime = 0;
+ }
+
+ /**
+ * Add a system to the engine
+ */
+ addSystem(system) {
+ if (system instanceof System) {
+ system.init(this);
+ this.systems.push(system);
+ // Sort by priority (lower priority runs first)
+ this.systems.sort((a, b) => a.priority - b.priority);
+ }
+ return this;
+ }
+
+ /**
+ * Emit an event locally
+ */
+ emit(event, data) {
+ this.events.emit(event, data);
+ }
+
+ /**
+ * Subscribe to an event
+ */
+ on(event, callback) {
+ return this.events.on(event, callback);
+ }
+
+ /**
+ * Create and add an entity
+ */
+ createEntity() {
+ const entity = new Entity();
+ this.entities.push(entity);
+ return entity;
+ }
+
+ /**
+ * Remove an entity
+ */
+ removeEntity(entity) {
+ const index = this.entities.indexOf(entity);
+ if (index > -1) {
+ this.entities.splice(index, 1);
+ }
+ }
+
+ /**
+ * Get all entities
+ */
+ getEntities() {
+ return this.entities.filter(e => e.active);
+ }
+
+ /**
+ * Main game loop
+ */
+ start() {
+ if (this.running) return;
+ this.running = true;
+ this.lastTime = performance.now();
+ this.gameLoop();
+ }
+
+ /**
+ * Stop the game loop
+ */
+ stop() {
+ this.running = false;
+ }
+
+ /**
+ * Game loop using requestAnimationFrame
+ */
+ gameLoop = (currentTime = 0) => {
+ if (!this.running) return;
+
+ // Calculate delta time
+ this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
+ this.lastTime = currentTime;
+
+ // Clamp delta time to prevent large jumps
+ this.deltaTime = Math.min(this.deltaTime, 0.1);
+
+ // Update all systems
+ const menuSystem = this.systems.find(s => s.name === 'MenuSystem');
+ const gameState = menuSystem ? menuSystem.getGameState() : 'playing';
+ const isPaused = gameState === 'paused' || gameState === 'start';
+
+ this.systems.forEach(system => {
+ // Skip game systems if paused/start menu (but allow MenuSystem, UISystem, and RenderSystem)
+ if (isPaused && system.name !== 'MenuSystem' && system.name !== 'UISystem' && system.name !== 'RenderSystem') {
+ return;
+ }
+ system.update(this.deltaTime, this.entities);
+ });
+
+ // Update input system's previous states at end of frame
+ const inputSystem = this.systems.find(s => s.name === 'InputSystem');
+ if (inputSystem && inputSystem.updatePreviousStates) {
+ inputSystem.updatePreviousStates();
+ }
+
+ // Continue loop
+ requestAnimationFrame(this.gameLoop);
+ }
+
+ /**
+ * Clear the canvas
+ */
+ clear() {
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+}
+
diff --git a/src/core/Entity.js b/src/core/Entity.js
new file mode 100644
index 0000000..0671938
--- /dev/null
+++ b/src/core/Entity.js
@@ -0,0 +1,58 @@
+/**
+ * Entity class - represents a game object with a unique ID
+ * Entities are just containers for components
+ */
+export class Entity {
+ static nextId = 0;
+
+ constructor() {
+ this.id = Entity.nextId++;
+ this.components = new Map();
+ this.active = true;
+ }
+
+ /**
+ * Add a component to this entity
+ */
+ addComponent(component) {
+ this.components.set(component.type, component);
+ return this;
+ }
+
+ /**
+ * Get a component by type
+ */
+ getComponent(type) {
+ return this.components.get(type);
+ }
+
+ /**
+ * Check if entity has a component
+ */
+ hasComponent(type) {
+ return this.components.has(type);
+ }
+
+ /**
+ * Check if entity has all specified components
+ */
+ hasComponents(...types) {
+ return types.every(type => this.components.has(type));
+ }
+
+ /**
+ * Remove a component
+ */
+ removeComponent(type) {
+ this.components.delete(type);
+ return this;
+ }
+
+ /**
+ * Get all components
+ */
+ getAllComponents() {
+ return Array.from(this.components.values());
+ }
+}
+
diff --git a/src/core/EventBus.js b/src/core/EventBus.js
new file mode 100644
index 0000000..6950164
--- /dev/null
+++ b/src/core/EventBus.js
@@ -0,0 +1,57 @@
+/**
+ * Lightweight EventBus for pub/sub communication between systems
+ */
+export const Events = {
+ // Combat Events
+ DAMAGE_DEALT: 'combat:damage_dealt',
+ ENTITY_DIED: 'combat:entity_died',
+
+ // Evolution Events
+ EVOLVED: 'evolution:evolved',
+ MUTATION_GAINED: 'evolution:mutation_gained',
+
+ // Leveling Events
+ EXP_GAINED: 'stats:exp_gained',
+ LEVEL_UP: 'stats:level_up',
+
+ // Skill Events
+ SKILL_LEARNED: 'skills:learned',
+ SKILL_COOLDOWN_STARTED: 'skills:cooldown_started'
+};
+
+export class EventBus {
+ constructor() {
+ this.listeners = new Map();
+ }
+
+ /**
+ * Subscribe to an event
+ */
+ on(event, callback) {
+ if (!this.listeners.has(event)) {
+ this.listeners.set(event, []);
+ }
+ this.listeners.get(event).push(callback);
+ return () => this.off(event, callback);
+ }
+
+ /**
+ * Unsubscribe from an event
+ */
+ off(event, callback) {
+ if (!this.listeners.has(event)) return;
+ const callbacks = this.listeners.get(event);
+ const index = callbacks.indexOf(callback);
+ if (index > -1) {
+ callbacks.splice(index, 1);
+ }
+ }
+
+ /**
+ * Emit an event
+ */
+ emit(event, data) {
+ if (!this.listeners.has(event)) return;
+ this.listeners.get(event).forEach(callback => callback(data));
+ }
+}
diff --git a/src/core/System.js b/src/core/System.js
new file mode 100644
index 0000000..6b18131
--- /dev/null
+++ b/src/core/System.js
@@ -0,0 +1,45 @@
+/**
+ * Base System class for ECS architecture
+ * Systems contain logic that operates on entities with specific components
+ */
+export class System {
+ constructor(name) {
+ this.name = name;
+ this.requiredComponents = [];
+ this.priority = 0; // Lower priority runs first
+ }
+
+ /**
+ * Check if an entity matches this system's requirements
+ */
+ matches(entity) {
+ if (!entity.active) return false;
+ return this.requiredComponents.every(componentType =>
+ entity.hasComponent(componentType)
+ );
+ }
+
+ /**
+ * Update method - override in subclasses
+ */
+ update(deltaTime, entities) {
+ // Filter entities that match this system's requirements
+ const matchingEntities = entities.filter(entity => this.matches(entity));
+ this.process(deltaTime, matchingEntities);
+ }
+
+ /**
+ * Process matching entities - override in subclasses
+ */
+ process(_deltaTime, _entities) {
+ // Override in subclasses
+ }
+
+ /**
+ * Called when system is added to engine
+ */
+ init(engine) {
+ this.engine = engine;
+ }
+}
+
diff --git a/src/items/Item.js b/src/items/Item.js
new file mode 100644
index 0000000..bbf314b
--- /dev/null
+++ b/src/items/Item.js
@@ -0,0 +1,13 @@
+/**
+ * Base Item class
+ */
+export class Item {
+ constructor(id, name, type = 'consumable') {
+ this.id = id;
+ this.name = name;
+ this.type = type; // 'consumable', 'weapon', 'armor', 'accessory'
+ this.description = '';
+ this.value = 0;
+ }
+}
+
diff --git a/src/items/ItemRegistry.js b/src/items/ItemRegistry.js
new file mode 100644
index 0000000..91e44cb
--- /dev/null
+++ b/src/items/ItemRegistry.js
@@ -0,0 +1,53 @@
+import { Item } from './Item.js';
+
+/**
+ * Registry for all items in the game
+ */
+export class ItemRegistry {
+ static items = new Map();
+
+ static {
+ // Register items
+ // Weapons
+ this.register(this.createWeapon('iron_sword', 'Iron Sword', 15, 5));
+ this.register(this.createWeapon('steel_claw', 'Steel Claw', 20, 3));
+
+ // Armor
+ this.register(this.createArmor('leather_armor', 'Leather Armor', 10, 5));
+
+ // Consumables
+ this.register(this.createConsumable('health_potion', 'Health Potion', 50));
+ }
+
+ static register(item) {
+ this.items.set(item.id, item);
+ }
+
+ static get(id) {
+ return this.items.get(id);
+ }
+
+ static createWeapon(id, name, damage, speed) {
+ const item = new Item(id, name, 'weapon');
+ item.damage = damage;
+ item.attackSpeed = speed;
+ item.description = `Weapon: +${damage} damage, ${speed} speed`;
+ return item;
+ }
+
+ static createArmor(id, name, defense, hp) {
+ const item = new Item(id, name, 'armor');
+ item.defense = defense;
+ item.maxHp = hp;
+ item.description = `Armor: +${defense} defense, +${hp} HP`;
+ return item;
+ }
+
+ static createConsumable(id, name, healAmount) {
+ const item = new Item(id, name, 'consumable');
+ item.healAmount = healAmount;
+ item.description = `Restores ${healAmount} HP`;
+ return item;
+ }
+}
+
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..7c6f278
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,158 @@
+import { Engine } from './core/Engine.js';
+import { InputSystem } from './systems/InputSystem.js';
+import { MovementSystem } from './systems/MovementSystem.js';
+import { PlayerControllerSystem } from './systems/PlayerControllerSystem.js';
+import { CombatSystem } from './systems/CombatSystem.js';
+import { AISystem } from './systems/AISystem.js';
+import { AbsorptionSystem } from './systems/AbsorptionSystem.js';
+import { SkillSystem } from './systems/SkillSystem.js';
+import { StealthSystem } from './systems/StealthSystem.js';
+import { ProjectileSystem } from './systems/ProjectileSystem.js';
+import { SkillEffectSystem } from './systems/SkillEffectSystem.js';
+import { HealthRegenerationSystem } from './systems/HealthRegenerationSystem.js';
+import { DeathSystem } from './systems/DeathSystem.js';
+import { MenuSystem } from './systems/MenuSystem.js';
+import { RenderSystem } from './systems/RenderSystem.js';
+import { UISystem } from './systems/UISystem.js';
+
+// Components
+import { Position } from './components/Position.js';
+import { Velocity } from './components/Velocity.js';
+import { Sprite } from './components/Sprite.js';
+import { Health } from './components/Health.js';
+import { Stats } from './components/Stats.js';
+import { Evolution } from './components/Evolution.js';
+import { Skills } from './components/Skills.js';
+import { Inventory } from './components/Inventory.js';
+import { Combat } from './components/Combat.js';
+import { Stealth } from './components/Stealth.js';
+import { AI } from './components/AI.js';
+import { Absorbable } from './components/Absorbable.js';
+import { SkillProgress } from './components/SkillProgress.js';
+
+// Initialize game
+const canvas = document.getElementById('game-canvas');
+if (!canvas) {
+ console.error('Canvas element not found!');
+} else {
+ const engine = new Engine(canvas);
+
+ // Add systems in order
+ engine.addSystem(new MenuSystem(engine));
+ engine.addSystem(new InputSystem());
+ engine.addSystem(new PlayerControllerSystem());
+ engine.addSystem(new StealthSystem());
+ engine.addSystem(new AISystem());
+ engine.addSystem(new MovementSystem());
+ engine.addSystem(new CombatSystem());
+ engine.addSystem(new ProjectileSystem());
+ engine.addSystem(new AbsorptionSystem());
+ engine.addSystem(new SkillSystem());
+ engine.addSystem(new SkillEffectSystem());
+ engine.addSystem(new HealthRegenerationSystem());
+ engine.addSystem(new DeathSystem());
+ engine.addSystem(new RenderSystem(engine));
+ engine.addSystem(new UISystem(engine));
+
+ // Create player entity
+ const player = engine.createEntity();
+ player.addComponent(new Position(512, 384));
+ player.addComponent(new Velocity(0, 0, 200));
+ player.addComponent(new Sprite('#00ff96', 40, 40, 'slime'));
+ player.addComponent(new Health(100));
+ player.addComponent(new Stats());
+ player.addComponent(new Evolution());
+
+ // Give player a starting skill so they can test it
+ const playerSkills = new Skills();
+ playerSkills.addSkill('slime_gun', false); // Add slime_gun as starting skill (basic slime ability)
+ player.addComponent(playerSkills);
+
+ player.addComponent(new Inventory());
+ player.addComponent(new Combat());
+ player.addComponent(new Stealth());
+ player.addComponent(new SkillProgress()); // Track skill learning progress
+
+ // Create creatures
+ function createCreature(engine, x, y, type) {
+ const creature = engine.createEntity();
+ creature.addComponent(new Position(x, y));
+ creature.addComponent(new Velocity(0, 0, 100));
+
+ let color, evolutionData, skills;
+
+ switch (type) {
+ case 'humanoid':
+ color = '#ff5555'; // Humanoid red
+ evolutionData = { human: 10, beast: 0, slime: -2 };
+ skills = ['fire_breath'];
+ break;
+ case 'beast':
+ color = '#ffaa00'; // Beast orange
+ evolutionData = { human: 0, beast: 10, slime: -2 };
+ skills = ['pounce'];
+ break;
+ case 'elemental':
+ color = '#00bfff';
+ evolutionData = { human: 3, beast: 3, slime: 8 };
+ skills = ['fire_breath'];
+ break;
+ default:
+ color = '#888888';
+ evolutionData = { human: 2, beast: 2, slime: 2 };
+ skills = [];
+ }
+
+ creature.addComponent(new Sprite(color, 25, 25, 'circle'));
+ creature.addComponent(new Health(50 + Math.random() * 30));
+ creature.addComponent(new Stats());
+ creature.addComponent(new Combat());
+ creature.addComponent(new AI('wander'));
+
+ const absorbable = new Absorbable();
+ absorbable.setEvolutionData(evolutionData.human, evolutionData.beast, evolutionData.slime);
+ skills.forEach(skill => absorbable.addSkill(skill, 0.3));
+ creature.addComponent(absorbable);
+
+ return creature;
+ }
+
+ // Spawn initial creatures
+ for (let i = 0; i < 8; i++) {
+ const x = 100 + Math.random() * 824;
+ const y = 100 + Math.random() * 568;
+ const types = ['humanoid', 'beast', 'elemental'];
+ const type = types[Math.floor(Math.random() * types.length)];
+ createCreature(engine, x, y, type);
+ }
+
+ // Spawn new creatures periodically
+ setInterval(() => {
+ const existingCreatures = engine.getEntities().filter(e =>
+ e.hasComponent('AI') && e !== player
+ );
+
+ if (existingCreatures.length < 10) {
+ const x = 100 + Math.random() * 824;
+ const y = 100 + Math.random() * 568;
+ const types = ['humanoid', 'beast', 'elemental'];
+ const type = types[Math.floor(Math.random() * types.length)];
+ createCreature(engine, x, y, type);
+ }
+ }, 5000);
+
+ // Focus canvas for keyboard input
+ canvas.focus();
+
+ // Start engine but MenuSystem will control when gameplay begins
+ engine.start();
+
+ // Make engine globally available for debugging
+ window.gameEngine = engine;
+ window.player = player;
+
+ // Re-focus canvas on click
+ canvas.addEventListener('click', () => {
+ canvas.focus();
+ });
+}
diff --git a/src/skills/Skill.js b/src/skills/Skill.js
new file mode 100644
index 0000000..2ed22ee
--- /dev/null
+++ b/src/skills/Skill.js
@@ -0,0 +1,32 @@
+/**
+ * Base Skill class
+ */
+export class Skill {
+ constructor(id, name, cooldown = 2.0) {
+ this.id = id;
+ this.name = name;
+ this.cooldown = cooldown;
+ this.description = '';
+ }
+
+ /**
+ * Activate the skill
+ * @param {Entity} caster - Entity using the skill
+ * @param {Engine} engine - Game engine
+ * @returns {boolean} - Whether skill was successfully activated
+ */
+ activate(_caster, _engine) {
+ // Override in subclasses
+ return false;
+ }
+
+
+ /**
+ * Check if skill can be used
+ */
+ canUse(caster, _engine) {
+ const skills = caster.getComponent('Skills');
+ if (!skills) return false;
+ return !skills.isOnCooldown(this.id);
+ }
+}
diff --git a/src/skills/SkillRegistry.js b/src/skills/SkillRegistry.js
new file mode 100644
index 0000000..7dc7fe2
--- /dev/null
+++ b/src/skills/SkillRegistry.js
@@ -0,0 +1,36 @@
+import { SlimeGun } from './skills/WaterGun.js'; // File still named WaterGun.js but class is SlimeGun
+import { FireBreath } from './skills/FireBreath.js';
+import { Pounce } from './skills/Pounce.js';
+import { StealthMode } from './skills/StealthMode.js';
+
+/**
+ * Registry for all skills in the game
+ */
+export class SkillRegistry {
+ static skills = new Map();
+
+ static {
+ // Register all skills
+ this.register(new SlimeGun());
+ this.register(new FireBreath());
+ this.register(new Pounce());
+ this.register(new StealthMode());
+ }
+
+ static register(skill) {
+ this.skills.set(skill.id, skill);
+ }
+
+ static get(id) {
+ return this.skills.get(id);
+ }
+
+ static getAll() {
+ return Array.from(this.skills.values());
+ }
+
+ static has(id) {
+ return this.skills.has(id);
+ }
+}
+
diff --git a/src/skills/skills/FireBreath.js b/src/skills/skills/FireBreath.js
new file mode 100644
index 0000000..e9f4603
--- /dev/null
+++ b/src/skills/skills/FireBreath.js
@@ -0,0 +1,83 @@
+import { Skill } from '../Skill.js';
+
+export class FireBreath extends Skill {
+ constructor() {
+ super('fire_breath', 'Fire Breath', 3.0);
+ this.description = 'Breathe fire in a cone';
+ this.damage = 25;
+ this.range = 150;
+ this.coneAngle = Math.PI / 3; // 60 degrees
+ this.duration = 0.5; // Animation duration
+ }
+
+ activate(caster, engine) {
+ if (!this.canUse(caster, engine)) return false;
+
+ const position = caster.getComponent('Position');
+ const stats = caster.getComponent('Stats');
+ const skills = caster.getComponent('Skills');
+ const inputSystem = engine.systems.find(s => s.name === 'InputSystem');
+
+ if (!position || !skills) return false;
+
+ // Calculate direction from player to mouse
+ let fireAngle = position.rotation;
+ if (inputSystem) {
+ const mouse = inputSystem.getMousePosition();
+ const dx = mouse.x - position.x;
+ const dy = mouse.y - position.y;
+ if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) {
+ fireAngle = Math.atan2(dy, dx);
+ position.rotation = fireAngle;
+ }
+ }
+
+ skills.setCooldown(this.id, this.cooldown);
+
+ // Add visual effect
+ const skillEffectSystem = engine.systems.find(s => s.name === 'SkillEffectSystem');
+ if (skillEffectSystem) {
+ skillEffectSystem.addEffect({
+ type: 'fire_breath',
+ x: position.x,
+ y: position.y,
+ angle: fireAngle,
+ range: this.range,
+ coneAngle: this.coneAngle,
+ lifetime: this.duration,
+ time: 0
+ });
+ }
+
+ // Damage all enemies in cone
+ const entities = engine.getEntities();
+ const damage = this.damage + (stats ? stats.intelligence * 0.5 : 0);
+
+ entities.forEach(entity => {
+ if (entity.id === caster.id) return;
+ if (!entity.hasComponent('Health')) return;
+
+ const targetPos = entity.getComponent('Position');
+ if (!targetPos) return;
+
+ const dx = targetPos.x - position.x;
+ const dy = targetPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= this.range) {
+ const angle = Math.atan2(dy, dx);
+ const angleDiff = Math.abs(angle - fireAngle);
+ const normalizedDiff = Math.abs(((angleDiff % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2));
+ const minDiff = Math.min(normalizedDiff, Math.PI * 2 - normalizedDiff);
+
+ if (minDiff < this.coneAngle / 2) {
+ const health = entity.getComponent('Health');
+ health.takeDamage(damage);
+ }
+ }
+ });
+
+ return true;
+ }
+}
+
diff --git a/src/skills/skills/Pounce.js b/src/skills/skills/Pounce.js
new file mode 100644
index 0000000..a82d7d7
--- /dev/null
+++ b/src/skills/skills/Pounce.js
@@ -0,0 +1,97 @@
+import { Skill } from '../Skill.js';
+
+export class Pounce extends Skill {
+ constructor() {
+ super('pounce', 'Pounce', 2.0);
+ this.description = 'Leap forward and damage enemies';
+ this.damage = 20;
+ this.range = 100;
+ this.dashSpeed = 400;
+ this.dashDuration = 0.2; // How long the dash lasts
+ }
+
+ activate(caster, engine) {
+ if (!this.canUse(caster, engine)) return false;
+
+ const position = caster.getComponent('Position');
+ const velocity = caster.getComponent('Velocity');
+ const stats = caster.getComponent('Stats');
+ const skills = caster.getComponent('Skills');
+ const inputSystem = engine.systems.find(s => s.name === 'InputSystem');
+
+ if (!position || !velocity || !skills) return false;
+
+ // Calculate direction from player to mouse
+ let dashAngle = position.rotation;
+ if (inputSystem) {
+ const mouse = inputSystem.getMousePosition();
+ const dx = mouse.x - position.x;
+ const dy = mouse.y - position.y;
+ if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) {
+ dashAngle = Math.atan2(dy, dx);
+ position.rotation = dashAngle;
+ }
+ }
+
+ skills.setCooldown(this.id, this.cooldown);
+
+ // Store start position for effect
+ const startX = position.x;
+ const startY = position.y;
+
+ // Dash forward
+ velocity.vx = Math.cos(dashAngle) * this.dashSpeed;
+ velocity.vy = Math.sin(dashAngle) * this.dashSpeed;
+
+ // Add visual effect
+ const skillEffectSystem = engine.systems.find(s => s.name === 'SkillEffectSystem');
+ if (skillEffectSystem) {
+ skillEffectSystem.addEffect({
+ type: 'pounce',
+ startX: startX,
+ startY: startY,
+ angle: dashAngle,
+ speed: this.dashSpeed,
+ lifetime: this.dashDuration,
+ time: 0
+ });
+ }
+
+ // Damage enemies at destination after dash
+ setTimeout(() => {
+ const entities = engine.getEntities();
+ const damage = this.damage + (stats ? stats.strength * 0.4 : 0);
+
+ entities.forEach(entity => {
+ if (entity.id === caster.id) return;
+ if (!entity.hasComponent('Health')) return;
+
+ const targetPos = entity.getComponent('Position');
+ if (!targetPos) return;
+
+ const dx = targetPos.x - position.x;
+ const dy = targetPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= this.range) {
+ const health = entity.getComponent('Health');
+ health.takeDamage(damage);
+
+ // Add impact effect
+ if (skillEffectSystem) {
+ skillEffectSystem.addEffect({
+ type: 'pounce_impact',
+ x: position.x,
+ y: position.y,
+ lifetime: 0.3,
+ time: 0
+ });
+ }
+ }
+ });
+ }, this.dashDuration * 1000);
+
+ return true;
+ }
+}
+
diff --git a/src/skills/skills/StealthMode.js b/src/skills/skills/StealthMode.js
new file mode 100644
index 0000000..368104b
--- /dev/null
+++ b/src/skills/skills/StealthMode.js
@@ -0,0 +1,35 @@
+import { Skill } from '../Skill.js';
+
+export class StealthMode extends Skill {
+ constructor() {
+ super('stealth_mode', 'Stealth Mode', 5.0);
+ this.description = 'Enter stealth mode';
+ this.duration = 10.0;
+ }
+
+ activate(caster, engine) {
+ if (!this.canUse(caster, engine)) return false;
+
+ const stealth = caster.getComponent('Stealth');
+ const skills = caster.getComponent('Skills');
+ const evolution = caster.getComponent('Evolution');
+
+ if (!stealth || !skills) return false;
+
+ skills.setCooldown(this.id, this.cooldown);
+
+ // Determine stealth type from evolution
+ const form = evolution ? evolution.getDominantForm() : 'slime';
+ stealth.enterStealth(form);
+
+ // Auto-exit after duration
+ setTimeout(() => {
+ if (stealth.isStealthed) {
+ stealth.exitStealth();
+ }
+ }, this.duration * 1000);
+
+ return true;
+ }
+}
+
diff --git a/src/skills/skills/WaterGun.js b/src/skills/skills/WaterGun.js
new file mode 100644
index 0000000..e5341e3
--- /dev/null
+++ b/src/skills/skills/WaterGun.js
@@ -0,0 +1,84 @@
+import { Skill } from '../Skill.js';
+import { Position } from '../../components/Position.js';
+import { Velocity } from '../../components/Velocity.js';
+import { Sprite } from '../../components/Sprite.js';
+import { Health } from '../../components/Health.js';
+
+export class SlimeGun extends Skill {
+ constructor() {
+ super('slime_gun', 'Slime Gun', 1.0);
+ this.description = 'Shoot a blob of slime at enemies (costs 1 HP)';
+ this.damage = 15;
+ this.range = 800; // Long range for a gun
+ this.speed = 600; // Faster projectile
+ this.hpCost = 1;
+ }
+
+ activate(caster, engine) {
+ if (!this.canUse(caster, engine)) return false;
+
+ const position = caster.getComponent('Position');
+ const health = caster.getComponent('Health');
+ const stats = caster.getComponent('Stats');
+ const skills = caster.getComponent('Skills');
+ const inputSystem = engine.systems.find(s => s.name === 'InputSystem');
+
+ if (!position || !skills || !health) return false;
+
+ // Check if we have enough HP
+ if (health.hp <= this.hpCost) {
+ return false; // Can't use if it would kill us
+ }
+
+ // Calculate direction from player to mouse
+ let shootAngle = position.rotation;
+ if (inputSystem) {
+ const mouse = inputSystem.getMousePosition();
+ const dx = mouse.x - position.x;
+ const dy = mouse.y - position.y;
+ if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) {
+ shootAngle = Math.atan2(dy, dx);
+ }
+ }
+
+ // Cost HP (sacrificing slime)
+ health.takeDamage(this.hpCost);
+
+ // Set cooldown
+ skills.setCooldown(this.id, this.cooldown);
+
+ // Create projectile (slime blob)
+ const projectile = engine.createEntity();
+ const startX = position.x;
+ const startY = position.y;
+ projectile.addComponent(new Position(startX, startY));
+
+ // Create velocity with high maxSpeed for projectiles
+ const projectileVelocity = new Velocity(
+ Math.cos(shootAngle) * this.speed,
+ Math.sin(shootAngle) * this.speed
+ );
+ projectileVelocity.maxSpeed = this.speed * 2; // Allow projectiles to move fast
+ projectile.addComponent(projectileVelocity);
+ // Slime-colored projectile
+ projectile.addComponent(new Sprite('#00ff96', 10, 10, 'slime'));
+
+ // Projectile has temporary health for collision detection
+ const projectileHealth = new Health(1);
+ projectileHealth.isProjectile = true;
+ projectile.addComponent(projectileHealth);
+
+ // Store projectile data
+ projectile.damage = this.damage + (stats ? stats.intelligence * 0.3 : 0);
+ projectile.owner = caster.id;
+ projectile.startX = startX;
+ projectile.startY = startY;
+ projectile.maxRange = this.range;
+ projectile.speed = this.speed;
+ // Lifetime as backup (should be longer than range travel time)
+ projectile.lifetime = (this.range / this.speed) + 1.0;
+
+ return true;
+ }
+}
+
diff --git a/src/systems/AISystem.js b/src/systems/AISystem.js
new file mode 100644
index 0000000..c156861
--- /dev/null
+++ b/src/systems/AISystem.js
@@ -0,0 +1,205 @@
+import { System } from '../core/System.js';
+import { GameConfig } from '../GameConfig.js';
+
+export class AISystem extends System {
+ constructor() {
+ super('AISystem');
+ this.requiredComponents = ['Position', 'Velocity', 'AI'];
+ this.priority = 15;
+ }
+
+ process(deltaTime, entities) {
+ const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
+ const player = playerController ? playerController.getPlayerEntity() : null;
+ const playerPos = player?.getComponent('Position');
+ const config = GameConfig.AI;
+
+ entities.forEach(entity => {
+ const ai = entity.getComponent('AI');
+ const position = entity.getComponent('Position');
+ const velocity = entity.getComponent('Velocity');
+ const _stealth = entity.getComponent('Stealth');
+
+ if (!ai || !position || !velocity) return;
+
+ // Update wander timer
+ ai.wanderChangeTime += deltaTime;
+
+ // Detect player
+ if (playerPos) {
+ const dx = playerPos.x - position.x;
+ const dy = playerPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ // Update awareness based on distance and player stealth
+ const playerStealth = player?.getComponent('Stealth');
+ const playerVisibility = playerStealth ? playerStealth.visibility : 1.0;
+
+ if (distance < ai.alertRadius) {
+ const detectionChance = (1 - distance / ai.alertRadius) * playerVisibility;
+ ai.updateAwareness(detectionChance * deltaTime * config.awarenessGainMultiplier);
+ } else {
+ ai.updateAwareness(-deltaTime * config.awarenessLossRate); // Lose awareness over time
+ }
+
+ // Biological Reputation Logic
+ const playerEvolution = player?.getComponent('Evolution');
+ const playerForm = playerEvolution ? playerEvolution.getDominantForm() : 'slime';
+ const entityType = entity.getComponent('Sprite')?.color === '#ffaa00' ? 'beast' :
+ entity.getComponent('Sprite')?.color === '#ff5555' ? 'humanoid' : 'other';
+
+ // Check if player is "one of us" or "too scary"
+ let isPassive = false;
+ let shouldFlee = false;
+
+ if (entityType === 'humanoid' && playerForm === 'human') {
+ // Humanoids are passive to human-form slime unless awareness is maxed (hostile action taken)
+ if (ai.awareness < config.passiveAwarenessThreshold) isPassive = true;
+ } else if (entityType === 'beast' && playerForm === 'beast') {
+ // Beasts might flee from a dominant beast player
+ const playerStats = player?.getComponent('Stats');
+ const entityStats = entity.getComponent('Stats');
+ if (playerStats && entityStats && playerStats.level > entityStats.level) {
+ shouldFlee = true;
+ }
+ }
+
+ // Behavior based on awareness, reputation, and distance
+ if (shouldFlee && ai.awareness > config.fleeAwarenessThreshold) {
+ ai.setBehavior('flee');
+ ai.state = 'fleeing';
+ ai.setTarget(player.id);
+ } else if (isPassive) {
+ if (ai.behaviorType === 'chase' || ai.behaviorType === 'combat') {
+ ai.setBehavior('wander');
+ ai.state = 'idle';
+ ai.clearTarget();
+ }
+ } else if (ai.awareness > config.detectionAwarenessThreshold && distance < ai.chaseRadius) {
+ if (ai.behaviorType !== 'flee') {
+ // Check if in attack range - if so, use combat behavior
+ const combat = entity.getComponent('Combat');
+ if (combat && distance <= combat.attackRange) {
+ ai.setBehavior('combat');
+ ai.state = 'combat';
+ } else {
+ ai.setBehavior('chase');
+ ai.state = 'chasing';
+ }
+ ai.setTarget(player.id);
+ }
+ } else if (ai.awareness < 0.3) {
+ if (ai.behaviorType === 'chase' || ai.behaviorType === 'combat') {
+ ai.setBehavior('wander');
+ ai.state = 'idle';
+ ai.clearTarget();
+ }
+ } else if (ai.behaviorType === 'chase') {
+ // Update from chase to combat if in range
+ const combat = entity.getComponent('Combat');
+ if (combat && distance <= combat.attackRange) {
+ ai.setBehavior('combat');
+ ai.state = 'combat';
+ }
+ }
+ }
+
+ // Execute behavior
+ switch (ai.behaviorType) {
+ case 'wander':
+ this.wander(entity, ai, velocity, deltaTime);
+ break;
+ case 'chase':
+ this.chase(entity, ai, velocity, position, playerPos);
+ break;
+ case 'flee':
+ this.flee(entity, ai, velocity, position, playerPos);
+ break;
+ case 'combat':
+ this.combat(entity, ai, velocity, position, playerPos);
+ break;
+ }
+ });
+ }
+
+ wander(entity, ai, velocity, _deltaTime) {
+ ai.state = 'moving';
+
+ // Change direction periodically
+ if (ai.wanderChangeTime >= ai.wanderChangeInterval) {
+ ai.wanderDirection = Math.random() * Math.PI * 2;
+ ai.wanderChangeTime = 0;
+ ai.wanderChangeInterval = 1 + Math.random() * 2;
+ }
+
+ velocity.vx = Math.cos(ai.wanderDirection) * ai.wanderSpeed;
+ velocity.vy = Math.sin(ai.wanderDirection) * ai.wanderSpeed;
+ }
+
+ chase(entity, ai, velocity, position, targetPos) {
+ if (!targetPos) return;
+
+ const dx = targetPos.x - position.x;
+ const dy = targetPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ // Check if we should switch to combat
+ const combat = entity.getComponent('Combat');
+ if (combat && distance <= combat.attackRange) {
+ ai.setBehavior('combat');
+ ai.state = 'combat';
+ return;
+ }
+
+ ai.state = 'chasing';
+ if (distance > 0.1) {
+ const speed = ai.wanderSpeed * 1.5;
+ velocity.vx = (dx / distance) * speed;
+ velocity.vy = (dy / distance) * speed;
+ } else {
+ velocity.vx = 0;
+ velocity.vy = 0;
+ }
+ }
+
+ flee(entity, ai, velocity, position, targetPos) {
+ if (!targetPos) return;
+
+ ai.state = 'fleeing';
+ const dx = position.x - targetPos.x;
+ const dy = position.y - targetPos.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance > 0.1) {
+ const speed = ai.wanderSpeed * 1.2;
+ velocity.vx = (dx / distance) * speed;
+ velocity.vy = (dy / distance) * speed;
+ }
+ }
+
+ combat(entity, ai, velocity, position, targetPos) {
+ if (!targetPos) return;
+
+ ai.state = 'attacking';
+ // Stop moving when in combat range - let CombatSystem handle attacks
+ const dx = targetPos.x - position.x;
+ const dy = targetPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ const combat = entity.getComponent('Combat');
+ if (combat && distance > combat.attackRange) {
+ // Move closer if out of range
+ const speed = ai.wanderSpeed;
+ velocity.vx = (dx / distance) * speed;
+ velocity.vy = (dy / distance) * speed;
+ } else {
+ // Stop and face target
+ velocity.vx *= 0.5;
+ velocity.vy *= 0.5;
+ if (position) {
+ position.rotation = Math.atan2(dy, dx);
+ }
+ }
+ }
+}
+
diff --git a/src/systems/AbsorptionSystem.js b/src/systems/AbsorptionSystem.js
new file mode 100644
index 0000000..0bbcd97
--- /dev/null
+++ b/src/systems/AbsorptionSystem.js
@@ -0,0 +1,176 @@
+import { System } from '../core/System.js';
+import { GameConfig } from '../GameConfig.js';
+import { Events } from '../core/EventBus.js';
+
+export class AbsorptionSystem extends System {
+ constructor() {
+ super('AbsorptionSystem');
+ this.requiredComponents = ['Position', 'Absorbable'];
+ this.priority = 25;
+ this.absorptionEffects = []; // Visual effects
+ }
+
+ process(deltaTime, _entities) {
+ const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
+ const player = playerController ? playerController.getPlayerEntity() : null;
+
+ if (!player) return;
+
+ const playerPos = player.getComponent('Position');
+ const playerEvolution = player.getComponent('Evolution');
+ const playerSkills = player.getComponent('Skills');
+ const playerStats = player.getComponent('Stats');
+ const skillProgress = player.getComponent('SkillProgress');
+
+ if (!playerPos || !playerEvolution) return;
+
+ // Get ALL entities (including inactive ones) for absorption check
+ const allEntities = this.engine.entities; // Get raw entities array, not filtered
+
+ const config = GameConfig.Absorption;
+
+ // Check for absorbable entities near player
+ allEntities.forEach(entity => {
+ if (entity === player) return;
+ // Allow inactive entities if they're dead and absorbable
+ if (!entity.active) {
+ const health = entity.getComponent('Health');
+ const absorbable = entity.getComponent('Absorbable');
+ // Only process inactive entities if they're dead and not yet absorbed
+ if (!health || !health.isDead() || !absorbable || absorbable.absorbed) {
+ return;
+ }
+ }
+ if (!entity.hasComponent('Absorbable')) return;
+ if (!entity.hasComponent('Health')) return;
+
+ const absorbable = entity.getComponent('Absorbable');
+ const health = entity.getComponent('Health');
+ const entityPos = entity.getComponent('Position');
+
+ if (!entityPos) return;
+
+ // Check if creature is dead and in absorption range
+ if (health.isDead() && !absorbable.absorbed) {
+ const dx = playerPos.x - entityPos.x;
+ const dy = playerPos.y - entityPos.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= config.range) {
+ this.absorbEntity(player, entity, absorbable, playerEvolution, playerSkills, playerStats, skillProgress);
+ }
+ }
+ });
+
+ // Update visual effects
+ this.updateEffects(deltaTime);
+ }
+
+ absorbEntity(player, entity, absorbable, evolution, skills, stats, skillProgress) {
+ if (absorbable.absorbed) return;
+
+ absorbable.absorbed = true;
+ const entityPos = entity.getComponent('Position');
+ const health = player.getComponent('Health');
+ const config = GameConfig.Absorption;
+
+ // Add evolution points
+ evolution.addEvolution(
+ absorbable.evolutionData.human,
+ absorbable.evolutionData.beast,
+ absorbable.evolutionData.slime
+ );
+
+ // Track skill progress (need to absorb multiple times to learn)
+ // Always track progress for ALL skills the enemy has, regardless of roll
+ if (skillProgress && absorbable.skillsGranted && absorbable.skillsGranted.length > 0) {
+ absorbable.skillsGranted.forEach(skill => {
+ // Always add progress when absorbing an enemy with this skill
+ const currentProgress = skillProgress.addSkillProgress(skill.id);
+ const required = skillProgress.requiredAbsorptions;
+
+ // If we've absorbed enough, learn the skill
+ if (currentProgress >= required && !skills.hasSkill(skill.id)) {
+ skills.addSkill(skill.id, false);
+ this.engine.emit(Events.SKILL_LEARNED, { id: skill.id });
+ console.log(`Learned skill: ${skill.id}!`);
+ }
+ });
+ }
+
+ // Heal from absorption (slime recovers by consuming)
+ if (health) {
+ const healPercent = config.healPercentMin + Math.random() * (config.healPercentMax - config.healPercentMin);
+ const healAmount = health.maxHp * healPercent;
+ health.heal(healAmount);
+ }
+
+ // Check for mutation
+ if (absorbable.shouldMutate() && stats) {
+ this.applyMutation(stats);
+ evolution.checkMutations(stats, this.engine);
+ }
+
+ // Visual effect
+ if (entityPos) {
+ this.addAbsorptionEffect(entityPos.x, entityPos.y);
+ }
+
+ // Mark as absorbed - DeathSystem will handle removal after absorption window
+ // Don't remove immediately, let DeathSystem handle it
+ }
+
+ applyMutation(stats) {
+ // Random stat mutation
+ const mutations = [
+ { stat: 'strength', amount: 5 },
+ { stat: 'agility', amount: 5 },
+ { stat: 'intelligence', amount: 5 },
+ { stat: 'constitution', amount: 5 },
+ { stat: 'perception', amount: 5 },
+ ];
+
+ const mutation = mutations[Math.floor(Math.random() * mutations.length)];
+ stats[mutation.stat] += mutation.amount;
+
+ // Could also add negative mutations
+ if (Math.random() < 0.3) {
+ const negativeStat = mutations[Math.floor(Math.random() * mutations.length)];
+ stats[negativeStat.stat] = Math.max(1, stats[negativeStat.stat] - 2);
+ }
+ }
+
+ addAbsorptionEffect(x, y) {
+ for (let i = 0; i < 20; i++) {
+ this.absorptionEffects.push({
+ x,
+ y,
+ vx: (Math.random() - 0.5) * 200,
+ vy: (Math.random() - 0.5) * 200,
+ lifetime: 0.5 + Math.random() * 0.5,
+ size: 3 + Math.random() * 5,
+ color: `hsl(${120 + Math.random() * 60}, 100%, 50%)`
+ });
+ }
+ }
+
+ updateEffects(deltaTime) {
+ for (let i = this.absorptionEffects.length - 1; i >= 0; i--) {
+ const effect = this.absorptionEffects[i];
+ effect.x += effect.vx * deltaTime;
+ effect.y += effect.vy * deltaTime;
+ effect.lifetime -= deltaTime;
+ effect.vx *= 0.95;
+ effect.vy *= 0.95;
+
+ if (effect.lifetime <= 0) {
+ this.absorptionEffects.splice(i, 1);
+ }
+ }
+ }
+
+ getEffects() {
+ return this.absorptionEffects;
+ }
+}
+
diff --git a/src/systems/CombatSystem.js b/src/systems/CombatSystem.js
new file mode 100644
index 0000000..2334a89
--- /dev/null
+++ b/src/systems/CombatSystem.js
@@ -0,0 +1,190 @@
+import { System } from '../core/System.js';
+import { GameConfig } from '../GameConfig.js';
+import { Events } from '../core/EventBus.js';
+
+export class CombatSystem extends System {
+ constructor() {
+ super('CombatSystem');
+ this.requiredComponents = ['Position', 'Combat', 'Health'];
+ this.priority = 20;
+ }
+
+ process(deltaTime, entities) {
+ // Update combat cooldowns
+ entities.forEach(entity => {
+ const combat = entity.getComponent('Combat');
+ if (combat) {
+ combat.update(deltaTime);
+ }
+ });
+
+ // Handle player attacks
+ const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
+ const player = playerController ? playerController.getPlayerEntity() : null;
+
+ if (player && player.hasComponent('Combat')) {
+ this.handlePlayerCombat(player, deltaTime);
+ }
+
+ // Handle creature attacks
+ const creatures = entities.filter(e =>
+ e.hasComponent('AI') &&
+ e.hasComponent('Combat') &&
+ e !== player
+ );
+
+ creatures.forEach(creature => {
+ this.handleCreatureCombat(creature, player, deltaTime);
+ });
+
+ // Check for collisions and apply damage
+ this.processCombatCollisions(entities, deltaTime);
+ }
+
+ handlePlayerCombat(player, _deltaTime) {
+ const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem');
+ const combat = player.getComponent('Combat');
+ const position = player.getComponent('Position');
+
+ if (!inputSystem || !combat || !position) return;
+
+ const currentTime = Date.now() / 1000;
+
+ // Attack on mouse click or space (use justPressed to prevent spam)
+ const mouseClick = inputSystem.isMouseButtonJustPressed(0);
+ const spacePress = inputSystem.isKeyJustPressed(' ') || inputSystem.isKeyJustPressed('space');
+
+ if ((mouseClick || spacePress) && combat.canAttack(currentTime)) {
+ // Calculate attack direction from player to mouse
+ const mouse = inputSystem.getMousePosition();
+ const dx = mouse.x - position.x;
+ const dy = mouse.y - position.y;
+ const attackAngle = Math.atan2(dy, dx);
+
+ // Update player rotation to face attack direction
+ position.rotation = attackAngle;
+
+ combat.attack(currentTime, attackAngle);
+
+ // Check for nearby enemies to damage
+ this.performAttack(player, combat, position);
+ }
+ }
+
+ handleCreatureCombat(creature, player, _deltaTime) {
+ const ai = creature.getComponent('AI');
+ const combat = creature.getComponent('Combat');
+ const position = creature.getComponent('Position');
+ const playerPos = player?.getComponent('Position');
+
+ if (!ai || !combat || !position) return;
+
+ // Attack player if in range and aware (check both combat state and chase behavior)
+ if (playerPos && ai.awareness > 0.5 && (ai.state === 'combat' || ai.behaviorType === 'combat' || (ai.behaviorType === 'chase' && ai.awareness > 0.7))) {
+ const dx = playerPos.x - position.x;
+ const dy = playerPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= combat.attackRange) {
+ const currentTime = Date.now() / 1000;
+ if (combat.canAttack(currentTime)) {
+ const angle = Math.atan2(dy, dx);
+ combat.attack(currentTime, angle);
+ this.performAttack(creature, combat, position);
+ }
+ }
+ }
+ }
+
+ performAttack(attacker, combat, attackerPos) {
+ const entities = this.engine.getEntities();
+ const stats = attacker.getComponent('Stats');
+ const _baseDamage = stats ?
+ (combat.attackDamage + stats.strength * 0.5) :
+ combat.attackDamage;
+
+ entities.forEach(target => {
+ if (target === attacker) return;
+ if (!target.hasComponent('Health')) return;
+
+ const targetPos = target.getComponent('Position');
+ if (!targetPos) return;
+
+ // Check if in attack range and angle
+ const dx = targetPos.x - attackerPos.x;
+ const dy = targetPos.y - attackerPos.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance <= combat.attackRange) {
+ const angle = Math.atan2(dy, dx);
+ const angleDiff = Math.abs(angle - combat.attackDirection);
+ const normalizedDiff = Math.abs(((angleDiff % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2));
+ const minDiff = Math.min(normalizedDiff, Math.PI * 2 - normalizedDiff);
+
+ // Attack arc
+ const attackArc = GameConfig.Combat.defaultAttackArc;
+ if (minDiff < attackArc) {
+ const health = target.getComponent('Health');
+ const config = GameConfig.Combat;
+ const stats = attacker.getComponent('Stats');
+ const baseDamage = stats ? (combat.attackDamage + stats.strength * 0.5) : combat.attackDamage;
+
+ // Defense bonus from Hardened Shell
+ let finalDamage = baseDamage;
+ const targetEvolution = target.getComponent('Evolution');
+
+ if (targetEvolution && targetEvolution.mutationEffects.hardenedShell) {
+ finalDamage *= config.hardenedShellReduction;
+ }
+
+ const actualDamage = health.takeDamage(finalDamage);
+
+ // Emit event for UI/VFX
+ this.engine.emit(Events.DAMAGE_DEALT, {
+ x: targetPos.x,
+ y: targetPos.y,
+ value: actualDamage,
+ color: '#ffffff'
+ });
+
+ // Damage reflection from Electric Skin
+ if (targetEvolution && targetEvolution.mutationEffects.electricSkin) {
+ const attackerHealth = attacker.getComponent('Health');
+ if (attackerHealth) {
+ const reflectedDamage = actualDamage * config.damageReflectionPercent;
+ attackerHealth.takeDamage(reflectedDamage);
+
+ this.engine.emit(Events.DAMAGE_DEALT, {
+ x: attackerPos.x,
+ y: attackerPos.y,
+ value: reflectedDamage,
+ color: '#00ffff'
+ });
+ }
+ }
+
+ // If target is dead, emit event
+ if (health.isDead()) {
+ this.engine.emit(Events.ENTITY_DIED, { entity: target });
+ target.active = false;
+ }
+
+ // Apply knockback
+ const velocity = target.getComponent('Velocity');
+ if (velocity) {
+ const knockbackPower = config.knockbackPower;
+ const kx = Math.cos(angle) * knockbackPower;
+ const ky = Math.sin(angle) * knockbackPower;
+ velocity.vx += kx;
+ velocity.vy += ky;
+ }
+ }
+ }
+ });
+ }
+
+ processCombatCollisions(_entities, _deltaTime) {
+ // This can be expanded for projectile collisions, area effects, etc.
+ }
+}
+
diff --git a/src/systems/DeathSystem.js b/src/systems/DeathSystem.js
new file mode 100644
index 0000000..b5908b7
--- /dev/null
+++ b/src/systems/DeathSystem.js
@@ -0,0 +1,56 @@
+import { System } from '../core/System.js';
+
+/**
+ * System to handle entity death - removes dead entities immediately
+ */
+export class DeathSystem extends System {
+ constructor() {
+ super('DeathSystem');
+ this.requiredComponents = ['Health'];
+ this.priority = 50; // Run after absorption (absorption is priority 25)
+ }
+
+ update(deltaTime, _entities) {
+ // Override to check ALL entities, not just active ones
+ // Get all entities including inactive ones to check dead entities
+ const allEntities = this.engine.entities;
+ this.process(deltaTime, allEntities);
+ }
+
+ process(deltaTime, allEntities) {
+ allEntities.forEach(entity => {
+ const health = entity.getComponent('Health');
+ if (!health) return;
+
+ // Check if entity is dead
+ if (health.isDead()) {
+ // Don't remove player
+ const evolution = entity.getComponent('Evolution');
+ if (evolution) return; // Player has Evolution component
+
+ // Mark as inactive immediately so it stops being processed by other systems
+ if (entity.active) {
+ entity.active = false;
+ entity.deathTime = Date.now(); // Set death time when first marked dead
+ }
+
+ // Check if it's absorbable - if so, give a short window for absorption
+ const absorbable = entity.getComponent('Absorbable');
+ if (absorbable && !absorbable.absorbed) {
+ // Give 3 seconds for player to absorb, then remove
+ const timeSinceDeath = (Date.now() - entity.deathTime) / 1000;
+ if (timeSinceDeath > 3.0) {
+ this.engine.removeEntity(entity);
+ }
+ } else {
+ // Not absorbable or already absorbed - remove after short delay
+ const timeSinceDeath = (Date.now() - entity.deathTime) / 1000;
+ if (timeSinceDeath > 0.5) {
+ this.engine.removeEntity(entity);
+ }
+ }
+ }
+ });
+ }
+}
+
diff --git a/src/systems/HealthRegenerationSystem.js b/src/systems/HealthRegenerationSystem.js
new file mode 100644
index 0000000..47468be
--- /dev/null
+++ b/src/systems/HealthRegenerationSystem.js
@@ -0,0 +1,27 @@
+import { System } from '../core/System.js';
+
+/**
+ * System to handle health regeneration
+ */
+export class HealthRegenerationSystem extends System {
+ constructor() {
+ super('HealthRegenerationSystem');
+ this.requiredComponents = ['Health'];
+ this.priority = 35;
+ }
+
+ process(deltaTime, entities) {
+ entities.forEach(entity => {
+ const health = entity.getComponent('Health');
+ if (!health || health.regeneration <= 0) return;
+
+ // Regenerate health over time
+ // Only regenerate if not recently damaged (5 seconds)
+ const timeSinceDamage = (Date.now() - health.lastDamageTime) / 1000;
+ if (timeSinceDamage > 5) {
+ health.heal(health.regeneration * deltaTime);
+ }
+ });
+ }
+}
+
diff --git a/src/systems/InputSystem.js b/src/systems/InputSystem.js
new file mode 100644
index 0000000..9a45fc6
--- /dev/null
+++ b/src/systems/InputSystem.js
@@ -0,0 +1,153 @@
+import { System } from '../core/System.js';
+
+export class InputSystem extends System {
+ constructor() {
+ super('InputSystem');
+ this.requiredComponents = []; // No required components - handles input globally
+ this.priority = 0; // Run first
+
+ this.keys = {};
+ this.keysPrevious = {}; // Track previous frame key states
+ this.mouse = {
+ x: 0,
+ y: 0,
+ buttons: {},
+ buttonsPrevious: {}
+ };
+ }
+
+ init(engine) {
+ super.init(engine);
+ this.setupEventListeners();
+ }
+
+ setupEventListeners() {
+ window.addEventListener('keydown', (e) => {
+ const key = e.key.toLowerCase();
+ const code = e.code.toLowerCase();
+
+ // Store by key name
+ this.keys[key] = true;
+ this.keys[code] = true;
+
+ // Handle special keys
+ if (key === ' ') {
+ this.keys['space'] = true;
+ }
+ if (code === 'space') {
+ this.keys['space'] = true;
+ }
+
+ // Arrow keys
+ if (code === 'arrowup') this.keys['arrowup'] = true;
+ if (code === 'arrowdown') this.keys['arrowdown'] = true;
+ if (code === 'arrowleft') this.keys['arrowleft'] = true;
+ if (code === 'arrowright') this.keys['arrowright'] = true;
+
+ // Prevent default for game keys
+ if ([' ', 'w', 'a', 's', 'd', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(key)) {
+ e.preventDefault();
+ }
+ });
+
+ window.addEventListener('keyup', (e) => {
+ const key = e.key.toLowerCase();
+ const code = e.code.toLowerCase();
+
+ this.keys[key] = false;
+ this.keys[code] = false;
+
+ // Handle special keys
+ if (key === ' ') {
+ this.keys['space'] = false;
+ }
+ if (code === 'space') {
+ this.keys['space'] = false;
+ }
+
+ // Arrow keys
+ if (code === 'arrowup') this.keys['arrowup'] = false;
+ if (code === 'arrowdown') this.keys['arrowdown'] = false;
+ if (code === 'arrowleft') this.keys['arrowleft'] = false;
+ if (code === 'arrowright') this.keys['arrowright'] = false;
+ });
+
+ window.addEventListener('mousemove', (e) => {
+ if (this.engine && this.engine.canvas) {
+ const canvas = this.engine.canvas;
+ const rect = canvas.getBoundingClientRect();
+ this.mouse.x = e.clientX - rect.left;
+ this.mouse.y = e.clientY - rect.top;
+ }
+ });
+
+ window.addEventListener('mousedown', (e) => {
+ this.mouse.buttons[e.button] = true;
+ });
+
+ window.addEventListener('mouseup', (e) => {
+ this.mouse.buttons[e.button] = false;
+ });
+ }
+
+ process(_deltaTime, _entities) {
+ // Don't update previous states here - that happens at end of frame
+ // This allows other systems to check isKeyJustPressed during the frame
+ }
+
+ /**
+ * Update previous states - called at end of frame
+ */
+ updatePreviousStates() {
+ // Deep copy current states to previous for next frame
+ this.keysPrevious = {};
+ for (const key in this.keys) {
+ this.keysPrevious[key] = this.keys[key];
+ }
+ this.mouse.buttonsPrevious = {};
+ for (const button in this.mouse.buttons) {
+ this.mouse.buttonsPrevious[button] = this.mouse.buttons[button];
+ }
+ }
+
+ /**
+ * Check if a key is currently pressed
+ */
+ isKeyPressed(key) {
+ return this.keys[key.toLowerCase()] === true;
+ }
+
+ /**
+ * Check if a key was just pressed (not held from previous frame)
+ */
+ isKeyJustPressed(key) {
+ const keyLower = key.toLowerCase();
+ const isPressed = this.keys[keyLower] === true;
+ const wasPressed = this.keysPrevious[keyLower] === true;
+ return isPressed && !wasPressed;
+ }
+
+ /**
+ * Get mouse position
+ */
+ getMousePosition() {
+ return { x: this.mouse.x, y: this.mouse.y };
+ }
+
+ /**
+ * Check if mouse button is pressed
+ */
+ isMouseButtonPressed(button = 0) {
+ return this.mouse.buttons[button] === true;
+ }
+
+ /**
+ * Check if mouse button was just pressed
+ */
+ isMouseButtonJustPressed(button = 0) {
+ const isPressed = this.mouse.buttons[button] === true;
+ const wasPressed = this.mouse.buttonsPrevious[button] === true;
+ return isPressed && !wasPressed;
+ }
+}
+
diff --git a/src/systems/MenuSystem.js b/src/systems/MenuSystem.js
new file mode 100644
index 0000000..4c44103
--- /dev/null
+++ b/src/systems/MenuSystem.js
@@ -0,0 +1,110 @@
+import { System } from '../core/System.js';
+
+/**
+ * System to handle game menus (start, pause)
+ */
+export class MenuSystem extends System {
+ constructor(engine) {
+ super('MenuSystem');
+ this.requiredComponents = []; // No required components
+ this.priority = 1; // Run early
+ this.engine = engine;
+ this.ctx = engine.ctx;
+ this.gameState = 'start'; // 'start', 'playing', 'paused'
+ this.paused = false;
+ }
+
+ init(engine) {
+ super.init(engine);
+ this.setupInput();
+ }
+
+ setupInput() {
+ window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') {
+ if (this.gameState === 'playing') {
+ this.togglePause();
+ }
+ }
+ if (e.key === 'Enter' || e.key === ' ') {
+ if (this.gameState === 'start') {
+ this.startGame();
+ } else if (this.gameState === 'paused') {
+ this.resumeGame();
+ }
+ }
+ });
+ }
+
+ startGame() {
+ this.gameState = 'playing';
+ this.paused = false;
+ if (!this.engine.running) {
+ this.engine.start();
+ }
+ }
+
+ togglePause() {
+ if (this.gameState === 'playing') {
+ this.gameState = 'paused';
+ this.paused = true;
+ } else if (this.gameState === 'paused') {
+ this.resumeGame();
+ }
+ }
+
+ resumeGame() {
+ this.gameState = 'playing';
+ this.paused = false;
+ }
+
+ process(_deltaTime, _entities) {
+ // Don't update game systems if paused or at start menu
+ if (this.gameState === 'paused' || this.gameState === 'start') {
+ // Pause all other systems
+ this.engine.systems.forEach(system => {
+ if (system !== this && system.name !== 'MenuSystem' && system.name !== 'UISystem') {
+ // Systems will check game state themselves
+ }
+ });
+ }
+ }
+
+ drawMenu() {
+ const ctx = this.ctx;
+ const width = this.engine.canvas.width;
+ const height = this.engine.canvas.height;
+
+ // Dark overlay
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+ ctx.fillRect(0, 0, width, height);
+
+ ctx.fillStyle = '#ffffff';
+ ctx.font = 'bold 48px Courier New';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ if (this.gameState === 'start') {
+ ctx.fillText('SLIME GENESIS', width / 2, height / 2 - 100);
+ ctx.font = '24px Courier New';
+ ctx.fillText('Press ENTER or SPACE to Start', width / 2, height / 2);
+ ctx.font = '16px Courier New';
+ ctx.fillText('WASD: Move | Mouse: Aim | Click/Space: Attack', width / 2, height / 2 + 50);
+ ctx.fillText('Shift: Stealth | 1-9: Skills | ESC: Pause', width / 2, height / 2 + 80);
+ } else if (this.gameState === 'paused') {
+ ctx.fillText('PAUSED', width / 2, height / 2 - 50);
+ ctx.font = '24px Courier New';
+ ctx.fillText('Press ENTER or SPACE to Resume', width / 2, height / 2);
+ ctx.fillText('Press ESC to Pause/Unpause', width / 2, height / 2 + 40);
+ }
+ }
+
+ getGameState() {
+ return this.gameState;
+ }
+
+ isPaused() {
+ return this.paused || this.gameState === 'start';
+ }
+}
+
diff --git a/src/systems/MovementSystem.js b/src/systems/MovementSystem.js
new file mode 100644
index 0000000..b705139
--- /dev/null
+++ b/src/systems/MovementSystem.js
@@ -0,0 +1,62 @@
+import { System } from '../core/System.js';
+
+export class MovementSystem extends System {
+ constructor() {
+ super('MovementSystem');
+ this.requiredComponents = ['Position', 'Velocity'];
+ this.priority = 10;
+ }
+
+ process(deltaTime, entities) {
+ entities.forEach(entity => {
+ const position = entity.getComponent('Position');
+ const velocity = entity.getComponent('Velocity');
+ const health = entity.getComponent('Health');
+
+ if (!position || !velocity) return;
+
+ // Check if this is a projectile
+ const isProjectile = health && health.isProjectile;
+
+ // Apply velocity with max speed limit (skip for projectiles)
+ if (!isProjectile) {
+ const speed = Math.sqrt(velocity.vx * velocity.vx + velocity.vy * velocity.vy);
+ if (speed > velocity.maxSpeed) {
+ const factor = velocity.maxSpeed / speed;
+ velocity.vx *= factor;
+ velocity.vy *= factor;
+ }
+ }
+
+ // Update position
+ position.x += velocity.vx * deltaTime;
+ position.y += velocity.vy * deltaTime;
+
+ // Apply friction (skip for projectiles - they should maintain speed)
+ if (!isProjectile) {
+ const friction = 0.9;
+ velocity.vx *= Math.pow(friction, deltaTime * 60);
+ velocity.vy *= Math.pow(friction, deltaTime * 60);
+ }
+
+ // Boundary checking
+ const canvas = this.engine.canvas;
+ if (position.x < 0) {
+ position.x = 0;
+ velocity.vx = 0;
+ } else if (position.x > canvas.width) {
+ position.x = canvas.width;
+ velocity.vx = 0;
+ }
+
+ if (position.y < 0) {
+ position.y = 0;
+ velocity.vy = 0;
+ } else if (position.y > canvas.height) {
+ position.y = canvas.height;
+ velocity.vy = 0;
+ }
+ });
+ }
+}
+
diff --git a/src/systems/PlayerControllerSystem.js b/src/systems/PlayerControllerSystem.js
new file mode 100644
index 0000000..397dfe0
--- /dev/null
+++ b/src/systems/PlayerControllerSystem.js
@@ -0,0 +1,69 @@
+import { System } from '../core/System.js';
+
+export class PlayerControllerSystem extends System {
+ constructor() {
+ super('PlayerControllerSystem');
+ this.requiredComponents = ['Position', 'Velocity'];
+ this.priority = 5;
+ this.playerEntity = null;
+ }
+
+ process(deltaTime, entities) {
+ // Find player entity (first entity with player tag or specific component)
+ if (!this.playerEntity) {
+ this.playerEntity = entities.find(e => e.hasComponent('Evolution'));
+ }
+
+ if (!this.playerEntity) return;
+
+ const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem');
+ if (!inputSystem) return;
+
+ const velocity = this.playerEntity.getComponent('Velocity');
+ const position = this.playerEntity.getComponent('Position');
+ if (!velocity || !position) return;
+
+ // Movement input
+ let moveX = 0;
+ let moveY = 0;
+ const moveSpeed = 200;
+
+ if (inputSystem.isKeyPressed('w') || inputSystem.isKeyPressed('arrowup')) {
+ moveY -= 1;
+ }
+ if (inputSystem.isKeyPressed('s') || inputSystem.isKeyPressed('arrowdown')) {
+ moveY += 1;
+ }
+ if (inputSystem.isKeyPressed('a') || inputSystem.isKeyPressed('arrowleft')) {
+ moveX -= 1;
+ }
+ if (inputSystem.isKeyPressed('d') || inputSystem.isKeyPressed('arrowright')) {
+ moveX += 1;
+ }
+
+ // Normalize diagonal movement
+ if (moveX !== 0 && moveY !== 0) {
+ moveX *= 0.707;
+ moveY *= 0.707;
+ }
+
+ // Apply movement
+ velocity.vx = moveX * moveSpeed;
+ velocity.vy = moveY * moveSpeed;
+
+ // Face mouse or movement direction
+ const mouse = inputSystem.getMousePosition();
+ const dx = mouse.x - position.x;
+ const dy = mouse.y - position.y;
+ if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) {
+ position.rotation = Math.atan2(dy, dx);
+ } else if (moveX !== 0 || moveY !== 0) {
+ position.rotation = Math.atan2(moveY, moveX);
+ }
+ }
+
+ getPlayerEntity() {
+ return this.playerEntity;
+ }
+}
+
diff --git a/src/systems/ProjectileSystem.js b/src/systems/ProjectileSystem.js
new file mode 100644
index 0000000..6e4e9ee
--- /dev/null
+++ b/src/systems/ProjectileSystem.js
@@ -0,0 +1,83 @@
+import { System } from '../core/System.js';
+
+export class ProjectileSystem extends System {
+ constructor() {
+ super('ProjectileSystem');
+ this.requiredComponents = ['Position', 'Velocity'];
+ this.priority = 18;
+ }
+
+ process(deltaTime, entities) {
+ const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
+ const _player = playerController ? playerController.getPlayerEntity() : null;
+
+ entities.forEach(entity => {
+ const health = entity.getComponent('Health');
+ if (!health || !health.isProjectile) return;
+
+ const position = entity.getComponent('Position');
+ if (!position) return;
+
+ // Check range - remove if traveled beyond max range
+ if (entity.startX !== undefined && entity.startY !== undefined && entity.maxRange !== undefined) {
+ const dx = position.x - entity.startX;
+ const dy = position.y - entity.startY;
+ const distanceTraveled = Math.sqrt(dx * dx + dy * dy);
+
+ if (distanceTraveled >= entity.maxRange) {
+ this.engine.removeEntity(entity);
+ return;
+ }
+ }
+
+ // Check lifetime as backup
+ if (entity.lifetime !== undefined) {
+ entity.lifetime -= deltaTime;
+ if (entity.lifetime <= 0) {
+ this.engine.removeEntity(entity);
+ return;
+ }
+ }
+
+ // Check collisions with enemies
+ const allEntities = this.engine.getEntities();
+ allEntities.forEach(target => {
+ if (target.id === entity.owner) return;
+ if (target.id === entity.id) return;
+ if (!target.hasComponent('Health')) return;
+ if (target.getComponent('Health').isProjectile) return;
+
+ const targetPos = target.getComponent('Position');
+ if (!targetPos) return;
+
+ const dx = targetPos.x - position.x;
+ const dy = targetPos.y - position.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < 20) {
+ // Hit!
+ const targetHealth = target.getComponent('Health');
+ const damage = entity.damage || 10;
+ targetHealth.takeDamage(damage);
+
+ // If target is dead, mark it for immediate removal
+ if (targetHealth.isDead()) {
+ target.active = false;
+ // DeathSystem will handle removal
+ }
+
+ // Remove projectile
+ this.engine.removeEntity(entity);
+ }
+ });
+
+ // Boundary check
+ const canvas = this.engine.canvas;
+ if (position.x < 0 || position.x > canvas.width ||
+ position.y < 0 || position.y > canvas.height) {
+ this.engine.removeEntity(entity);
+ }
+ });
+ }
+}
+
diff --git a/src/systems/RenderSystem.js b/src/systems/RenderSystem.js
new file mode 100644
index 0000000..dcedd95
--- /dev/null
+++ b/src/systems/RenderSystem.js
@@ -0,0 +1,437 @@
+import { System } from '../core/System.js';
+
+export class RenderSystem extends System {
+ constructor(engine) {
+ super('RenderSystem');
+ this.requiredComponents = ['Position', 'Sprite'];
+ this.priority = 100; // Render last
+ this.engine = engine;
+ this.ctx = engine.ctx;
+ }
+
+ process(deltaTime, _entities) {
+ // Clear canvas
+ this.engine.clear();
+
+ // Draw background
+ this.drawBackground();
+
+ // Draw entities
+ // Get all entities including inactive ones for rendering dead absorbable entities
+ const allEntities = this.engine.entities;
+ allEntities.forEach(entity => {
+ const health = entity.getComponent('Health');
+ const evolution = entity.getComponent('Evolution');
+
+ // Skip inactive entities UNLESS they're dead and absorbable (for absorption window)
+ if (!entity.active) {
+ const absorbable = entity.getComponent('Absorbable');
+ if (health && health.isDead() && absorbable && !absorbable.absorbed) {
+ // Render dead absorbable entities even if inactive (fade them out)
+ this.drawEntity(entity, deltaTime, true); // Pass fade flag
+ return;
+ }
+ return; // Skip other inactive entities
+ }
+
+ // Don't render dead non-player entities (unless they're absorbable, handled above)
+ if (health && health.isDead() && !evolution) {
+ const absorbable = entity.getComponent('Absorbable');
+ if (!absorbable || absorbable.absorbed) {
+ return; // Skip dead non-absorbable entities
+ }
+ }
+
+ this.drawEntity(entity, deltaTime);
+ });
+
+ // Draw skill effects
+ this.drawSkillEffects();
+ }
+
+ drawBackground() {
+ const ctx = this.ctx;
+ const width = this.engine.canvas.width;
+ const height = this.engine.canvas.height;
+
+ // Cave background with gradient
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
+ gradient.addColorStop(0, '#0f0f1f');
+ gradient.addColorStop(1, '#1a1a2e');
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, width, height);
+
+ // Add cave features with better visuals
+ ctx.fillStyle = '#2a2a3e';
+ for (let i = 0; i < 20; i++) {
+ const x = (i * 70 + Math.sin(i) * 30) % width;
+ const y = (i * 50 + Math.cos(i) * 40) % height;
+ const size = 25 + (i % 4) * 15;
+
+ // Add shadow
+ ctx.shadowBlur = 20;
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ }
+
+ // Add some ambient lighting
+ const lightGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, 400);
+ lightGradient.addColorStop(0, 'rgba(100, 150, 200, 0.1)');
+ lightGradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
+ ctx.fillStyle = lightGradient;
+ ctx.fillRect(0, 0, width, height);
+ }
+
+ drawEntity(entity, deltaTime, isDeadFade = false) {
+ const position = entity.getComponent('Position');
+ const sprite = entity.getComponent('Sprite');
+ const health = entity.getComponent('Health');
+
+ if (!position || !sprite) return;
+
+ this.ctx.save();
+ // Fade out dead entities
+ let alpha = sprite.alpha;
+ if (isDeadFade && health && health.isDead()) {
+ const absorbable = entity.getComponent('Absorbable');
+ if (absorbable && !absorbable.absorbed) {
+ // Calculate fade based on time since death
+ const deathTime = entity.deathTime || Date.now();
+ const timeSinceDeath = (Date.now() - deathTime) / 1000;
+ const fadeTime = 3.0; // 3 seconds to fade (matches DeathSystem removal time)
+ alpha = Math.max(0.3, 1.0 - (timeSinceDeath / fadeTime));
+ }
+ }
+ this.ctx.globalAlpha = alpha;
+ this.ctx.translate(position.x, position.y);
+ this.ctx.rotate(position.rotation);
+ this.ctx.scale(sprite.scale, sprite.scale);
+
+ // Update animation time for slime morphing
+ if (sprite.shape === 'slime') {
+ sprite.animationTime += deltaTime;
+ sprite.morphAmount = Math.sin(sprite.animationTime * 3) * 0.2 + 0.8;
+ }
+
+ // Draw based on shape
+ this.ctx.fillStyle = sprite.color;
+
+ if (sprite.shape === 'circle' || sprite.shape === 'slime') {
+ this.drawSlime(sprite);
+ } else if (sprite.shape === 'rect') {
+ this.ctx.fillRect(-sprite.width / 2, -sprite.height / 2, sprite.width, sprite.height);
+ }
+
+ // Draw health bar if entity has health
+ if (health && health.maxHp > 0) {
+ this.drawHealthBar(health, sprite);
+ }
+
+ // Draw combat indicator if attacking
+ const combat = entity.getComponent('Combat');
+ if (combat && combat.isAttacking) {
+ // Draw attack indicator relative to entity's current rotation
+ // Since we're already rotated, we need to draw relative to 0,0 forward
+ this.drawAttackIndicator(combat, position);
+ }
+
+ // Draw stealth indicator
+ const stealth = entity.getComponent('Stealth');
+ if (stealth && stealth.isStealthed) {
+ this.drawStealthIndicator(stealth, sprite);
+ }
+
+ // Mutation Visual Effects
+ const evolution = entity.getComponent('Evolution');
+ if (evolution) {
+ if (evolution.mutationEffects.glowingBody) {
+ // Draw light aura
+ const auraGradient = this.ctx.createRadialGradient(0, 0, 0, 0, 0, sprite.width * 2);
+ auraGradient.addColorStop(0, 'rgba(255, 255, 200, 0.2)');
+ auraGradient.addColorStop(1, 'rgba(255, 255, 200, 0)');
+ this.ctx.fillStyle = auraGradient;
+ this.ctx.beginPath();
+ this.ctx.arc(0, 0, sprite.width * 2, 0, Math.PI * 2);
+ this.ctx.fill();
+ }
+ if (evolution.mutationEffects.electricSkin) {
+ // Add tiny sparks
+ if (Math.random() < 0.2) {
+ this.ctx.strokeStyle = '#00ffff';
+ this.ctx.lineWidth = 2;
+ this.ctx.beginPath();
+ const sparkX = (Math.random() - 0.5) * sprite.width;
+ const sparkY = (Math.random() - 0.5) * sprite.height;
+ this.ctx.moveTo(sparkX, sparkY);
+ this.ctx.lineTo(sparkX + (Math.random() - 0.5) * 10, sparkY + (Math.random() - 0.5) * 10);
+ this.ctx.stroke();
+ }
+ }
+ if (evolution.mutationEffects.hardenedShell) {
+ // Darker, thicker border
+ this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
+ this.ctx.lineWidth = 3;
+ this.ctx.stroke();
+ }
+ }
+
+ this.ctx.restore();
+ }
+
+ drawSlime(sprite) {
+ const ctx = this.ctx;
+ const baseRadius = Math.min(sprite.width, sprite.height) / 2;
+
+ if (sprite.shape === 'slime') {
+ // Animated slime blob with morphing and better visuals
+ ctx.shadowBlur = 15;
+ ctx.shadowColor = sprite.color;
+
+ // Main body with morphing
+ ctx.beginPath();
+ const points = 16;
+ for (let i = 0; i < points; i++) {
+ const angle = (i / points) * Math.PI * 2;
+ const morph1 = Math.sin(angle * 2 + sprite.animationTime * 2) * 0.15;
+ const morph2 = Math.cos(angle * 3 + sprite.animationTime * 1.5) * 0.1;
+ const radius = baseRadius * (sprite.morphAmount + morph1 + morph2);
+ const x = Math.cos(angle) * radius;
+ const y = Math.sin(angle) * radius;
+
+ if (i === 0) {
+ ctx.moveTo(x, y);
+ } else {
+ ctx.lineTo(x, y);
+ }
+ }
+ ctx.closePath();
+ ctx.fill();
+
+ // Inner glow
+ const innerGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, baseRadius * 0.8);
+ innerGradient.addColorStop(0, 'rgba(255, 255, 255, 0.4)');
+ innerGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
+ ctx.fillStyle = innerGradient;
+ ctx.beginPath();
+ ctx.arc(0, 0, baseRadius * 0.8, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Highlight
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
+ ctx.beginPath();
+ ctx.arc(-baseRadius * 0.3, -baseRadius * 0.3, baseRadius * 0.35, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.fillStyle = sprite.color;
+ ctx.shadowBlur = 0;
+ } else {
+ // Simple circle with glow
+ ctx.shadowBlur = 10;
+ ctx.shadowColor = sprite.color;
+ ctx.beginPath();
+ ctx.arc(0, 0, baseRadius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ }
+ }
+
+ drawHealthBar(health, sprite) {
+ const ctx = this.ctx;
+ const barWidth = sprite.width * 1.5;
+ const barHeight = 4;
+ const yOffset = sprite.height / 2 + 10;
+
+ // Background
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
+ ctx.fillRect(-barWidth / 2, -yOffset, barWidth, barHeight);
+
+ // Health fill
+ const healthPercent = health.hp / health.maxHp;
+ ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000';
+ ctx.fillRect(-barWidth / 2, -yOffset, barWidth * healthPercent, barHeight);
+
+ // Border
+ ctx.strokeStyle = '#ffffff';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(-barWidth / 2, -yOffset, barWidth, barHeight);
+ }
+
+ drawAttackIndicator(combat, _position) {
+ const ctx = this.ctx;
+ const length = 50;
+ const attackProgress = 1.0 - (combat.attackCooldown / 0.3); // 0 to 1 during attack animation
+
+ // Since we're already in entity's rotated coordinate space (ctx.rotate was applied),
+ // and position.rotation should match combat.attackDirection (set in CombatSystem),
+ // we just draw forward (angle 0) in local space
+ const angle = 0; // Forward in local rotated space
+
+ // Draw slime tentacle/extension
+ ctx.strokeStyle = `rgba(0, 255, 150, ${0.8 * attackProgress})`;
+ ctx.fillStyle = `rgba(0, 255, 150, ${0.6 * attackProgress})`;
+ ctx.lineWidth = 8;
+ ctx.lineCap = 'round';
+
+ // Tentacle extends outward during attack (forward from entity)
+ const tentacleLength = length * attackProgress;
+ const tentacleEndX = Math.cos(angle) * tentacleLength;
+ const tentacleEndY = Math.sin(angle) * tentacleLength;
+
+ // Draw curved tentacle
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ // Add slight curve to tentacle
+ const midX = Math.cos(angle) * tentacleLength * 0.5;
+ const midY = Math.sin(angle) * tentacleLength * 0.5;
+ const perpX = -Math.sin(angle) * 5 * attackProgress;
+ const perpY = Math.cos(angle) * 5 * attackProgress;
+ ctx.quadraticCurveTo(midX + perpX, midY + perpY, tentacleEndX, tentacleEndY);
+ ctx.stroke();
+
+ // Draw impact point
+ if (attackProgress > 0.5) {
+ ctx.beginPath();
+ ctx.arc(tentacleEndX, tentacleEndY, 6 * attackProgress, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ drawStealthIndicator(stealth, sprite) {
+ const ctx = this.ctx;
+ const radius = Math.max(sprite.width, sprite.height) / 2 + 5;
+
+ // Stealth ring
+ ctx.strokeStyle = `rgba(0, 255, 150, ${1 - stealth.visibility})`;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // Visibility indicator
+ if (stealth.visibility > 0.3) {
+ ctx.fillStyle = `rgba(255, 0, 0, ${(stealth.visibility - 0.3) * 2})`;
+ ctx.beginPath();
+ ctx.arc(0, -radius - 10, 3, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ drawSkillEffects() {
+ const skillEffectSystem = this.engine.systems.find(s => s.name === 'SkillEffectSystem');
+ if (!skillEffectSystem) return;
+
+ const effects = skillEffectSystem.getEffects();
+ const ctx = this.ctx;
+
+ effects.forEach(effect => {
+ ctx.save();
+
+ switch (effect.type) {
+ case 'fire_breath':
+ this.drawFireBreath(ctx, effect);
+ break;
+ case 'pounce':
+ this.drawPounce(ctx, effect);
+ break;
+ case 'pounce_impact':
+ this.drawPounceImpact(ctx, effect);
+ break;
+ }
+
+ ctx.restore();
+ });
+ }
+
+ drawFireBreath(ctx, effect) {
+ const progress = Math.min(1.0, effect.time / effect.lifetime); // Clamp to 0-1
+ const alpha = Math.max(0, 1.0 - progress); // Ensure non-negative
+
+ // Draw fire cone
+ ctx.translate(effect.x, effect.y);
+ ctx.rotate(effect.angle);
+
+ // Cone gradient
+ const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, effect.range);
+ gradient.addColorStop(0, `rgba(255, 100, 0, ${alpha * 0.8})`);
+ gradient.addColorStop(0.5, `rgba(255, 200, 0, ${alpha * 0.6})`);
+ gradient.addColorStop(1, `rgba(255, 50, 0, ${alpha * 0.3})`);
+
+ ctx.fillStyle = gradient;
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.arc(0, 0, effect.range, -effect.coneAngle / 2, effect.coneAngle / 2);
+ ctx.closePath();
+ ctx.fill();
+
+ // Fire particles
+ for (let i = 0; i < 20; i++) {
+ const angle = (Math.random() - 0.5) * effect.coneAngle;
+ const dist = Math.random() * effect.range * progress;
+ const x = Math.cos(angle) * dist;
+ const y = Math.sin(angle) * dist;
+ const size = 3 + Math.random() * 5;
+
+ ctx.fillStyle = `rgba(255, ${150 + Math.random() * 100}, 0, ${alpha})`;
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ drawPounce(ctx, effect) {
+ const progress = Math.min(1.0, effect.time / effect.lifetime); // Clamp to 0-1
+ const currentX = effect.startX + Math.cos(effect.angle) * effect.speed * effect.time;
+ const currentY = effect.startY + Math.sin(effect.angle) * effect.speed * effect.time;
+
+ // Draw dash trail
+ const alpha = Math.max(0, 1.0 - progress); // Ensure non-negative
+ ctx.strokeStyle = `rgba(255, 200, 0, ${alpha})`;
+ ctx.lineWidth = 4;
+ ctx.lineCap = 'round';
+ ctx.beginPath();
+ ctx.moveTo(effect.startX, effect.startY);
+ ctx.lineTo(currentX, currentY);
+ ctx.stroke();
+
+ // Draw impact point
+ const radius = Math.max(0, 15 * (1 - progress)); // Ensure non-negative radius
+ if (radius > 0) {
+ ctx.fillStyle = `rgba(255, 150, 0, ${alpha})`;
+ ctx.beginPath();
+ ctx.arc(currentX, currentY, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ drawPounceImpact(ctx, effect) {
+ const progress = Math.min(1.0, effect.time / effect.lifetime); // Clamp to 0-1
+ const alpha = Math.max(0, 1.0 - progress); // Ensure non-negative
+ const size = Math.max(0, 30 * (1 - progress)); // Ensure non-negative size
+
+ if (size > 0 && alpha > 0) {
+ // Impact ring
+ ctx.strokeStyle = `rgba(255, 200, 0, ${alpha})`;
+ ctx.lineWidth = 3;
+ ctx.beginPath();
+ ctx.arc(effect.x, effect.y, size, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // Impact particles
+ for (let i = 0; i < 8; i++) {
+ const angle = (i / 8) * Math.PI * 2;
+ const dist = size * 0.7;
+ const x = effect.x + Math.cos(angle) * dist;
+ const y = effect.y + Math.sin(angle) * dist;
+
+ ctx.fillStyle = `rgba(255, 150, 0, ${alpha})`;
+ ctx.beginPath();
+ ctx.arc(x, y, 4, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+ }
+}
+
diff --git a/src/systems/SkillEffectSystem.js b/src/systems/SkillEffectSystem.js
new file mode 100644
index 0000000..181a384
--- /dev/null
+++ b/src/systems/SkillEffectSystem.js
@@ -0,0 +1,41 @@
+import { System } from '../core/System.js';
+
+/**
+ * System to track and render skill effects (Fire Breath, Pounce, etc.)
+ */
+export class SkillEffectSystem extends System {
+ constructor() {
+ super('SkillEffectSystem');
+ this.requiredComponents = []; // No required components
+ this.priority = 50; // Run after skills but before rendering
+ this.activeEffects = [];
+ }
+
+ process(deltaTime, _entities) {
+ // Update all active effects
+ for (let i = this.activeEffects.length - 1; i >= 0; i--) {
+ const effect = this.activeEffects[i];
+ effect.lifetime -= deltaTime;
+ effect.time += deltaTime;
+
+ if (effect.lifetime <= 0) {
+ this.activeEffects.splice(i, 1);
+ }
+ }
+ }
+
+ /**
+ * Add a skill effect
+ */
+ addEffect(effect) {
+ this.activeEffects.push(effect);
+ }
+
+ /**
+ * Get all active effects
+ */
+ getEffects() {
+ return this.activeEffects;
+ }
+}
+
diff --git a/src/systems/SkillSystem.js b/src/systems/SkillSystem.js
new file mode 100644
index 0000000..bdf8598
--- /dev/null
+++ b/src/systems/SkillSystem.js
@@ -0,0 +1,53 @@
+import { System } from '../core/System.js';
+import { SkillRegistry } from '../skills/SkillRegistry.js';
+
+export class SkillSystem extends System {
+ constructor() {
+ super('SkillSystem');
+ this.requiredComponents = ['Skills'];
+ this.priority = 30;
+ }
+
+ process(deltaTime, entities) {
+ const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem');
+ if (!inputSystem) return;
+
+ entities.forEach(entity => {
+ const skills = entity.getComponent('Skills');
+ if (!skills) return;
+
+ // Update cooldowns
+ skills.updateCooldowns(deltaTime);
+
+ // Check for skill activation (number keys 1-9)
+ for (let i = 1; i <= 9; i++) {
+ const key = i.toString();
+ if (inputSystem.isKeyJustPressed(key)) {
+ const skillIndex = i - 1;
+ if (skillIndex < skills.activeSkills.length) {
+ const skillId = skills.activeSkills[skillIndex];
+ if (!skills.isOnCooldown(skillId)) {
+ this.activateSkill(entity, skillId);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ activateSkill(entity, skillId) {
+ const skill = SkillRegistry.get(skillId);
+ if (!skill) {
+ console.warn(`Skill not found: ${skillId}`);
+ return;
+ }
+
+ if (skill.activate(entity, this.engine)) {
+ const skills = entity.getComponent('Skills');
+ if (skills) {
+ skills.setCooldown(skillId, skill.cooldown);
+ }
+ }
+ }
+}
+
diff --git a/src/systems/StealthSystem.js b/src/systems/StealthSystem.js
new file mode 100644
index 0000000..9ea7ec9
--- /dev/null
+++ b/src/systems/StealthSystem.js
@@ -0,0 +1,74 @@
+import { System } from '../core/System.js';
+
+export class StealthSystem extends System {
+ constructor() {
+ super('StealthSystem');
+ this.requiredComponents = ['Stealth'];
+ this.priority = 12;
+ }
+
+ process(deltaTime, entities) {
+ const inputSystem = this.engine.systems.find(s => s.name === 'InputSystem');
+ const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
+ const player = playerController ? playerController.getPlayerEntity() : null;
+
+ entities.forEach(entity => {
+ const stealth = entity.getComponent('Stealth');
+ const velocity = entity.getComponent('Velocity');
+ const combat = entity.getComponent('Combat');
+ const evolution = entity.getComponent('Evolution');
+
+ if (!stealth) return;
+
+ // Determine stealth type based on evolution
+ if (evolution) {
+ const form = evolution.getDominantForm();
+ stealth.stealthType = form;
+ }
+
+ // Check if player wants to toggle stealth
+ if (entity === player && inputSystem) {
+ const shiftPress = inputSystem.isKeyJustPressed('shift');
+ if (shiftPress) {
+ if (stealth.isStealthed) {
+ stealth.exitStealth();
+ } else {
+ stealth.enterStealth(stealth.stealthType);
+ }
+ }
+ }
+
+ // Update stealth based on movement and combat
+ const isMoving = velocity && (Math.abs(velocity.vx) > 1 || Math.abs(velocity.vy) > 1);
+ const isInCombat = combat && combat.isAttacking;
+
+ stealth.updateStealth(isMoving, isInCombat);
+
+ // Form-specific stealth bonuses
+ if (stealth.isStealthed) {
+ switch (stealth.stealthType) {
+ case 'slime':
+ // Slime can be very hidden when not moving
+ if (!isMoving) {
+ stealth.visibility = Math.max(0.05, stealth.visibility - deltaTime * 0.2);
+ }
+ break;
+ case 'beast':
+ // Beast stealth is better when moving slowly
+ if (isMoving && velocity) {
+ const speed = Math.sqrt(velocity.vx * velocity.vx + velocity.vy * velocity.vy);
+ if (speed < 50) {
+ stealth.visibility = Math.max(0.1, stealth.visibility - deltaTime * 0.1);
+ }
+ }
+ break;
+ case 'human':
+ // Human stealth is more consistent
+ stealth.visibility = Math.max(0.2, stealth.visibility - deltaTime * 0.05);
+ break;
+ }
+ }
+ });
+ }
+}
+
diff --git a/src/systems/UISystem.js b/src/systems/UISystem.js
new file mode 100644
index 0000000..c6ea2d4
--- /dev/null
+++ b/src/systems/UISystem.js
@@ -0,0 +1,311 @@
+import { System } from '../core/System.js';
+import { SkillRegistry } from '../skills/SkillRegistry.js';
+import { Events } from '../core/EventBus.js';
+
+export class UISystem extends System {
+ constructor(engine) {
+ super('UISystem');
+ this.requiredComponents = []; // No required components - renders UI
+ this.priority = 200; // Render after everything else
+ this.engine = engine;
+ this.ctx = engine.ctx;
+
+ this.damageNumbers = [];
+ this.notifications = [];
+
+ // Subscribe to events
+ engine.on(Events.DAMAGE_DEALT, (data) => this.addDamageNumber(data));
+ engine.on(Events.MUTATION_GAINED, (data) => this.addNotification(`Mutation Gained: ${data.name}`));
+ }
+
+ addDamageNumber(data) {
+ this.damageNumbers.push({
+ x: data.x,
+ y: data.y,
+ value: Math.floor(data.value),
+ color: data.color || '#ffffff',
+ lifetime: 1.0,
+ vy: -50
+ });
+ }
+
+ addNotification(text) {
+ this.notifications.push({
+ text,
+ lifetime: 3.0,
+ alpha: 1.0
+ });
+ }
+
+ process(deltaTime, _entities) {
+ // Update damage numbers
+ this.updateDamageNumbers(deltaTime);
+ this.updateNotifications(deltaTime);
+
+ const menuSystem = this.engine.systems.find(s => s.name === 'MenuSystem');
+ const gameState = menuSystem ? menuSystem.getGameState() : 'playing';
+
+ // Only draw menu overlay if in start or paused state
+ if (gameState === 'start' || gameState === 'paused') {
+ if (menuSystem) {
+ menuSystem.drawMenu();
+ }
+ // Don't draw game UI when menu is showing
+ return;
+ }
+
+ const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
+ const player = playerController ? playerController.getPlayerEntity() : null;
+
+ if (!player) return;
+
+ // Draw UI
+ this.drawHUD(player);
+ this.drawSkills(player);
+ this.drawStats(player);
+ this.drawSkillProgress(player);
+ this.drawDamageNumbers();
+ this.drawNotifications();
+ this.drawAbsorptionEffects();
+ }
+
+ drawHUD(player) {
+ const health = player.getComponent('Health');
+ const stats = player.getComponent('Stats');
+ const evolution = player.getComponent('Evolution');
+ const skills = player.getComponent('Skills');
+
+ if (!health || !stats || !evolution) return;
+
+ const ctx = this.ctx;
+ const _width = this.engine.canvas.width;
+ const _height = this.engine.canvas.height;
+
+ // Health bar
+ const barWidth = 200;
+ const barHeight = 20;
+ const barX = 20;
+ const barY = 20;
+
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+ ctx.fillRect(barX, barY, barWidth, barHeight);
+
+ const healthPercent = health.hp / health.maxHp;
+ ctx.fillStyle = healthPercent > 0.5 ? '#00ff00' : healthPercent > 0.25 ? '#ffff00' : '#ff0000';
+ ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight);
+
+ ctx.strokeStyle = '#ffffff';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(barX, barY, barWidth, barHeight);
+
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '14px Courier New';
+ ctx.fillText(`HP: ${Math.ceil(health.hp)}/${health.maxHp}`, barX + 5, barY + 15);
+
+ // Evolution display
+ const form = evolution.getDominantForm();
+ const formY = barY + barHeight + 10;
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '12px Courier New';
+ ctx.fillText(`Form: ${form.toUpperCase()}`, barX, formY);
+ ctx.fillText(`Human: ${evolution.human.toFixed(1)} | Beast: ${evolution.beast.toFixed(1)} | Slime: ${evolution.slime.toFixed(1)}`, barX, formY + 15);
+
+ // Instructions
+ const instructionsY = formY + 40;
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
+ ctx.font = '11px Courier New';
+ ctx.fillText('WASD: Move | Mouse: Aim | Click/Space: Attack', barX, instructionsY);
+ ctx.fillText('Shift: Stealth | 1-9: Skills (Press 1 for Slime Gun)', barX, instructionsY + 15);
+
+ // Show skill hint if player has skills
+ if (skills && skills.activeSkills.length > 0) {
+ ctx.fillStyle = '#00ff96';
+ ctx.fillText(`You have ${skills.activeSkills.length} skill(s)! Press 1-${skills.activeSkills.length} to use them.`, barX, instructionsY + 30);
+ } else {
+ ctx.fillStyle = '#ffaa00';
+ ctx.fillText('Defeat and absorb creatures 5 times to learn their skills!', barX, instructionsY + 30);
+ }
+
+ // Health regeneration hint
+ ctx.fillStyle = '#00aaff';
+ ctx.fillText('Health regenerates when not in combat', barX, instructionsY + 45);
+ }
+
+ drawSkills(player) {
+ const skills = player.getComponent('Skills');
+ if (!skills) return;
+
+ const ctx = this.ctx;
+ const width = this.engine.canvas.width;
+ const startX = width - 250;
+ const startY = 20;
+
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+ ctx.fillRect(startX, startY, 230, 30 + skills.activeSkills.length * 30);
+
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '14px Courier New';
+ ctx.fillText('Skills:', startX + 10, startY + 20);
+
+ skills.activeSkills.forEach((skillId, index) => {
+ const y = startY + 40 + index * 30;
+ const key = (index + 1).toString();
+ const onCooldown = skills.isOnCooldown(skillId);
+ const cooldown = skills.getCooldown(skillId);
+
+ // Get skill name from registry for display
+ const skill = SkillRegistry.get(skillId);
+ const skillName = skill ? skill.name : skillId.replace('_', ' ');
+
+ ctx.fillStyle = onCooldown ? '#888888' : '#00ff96';
+ ctx.fillText(`${key}. ${skillName}${onCooldown ? ` (${cooldown.toFixed(1)}s)` : ''}`, startX + 10, y);
+ });
+ }
+
+ drawStats(player) {
+ const stats = player.getComponent('Stats');
+ if (!stats) return;
+
+ const ctx = this.ctx;
+ const width = this.engine.canvas.width;
+ const startX = width - 250;
+ const startY = 200;
+
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+ ctx.fillRect(startX, startY, 230, 150);
+
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '12px Courier New';
+ let y = startY + 20;
+ ctx.fillText('Stats:', startX + 10, y);
+ y += 20;
+ ctx.fillText(`STR: ${stats.strength}`, startX + 10, y);
+ y += 15;
+ ctx.fillText(`AGI: ${stats.agility}`, startX + 10, y);
+ y += 15;
+ ctx.fillText(`INT: ${stats.intelligence}`, startX + 10, y);
+ y += 15;
+ ctx.fillText(`CON: ${stats.constitution}`, startX + 10, y);
+ y += 15;
+ ctx.fillText(`PER: ${stats.perception}`, startX + 10, y);
+ }
+
+ drawSkillProgress(player) {
+ const skillProgress = player.getComponent('SkillProgress');
+ if (!skillProgress) return;
+
+ const ctx = this.ctx;
+ const width = this.engine.canvas.width;
+ const startX = width - 250;
+ const startY = 360;
+
+ const progress = skillProgress.getAllProgress();
+ if (progress.size === 0) return;
+
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+ ctx.fillRect(startX, startY, 230, 30 + progress.size * 25);
+
+ ctx.fillStyle = '#ffffff';
+ ctx.font = '12px Courier New';
+ ctx.fillText('Skill Progress:', startX + 10, startY + 20);
+
+ let y = startY + 35;
+ progress.forEach((count, skillId) => {
+ const required = skillProgress.requiredAbsorptions;
+ const _percent = Math.min(100, (count / required) * 100);
+ const skill = SkillRegistry.get(skillId);
+ const skillName = skill ? skill.name : skillId.replace('_', ' ');
+
+ ctx.fillStyle = count >= required ? '#00ff00' : '#ffff00';
+ ctx.fillText(`${skillName}: ${count}/${required}`, startX + 10, y);
+ y += 20;
+ });
+ }
+
+ updateDamageNumbers(deltaTime) {
+ for (let i = this.damageNumbers.length - 1; i >= 0; i--) {
+ const num = this.damageNumbers[i];
+ num.lifetime -= deltaTime;
+ num.y += num.vy * deltaTime;
+ num.vy *= 0.95;
+ if (num.lifetime <= 0) this.damageNumbers.splice(i, 1);
+ }
+ }
+
+ updateNotifications(deltaTime) {
+ for (let i = this.notifications.length - 1; i >= 0; i--) {
+ const note = this.notifications[i];
+ note.lifetime -= deltaTime;
+ if (note.lifetime < 0.5) note.alpha = note.lifetime * 2;
+ if (note.lifetime <= 0) this.notifications.splice(i, 1);
+ }
+ }
+
+ drawDamageNumbers() {
+ const ctx = this.ctx;
+ this.damageNumbers.forEach(num => {
+ const alpha = Math.min(1, num.lifetime);
+ const size = 14 + Math.min(num.value / 2, 10);
+
+ ctx.font = `bold ${size}px Courier New`;
+ // Shadow
+ ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.5})`;
+ ctx.fillText(num.value.toString(), num.x + 2, num.y + 2);
+
+ // Main text
+ ctx.fillStyle = num.color.startsWith('rgba') ? num.color : `rgba(${this.hexToRgb(num.color)}, ${alpha})`;
+ ctx.fillText(num.value.toString(), num.x, num.y);
+ });
+ }
+
+ hexToRgb(hex) {
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return `${r}, ${g}, ${b}`;
+ }
+
+ drawNotifications() {
+ const ctx = this.ctx;
+ const width = this.engine.canvas.width;
+
+ this.notifications.forEach((note, index) => {
+ ctx.fillStyle = `rgba(255, 255, 0, ${note.alpha})`;
+ ctx.font = 'bold 20px Courier New';
+ ctx.textAlign = 'center';
+ ctx.fillText(note.text, width / 2, 100 + index * 30);
+ ctx.textAlign = 'left';
+ });
+ }
+
+ drawAbsorptionEffects() {
+ const absorptionSystem = this.engine.systems.find(s => s.name === 'AbsorptionSystem');
+ if (!absorptionSystem || !absorptionSystem.getEffects) return;
+
+ const effects = absorptionSystem.getEffects();
+ const ctx = this.ctx;
+
+ effects.forEach(effect => {
+ const alpha = Math.min(1, effect.lifetime * 2);
+
+ // Glow effect
+ ctx.shadowBlur = 10;
+ ctx.shadowColor = effect.color;
+ ctx.fillStyle = effect.color;
+ ctx.globalAlpha = alpha;
+ ctx.beginPath();
+ ctx.arc(effect.x, effect.y, effect.size, 0, Math.PI * 2);
+ ctx.fill();
+
+ // Inner bright core
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.beginPath();
+ ctx.arc(effect.x, effect.y, effect.size * 0.5, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.globalAlpha = 1.0;
+ ctx.shadowBlur = 0;
+ });
+ }
+}
+
diff --git a/src/world/World.js b/src/world/World.js
new file mode 100644
index 0000000..9afa6e8
--- /dev/null
+++ b/src/world/World.js
@@ -0,0 +1,27 @@
+/**
+ * World manager - handles areas and world state
+ */
+export class World {
+ constructor() {
+ this.areas = [];
+ this.currentArea = null;
+ this.areas.push({
+ id: 'cave',
+ name: 'Dark Cave',
+ type: 'cave',
+ spawnTypes: ['humanoid', 'beast', 'elemental'],
+ spawnRate: 0.5
+ });
+
+ this.currentArea = this.areas[0];
+ }
+
+ getCurrentArea() {
+ return this.currentArea;
+ }
+
+ getSpawnTypes() {
+ return this.currentArea ? this.currentArea.spawnTypes : ['beast'];
+ }
+}
+
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..5f09b7e
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,25 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ build: {
+ minify: 'terser',
+ terserOptions: {
+ compress: {
+ drop_console: true,
+ drop_debugger: true,
+ },
+ mangle: {
+ toplevel: true,
+ },
+ format: {
+ comments: false,
+ },
+ },
+ rollupOptions: {
+ output: {
+ manualChunks: undefined,
+ },
+ },
+ sourcemap: false,
+ },
+});