From 4a4fa05ce4819cf97f4e9945ce4396e5e212f460 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Montoya Date: Tue, 6 Jan 2026 14:02:09 -0500 Subject: [PATCH] feat: add poc --- .gitignore | 6 + Dockerfile | 25 + README.md | 132 ++ docker-compose.yml | 9 + eslint.config.js | 26 + index.html | 31 + package-lock.json | 2281 +++++++++++++++++++++++ package.json | 20 + src/GameConfig.js | 38 + src/components/AI.js | 52 + src/components/Absorbable.js | 54 + src/components/Combat.js | 52 + src/components/Evolution.js | 105 ++ src/components/Health.js | 29 + src/components/Inventory.js | 71 + src/components/Position.js | 11 + src/components/SkillProgress.js | 45 + src/components/Skills.js | 69 + src/components/Sprite.js | 18 + src/components/Stats.js | 55 + src/components/Stealth.js | 48 + src/components/Velocity.js | 11 + src/core/Component.js | 14 + src/core/Engine.js | 139 ++ src/core/Entity.js | 58 + src/core/EventBus.js | 57 + src/core/System.js | 45 + src/items/Item.js | 13 + src/items/ItemRegistry.js | 53 + src/main.js | 158 ++ src/skills/Skill.js | 32 + src/skills/SkillRegistry.js | 36 + src/skills/skills/FireBreath.js | 83 + src/skills/skills/Pounce.js | 97 + src/skills/skills/StealthMode.js | 35 + src/skills/skills/WaterGun.js | 84 + src/systems/AISystem.js | 205 ++ src/systems/AbsorptionSystem.js | 176 ++ src/systems/CombatSystem.js | 190 ++ src/systems/DeathSystem.js | 56 + src/systems/HealthRegenerationSystem.js | 27 + src/systems/InputSystem.js | 153 ++ src/systems/MenuSystem.js | 110 ++ src/systems/MovementSystem.js | 62 + src/systems/PlayerControllerSystem.js | 69 + src/systems/ProjectileSystem.js | 83 + src/systems/RenderSystem.js | 437 +++++ src/systems/SkillEffectSystem.js | 41 + src/systems/SkillSystem.js | 53 + src/systems/StealthSystem.js | 74 + src/systems/UISystem.js | 311 +++ src/world/World.js | 27 + vite.config.js | 25 + 53 files changed, 6191 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/GameConfig.js create mode 100644 src/components/AI.js create mode 100644 src/components/Absorbable.js create mode 100644 src/components/Combat.js create mode 100644 src/components/Evolution.js create mode 100644 src/components/Health.js create mode 100644 src/components/Inventory.js create mode 100644 src/components/Position.js create mode 100644 src/components/SkillProgress.js create mode 100644 src/components/Skills.js create mode 100644 src/components/Sprite.js create mode 100644 src/components/Stats.js create mode 100644 src/components/Stealth.js create mode 100644 src/components/Velocity.js create mode 100644 src/core/Component.js create mode 100644 src/core/Engine.js create mode 100644 src/core/Entity.js create mode 100644 src/core/EventBus.js create mode 100644 src/core/System.js create mode 100644 src/items/Item.js create mode 100644 src/items/ItemRegistry.js create mode 100644 src/main.js create mode 100644 src/skills/Skill.js create mode 100644 src/skills/SkillRegistry.js create mode 100644 src/skills/skills/FireBreath.js create mode 100644 src/skills/skills/Pounce.js create mode 100644 src/skills/skills/StealthMode.js create mode 100644 src/skills/skills/WaterGun.js create mode 100644 src/systems/AISystem.js create mode 100644 src/systems/AbsorptionSystem.js create mode 100644 src/systems/CombatSystem.js create mode 100644 src/systems/DeathSystem.js create mode 100644 src/systems/HealthRegenerationSystem.js create mode 100644 src/systems/InputSystem.js create mode 100644 src/systems/MenuSystem.js create mode 100644 src/systems/MovementSystem.js create mode 100644 src/systems/PlayerControllerSystem.js create mode 100644 src/systems/ProjectileSystem.js create mode 100644 src/systems/RenderSystem.js create mode 100644 src/systems/SkillEffectSystem.js create mode 100644 src/systems/SkillSystem.js create mode 100644 src/systems/StealthSystem.js create mode 100644 src/systems/UISystem.js create mode 100644 src/world/World.js create mode 100644 vite.config.js 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, + }, +});