initial commit

This commit is contained in:
Jarian 2026-07-03 01:08:48 +00:00
commit 35d2c51ff1
48 changed files with 10124 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/

58
AGENTS.md Normal file
View File

@ -0,0 +1,58 @@
# AGENTS.md — Zelda Dungeon Quest
## Run Commands
- **Browser dev**: `npm run dev` → `npx serve . -p 3000`, open `http://localhost:3000`
- **Electron**: `npm start` (requires `--no-sandbox` on Linux, or `ELECTRON_DISABLE_SANDBOX=1`)
- **Node tests**: `node test-full-node.js` (VM-based, simulates browser env)
- **Regenerate bosses**: `node generate-bosses.js` (rewrites `src/data/bosses.js`)
## Module System — Critical
This project uses a **custom browser polyfill** (`src/browser-polyfill.js`) that makes Node-style `module.exports` / `require()` work in the browser. Every source file must follow this pattern:
1. **Export via `module.exports`** at the end of each file:
```js
if (typeof module !== 'undefined' && module.exports) {
module.exports = { exportedName1, exportedName2 };
}
```
2. **Symbols go to `window`** — the polyfill's `module.exports` setter mirrors all exported properties to `window`. Cross-module access reads from `window`.
3. **`moduleKeys` registry** — `src/browser-polyfill.js` maintains a `moduleKeys` map that tells `require()` which `window` keys belong to each module path. **When you add new exports to any module, you MUST add them to the matching entry in `moduleKeys`, or `require()` will not return them.**
4. **Script load order** — `index.html` loads scripts in a strict sequence. The polyfill loads first, then modules in dependency order. Adding a new script requires inserting it at the correct position in `index.html`.
## File Architecture
```
renderer.js ← Legacy prototype (3-room, standalone). NOT used by index.html.
src/app.js ← Bootstrap: init, game loop, screen switching
src/browser-polyfill.js ← CommonJS polyfill (moduleKeys registry)
src/core/ ← Constants, RNG, utilities (loaded first)
src/data/ ← Static definitions: themes, dungeons(100), enemies(22), weapons(12), bosses(100), items
src/generator/ ← Procedural generation: dungeon layout, rooms, puzzles, placement
src/game/ ← Engine: state, movement, combat, AI, items, transitions
src/input/ ← Keyboard tracking, action bindings
src/rendering/ ← Canvas rendering: tiles, entities, HUD, effects
src/menu/ ← Title screen, dungeon select, inventory overlay, end screens
src/save/ ← localStorage save/load
```
Grid: 32×24 tiles, 32px each. Canvas: 640×480.
## Known Bug
`src/app.js:390` — The `init()` function's closing `}` appears before lines that should be inside it (screen setup, game loop start, debug logging). Lines 392-406 are outside the function scope. Move the closing brace to after line 406 to fix.
## Conventions
- All source files start with `'use strict'`
- Constants live in `src/core/constants.js`; other modules import via `require`
- `gameState` and `inputSystem` are module-level globals exposed to `window` by `app.js`
- `ctx`, `debugCtx`, `menuOverlay` are exposed to `window` by `app.js` for cross-module rendering
- Screen states use string constants: `SCREEN_TITLE`, `SCREEN_DUNGEON_SELECT`, etc.
- Debug mode: append `?debug=1` to URL; `F1` toggles overlay in-game
- Console API (browser): `_genDungeon(id)`, `spawnEnemy(type)`, `giveWeapon(name)`, `teleportToRoom(idx)`, `dumpState()`

225
PLAN.md Normal file
View File

@ -0,0 +1,225 @@
# Zelda Dungeon Expansion — Plan Document
## Goal
Expand from 3-room prototype to **100 procedurally-generated dungeons**, each with **20-100 rooms**, **20+ enemy types**, **12 weapons**, **100 unique bosses**, dungeon selection menu, save system, and sequential progression.
## Target: Browser-First
- Runs in a standard web browser (no Electron dependency for testing)
- Electron wrapper added later via existing `main.js` / `preload.js`
- Playtesting via `npx serve` or simple HTTP server
---
## Architecture
### File Structure
```
zelda-dungeon/
├── index.html ← Main entry point (browser), loads all scripts
├── main.js ← Electron main process (untouched for now)
├── preload.js ← Electron IPC bridge (updated for save data)
├── package.json ← Add "dev" script for browser testing
│
├── src/
│ ├── core/
│ │ ├── constants.js ← TILE, COLS, ROWS, tile types, timing, COLORS
│ │ ├── rng.js ← Seeded RNG (mulberry32) for reproducibility
│ │ └── utils.js ← Helper functions (createBorderLayout, drawHeart, etc.)
│ │
│ ├── data/
│ │ ├── themes.js ← Dungeon themes (Forest, Desert, Mountain, etc.) with colors
│ │ ├── dungeons.js ← 100 dungeon definitions (name, theme, difficulty, seed, room count)
│ │ ├── enemies.js ← 22 enemy type definitions (stats, AI, draw functions)
│ │ ├── bosses.js ← 100 boss definitions (one per dungeon)
│ │ ├── weapons.js ← 12 weapon definitions (damage, range, effects, draw functions)
│ │ └── items.js ← Consumables (potions, heart pieces, arrows, bombs, keys)
│ │
│ ├── generator/
│ │ ├── dungeon.js ← Generates full dungeon from definition (rooms + connections)
│ │ ├── rooms.js ← Generates individual rooms (layout, enemies, items, puzzles)
│ │ ├── puzzles.js ← Puzzle generation (rock-push, levers, keys, switches)
│ │ └── placement.js ← Placement logic (enemy/item density by difficulty tier)
│ │
│ ├── game/
│ │ ├── state.js ← Game state management (player, room, dungeon, menus)
│ │ ├── combat.js ← Attack resolution, damage, effects
│ │ ├── movement.js ← Player movement, rock pushing, exit handling
│ │ ├── ai.js ← Enemy AI (chase, patrol, shoot, special behaviors)
│ │ ├── items.js ← Item pickup, chest opening, inventory management
│ │ └── transitions.js ← Room transitions, screen fades
│ │
│ ├── rendering/
│ │ ├── renderer.js ← Canvas drawing (floor, walls, torches, entities)
│ │ ├── draw-enemies.js ← Per-enemy-type draw functions
│ │ ├── draw-bosses.js ← Boss draw functions (larger, multi-phase visuals)
│ │ ├── draw-weapons.js ← Weapon draw functions
│ │ ├── hud.js ← HUD rendering (hearts, weapon, ammo, room info)
│ │ └── effects.js ← Particles, hit flashes, screen transitions
│ │
│ ├── input/
│ │ ├── keyboard.js ← Key state tracking, action binding
│ │ └── actions.js ← Action definitions (move, attack, use item, select)
│ │
│ ├── menu/
│ │ ├── title-screen.js ← Title screen rendering + interaction
│ │ ├── dungeon-select.js ← Dungeon grid, filtering, info panel
│ │ ├── hud-overlay.js ← In-game menu (inventory, weapon switch)
│ │ └── screens.js ← Death/win/completion screens
│ │
│ ├── save/
│ │ └── save.js ← localStorage save/load, progress tracking
│ │
│ └── app.js ← Main bootstrap (init, game loop, screen switching)
```
---
## Implementation Phases
### Phase 1: Core Infrastructure
- [ ] Create directory structure
- [ ] `constants.js` — All shared constants (TILE, COLS, ROWS, COLORS, timing)
- [ ] `rng.js` — Seeded PRNG (mulberry32)
- [ ] `utils.js` — Reusable helpers (createBorderLayout, drawHeart, DIRS, etc.)
- [ ] `index.html` — New HTML that loads all scripts, canvas, menu div
- [ ] `app.js` — Bootstrap: init → title screen → game loop
- [ ] Validate: Open in browser, see title screen
### Phase 2: Data Definitions
- [ ] `themes.js` — 8 themes (Forest, Desert, Mountain, Underground, Ocean, Sky, Shadow, Final) with color palettes
- [ ] `dungeons.js` — 100 dungeon definitions (generated procedurally from themes)
- [ ] `enemies.js` — 22 enemy types with stats, behaviors, colors, draw functions
- [ ] `weapons.js` — 12 weapons with damage, range, cooldown, effects
- [ ] `bosses.js` — 100 bosses (one per dungeon), template-based with variations
- [ ] `items.js` — Consumables definitions
- [ ] Validate: Data files load without errors, can inspect in console
### Phase 3: Procedural Generation
- [ ] `generator/dungeon.js` — BSP/DFS room layout generation from seed
- [ ] `generator/rooms.js` — Room type assignment, layout generation
- [ ] `generator/puzzles.js` — Puzzle generation (rock-push, levers, keys)
- [ ] `generator/placement.js` — Enemy/item placement by difficulty tier
- [ ] Validate: Generate dungeon #1, inspect rooms, verify connectivity
### Phase 4: Game Engine
- [ ] `game/state.js` — Game state (player, current room, inventory, progress)
- [ ] `game/movement.js` — Player movement, rock pushing, collision
- [ ] `game/combat.js` — Attack resolution, weapon effects, damage
- [ ] `game/ai.js` — Enemy AI (chase, patrol, ranged, special)
- [ ] `game/items.js` — Chest opening, item pickup, inventory
- [ ] `game/transitions.js` — Room transitions, screen fades
- [ ] `input/keyboard.js` — Key tracking, action dispatch
- [ ] `input/actions.js` — Action definitions
- [ ] Validate: Play through generated dungeon #1 in browser
### Phase 5: Rendering
- [ ] `rendering/renderer.js` — Floor, walls, torches (theme-colored)
- [ ] `rendering/draw-enemies.js` — All 22 enemy draw functions
- [ ] `rendering/draw-bosses.js` — Boss draw functions
- [ ] `rendering/draw-weapons.js` — Weapon visual indicators
- [ ] `rendering/hud.js` — Hearts, weapon icon, ammo, room counter
- [ ] `rendering/effects.js` — Particles, hit flash, screen transitions
- [ ] Validate: Visual rendering looks correct for all entity types
### Phase 6: Menu System
- [ ] `menu/title-screen.js` — Title, start button, controls
- [ ] `menu/dungeon-select.js` — 10x10 grid, theme filter, difficulty, lock status
- [ ] `menu/screens.js` — Death, win, dungeon completion screens
- [ ] Validate: Navigate menus, select dungeon, enter game
### Phase 7: Save System
- [ ] `save/save.js` — localStorage persistence
- [ ] Save format: `{ completedDungeons: [], weapons: [], heartPieces: N, totalRooms: N }`
- [ ] Auto-save on dungeon completion
- [ ] Resume functionality
- [ ] Validate: Complete dungeon → close browser → reopen → progress saved
### Phase 8: Integration & Polish
- [ ] Connect all systems end-to-end
- [ ] Playtest first 10 dungeons
- [ ] Balance difficulty curve
- [ ] Screen transition effects
- [ ] Error handling, edge cases
- [ ] Validate: Full playthrough dungeon #1 → #10
### Phase 9: Electron Integration (Later)
- [ ] Update `preload.js` — expose save/load IPC
- [ ] Update `main.js` — if needed
- [ ] Validate: `npm start` works in Electron
---
## Technical Decisions
### Seeded RNG
- Mulberry32 algorithm: fast, deterministic, good quality for game generation
- Same seed → same dungeon every time
- Seeds stored in `dungeons.js` definitions
### Room Generation Algorithm
- Randomized DFS (depth-first search) for organic-feeling dungeons
- Room sizes: 5x5 to 12x12 tiles
- Corridors connect rooms (1-tile wide, L-shaped)
- Room types assigned by dungeon phase (early/mid/late)
### Difficulty Scaling
- Dungeon difficulty: 1-10 (based on index)
- Room count: 20 (difficulty 1-2) → 100 (difficulty 10)
- Enemy density: 0-1 per room (easy) → 2-4 per room (hard)
- Enemy types unlocked by difficulty tier
- Boss HP scales with dungeon difficulty
### State Management
- Single `game` object with nested state
- Screen states: `TITLE`, `DUNGEON_SELECT`, `PLAYING`, `DEAD`, `DUNGEON_COMPLETE`, `GAME_COMPLETE`
- Room data regenerated from seed on entry (no permanent room state)
- Player state persists across rooms within a dungeon
- Inventory persists across dungeons
### Combat System
- Weapon-based damage (varies by weapon type)
- Attack cooldowns per weapon
- Ranged weapons (bow, slingshot, rods) have projectile system
- Special effects: fire (AoE), ice (freeze), boomerang (return), bombs (destroy walls)
- Enemy HP, defense, attack damage, attack cooldown
### Enemy AI Behaviors
- `chase`: Pathfind toward player (existing behavior)
- `patrol`: Wander predefined path
- `ranged`: Shoot projectiles at player
- `stationary`: Activate on proximity
- `charge`: Rush at player in straight line
- `teleport`: Random teleportation
- `explode`: Explode on contact or timer
---
## Playtesting Hooks
### Debug Mode
- `?debug=1` URL parameter enables debug overlay
- Shows: room grid, collision boxes, AI state, generation seed
- `F1` toggles debug overlay during gameplay
- `F2` forces next room (skip puzzles)
- `F3` god mode (invincible)
- `F4` spawn debug enemy at player position
### Console API
- `generateDungeon(id)` — Generate and inspect any dungeon
- `spawnEnemy(type)` — Spawn enemy at player position
- `giveWeapon(name)` — Add weapon to inventory
- `teleportToRoom(idx)` — Jump to room by index
- `dumpState()` — Print full game state to console
### Performance
- Room rendering only draws visible tiles + buffer
- Enemy AI updates throttled by type
- Particle system capped at 100 particles
---
## Browser Testing
- `npm run dev` → `npx serve .` for local HTTP server
- Open `http://localhost:3000` in browser
- `npm start` → Electron app (for later)

549
generate-bosses.js Normal file
View File

@ -0,0 +1,549 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const { DUNGEONS } = require('./src/data/dungeons');
const themeColors = {
stone: { body: '#696969', eye: '#FFD700', accent: '#A9A9A9' },
forest: { body: '#228B22', eye: '#90EE90', accent: '#006400' },
desert: { body: '#DAA520', eye: '#8B4513', accent: '#FFD700' },
mountain: { body: '#4682B4', eye: '#E0E0E0', accent: '#87CEEB' },
underground: { body: '#4B0082', eye: '#DA70D6', accent: '#9400D3' },
ocean: { body: '#006994', eye: '#00CED1', accent: '#4682B4' },
sky: { body: '#FFD700', eye: '#FFFACD', accent: '#FF8C00' },
shadow: { body: '#1A0A2A', eye: '#9400D3', accent: '#4B0082' },
final: { body: '#8B0000', eye: '#FFD700', accent: '#FF4500' },
};
const templateAssignments = {
0: 'gleeok', 11: 'gleeok', 22: 'gleeok', 33: 'gleeok', 44: 'gleeok',
55: 'gleeok', 66: 'gleeok', 77: 'gleeok', 88: 'gleeok', 99: 'gleeok',
1: 'darknut', 10: 'darknut', 21: 'darknut', 32: 'darknut', 43: 'darknut',
54: 'darknut', 65: 'darknut', 76: 'darknut', 87: 'darknut', 98: 'darknut',
2: 'wizzrobe', 12: 'wizzrobe', 23: 'wizzrobe', 34: 'wizzrobe', 45: 'wizzrobe',
56: 'wizzrobe', 67: 'wizzrobe', 78: 'wizzrobe', 89: 'wizzrobe', 97: 'wizzrobe',
3: 'moldorm', 13: 'moldorm', 24: 'moldorm', 35: 'moldorm', 46: 'moldorm',
57: 'moldorm', 68: 'moldorm', 79: 'moldorm', 90: 'moldorm', 96: 'moldorm',
4: 'genie', 14: 'genie', 25: 'genie', 36: 'genie', 47: 'genie',
58: 'genie', 69: 'genie', 80: 'genie', 91: 'genie', 95: 'genie',
5: 'liklike', 15: 'liklike', 26: 'liklike', 37: 'liklike', 48: 'liklike',
59: 'liklike', 70: 'liklike', 81: 'liklike', 92: 'liklike', 94: 'liklike',
6: 'aquamentus', 16: 'aquamentus', 27: 'aquamentus', 38: 'aquamentus', 49: 'aquamentus',
60: 'aquamentus', 71: 'aquamentus', 82: 'aquamentus', 93: 'aquamentus',
7: 'manhandla', 17: 'manhandla', 28: 'manhandla', 39: 'manhandla', 50: 'manhandla',
61: 'manhandla', 72: 'manhandla', 83: 'manhandla',
8: 'gleeok_variant', 18: 'gleeok_variant', 29: 'gleeok_variant', 40: 'gleeok_variant',
51: 'gleeok_variant', 62: 'gleeok_variant', 73: 'gleeok_variant', 84: 'gleeok_variant',
9: 'agahnim', 19: 'agahnim', 30: 'agahnim', 41: 'agahnim', 52: 'agahnim',
63: 'agahnim', 74: 'agahnim', 85: 'agahnim',
// Fill remaining gaps not covered by the mod-11 pattern
20: 'genie', 31: 'liklike', 42: 'moldorm', 53: 'wizzrobe', 64: 'darknut', 75: 'gleeok',
86: 'moldorm',
};
// Verify all 100 indices covered
for (let i = 0; i < 100; i++) {
if (!templateAssignments[i]) {
console.error('ERROR: No template for index ' + i);
process.exit(1);
}
}
const nameTemplates = {
gleeok: {
stone: 'Stone Gleeok', forest: 'Verdant Gleeok', desert: 'Sand Gleeok',
mountain: 'Frost Gleeok', underground: 'Shadow Gleeok', ocean: 'Tidal Gleeok',
sky: 'Storm Gleeok', shadow: 'Dark Gleeok', final: 'Infernal Gleeok',
},
darknut: {
stone: 'Stone Darknut', forest: 'Green Darknut', desert: 'Sand Darknut',
mountain: 'Iron Darknut', underground: 'Deep Darknut', ocean: 'Abyssal Darknut',
sky: 'Sky Darknut', shadow: 'Shadow Darknut', final: 'Demon Darknut',
},
wizzrobe: {
stone: 'Stone Wizzrobe', forest: 'Thorn Wizzrobe', desert: 'Mirage Wizzrobe',
mountain: 'Frost Wizzrobe', underground: 'Crystal Wizzrobe', ocean: 'Tide Wizzrobe',
sky: 'Storm Wizzrobe', shadow: 'Void Wizzrobe', final: 'Dark Wizzrobe',
},
moldorm: {
stone: 'Stone Moldorm', forest: 'Root Moldorm', desert: 'Dune Moldorm',
mountain: 'Frost Moldorm', underground: 'Deep Moldorm', ocean: 'Abyssal Moldorm',
sky: 'Cloud Moldorm', shadow: 'Shadow Moldorm', final: 'Abyssal Moldorm',
},
genie: {
stone: 'Stone Genie', forest: 'Moss Genie', desert: 'Sand Genie',
mountain: 'Ice Genie', underground: 'Crystal Genie', ocean: 'Tidal Genie',
sky: 'Sky Genie', shadow: 'Dark Genie', final: 'Demon Genie',
},
liklike: {
stone: 'Stone Like Like', forest: 'Moss Like Like', desert: 'Dune Like Like',
mountain: 'Frost Like Like', underground: 'Deep Like Like', ocean: 'Abyssal Like Like',
sky: 'Cloud Like Like', shadow: 'Shadow Like Like', final: 'Abyssal Like Like',
},
aquamentus: {
stone: 'Stone Aquamentus', forest: 'Verdant Aquamentus', desert: 'Sand Aquamentus',
mountain: 'Frost Aquamentus', underground: 'Crystal Aquamentus', ocean: 'Tidal Aquamentus',
sky: 'Storm Aquamentus', shadow: 'Dark Aquamentus', final: 'Infernal Aquamentus',
},
manhandla: {
stone: 'Stone Manhandla', forest: 'Verdant Manhandla', desert: 'Dune Manhandla',
mountain: 'Frost Manhandla', underground: 'Crystal Manhandla', ocean: 'Abyssal Manhandla',
sky: 'Storm Manhandla', shadow: 'Dark Manhandla', final: 'Infernal Manhandla',
},
gleeok_variant: {
stone: 'Stone Ice Gleeok', forest: 'Forest Ice Gleeok', desert: 'Desert Ice Gleeok',
mountain: 'Glacier Gleeok', underground: 'Shadow Ice Gleeok', ocean: 'Abyssal Water Gleeok',
sky: 'Sky Ice Gleeok', shadow: 'Dark Ice Gleeok', final: 'Infernal Ice Gleeok',
},
agahnim: {
stone: 'Stone Agahnim', forest: 'Verdant Agahnim', desert: 'Sand Agahnim',
mountain: 'Frost Agahnim', underground: 'Crystal Agahnim', ocean: 'Tidal Agahnim',
sky: 'Storm Agahnim', shadow: 'Void Agahnim', final: 'Dark Agahnim',
},
};
function getStatsByDifficulty(diff) {
if (diff <= 2) return { hp: 20 + (diff - 1) * 10, damage: 3 + (diff - 1), defense: diff - 1 };
if (diff <= 4) return { hp: 40 + (diff - 3) * 20, damage: 4 + (diff - 3), defense: Math.min(diff - 2, 2) };
if (diff <= 6) return { hp: 80 + (diff - 5) * 35, damage: 5 + (diff - 5), defense: Math.min(diff - 4, 3) };
if (diff <= 8) return { hp: 150 + (diff - 7) * 50, damage: 6 + (diff - 7) * 2, defense: Math.min(diff - 5, 4) };
return { hp: 250 + (diff - 9) * 250, damage: 8 + (diff - 9) * 7, defense: Math.min(diff - 6, 5) };
}
function generatePhases(template, diff, baseDamage) {
const phases = [];
const scaledDmg = Math.round(baseDamage * 1.3);
switch (template) {
case 'gleeok':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'aoe', damage: scaledDmg, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2 },
{ type: 'melee', damage: baseDamage, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
if (diff >= 4) {
phases.push({
hpThreshold: 60,
attacks: [
{ type: 'aoe', damage: scaledDmg + 1, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5 },
{ type: 'melee', damage: baseDamage + 1, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
}
if (diff >= 7) {
phases.push({
hpThreshold: 30,
attacks: [
{ type: 'aoe', damage: scaledDmg + 2, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 3 },
{ type: 'melee', damage: baseDamage + 2, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
}
break;
case 'gleeok_variant':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'aoe', damage: scaledDmg, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5 },
{ type: 'melee', damage: baseDamage, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
if (diff >= 4) {
phases.push({
hpThreshold: 60,
attacks: [
{ type: 'aoe', damage: scaledDmg + 1, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2 },
{ type: 'melee', damage: baseDamage + 1, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
}
if (diff >= 7) {
phases.push({
hpThreshold: 30,
attacks: [
{ type: 'aoe', damage: scaledDmg + 2, cooldown: 1400, count: 4, spreadAngle: 360, projectileSpeed: 2.5 },
{ type: 'melee', damage: baseDamage + 2, cooldown: 1100, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
}
break;
case 'darknut':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'melee', damage: baseDamage, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
phases.push({
hpThreshold: 50,
attacks: [
{ type: 'melee', damage: scaledDmg, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0 },
{ type: 'melee', damage: scaledDmg, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0 },
],
});
if (diff >= 6) {
phases.push({
hpThreshold: 25,
attacks: [
{ type: 'melee', damage: scaledDmg + 2, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0 },
{ type: 'aoe', damage: baseDamage, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0 },
],
});
}
break;
case 'wizzrobe':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'projectile', damage: scaledDmg, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3 },
],
});
if (diff >= 3) {
phases.push({
hpThreshold: 40,
attacks: [
{ type: 'projectile', damage: scaledDmg + 1, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5 },
{ type: 'summon', damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: diff < 6 ? 'keese' : 'stalfos' },
],
});
}
break;
case 'moldorm':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'melee', damage: baseDamage, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
phases.push({
hpThreshold: 30,
attacks: [
{ type: 'aoe', damage: scaledDmg, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0 },
{ type: 'melee', damage: scaledDmg, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
break;
case 'genie':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'projectile', damage: scaledDmg, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5 },
],
});
if (diff >= 3) {
phases.push({
hpThreshold: 50,
attacks: [
{ type: 'projectile', damage: scaledDmg, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5 },
],
});
}
if (diff >= 6) {
phases.push({
hpThreshold: 20,
attacks: [
{ type: 'aoe', damage: scaledDmg + 1, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3 },
{ type: 'projectile', damage: scaledDmg, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5 },
],
});
}
break;
case 'liklike':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'melee', damage: baseDamage, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0 },
],
});
if (diff >= 3) {
phases.push({
hpThreshold: 40,
attacks: [
{ type: 'melee', damage: scaledDmg, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0 },
{ type: 'summon', damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: 'slime' },
],
});
}
break;
case 'aquamentus':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'projectile', damage: scaledDmg, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2 },
],
});
phases.push({
hpThreshold: 50,
attacks: [
{ type: 'aoe', damage: scaledDmg, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5 },
],
});
if (diff >= 6) {
phases.push({
hpThreshold: 20,
attacks: [
{ type: 'aoe', damage: scaledDmg + 2, cooldown: 1200, count: 3, spreadAngle: 180, projectileSpeed: 3 },
],
});
}
break;
case 'manhandla':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'melee', damage: baseDamage, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0 },
],
});
if (diff >= 3) {
phases.push({
hpThreshold: 30,
attacks: [
{ type: 'aoe', damage: scaledDmg, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2 },
{ type: 'melee', damage: scaledDmg, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0 },
],
});
}
break;
case 'agahnim':
phases.push({
hpThreshold: 100,
attacks: [
{ type: 'projectile', damage: scaledDmg, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5 },
],
});
phases.push({
hpThreshold: 60,
attacks: [
{ type: 'melee', damage: scaledDmg, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0 },
{ type: 'projectile', damage: scaledDmg, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5 },
],
});
phases.push({
hpThreshold: 30,
attacks: [
{ type: 'projectile', damage: scaledDmg, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5 },
],
});
phases.push({
hpThreshold: 10,
attacks: [
{ type: 'melee', damage: scaledDmg + 2, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0 },
{ type: 'aoe', damage: scaledDmg, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3 },
],
});
break;
}
return phases;
}
function generateDrops(template, diff) {
const drops = [{ item: 'heart_piece', weight: 100 }];
if (diff >= 2) drops.push({ item: 'healing_potion', weight: 30 });
if (diff >= 4) drops.push({ item: 'super_potion', weight: 15 });
if (diff >= 6) drops.push({ item: 'magic_potion', weight: 10 });
if (diff >= 8) drops.push({ item: 'heart_piece', weight: 20 });
switch (template) {
case 'gleeok':
case 'gleeok_variant':
if (diff >= 5) drops.push({ item: 'fire_rod', weight: 5 });
if (diff >= 8) drops.push({ item: 'master_sword', weight: 3 });
break;
case 'darknut':
if (diff >= 3) drops.push({ item: 'hammer', weight: 8 });
if (diff >= 7) drops.push({ item: 'magic_sword', weight: 3 });
break;
case 'wizzrobe':
if (diff >= 4) drops.push({ item: 'ice_rod', weight: 5 });
if (diff >= 7) drops.push({ item: 'magic_potion', weight: 15 });
break;
case 'agahnim':
if (diff >= 6) drops.push({ item: 'magic_sword', weight: 5 });
break;
case 'aquamentus':
if (diff >= 5) drops.push({ item: 'fire_rod', weight: 5 });
break;
case 'manhandla':
if (diff >= 4) drops.push({ item: 'boomerang', weight: 5 });
break;
case 'moldorm':
if (diff >= 6) drops.push({ item: 'hookshot', weight: 3 });
break;
case 'genie':
if (diff >= 5) drops.push({ item: 'magic_crystal', weight: 10 });
break;
case 'liklike':
if (diff >= 3) drops.push({ item: 'super_potion', weight: 20 });
break;
}
return drops;
}
function generateBosses() {
const bosses = [];
const descriptions = {
gleeok: 'A multi-headed dragon whose fiery breath devastates all.',
darknut: 'A heavily-armored knight wielding a massive sword.',
wizzrobe: 'A teleporting mage who commands destructive magic.',
moldorm: 'A giant segmented worm that charges relentlessly.',
genie: 'A lamp spirit that unleashes devastating elemental blasts.',
liklike: 'A voracious worm that steals hearts and shields.',
aquamentus: 'A dragon that breathes flames at everything in sight.',
manhandla: 'A spinning plant monster with razor-sharp limbs.',
gleeok_variant: 'A spectral dragon wielding ice and shadow.',
agahnim: 'A dark mage protected by impenetrable barriers.',
};
for (let i = 0; i < 100; i++) {
const dungeon = DUNGEONS[i];
const template = templateAssignments[i];
const theme = dungeon.theme;
const diff = dungeon.difficulty;
const stats = getStatsByDifficulty(diff);
const colors = themeColors[theme];
let hp = stats.hp;
let damage = stats.damage;
let defense = stats.defense;
let speed = 0;
let behavior = 'chase';
let drawFunction = 'drawGeneric';
switch (template) {
case 'gleeok':
case 'gleeok_variant':
hp = Math.round(hp * 1.3);
speed = 1.5;
behavior = 'charge';
drawFunction = 'drawGleeok';
break;
case 'darknut':
defense = Math.min(defense + 1, 5);
speed = 1.2;
behavior = 'chase';
drawFunction = 'drawDarknut';
break;
case 'wizzrobe':
damage = Math.round(damage * 0.8);
defense = Math.max(defense - 1, 0);
speed = 0;
behavior = 'teleport';
drawFunction = 'drawWizzrobe';
break;
case 'moldorm':
hp = Math.round(hp * 0.7);
speed = 2.0;
behavior = 'charge';
break;
case 'genie':
hp = Math.round(hp * 1.1);
speed = 0;
behavior = 'stationary';
break;
case 'liklike':
hp = Math.round(hp * 0.8);
speed = 1.0;
behavior = 'chase';
break;
case 'aquamentus':
hp = Math.round(hp * 1.2);
speed = 0;
behavior = 'stationary';
drawFunction = 'drawGleeok';
break;
case 'manhandla':
speed = 1.8;
behavior = 'charge';
break;
case 'agahnim':
damage = Math.round(damage * 1.2);
defense = Math.min(defense + 1, 5);
speed = 0;
behavior = 'stationary';
drawFunction = 'drawWizzrobe';
break;
}
const name = nameTemplates[template] ? nameTemplates[template][theme] : (theme + ' ' + template);
const phases = generatePhases(template, diff, damage);
const drops = generateDrops(template, diff);
bosses.push({
id: i, name, hp, damage, defense, speed, colors, behavior,
drawFunction, phases, drops, description: descriptions[template],
});
}
return bosses;
}
const bosses = generateBosses();
let output = "'use strict';\n\nconst BOSS_TYPES = [\n";
for (const boss of bosses) {
output += ' { id: ' + boss.id + ', ';
output += "name: '" + boss.name + "', ";
output += 'hp: ' + boss.hp + ', ';
output += 'damage: ' + boss.damage + ', ';
output += 'defense: ' + boss.defense + ', ';
output += 'speed: ' + boss.speed + ', ';
output += 'colors: { body: "' + boss.colors.body + '", eye: "' + boss.colors.eye + '", accent: "' + boss.colors.accent + '" }, ';
output += 'behavior: "' + boss.behavior + '", ';
output += 'drawFunction: "' + boss.drawFunction + '", ';
output += 'description: "' + boss.description + '", ';
output += 'phases: [';
for (let p = 0; p < boss.phases.length; p++) {
const phase = boss.phases[p];
output += '{ hpThreshold: ' + phase.hpThreshold + ', attacks: [';
for (let a = 0; a < phase.attacks.length; a++) {
const attack = phase.attacks[a];
output += '{ type: "' + attack.type + '", damage: ' + attack.damage + ', cooldown: ' + attack.cooldown + ', count: ' + attack.count + ', spreadAngle: ' + attack.spreadAngle + ', projectileSpeed: ' + attack.projectileSpeed;
if (attack.enemyType) output += ', enemyType: "' + attack.enemyType + '"';
output += '}';
if (a < phase.attacks.length - 1) output += ', ';
}
output += ']}';
if (p < boss.phases.length - 1) output += ', ';
}
output += '], ';
output += 'drops: [';
for (let d = 0; d < boss.drops.length; d++) {
const drop = boss.drops[d];
output += '{ item: "' + drop.item + '", weight: ' + drop.weight + ' }';
if (d < boss.drops.length - 1) output += ', ';
}
output += ']';
output += ' },\n';
}
output += '];\n\n';
output += 'function getBossType(id) {\n';
output += ' return BOSS_TYPES.find(b => b.id === id);\n';
output += '}\n\n';
output += 'module.exports = { BOSS_TYPES, getBossType };\n';
fs.writeFileSync(path.join(__dirname, 'src/data/bosses.js'), output);
console.log('Generated ' + bosses.length + ' bosses successfully.');
const { BOSS_TYPES } = require('./src/data/bosses');
console.log('Verification: ' + BOSS_TYPES.length + ' bosses loaded.');

263
index.html Normal file
View File

@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zelda Dungeon Quest</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
font-family: monospace;
user-select: none;
}
#game-container {
position: relative;
width: 640px;
height: 480px;
}
canvas {
border: 2px solid #555;
image-rendering: pixelated;
display: block;
}
#menu-overlay {
position: absolute;
top: 0; left: 0;
width: 640px;
height: 480px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
}
#menu-overlay.active {
pointer-events: auto;
}
.menu-title {
font-size: 36px;
font-weight: bold;
color: #FFD700;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
margin-bottom: 20px;
}
.menu-subtitle {
font-size: 16px;
color: #AAAAAA;
margin-bottom: 30px;
}
.menu-button {
font-family: monospace;
font-size: 16px;
color: #FFFFFF;
background: rgba(0,0,0,0.6);
border: 2px solid #FFD700;
padding: 8px 24px;
margin: 4px 0;
cursor: pointer;
transition: all 0.15s;
pointer-events: auto;
}
.menu-button:hover, .menu-button.selected {
background: #FFD700;
color: #000;
}
.menu-button:disabled {
opacity: 0.3;
cursor: default;
border-color: #444;
}
.menu-button:disabled:hover {
background: rgba(0,0,0,0.6);
color: #FFFFFF;
}
.dungeon-grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 3px;
max-width: 600px;
margin: 10px 0;
}
.dungeon-cell {
width: 56px;
height: 42px;
font-family: monospace;
font-size: 10px;
text-align: center;
line-height: 42px;
cursor: pointer;
border: 1px solid #555;
transition: all 0.1s;
pointer-events: auto;
}
.dungeon-cell:hover:not(.locked) {
transform: scale(1.1);
border-color: #FFD700;
}
.dungeon-cell.selected {
border: 2px solid #FFD700;
box-shadow: 0 0 6px rgba(255,215,0,0.5);
}
.dungeon-cell.locked {
opacity: 0.3;
cursor: default;
}
.dungeon-cell.completed {
border-color: #00FF00;
}
.dungeon-cell.completed::after {
content: '✓';
position: absolute;
bottom: 1px;
right: 2px;
font-size: 8px;
color: #00FF00;
}
.filter-tabs {
display: flex;
gap: 2px;
margin-bottom: 10px;
}
.filter-tab {
font-family: monospace;
font-size: 11px;
padding: 3px 8px;
background: rgba(0,0,0,0.5);
border: 1px solid #555;
color: #AAA;
cursor: pointer;
pointer-events: auto;
}
.filter-tab.active {
background: #FFD700;
color: #000;
border-color: #FFD700;
}
.info-panel {
font-family: monospace;
font-size: 12px;
color: #CCC;
background: rgba(0,0,0,0.7);
border: 1px solid #555;
padding: 8px 12px;
margin-top: 8px;
max-width: 400px;
text-align: center;
pointer-events: auto;
}
.info-panel .name {
color: #FFD700;
font-weight: bold;
font-size: 14px;
}
.info-panel .difficulty {
color: #FF6347;
}
.info-panel .theme {
color: #87CEEB;
}
.info-panel .rooms {
color: #90EE90;
}
.controls-hint {
font-size: 11px;
color: #888;
margin-top: 15px;
}
.debug-overlay {
position: absolute;
top: 0; left: 0;
width: 640px;
height: 480px;
pointer-events: none;
z-index: 20;
font-family: monospace;
font-size: 10px;
color: #0F0;
display: none;
}
.debug-overlay.visible {
display: block;
}
#debug-canvas {
position: absolute;
top: 0; left: 0;
pointer-events: none;
z-index: 19;
display: none;
}
#debug-canvas.visible {
display: block;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game" width="640" height="480"></canvas>
<canvas id="debug-canvas" width="640" height="480"></canvas>
<div id="menu-overlay"></div>
<div id="debug-overlay" class="debug-overlay"></div>
</div>
<!-- Browser polyfill: enables module.exports/require in browser -->
<script src="src/browser-polyfill.js"></script>
<!-- Core -->
<script src="src/core/constants.js"></script>
<script src="src/core/rng.js"></script>
<script src="src/core/utils.js"></script>
<!-- Data -->
<script src="src/data/themes.js"></script>
<script src="src/data/dungeons.js"></script>
<script src="src/data/enemies.js"></script>
<script src="src/data/weapons.js"></script>
<script src="src/data/bosses.js"></script>
<script src="src/data/items.js"></script>
<!-- Generator -->
<script src="src/generator/rooms.js"></script>
<script src="src/generator/puzzles.js"></script>
<script src="src/generator/placement.js"></script>
<script src="src/generator/dungeon.js"></script>
<!-- Save -->
<script src="src/save/save.js"></script>
<!-- Game Engine -->
<script src="src/game/state.js"></script>
<script src="src/game/movement.js"></script>
<script src="src/game/combat.js"></script>
<script src="src/game/ai.js"></script>
<script src="src/game/items.js"></script>
<script src="src/game/transitions.js"></script>
<!-- Input -->
<script src="src/input/keyboard.js"></script>
<script src="src/input/actions.js"></script>
<!-- Rendering -->
<script src="src/rendering/renderer.js"></script>
<script src="src/rendering/draw-enemies.js"></script>
<script src="src/rendering/draw-bosses.js"></script>
<script src="src/rendering/draw-weapons.js"></script>
<script src="src/rendering/hud.js"></script>
<script src="src/rendering/effects.js"></script>
<!-- Menu -->
<script src="src/menu/title-screen.js"></script>
<script src="src/menu/dungeon-select.js"></script>
<script src="src/menu/hud-overlay.js"></script>
<script src="src/menu/screens.js"></script>
<!-- Bootstrap -->
<script src="src/app.js"></script>
</body>
</html>

22
main.js Normal file
View File

@ -0,0 +1,22 @@
const { app, BrowserWindow } = require("electron");
const path = require("path");
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "preload.js"),
},
});
win.loadFile(path.join(__dirname, "index.html"));
}
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});

801
package-lock.json generated Normal file
View File

@ -0,0 +1,801 @@
{
"name": "zelda-dungeon",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "zelda-dungeon",
"version": "1.0.0",
"dependencies": {
"electron": "^28.0.0"
}
},
"node_modules/@electron/get": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
"got": "^11.8.5",
"progress": "^2.0.3",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"global-agent": "^3.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"license": "MIT"
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.19.130",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT",
"optional": true
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"license": "MIT",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"license": "MIT",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"license": "MIT",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"license": "MIT",
"optional": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"license": "MIT",
"optional": true
},
"node_modules/electron": {
"version": "28.3.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz",
"integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^18.11.18",
"extract-zip": "^2.0.1"
},
"bin": {
"electron": "cli.js"
},
"engines": {
"node": ">= 12.20.55"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"license": "MIT",
"optional": true
},
"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==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"engines": {
"node": ">=10.0"
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"define-properties": "^1.2.1",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"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==",
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC",
"optional": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"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==",
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"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==",
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"semver-compare": "^1.0.0",
"sprintf-js": "^1.1.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"license": "MIT",
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"license": "MIT",
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "zelda-dungeon",
"version": "1.0.0",
"description": "A top-down Legend of Zelda-style dungeon game",
"main": "main.js",
"scripts": {
"start": "electron . --no-sandbox",
"start:safe": "ELECTRON_DISABLE_SANDBOX=1 electron . --no-sandbox",
"dev": "npx serve . -p 3000"
},
"dependencies": {
"electron": "^28.0.0"
}
}

8
preload.js Normal file
View File

@ -0,0 +1,8 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
saveGame: (data) => ipcRenderer.send('save-game', data),
loadGame: () => ipcRenderer.invoke('load-game'),
clearSave: () => ipcRenderer.send('clear-save'),
hasSave: () => ipcRenderer.invoke('has-save'),
});

963
renderer.js Normal file
View File

@ -0,0 +1,963 @@
'use strict';
// === SETUP ===
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// === CONSTANTS ===
const TILE = 32;
const COLS = 20;
const ROWS = 15;
const T_FLOOR = 0;
const T_WALL = 1;
const T_TORCH = 5;
const MOVE_COOLDOWN = 100;
const ATTACK_COOLDOWN = 400;
const ATTACK_ANIM = 250;
const ENEMY_MOVE_INTERVAL = 800;
const ENEMY_ATTACK_INTERVAL = 1500;
const HIT_FLASH = 200;
const MESSAGE_DURATION = 2000;
const COLORS = {
floor: '#C4A265',
floorDark: '#A8884D',
wall: '#505050',
wallTop: '#6E6E6E',
wallFront: '#5A5A5A',
wallBottom: '#383838',
playerBody: '#228B22',
playerTunic: '#1A6B1A',
playerHead: '#FFD700',
playerEyes: '#000080',
playerHair: '#8B4513',
rock: '#888888',
rockDark: '#666666',
rockLight: '#AAAAAA',
target: '#FFD700',
targetGlow: 'rgba(255,215,0,0.3)',
chestClosed: '#8B4513',
chestGold: '#DAA520',
chestOpen: '#A0522D',
swordBlade: '#C0C0C0',
swordHandle: '#8B4513',
swordGem: '#FF0000',
goblin: '#8B0000',
goblinLight: '#CC2222',
goblinEye: '#00FF00',
goblinTeeth: '#FFFFFF',
torchStick: '#654321',
torchFlame: '#FF8C00',
torchGlow: 'rgba(255,140,0,0.15)',
door: '#654321',
doorFrame: '#8B7355',
doorKnob: '#FFD700',
heart: '#FF0000',
heartEmpty: '#444444',
msgText: '#FFFFFF',
overlay: 'rgba(0,0,0,0.75)',
hitFlash: 'rgba(255,0,0,0.3)',
};
const DIRS = [
{ x: 0, y: 1 },
{ x: 0, y: -1 },
{ x: -1, y: 0 },
{ x: 1, y: 0 },
];
const DIR_NAMES = ['Down', 'Up', 'Left', 'Right'];
// === HELPER FUNCTIONS ===
function createBorderLayout(walls, torches) {
const layout = [];
for (let y = 0; y < ROWS; y++) {
const row = [];
for (let x = 0; x < COLS; x++) {
if (y === 0 || y === ROWS - 1 || x === 0 || x === COLS - 1) {
row.push(T_WALL);
} else {
row.push(T_FLOOR);
}
}
layout.push(row);
}
for (const [wx, wy] of walls) {
if (wy >= 0 && wy < ROWS && wx >= 0 && wx < COLS) {
layout[wy][wx] = T_WALL;
}
}
for (const [tx, ty] of torches) {
if (ty >= 0 && ty < ROWS && tx >= 0 && tx < COLS) {
layout[ty][tx] = T_TORCH;
}
}
return layout;
}
function drawHeart(ctx, cx, cy, size, filled) {
ctx.save();
ctx.fillStyle = filled ? COLORS.heart : COLORS.heartEmpty;
ctx.beginPath();
const s = size * 0.5;
ctx.moveTo(cx, cy + s * 0.3);
ctx.bezierCurveTo(cx, cy - s * 0.5, cx - s * 1.2, cy - s * 0.5, cx - s, cy + s * 0.1);
ctx.bezierCurveTo(cx - s, cy + s * 0.7, cx, cy + s * 1.1, cx, cy + s * 1.1);
ctx.bezierCurveTo(cx, cy + s * 0.7, cx + s, cy + s * 0.1, cx + s, cy + s * 0.1);
ctx.bezierCurveTo(cx + s * 1.2, cy - s * 0.5, cx, cy - s * 0.5, cx, cy + s * 0.3);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// === ROOM DEFINITIONS ===
const ROOMS = [
// Room 1: The Stone Chamber (Rock Puzzle)
{
name: 'The Stone Chamber',
layout: createBorderLayout(
[[5,1],[6,1],[7,1],[8,1],[9,1],[5,2],[6,2],[7,2],[8,2],[9,2],[5,3],[6,3],[7,3],[8,3],[9,3],[5,4],[6,4],[7,4],[8,4],[9,4],[5,7],[6,7],[7,7],[8,7],[9,7],[5,8],[6,8],[7,8],[8,8],[9,8],[5,9],[6,9],[7,9],[8,9],[9,9],[5,10],[6,10],[7,10],[8,10],[9,10]],
[[2,1],[17,1],[2,13],[17,13]]
),
playerStart: { x: 1, y: 1 },
rocks: [{ x: 11, y: 4 }],
targets: [{ x: 11, y: 3 }],
chests: [],
enemies: [],
exit: { x: 10, y: 14, dir: 0 },
torches: [[2, 1], [17, 1], [2, 13], [17, 13]],
exitOpen: false,
},
// Room 2: The Armory (Sword Chest)
{
name: 'The Armory',
layout: createBorderLayout(
[[9,6],[10,6],[11,6],[9,8],[10,8],[11,8]],
[[2,1],[17,1],[2,13],[17,13]]
),
playerStart: { x: 1, y: 7 },
rocks: [],
targets: [],
chests: [{ x: 10, y: 7, opened: false }],
enemies: [],
exit: { x: 18, y: 7, dir: 3 },
torches: [[2, 1], [17, 1], [2, 13], [17, 13]],
exitOpen: true,
},
// Room 3: The Goblin Lair (Boss Fight)
{
name: 'The Goblin Lair',
layout: createBorderLayout(
[[5,1],[6,1],[7,1],[8,1],[9,1],[10,1],[11,1],[12,1],[13,1],[5,12],[6,12],[7,12],[8,12],[9,12],[10,12],[11,12],[12,12],[13,12]],
[[2,1],[17,1],[2,13],[17,13]]
),
playerStart: { x: 1, y: 7 },
rocks: [],
targets: [],
chests: [],
enemies: [{ x: 16, y: 7, hp: 3, maxHp: 3, moveTimer: 0, attackTimer: 0, hitTimer: 0 }],
exit: null,
torches: [[2, 1], [17, 1], [2, 13], [17, 13]],
exitOpen: false,
},
];
// === GAME STATE ===
let game = {
roomIdx: 0,
layout: null,
player: {
x: 0, y: 0,
hp: 12, maxHp: 12,
hasSword: false,
dir: 0,
moveTimer: 0,
attackTimer: 0,
attackAnim: 0,
hitTimer: 0,
},
rocks: [],
targets: [],
chests: [],
enemies: [],
exit: null,
exitOpen: false,
torches: [],
message: '',
messageTimer: 0,
won: false,
dead: false,
moveCooldown: 0,
};
// === INPUT ===
const keys = {};
document.addEventListener('keydown', (e) => {
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space','Enter'].includes(e.code)) {
e.preventDefault();
}
keys[e.code] = true;
if (e.code === 'Space') {
if (game.won || game.dead) return;
tryAttack();
}
if (e.code === 'Enter' && (game.won || game.dead)) {
restart();
}
});
document.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
// === GAME LOGIC ===
function loadRoom(idx) {
game.roomIdx = idx;
const room = ROOMS[idx];
game.layout = room.layout.map(row => [...row]);
game.player.x = room.playerStart.x;
game.player.y = room.playerStart.y;
game.rocks = room.rocks.map(r => ({ ...r }));
game.targets = room.targets.map(t => ({ ...t }));
game.chests = room.chests.map(c => ({ ...c }));
game.enemies = room.enemies.map(e => ({ ...e, moveTimer: ENEMY_MOVE_INTERVAL, attackTimer: ENEMY_ATTACK_INTERVAL, hitTimer: 0 }));
game.exit = room.exit ? { ...room.exit } : null;
game.exitOpen = room.exitOpen;
game.torches = room.torches ? room.torches.map(t => [...t]) : [];
game.message = room.name;
game.messageTimer = MESSAGE_DURATION;
}
function tryMove(dx, dy) {
if (game.won || game.dead) return;
const px = game.player.x;
const py = game.player.y;
const nx = px + dx;
const ny = py + dy;
if (dx === 0 && dy === 0) return;
if (dy === 1 && game.player.dir !== 0) game.player.dir = 0;
else if (dy === -1 && game.player.dir !== 1) game.player.dir = 1;
else if (dx === -1 && game.player.dir !== 2) game.player.dir = 2;
else if (dx === 1 && game.player.dir !== 3) game.player.dir = 3;
if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) return;
// Check exit before wall check so doors on border walls work
if (game.exit && game.exit.x === nx && game.exit.y === ny) {
tryExit();
return;
}
if (game.layout[ny][nx] === T_WALL) return;
const rock = game.rocks.find(r => r.x === nx && r.y === ny);
if (rock) {
const rnx = nx + dx;
const rny = ny + dy;
if (rnx < 0 || rnx >= COLS || rny < 0 || rny >= ROWS) return;
if (game.layout[rny][rnx] === T_WALL) return;
if (game.rocks.find(r => r.x === rnx && r.y === rny)) return;
if (game.enemies.find(e => e.x === rnx && e.y === rny)) return;
if (game.exit && game.exit.x === rnx && game.exit.y === rny) return;
if (game.chests.find(c => c.x === rnx && c.y === rny)) return;
rock.x = rnx;
rock.y = rny;
}
if (game.enemies.find(e => e.x === nx && e.y === ny)) return;
game.player.x = nx;
game.player.y = ny;
const chest = game.chests.find(c => c.x === nx && c.y === ny);
if (chest && !chest.opened) {
openChest(chest);
return;
}
checkPuzzle();
}
function openChest(chest) {
chest.opened = true;
game.player.hasSword = true;
game.player.hp = Math.min(game.player.hp + 6, game.player.maxHp);
game.message = 'You found the Master Sword! (+6 HP)';
game.messageTimer = MESSAGE_DURATION;
}
function tryAttack() {
if (game.player.attackTimer > 0) return;
if (!game.player.hasSword) {
game.message = 'You need a weapon!';
game.messageTimer = MESSAGE_DURATION;
return;
}
game.player.attackAnim = ATTACK_ANIM;
game.player.attackTimer = ATTACK_COOLDOWN;
const dir = DIRS[game.player.dir];
const ax = game.player.x + dir.x;
const ay = game.player.y + dir.y;
for (const enemy of game.enemies) {
if (enemy.x === ax && enemy.y === ay) {
enemy.hp -= 1;
enemy.hitTimer = HIT_FLASH;
if (enemy.hp <= 0) {
game.message = 'Enemy defeated!';
game.messageTimer = MESSAGE_DURATION;
}
}
}
game.enemies = game.enemies.filter(e => e.hp > 0);
if (game.enemies.length === 0 && !game.exit) {
game.won = true;
}
}
function updateEnemies(dt) {
for (const enemy of [...game.enemies]) {
enemy.moveTimer -= dt;
enemy.attackTimer -= dt;
if (enemy.hitTimer > 0) enemy.hitTimer -= dt;
if (enemy.moveTimer <= 0) {
enemy.moveTimer = ENEMY_MOVE_INTERVAL;
const dx = game.player.x - enemy.x;
const dy = game.player.y - enemy.y;
let bestDir = null;
let bestDist = Math.abs(dx) + Math.abs(dy);
for (let d = 0; d < 4; d++) {
const nx = enemy.x + DIRS[d].x;
const ny = enemy.y + DIRS[d].y;
if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) continue;
if (game.layout[ny][nx] === T_WALL) continue;
if (game.rocks.find(r => r.x === nx && r.y === ny)) continue;
if (game.enemies.find(e => e !== enemy && e.x === nx && e.y === ny)) continue;
if (game.chests.find(c => c.x === nx && c.y === ny)) continue;
const newDist = Math.abs(game.player.x - nx) + Math.abs(game.player.y - ny);
if (newDist < bestDist) {
bestDist = newDist;
bestDir = d;
}
}
if (bestDir !== null) {
enemy.x += DIRS[bestDir].x;
enemy.y += DIRS[bestDir].y;
}
}
if (Math.abs(enemy.x - game.player.x) + Math.abs(enemy.y - game.player.y) === 1) {
if (enemy.attackTimer <= 0) {
enemy.attackTimer = ENEMY_ATTACK_INTERVAL;
game.player.hp -= 2;
game.player.hitTimer = HIT_FLASH;
game.message = 'You were hit!';
game.messageTimer = MESSAGE_DURATION;
if (game.player.hp <= 0) {
game.player.hp = 0;
game.dead = true;
}
}
}
}
}
function tryExit() {
if (!game.exitOpen) {
game.message = 'The door is locked!';
game.messageTimer = MESSAGE_DURATION;
return;
}
const nextRoom = game.roomIdx + 1;
if (nextRoom >= ROOMS.length) {
game.won = true;
return;
}
loadRoom(nextRoom);
}
function checkPuzzle() {
if (game.targets.length === 0) return;
const allSolved = game.targets.every(t => game.rocks.some(r => r.x === t.x && r.y === t.y));
if (allSolved) {
game.exitOpen = true;
game.message = 'The door has opened!';
game.messageTimer = MESSAGE_DURATION;
}
}
function showMessage(text) {
game.message = text;
game.messageTimer = MESSAGE_DURATION;
}
function restart() {
game.won = false;
game.dead = false;
game.player.hp = game.player.maxHp;
game.player.hasSword = false;
game.player.attackTimer = 0;
game.player.attackAnim = 0;
game.player.hitTimer = 0;
game.message = '';
game.messageTimer = 0;
loadRoom(0);
}
// === RENDERING ===
function drawFloor(layout) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (layout[y][x] === T_FLOOR) {
const shade = ((x + y) % 2 === 0) ? COLORS.floor : COLORS.floorDark;
ctx.fillStyle = shade;
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
}
}
}
}
function drawWalls(layout) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (layout[y][x] === T_WALL) {
const px = x * TILE;
const py = y * TILE;
ctx.fillStyle = COLORS.wall;
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = COLORS.wallTop;
ctx.fillRect(px, py, TILE, 3);
ctx.fillStyle = COLORS.wallFront;
ctx.fillRect(px, py + 3, TILE, TILE - 6);
ctx.fillStyle = COLORS.wallBottom;
ctx.fillRect(px, py + TILE - 3, TILE, 3);
ctx.strokeStyle = '#404040';
ctx.lineWidth = 0.5;
ctx.strokeRect(px + 0.5, py + 0.5, TILE - 1, TILE - 1);
}
}
}
}
function drawTorches(layout) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (layout[y][x] === T_TORCH) {
const cx = x * TILE + TILE / 2;
const cy = y * TILE + TILE / 2;
ctx.fillStyle = COLORS.torchGlow;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 1.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = COLORS.floor;
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
ctx.fillStyle = COLORS.torchStick;
ctx.fillRect(cx - 2, cy + 2, 4, TILE * 0.4);
ctx.fillStyle = COLORS.torchFlame;
ctx.beginPath();
ctx.ellipse(cx, cy - 4, 4, 6, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFFF00';
ctx.beginPath();
ctx.ellipse(cx, cy - 3, 2, 3, 0, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
function drawTargets() {
for (const target of game.targets) {
const cx = target.x * TILE + TILE / 2;
const cy = target.y * TILE + TILE / 2;
const occupied = game.rocks.some(r => r.x === target.x && r.y === target.y);
ctx.fillStyle = COLORS.targetGlow;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = occupied ? '#00FF00' : COLORS.target;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 0.3, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = occupied ? '#00AA00' : '#CC9900';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 0.3, 0, Math.PI * 2);
ctx.stroke();
}
}
function drawRocks() {
for (const rock of game.rocks) {
const px = rock.x * TILE;
const py = rock.y * TILE;
const inset = 3;
ctx.fillStyle = COLORS.rock;
ctx.beginPath();
ctx.roundRect(px + inset, py + inset, TILE - inset * 2, TILE - inset * 2, 4);
ctx.fill();
ctx.fillStyle = COLORS.rockLight;
ctx.beginPath();
ctx.roundRect(px + inset + 1, py + inset + 1, (TILE - inset * 2) * 0.5, (TILE - inset * 2) * 0.4, 2);
ctx.fill();
ctx.fillStyle = COLORS.rockDark;
ctx.beginPath();
ctx.roundRect(px + inset + (TILE - inset * 2) * 0.4, py + inset + (TILE - inset * 2) * 0.5, (TILE - inset * 2) * 0.4, (TILE - inset * 2) * 0.4, 2);
ctx.fill();
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(px + inset, py + inset, TILE - inset * 2, TILE - inset * 2, 4);
ctx.stroke();
}
}
function drawChests() {
for (const chest of game.chests) {
const px = chest.x * TILE;
const py = chest.y * TILE;
if (chest.opened) {
ctx.fillStyle = COLORS.chestOpen;
ctx.fillRect(px + 2, py + 10, TILE - 4, TILE - 12);
ctx.fillStyle = COLORS.chestGold;
ctx.fillRect(px + 4, py + 12, TILE - 8, TILE - 14);
ctx.fillStyle = COLORS.chestClosed;
ctx.fillRect(px + 2, py + 2, TILE - 4, 6);
ctx.fillStyle = COLORS.chestGold;
ctx.fillRect(px + 4, py + 3, TILE - 8, 4);
} else {
ctx.fillStyle = COLORS.chestClosed;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.fillStyle = COLORS.chestGold;
ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8);
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#B8860B';
ctx.lineWidth = 1;
ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4);
}
}
}
function drawExit() {
if (!game.exit) return;
const px = game.exit.x * TILE;
const py = game.exit.y * TILE;
ctx.fillStyle = COLORS.doorFrame;
ctx.fillRect(px - 1, py - 1, TILE + 2, TILE + 2);
if (game.exitOpen) {
ctx.fillStyle = '#1A1A1A';
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
} else {
ctx.fillStyle = COLORS.door;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.strokeStyle = '#8B7355';
ctx.lineWidth = 1;
ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.fillStyle = COLORS.doorKnob;
ctx.beginPath();
const knobX = game.exit.dir === 3 ? px + TILE - 7 : px + 7;
ctx.arc(knobX, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
const arrowX = game.exit.dir === 3 ? px + TILE + 10 : px - 10;
ctx.fillText(game.exit.dir === 3 ? '→' : game.exit.dir === 2 ? '←' : game.exit.dir === 0 ? '↓' : '↑', arrowX, py + TILE / 2 + 4);
ctx.textAlign = 'left';
}
function drawEnemies() {
for (const enemy of game.enemies) {
const px = enemy.x * TILE;
const py = enemy.y * TILE;
const isHit = enemy.hitTimer > 0;
ctx.save();
if (isHit) {
ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.02) * 0.5;
}
ctx.fillStyle = isHit ? '#FFFFFF' : COLORS.goblin;
ctx.beginPath();
ctx.roundRect(px + 3, py + 4, TILE - 6, TILE - 8, 3);
ctx.fill();
ctx.fillStyle = COLORS.goblinLight;
ctx.beginPath();
ctx.roundRect(px + 4, py + 5, (TILE - 6) * 0.6, (TILE - 8) * 0.4, 2);
ctx.fill();
// Eyes
ctx.fillStyle = COLORS.goblinEye;
ctx.beginPath();
ctx.arc(px + TILE / 2 - 4, py + 10, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 4, py + 10, 2.5, 0, Math.PI * 2);
ctx.fill();
// Pupils
ctx.fillStyle = '#003300';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 4, py + 10, 1.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 4, py + 10, 1.2, 0, Math.PI * 2);
ctx.fill();
// Teeth
ctx.fillStyle = COLORS.goblinTeeth;
ctx.fillRect(px + TILE / 2 - 3, py + 16, 2, 2);
ctx.fillRect(px + TILE / 2 + 1, py + 16, 2, 2);
// HP bar
const barW = TILE - 4;
const hpRatio = enemy.hp / enemy.maxHp;
ctx.fillStyle = '#333333';
ctx.fillRect(px + 2, py - 4, barW, 4);
ctx.fillStyle = hpRatio > 0.5 ? '#00FF00' : hpRatio > 0.25 ? '#FFAA00' : '#FF0000';
ctx.fillRect(px + 2, py - 4, barW * hpRatio, 4);
ctx.restore();
}
}
function drawPlayer() {
const p = game.player;
const px = p.x * TILE;
const py = p.y * TILE;
const isHit = p.hitTimer > 0;
ctx.save();
if (isHit) {
ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.03) * 0.5;
}
const bodyColor = isHit ? '#FF6666' : COLORS.playerBody;
// Body
ctx.fillStyle = bodyColor;
ctx.fillRect(px + 6, py + 14, TILE - 12, TILE - 16);
// Tunic detail
ctx.fillStyle = COLORS.playerTunic;
ctx.fillRect(px + 8, py + 16, TILE - 16, TILE - 20);
// Head
ctx.fillStyle = COLORS.playerHead;
ctx.beginPath();
ctx.arc(px + TILE / 2, py + 10, 7, 0, Math.PI * 2);
ctx.fill();
// Hair
ctx.fillStyle = COLORS.playerHair;
ctx.beginPath();
ctx.arc(px + TILE / 2, py + 8, 7, Math.PI, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = COLORS.playerEyes;
const eyeOffX = DIRS[p.dir].x * 2;
const eyeOffY = DIRS[p.dir].y * 2;
ctx.fillRect(px + TILE / 2 - 3 + eyeOffX, py + 9 + eyeOffY, 2, 2);
ctx.fillRect(px + TILE / 2 + 1 + eyeOffX, py + 9 + eyeOffY, 2, 2);
// Sword indicator
if (p.hasSword) {
ctx.fillStyle = COLORS.swordBlade;
const sx = px + TILE / 2 + DIRS[p.dir].x * 10;
const sy = py + TILE / 2 + DIRS[p.dir].y * 10;
ctx.fillRect(sx - 2, sy - 2, 4, 4);
ctx.fillStyle = COLORS.swordHandle;
const hx = px + TILE / 2 + DIRS[p.dir].x * 5;
const hy = py + TILE / 2 + DIRS[p.dir].y * 5;
ctx.fillRect(hx - 1.5, hy - 1.5, 3, 3);
}
ctx.restore();
}
function drawAttackAnim() {
const p = game.player;
if (p.attackAnim <= 0 || !p.hasSword) return;
const px = p.x * TILE;
const py = p.y * TILE;
const dir = DIRS[p.dir];
ctx.save();
ctx.globalAlpha = p.attackAnim / ATTACK_ANIM;
const ax = px + TILE / 2 + dir.x * TILE * 0.6;
const ay = py + TILE / 2 + dir.y * TILE * 0.6;
// Sword blade
ctx.fillStyle = COLORS.swordBlade;
if (dir.x !== 0) {
ctx.fillRect(ax - 3, ay - TILE * 0.4, 6, TILE * 0.8);
} else {
ctx.fillRect(ax - TILE * 0.4, ay - 3, TILE * 0.8, 6);
}
// Slash effect
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 2;
ctx.globalAlpha = p.attackAnim / ATTACK_ANIM * 0.7;
if (dir.x !== 0) {
ctx.beginPath();
ctx.moveTo(ax, ay - TILE * 0.5);
ctx.lineTo(ax + dir.x * 8, ay);
ctx.lineTo(ax, ay + TILE * 0.5);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(ax - TILE * 0.5, ay);
ctx.lineTo(ax, ay + dir.y * 8);
ctx.lineTo(ax + TILE * 0.5, ay);
ctx.stroke();
}
ctx.restore();
}
function drawHUD() {
// Hearts
const totalHearts = Math.ceil(game.player.maxHp / 2);
const fullHearts = Math.floor(game.player.hp / 2);
const halfHeart = game.player.hp % 2 === 1;
for (let i = 0; i < totalHearts; i++) {
const hx = 12 + i * 22;
const hy = 12;
drawHeart(ctx, hx, hy, 14, i < fullHearts);
if (i === fullHearts && halfHeart) {
ctx.save();
ctx.beginPath();
ctx.clip();
drawHeart(ctx, hx, hy, 14, true);
ctx.restore();
}
}
// Sword icon
if (game.player.hasSword) {
ctx.fillStyle = COLORS.swordBlade;
ctx.fillRect(canvas.width - 30, 6, 4, 14);
ctx.fillStyle = COLORS.swordHandle;
ctx.fillRect(canvas.width - 32, 20, 8, 4);
ctx.fillStyle = COLORS.swordGem;
ctx.beginPath();
ctx.arc(canvas.width - 28, 6, 2, 0, Math.PI * 2);
ctx.fill();
}
// Room name
ctx.fillStyle = '#CCCCCC';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText(ROOMS[game.roomIdx].name, canvas.width / 2, canvas.height - 8);
ctx.textAlign = 'left';
}
function drawMessages() {
if (game.messageTimer <= 0) return;
ctx.save();
ctx.globalAlpha = Math.min(1, game.messageTimer / 300);
ctx.fillStyle = 'rgba(0,0,0,0.7)';
const textW = ctx.measureText(game.message).width;
const boxW = textW + 20;
const boxX = (canvas.width - boxW) / 2;
const boxY = canvas.height / 2 - 30;
ctx.fillRect(boxX, boxY, boxW, 24);
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 1;
ctx.strokeRect(boxX, boxY, boxW, 24);
ctx.fillStyle = COLORS.msgText;
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText(game.message, canvas.width / 2, boxY + 17);
ctx.textAlign = 'left';
ctx.restore();
}
function drawDeathScreen() {
ctx.fillStyle = COLORS.overlay;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#FF0000';
ctx.font = 'bold 36px monospace';
ctx.textAlign = 'center';
ctx.fillText('YOU DIED', canvas.width / 2, canvas.height / 2 - 20);
ctx.fillStyle = '#AAAAAA';
ctx.font = '16px monospace';
ctx.fillText('Press ENTER to restart', canvas.width / 2, canvas.height / 2 + 20);
ctx.textAlign = 'left';
}
function drawWinScreen() {
ctx.fillStyle = COLORS.overlay;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 36px monospace';
ctx.textAlign = 'center';
ctx.fillText('YOU WIN!', canvas.width / 2, canvas.height / 2 - 40);
ctx.fillStyle = '#FFFFFF';
ctx.font = '18px monospace';
ctx.fillText('The dungeon has been conquered!', canvas.width / 2, canvas.height / 2);
ctx.fillStyle = '#AAAAAA';
ctx.font = '14px monospace';
ctx.fillText('Press ENTER to play again', canvas.width / 2, canvas.height / 2 + 35);
ctx.textAlign = 'left';
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (game.dead) {
drawFloor(game.layout);
drawWalls(game.layout);
drawTorches(game.layout);
drawDeathScreen();
return;
}
if (game.won) {
drawFloor(game.layout);
drawWalls(game.layout);
drawTorches(game.layout);
drawWinScreen();
return;
}
drawFloor(game.layout);
drawTargets();
drawWalls(game.layout);
drawTorches(game.layout);
drawExit();
drawRocks();
drawChests();
drawEnemies();
drawPlayer();
drawAttackAnim();
// Hit flash overlay
if (game.player.hitTimer > 0) {
ctx.fillStyle = COLORS.hitFlash;
ctx.globalAlpha = game.player.hitTimer / HIT_FLASH;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1;
}
drawMessages();
drawHUD();
}
// === GAME LOOP ===
let lastTime = 0;
function gameLoop(timestamp) {
const dt = timestamp - lastTime;
lastTime = timestamp;
if (!game.won && !game.dead) {
// Handle movement input
if (game.player.moveTimer <= 0) {
let dx = 0, dy = 0;
if (keys['ArrowUp'] || keys['KeyW']) dy = -1;
else if (keys['ArrowDown'] || keys['KeyS']) dy = 1;
if (keys['ArrowLeft'] || keys['KeyA']) dx = -1;
else if (keys['ArrowRight'] || keys['KeyD']) dx = 1;
if (dx !== 0 || dy !== 0) {
tryMove(dx, dy);
game.player.moveTimer = MOVE_COOLDOWN;
}
}
// Update timers
if (game.player.moveTimer > 0) game.player.moveTimer -= dt;
if (game.player.attackTimer > 0) game.player.attackTimer -= dt;
if (game.player.attackAnim > 0) game.player.attackAnim -= dt;
if (game.player.hitTimer > 0) game.player.hitTimer -= dt;
if (game.messageTimer > 0) game.messageTimer -= dt;
// Update enemies
updateEnemies(dt);
// Check death
if (game.player.hp <= 0) {
game.player.hp = 0;
game.dead = true;
}
}
render();
requestAnimationFrame(gameLoop);
}
// === INIT ===
function init() {
loadRoom(0);
requestAnimationFrame(gameLoop);
}
init();

406
src/app.js Normal file
View File

@ -0,0 +1,406 @@
// === APP BOOTSTRAP ===
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const debugCanvas = document.getElementById('debug-canvas');
const debugCtx = debugCanvas.getContext('2d');
const menuOverlay = document.getElementById('menu-overlay');
const debugOverlay = document.getElementById('debug-overlay');
// Expose canvas contexts and DOM elements to window for other modules
window.ctx = ctx;
window.debugCtx = debugCtx;
window.menuOverlay = menuOverlay;
// Debug mode
const debugMode = new URLSearchParams(window.location.search).has('debug');
let debugVisible = false;
// Screen state
let currentScreen = SCREEN_TITLE;
let transitionProgress = 1; // 1 = fully visible, 0 = fully faded
let transitioning = false;
let targetScreen = null;
// Game loop timing
let lastTime = 0;
let animFrameId = null;
// === SCREEN MANAGEMENT ===
function switchScreen(screen) {
if (transitioning) return;
transitioning = true;
targetScreen = screen;
transitionProgress = 1;
}
function updateTransition(dt) {
if (!transitioning) return;
transitionProgress -= dt / TRANSITION_DURATION;
if (transitionProgress <= 0) {
transitionProgress = 1;
transitioning = false;
currentScreen = targetScreen;
targetScreen = null;
onScreenChange(currentScreen);
}
}
function onScreenChange(screen) {
menuOverlay.innerHTML = '';
menuOverlay.classList.remove('active');
inputSystem.clearKeys();
switch (screen) {
case SCREEN_TITLE:
renderTitleScreen(menuOverlay);
menuOverlay.classList.add('active');
break;
case SCREEN_DUNGEON_SELECT:
renderDungeonSelect(menuOverlay, saveData, onDungeonSelected);
menuOverlay.classList.add('active');
break;
case SCREEN_PLAYING:
if (!gameState.currentDungeon) return;
loadDungeonRoom(gameState.currentDungeon, gameState.currentRoomIdx);
break;
case SCREEN_DEAD:
renderDeathScreen(menuOverlay, gameState);
menuOverlay.classList.add('active');
break;
case SCREEN_DUNGEON_COMPLETE:
renderDungeonCompleteScreen(menuOverlay, gameState);
menuOverlay.classList.add('active');
break;
case SCREEN_GAME_COMPLETE:
renderGameCompleteScreen(menuOverlay, gameState);
menuOverlay.classList.add('active');
break;
case SCREEN_INVENTORY:
renderInventoryOverlay(menuOverlay, gameState);
menuOverlay.classList.add('active');
break;
}
}
function onDungeonSelected(dungeonIdx) {
gameState.currentDungeon = dungeonIdx;
gameState.currentRoomIdx = 0;
gameState.dungeonRooms = generateDungeon(DUNGEONS[dungeonIdx]);
gameState.player.hp = gameState.player.maxHp;
gameState.player.invincibleTimer = 1000;
switchScreen(SCREEN_PLAYING);
}
// === DEBUG ===
function toggleDebug() {
debugVisible = !debugVisible;
debugCanvas.classList.toggle('visible', debugVisible);
debugOverlay.classList.toggle('visible', debugVisible);
}
function renderDebug() {
if (!debugVisible) return;
debugCtx.clearRect(0, 0, CANVAS_W, CANVAS_H);
debugCtx.fillStyle = '#0F0';
debugCtx.font = '10px monospace';
let y = 10;
const lines = [];
lines.push(`Screen: ${currentScreen}`);
lines.push(`Dungeon: ${gameState.currentDungeon !== null ? DUNGEONS[gameState.currentDungeon].name : 'none'}`);
lines.push(`Room: ${gameState.currentRoomIdx}/${gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0}`);
lines.push(`Player HP: ${gameState.player.hp}/${gameState.player.maxHp}`);
lines.push(`Weapons: ${gameState.player.weapons.map(w => w.name).join(', ')}`);
lines.push(`Active: ${gameState.player.activeWeapon ? gameState.player.activeWeapon.name : 'none'}`);
lines.push(`Enemies: ${gameState.enemies.length}`);
lines.push(`Particles: ${gameState.particles ? gameState.particles.length : 0}`);
if (gameState.currentDungeon !== null && gameState.dungeonRooms && gameState.currentRoom < gameState.dungeonRooms.length) {
const room = gameState.currentRoom;
lines.push(`Room type: ${room.type}`);
lines.push(`Room enemies: ${room.enemies.length}`);
lines.push(`Exit open: ${gameState.exitOpen}`);
}
for (const line of lines) {
debugCtx.fillText(line, 5, y);
y += 12;
}
// Draw collision boxes on debug canvas
if (currentScreen === SCREEN_PLAYING && gameState.layout) {
debugCtx.strokeStyle = 'rgba(0,255,0,0.3)';
debugCtx.lineWidth = 0.5;
for (let y2 = 0; y2 < ROWS; y2++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y2][x] === TILE_TYPES.FLOOR) {
debugCtx.strokeRect(x * TILE, y2 * TILE, TILE, TILE);
}
}
}
}
}
// === CONSOLE API ===
window._genDungeon = function (id) {
const dungeon = DUNGEONS[id];
if (!dungeon) { console.error(`Dungeon ${id} not found`); return null; }
const rooms = generateDungeon(dungeon);
console.log(`Generated ${rooms.length} rooms for "${dungeon.name}" (seed: ${dungeon.seed})`);
console.table(rooms.map((r, i) => ({
idx: i, type: r.type, enemies: r.enemies.length, puzzles: r.puzzles ? r.puzzles.length : 0,
items: r.chests ? r.chests.length : 0, hasBoss: r.boss !== undefined
})));
return rooms;
};
window.spawnEnemy = function (type) {
if (currentScreen !== SCREEN_PLAYING) { console.error('Not in game'); return; }
const def = ENEMY_TYPES.find(e => e.name === type || e.id === type);
if (!def) { console.error(`Enemy type "${type}" not found`); return; }
const enemy = createEnemyInstance(def, gameState.player.x + 2, gameState.player.y);
gameState.enemies.push(enemy);
console.log(`Spawned ${def.name} at (${enemy.x}, ${enemy.y})`);
};
window.giveWeapon = function (name) {
if (currentScreen !== SCREEN_PLAYING) { console.error('Not in game'); return; }
const def = WEAPONS.find(w => w.name === name);
if (!def) { console.error(`Weapon "${name}" not found`); return; }
if (!gameState.player.weapons.find(w => w.name === def.name)) {
gameState.player.weapons.push({ ...def });
console.log(`Added ${def.name} to inventory`);
} else {
console.log(`${def.name} already in inventory`);
}
};
window.teleportToRoom = function (idx) {
if (currentScreen !== SCREEN_PLAYING || !gameState.dungeonRooms) {
console.error('Not in game');
return;
}
if (idx < 0 || idx >= gameState.dungeonRooms.length) {
console.error(`Room index ${idx} out of range [0, ${gameState.dungeonRooms.length - 1}]`);
return;
}
gameState.currentRoomIdx = idx;
loadDungeonRoom(gameState.currentDungeon, idx);
console.log(`Teleported to room ${idx}`);
};
window.dumpState = function () {
console.log('=== Game State ===');
console.log('Screen:', currentScreen);
console.log('Dungeon:', gameState.currentDungeon !== null ? DUNGEONS[gameState.currentDungeon].name : 'none');
console.log('Room:', gameState.currentRoomIdx);
console.log('Player:', JSON.stringify(gameState.player));
console.log('Enemies:', gameState.enemies.length);
console.log('Particles:', gameState.particles ? gameState.particles.length : 0);
};
// Keyboard shortcut for debug
document.addEventListener('keydown', (e) => {
if (e.code === 'F1') { e.preventDefault(); toggleDebug(); }
if (e.code === 'F2' && currentScreen === SCREEN_PLAYING) {
e.preventDefault();
if (gameState.exitOpen || gameState.currentRoomIdx >= (gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0)) {
if (gameState.currentRoomIdx >= (gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0)) {
completeDungeon();
}
} else {
gameState.exitOpen = true;
showMessage('Debug: Door opened');
tryExit();
}
}
if (e.code === 'F3' && currentScreen === SCREEN_PLAYING) {
e.preventDefault();
gameState.player.invincible = true;
console.log('God mode enabled');
}
if (e.code === 'F4' && currentScreen === SCREEN_PLAYING) {
e.preventDefault();
spawnEnemy('goblin');
}
});
// === MAIN GAME LOOP ===
function gameLoop(timestamp) {
const dt = Math.min(timestamp - lastTime, 50); // Cap delta to prevent spiral of death
lastTime = timestamp;
// Update transition
updateTransition(dt);
// Screen-specific updates
if (currentScreen === SCREEN_PLAYING && !transitioning) {
updateGame(dt);
}
// Update input
inputSystem.update(dt);
// Render
render();
// Debug overlay
renderDebug();
animFrameId = requestAnimationFrame(gameLoop);
}
function updateGame(dt) {
// Update player timers
if (gameState.player.moveTimer > 0) gameState.player.moveTimer -= dt;
if (gameState.player.attackTimer > 0) gameState.player.attackTimer -= dt;
if (gameState.player.attackAnim > 0) gameState.player.attackAnim -= dt;
if (gameState.player.hitTimer > 0) gameState.player.hitTimer -= dt;
if (gameState.player.invincibleTimer > 0) gameState.player.invincibleTimer -= dt;
else if (!gameState.player.invincible) gameState.player.invincibleTimer = 0;
if (gameState.messageTimer > 0) gameState.messageTimer -= dt;
// Update particles
if (gameState.particles) {
for (let i = gameState.particles.length - 1; i >= 0; i--) {
const p = gameState.particles[i];
p.life -= dt;
if (p.life <= 0) {
gameState.particles.splice(i, 1);
continue;
}
p.x += p.vx * (dt / 1000);
p.y += p.vy * (dt / 1000);
p.vy += (p.gravity || 0) * (dt / 1000);
p.alpha = (p.life / p.maxLife) * (p.maxAlpha || 1);
}
if (gameState.particles.length > MAX_PARTICLES) {
gameState.particles.splice(0, gameState.particles.length - MAX_PARTICLES);
}
}
// Handle movement
if (gameState.player.moveTimer <= 0 && !gameState.player.invincibleTimer) {
const move = inputSystem.getMove();
if (move) {
tryMove(move.dx, move.dy);
gameState.player.moveTimer = MOVE_COOLDOWN;
}
}
// Handle actions
if (inputSystem.getAction('attack') && gameState.player.attackTimer <= 0) {
tryAttack();
}
if (inputSystem.getAction('interact')) {
tryInteract();
}
if (inputSystem.getAction('menu') && currentScreen === SCREEN_PLAYING) {
switchScreen(SCREEN_INVENTORY);
}
if (inputSystem.getAction('back') && currentScreen === SCREEN_INVENTORY) {
switchScreen(SCREEN_PLAYING);
}
// Update enemies
updateEnemies(dt);
// Check death
if (gameState.player.hp <= 0 && !gameState.player.invincible) {
gameState.player.hp = 0;
switchScreen(SCREEN_DEAD);
}
}
// === RENDERING ===
function render() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
// Draw black background
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
if (currentScreen === SCREEN_PLAYING) {
renderGame();
}
// Draw transition overlay
if (transitioning || transitionProgress < 1) {
ctx.fillStyle = `rgba(0,0,0,${1 - transitionProgress})`;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
}
}
function renderGame() {
if (!gameState.layout) return;
const theme = DUNGEONS[gameState.currentDungeon] ? DUNGEONS[gameState.currentDungeon].theme : 'stone';
const themeData = THEMES[theme] || THEMES.stone;
renderFloor(themeData);
renderTargets();
renderWalls(themeData);
renderTorches(themeData);
renderDoors();
renderSoftwalls();
renderLava();
renderWater();
renderTraps();
renderLevers();
renderSwitches();
renderRocks();
renderChests();
renderItems();
renderEnemies();
if (gameState.currentBoss) renderBoss(gameState.currentBoss);
renderPlayer();
renderAttackAnim();
renderParticles();
// Hit flash overlay
if (gameState.player.hitTimer > 0) {
ctx.fillStyle = COLORS.hitFlash;
ctx.globalAlpha = gameState.player.hitTimer / HIT_FLASH;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.globalAlpha = 1;
}
renderMessages();
renderHUD();
}
// === INITIALIZATION ===
function init() {
// Initialize subsystems
initKeyboard();
initActions();
initGameState();
loadSaveData();
// Expose inputSystem and gameState to window for other modules
window.inputSystem = inputSystem;
window.gameState = gameState;
// Start with title screen
currentScreen = SCREEN_TITLE;
menuOverlay.innerHTML = '';
renderTitleScreen(menuOverlay);
menuOverlay.classList.add('active');
// Start game loop
lastTime = performance.now();
requestAnimationFrame(gameLoop);
console.log('Zelda Dungeon Quest initialized');
console.log('Debug mode:', debugMode ? 'ON (?debug=1)' : 'OFF');
console.log('Controls: F1=debug, F2=skip room, F3=god mode, F4=spawn enemy');
console.log('Console API: generateDungeon(id), spawnEnemy(type), giveWeapon(name), teleportToRoom(idx), dumpState()');
}
// Boot!
init();

131
src/browser-polyfill.js Normal file
View File

@ -0,0 +1,131 @@
'use strict';
// Browser polyfill: makes Node.js CommonJS (module.exports / require) work in browser
// Load this BEFORE all other scripts in index.html
(function () {
// Global `module` object - its exports setter mirrors to window
var moduleObj = { exports: {} };
Object.defineProperty(moduleObj, 'exports', {
set: function (val) {
this._exports = val || {};
// Mirror all exported properties to window for cross-script access
if (val && typeof val === 'object') {
for (var key in val) {
if (Object.prototype.hasOwnProperty.call(val, key)) {
window[key] = val[key];
}
}
}
},
get: function () {
return this._exports || {};
},
});
moduleObj._exports = {};
window.module = moduleObj;
// Module path -> exported keys mapping for require() lookups
var moduleKeys = {
'src/core/constants': ['TILE', 'COLS', 'ROWS', 'TILE_TYPES', 'COLORS',
'T_FLOOR', 'T_WALL', 'T_SOFTWALL', 'T_LAVA', 'T_WATER', 'T_TORCH',
'T_DOOR', 'T_LEVER', 'T_SWITCH', 'T_TRAP', 'T_TARGET', 'T_CHEST',
'T_EXIT', 'T_ROCK', 'T_ITEM',
'MOVE_COOLDOWN', 'ATTACK_COOLDOWN', 'ATTACK_ANIM',
'ENEMY_MOVE_INTERVAL', 'ENEMY_ATTACK_INTERVAL',
'HIT_FLASH', 'MESSAGE_DURATION', 'TRANSITION_DURATION',
'DIRS', 'DIR_NAMES', 'OPPOSITE_DIR',
'SCREEN_TITLE', 'SCREEN_DUNGEON_SELECT', 'SCREEN_PLAYING',
'SCREEN_DEAD', 'SCREEN_DUNGEON_COMPLETE', 'SCREEN_GAME_COMPLETE',
'SCREEN_INVENTORY',
'CANVAS_W', 'CANVAS_H',
'MAX_HEARTS', 'MAX_INVENTORY_SLOTS', 'MAX_PARTICLES'],
'src/core/rng': ['createRNG', 'mulberry32', 'randInt', 'randFloat', 'randElement', 'shuffle', 'weightedChoice'],
'src/core/utils': ['createBorderLayout', 'createEmptyLayout', 'drawHeart', 'drawHeartContainer',
'manhattanDist', 'inBounds', 'cloneLayout', 'clamp', 'lerp',
'isPassable', 'isWall', 'isSoftWall', 'getFloorColor', 'getWallColors', 'formatNumber'],
'src/generator/dungeon': ['generateDungeon'],
'src/generator/rooms': ['generateRoomLayout', 'getRoomLayoutTemplate', 'applyThemeToRoom'],
'src/generator/puzzles': ['generatePuzzles'],
'src/generator/placement': ['generatePlacement', 'placeEnemies', 'placeItems', 'placeBoss',
'findFloorPosition', 'getEnemyPool', 'getRupeeType'],
'src/generator/index': ['generateDungeon', 'generateRoomLayout', 'generatePuzzles', 'generatePlacement'],
'src/data/themes': ['THEMES'],
'src/data/dungeons': ['DUNGEONS'],
'src/data/enemies': ['ENEMY_TYPES', 'getEnemyType'],
'src/data/weapons': ['WEAPONS', 'getWeapon'],
'src/data/bosses': ['BOSS_TYPES', 'getBossType'],
'src/data/items': ['ITEMS', 'getItem'],
'src/game/state': ['initGameState', 'loadDungeonRoom', 'showMessage', 'resetPlayer'],
'src/game/movement': ['tryMove', 'tryInteract', 'tryExit', 'completeDungeon'],
'src/game/combat': ['tryAttack', 'dealDamage', 'spawnProjectile', 'updateProjectiles'],
'src/game/ai': ['updateEnemies', 'updateEnemyMovement', 'moveToward', 'enemyAttack', 'bossAttack', 'createEnemyInstance'],
'src/game/items': ['openChest', 'collectFloorItem', 'useItem', 'switchWeapon'],
'src/game/transitions': ['startTransition', 'getTransitionAlpha', 'isTransitionComplete'],
'src/input/keyboard': ['initKeyboard', 'isKeyDown', 'isKeyJustPressed', 'setKeyHandlers', 'resetKeys'],
'src/input/actions': ['initActions', 'getActionState', 'getAction', 'getActiveActions'],
'src/rendering/renderer': ['renderFloor', 'renderWalls', 'renderTorches', 'renderDoors', 'renderSoftwalls', 'renderLava', 'renderWater', 'renderTraps', 'renderLevers', 'renderSwitches', 'renderTargets', 'renderRocks', 'renderChests', 'renderItems', 'renderExit', 'renderPlayer', 'renderAttackAnim', 'renderMessages'],
'src/rendering/draw-enemies': ['renderEnemies', 'drawEnemy', 'drawSlime', 'drawKeese', 'drawOctorok', 'drawMoblin', 'drawStalfos', 'drawGoriya', 'drawArmos', 'drawLynel'],
'src/rendering/draw-bosses': ['renderBoss', 'drawGenericBoss', 'drawGleeok', 'drawDarknut', 'drawWizzrobe'],
'src/rendering/draw-weapons': ['drawWeaponHeld', 'drawWeaponInHUD', 'drawSwordHeld', 'drawMasterSwordHeld', 'drawBowHeld', 'drawBoomerangHeld', 'drawBombHeld', 'drawHammerHeld', 'drawFireRodHeld', 'drawIceRodHeld', 'drawSlingshotHeld', 'drawHookshotHeld', 'drawWeaponIcon'],
'src/rendering/hud': ['renderHUD'],
'src/rendering/effects': ['spawnParticles', 'spawnHitParticles', 'spawnDefeatParticles', 'spawnExplosionParticles', 'renderParticles', 'renderProjectiles'],
'src/menu/title-screen': ['renderTitleScreen', 'createMenuButton'],
'src/menu/dungeon-select': ['renderDungeonSelect'],
'src/menu/hud-overlay': ['renderInventoryOverlay'],
'src/menu/screens': ['renderDeathScreen', 'renderDungeonCompleteScreen', 'renderGameCompleteScreen'],
'src/save/save': ['saveData', 'loadSaveData', 'saveProgress', 'markDungeonComplete', 'addWeapon', 'addHeartPiece', 'addStats', 'isDungeonCompleted', 'isDungeonUnlocked', 'getSaveSummary', 'clearSave'],
};
// Resolve a require path to a canonical module key
function resolvePath(req, callerPath) {
// Strip leading / for consistency with moduleKeys
if (callerPath && callerPath.startsWith('/')) callerPath = callerPath.substring(1);
// Direct match
if (moduleKeys.hasOwnProperty(req)) return req;
if (req.startsWith('./') || req.startsWith('../')) {
var dir = callerPath.substring(0, callerPath.lastIndexOf('/') + 1);
var resolved;
if (req.startsWith('./')) {
resolved = dir + req.substring(2);
} else {
var segments = req.substring(2).split('/');
var baseSegments = dir.split('/').filter(Boolean);
var dotDotCount = 0;
for (var i = 0; i + 2 < req.length && req.substring(i, i + 3) === '../'; i += 3) dotDotCount++;
for (var u = 0; u < dotDotCount; u++) baseSegments.pop();
var rest = req.substring(dotDotCount * 3);
resolved = baseSegments.join('/') + '/' + rest;
}
// Normalize
while (resolved.includes('/../')) {
resolved = resolved.replace(/[^\/]+\/\.\.\//, '');
}
if (moduleKeys.hasOwnProperty(resolved)) return resolved;
}
return null;
}
// Global `require` function
window.require = function (reqPath) {
var script = document.currentScript;
var origin = window.location ? window.location.origin : '';
var callerPath = script ? (script.src || script.getAttribute('src') || '').replace(origin, '') : '';
var modKey = resolvePath(reqPath, callerPath);
if (!modKey) {
console.warn('[require] Module not found: "' + reqPath + '" from "' + callerPath + '"');
return {};
}
var result = {};
var keys = moduleKeys[modKey];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
result[key] = window[key] !== undefined ? window[key] : undefined;
}
return result;
};
})();

151
src/core/constants.js Normal file
View File

@ -0,0 +1,151 @@
'use strict';
// Tile dimensions
const TILE = 32;
const COLS = 32;
const ROWS = 24;
// Tile types
const TILE_TYPES = {
FLOOR: 0,
WALL: 1,
SOFTWALL: 2,
LAVA: 3,
WATER: 4,
TORCH: 5,
DOOR: 6,
LEVER: 7,
SWITCH: 8,
TRAP: 9,
TARGET: 10,
CHEST: 11,
EXIT: 12,
ROCK: 13,
ITEM: 14,
};
// Backward-compatible individual constants
const T_FLOOR = TILE_TYPES.FLOOR;
const T_WALL = TILE_TYPES.WALL;
const T_SOFTWALL = TILE_TYPES.SOFTWALL;
const T_LAVA = TILE_TYPES.LAVA;
const T_WATER = TILE_TYPES.WATER;
const T_TORCH = TILE_TYPES.TORCH;
const T_DOOR = TILE_TYPES.DOOR;
const T_LEVER = TILE_TYPES.LEVER;
const T_SWITCH = TILE_TYPES.SWITCH;
const T_TRAP = TILE_TYPES.TRAP;
const T_TARGET = TILE_TYPES.TARGET;
const T_CHEST = TILE_TYPES.CHEST;
const T_EXIT = TILE_TYPES.EXIT;
const T_ROCK = TILE_TYPES.ROCK;
const T_ITEM = TILE_TYPES.ITEM;
// Timing constants
const MOVE_COOLDOWN = 100;
const ATTACK_COOLDOWN = 400;
const ATTACK_ANIM = 250;
const ENEMY_MOVE_INTERVAL = 800;
const ENEMY_ATTACK_INTERVAL = 1500;
const HIT_FLASH = 200;
const MESSAGE_DURATION = 2000;
const TRANSITION_DURATION = 600;
// Direction vectors: [Down, Up, Left, Right]
const DIRS = [
{ x: 0, y: 1 },
{ x: 0, y: -1 },
{ x: -1, y: 0 },
{ x: 1, y: 0 },
];
const DIR_NAMES = ['Down', 'Up', 'Left', 'Right'];
// Opposite direction lookup
const OPPOSITE_DIR = [1, 0, 3, 2];
// Color palette (base stone dungeon theme)
const COLORS = {
floor: '#C4A265',
floorDark: '#A8884D',
wall: '#505050',
wallTop: '#6E6E6E',
wallFront: '#5A5A5A',
wallBottom: '#383838',
playerBody: '#228B22',
playerTunic: '#1A6B1A',
playerHead: '#FFD700',
playerEyes: '#000080',
playerHair: '#8B4513',
rock: '#888888',
rockDark: '#666666',
rockLight: '#AAAAAA',
target: '#FFD700',
targetGlow: 'rgba(255,215,0,0.3)',
chestClosed: '#8B4513',
chestGold: '#DAA520',
chestOpen: '#A0522D',
swordBlade: '#C0C0C0',
swordHandle: '#8B4513',
swordGem: '#FF0000',
goblin: '#8B0000',
goblinLight: '#CC2222',
goblinEye: '#00FF00',
goblinTeeth: '#FFFFFF',
torchStick: '#654321',
torchFlame: '#FF8C00',
torchGlow: 'rgba(255,140,0,0.15)',
door: '#654321',
doorFrame: '#8B7355',
doorKnob: '#FFD700',
heart: '#FF0000',
heartEmpty: '#444444',
msgText: '#FFFFFF',
overlay: 'rgba(0,0,0,0.75)',
hitFlash: 'rgba(255,0,0,0.3)',
softwall: '#705040',
softwallDark: '#503828',
lava: '#FF4500',
lavaLight: '#FF6347',
water: '#4169AA',
waterLight: '#5F9EA0',
lever: '#B8860B',
switchFloor: '#DAA520',
trap: '#8B008B',
};
// Screen states
const SCREEN_TITLE = 'title';
const SCREEN_DUNGEON_SELECT = 'dungeon_select';
const SCREEN_PLAYING = 'playing';
const SCREEN_DEAD = 'dead';
const SCREEN_DUNGEON_COMPLETE = 'dungeon_complete';
const SCREEN_GAME_COMPLETE = 'game_complete';
const SCREEN_INVENTORY = 'inventory';
// Canvas dimensions
const CANVAS_W = 640;
const CANVAS_H = 480;
// Max values
const MAX_HEARTS = 13; // 26 HP
const MAX_INVENTORY_SLOTS = 12;
const MAX_PARTICLES = 100;
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
TILE, COLS, ROWS, TILE_TYPES,
T_FLOOR, T_WALL, T_SOFTWALL, T_LAVA, T_WATER, T_TORCH,
T_DOOR, T_LEVER, T_SWITCH, T_TRAP, T_TARGET, T_CHEST,
T_EXIT, T_ROCK, T_ITEM,
MOVE_COOLDOWN, ATTACK_COOLDOWN, ATTACK_ANIM,
ENEMY_MOVE_INTERVAL, ENEMY_ATTACK_INTERVAL,
HIT_FLASH, MESSAGE_DURATION, TRANSITION_DURATION,
DIRS, DIR_NAMES, OPPOSITE_DIR,
COLORS,
SCREEN_TITLE, SCREEN_DUNGEON_SELECT, SCREEN_PLAYING,
SCREEN_DEAD, SCREEN_DUNGEON_COMPLETE, SCREEN_GAME_COMPLETE,
SCREEN_INVENTORY,
CANVAS_W, CANVAS_H,
MAX_HEARTS, MAX_INVENTORY_SLOTS, MAX_PARTICLES,
};
}

58
src/core/rng.js Normal file
View File

@ -0,0 +1,58 @@
'use strict';
// Mulberry32 - fast, deterministic PRNG
function mulberry32(seed) {
return function () {
seed |= 0;
seed = seed + 0x602a2ebf | 0;
let t = Math.imul(seed ^ seed >>> 16, 0x6d349c2b);
t = Math.imul(t ^ t >>> 16, 0x04c35085) | 0;
t = t ^ t >>> 16;
return (t >>> 0) / 4294967296;
};
}
// Create a seeded RNG instance
function createRNG(seed) {
return mulberry32(seed);
}
// Random integer in [min, max] inclusive
function randInt(rng, min, max) {
return Math.floor(rng() * (max - min + 1)) + min;
}
// Random float in [0, 1)
function randFloat(rng) {
return rng();
}
// Pick random element from array
function randElement(rng, arr) {
return arr[Math.floor(rng() * arr.length)];
}
// Shuffle array in place (Fisher-Yates)
function shuffle(rng, arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// Random choice with weights
function weightedChoice(rng, items) {
const total = items.reduce((sum, item) => sum + item.weight, 0);
let r = rng() * total;
for (const item of items) {
r -= item.weight;
if (r <= 0) return item.value;
}
return items[items.length - 1].value;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { createRNG, mulberry32, randInt, randFloat, randElement, shuffle, weightedChoice };
}

131
src/core/utils.js Normal file
View File

@ -0,0 +1,131 @@
'use strict';
// Browser: use window globals; Node: use require
const _Const = typeof require !== 'undefined' ? require('./constants') : { TILE, COLS, ROWS, TILE_TYPES, COLORS };
// Create a layout grid with border walls and optional interior walls/torches
function createBorderLayout(walls, torches) {
const layout = [];
for (let y = 0; y < _Const.ROWS; y++) {
const row = [];
for (let x = 0; x < _Const.COLS; x++) {
if (y === 0 || y === _Const.ROWS - 1 || x === 0 || x === _Const.COLS - 1) {
row.push(_Const.TILE_TYPES.WALL);
} else {
row.push(_Const.TILE_TYPES.FLOOR);
}
}
layout.push(row);
}
if (walls) {
for (const [wx, wy] of walls) {
if (wy >= 0 && wy < _Const.ROWS && wx >= 0 && wx < _Const.COLS) {
layout[wy][wx] = _Const.TILE_TYPES.WALL;
}
}
}
if (torches) {
for (const [tx, ty] of torches) {
if (ty >= 0 && ty < _Const.ROWS && tx >= 0 && tx < _Const.COLS) {
layout[ty][tx] = _Const.TILE_TYPES.TORCH;
}
}
}
return layout;
}
// Create empty layout (all floor, border walls)
function createEmptyLayout() {
return createBorderLayout([], []);
}
// Draw a heart shape on canvas
function drawHeart(ctx, cx, cy, size, filled) {
ctx.save();
ctx.fillStyle = filled ? _Const.COLORS.heart : _Const.COLORS.heartEmpty;
ctx.beginPath();
const s = size * 0.5;
ctx.moveTo(cx, cy + s * 0.3);
ctx.bezierCurveTo(cx, cy - s * 0.5, cx - s * 1.2, cy - s * 0.5, cx - s, cy + s * 0.1);
ctx.bezierCurveTo(cx - s, cy + s * 0.7, cx, cy + s * 1.1, cx, cy + s * 1.1);
ctx.bezierCurveTo(cx, cy + s * 0.7, cx + s, cy + s * 0.1, cx + s, cy + s * 0.1);
ctx.bezierCurveTo(cx + s * 1.2, cy - s * 0.5, cx, cy - s * 0.5, cx, cy + s * 0.3);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Draw a heart container (full 2-HP heart)
function drawHeartContainer(ctx, cx, cy) {
drawHeart(ctx, cx, cy, 14, true);
}
// Manhattan distance
function manhattanDist(x1, y1, x2, y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
// Check if position is within bounds
function inBounds(x, y) {
return x >= 0 && x < _Const.COLS && y >= 0 && y < _Const.ROWS;
}
// Deep clone a 2D array
function cloneLayout(layout) {
return layout.map(row => [...row]);
}
// Clamp value between min and max
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Linear interpolation
function lerp(a, b, t) {
return a + (b - a) * t;
}
// Check if tile is passable (not wall)
function isPassable(layout, x, y) {
if (!inBounds(x, y)) return false;
const tile = layout[y][x];
return tile !== _Const.TILE_TYPES.WALL && tile !== _Const.TILE_TYPES.LAVA;
}
// Check if tile is a wall
function isWall(layout, x, y) {
if (!inBounds(x, y)) return true;
return layout[y][x] === _Const.TILE_TYPES.WALL;
}
// Check if tile is a soft wall (breakable with bombs/hammer)
function isSoftWall(layout, x, y) {
if (!inBounds(x, y)) return false;
return layout[y][x] === _Const.TILE_TYPES.SOFTWALL;
}
// Get floor color for theme
function getFloorColor(theme, x, y) {
const base = theme.floorColor || _Const.COLORS.floor;
const dark = theme.floorDarkColor || _Const.COLORS.floorDark;
return ((x + y) % 2 === 0) ? base : dark;
}
// Get wall colors for theme
function getWallColors(theme) {
return {
wall: theme.wallColor || _Const.COLORS.wall,
top: theme.wallTopColor || _Const.COLORS.wallTop,
front: theme.wallFrontColor || _Const.COLORS.wallFront,
bottom: theme.wallBottomColor || _Const.COLORS.wallBottom,
};
}
// Format number with commas
function formatNumber(n) {
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { createBorderLayout, createEmptyLayout, drawHeart, drawHeartContainer, manhattanDist, inBounds, cloneLayout, clamp, lerp, isPassable, isWall, isSoftWall, getFloorColor, getWallColors, formatNumber };
}

112
src/data/bosses.js Normal file
View File

@ -0,0 +1,112 @@
'use strict';
const BOSS_TYPES = [
{ id: 0, name: 'Stone Gleeok', hp: 26, damage: 3, defense: 0, speed: 1.5, colors: { body: "#696969", eye: "#FFD700", accent: "#A9A9A9" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 4, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 3, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }] },
{ id: 1, name: 'Stone Darknut', hp: 20, damage: 3, defense: 1, speed: 1.2, colors: { body: "#696969", eye: "#FFD700", accent: "#A9A9A9" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 3, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 4, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 4, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }] },
{ id: 2, name: 'Stone Wizzrobe', hp: 20, damage: 2, defense: 0, speed: 0, colors: { body: "#696969", eye: "#FFD700", accent: "#A9A9A9" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 3, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }] },
{ id: 3, name: 'Root Moldorm', hp: 21, damage: 4, defense: 1, speed: 2, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 5, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 5, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 4, name: 'Moss Genie', hp: 33, damage: 4, defense: 1, speed: 0, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 5, name: 'Moss Like Like', hp: 16, damage: 3, defense: 0, speed: 1, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 3, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }] },
{ id: 6, name: 'Verdant Aquamentus', hp: 36, damage: 4, defense: 1, speed: 0, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 5, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 7, name: 'Verdant Manhandla', hp: 20, damage: 3, defense: 0, speed: 1.8, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 3, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }] },
{ id: 8, name: 'Forest Ice Gleeok', hp: 39, damage: 4, defense: 1, speed: 1.5, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 5, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 4, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 9, name: 'Verdant Agahnim', hp: 30, damage: 5, defense: 2, speed: 0, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 7, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 7, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 9, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 7, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 10, name: 'Green Darknut', hp: 30, damage: 4, defense: 2, speed: 1.2, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 5, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 5, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 11, name: 'Verdant Gleeok', hp: 52, damage: 4, defense: 1, speed: 1.5, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 5, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 4, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 12, name: 'Thorn Wizzrobe', hp: 30, damage: 3, defense: 0, speed: 0, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 4, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 13, name: 'Root Moldorm', hp: 28, damage: 4, defense: 1, speed: 2, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 5, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 5, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 14, name: 'Moss Genie', hp: 33, damage: 4, defense: 1, speed: 0, colors: { body: "#228B22", eye: "#90EE90", accent: "#006400" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 15, name: 'Dune Like Like', hp: 32, damage: 4, defense: 1, speed: 1, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 20 }] },
{ id: 16, name: 'Sand Aquamentus', hp: 36, damage: 4, defense: 1, speed: 0, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 5, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 17, name: 'Dune Manhandla', hp: 30, damage: 4, defense: 1, speed: 1.8, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 18, name: 'Desert Ice Gleeok', hp: 52, damage: 4, defense: 1, speed: 1.5, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 5, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 4, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 19, name: 'Sand Agahnim', hp: 40, damage: 5, defense: 2, speed: 0, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 7, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 7, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 9, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 7, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 20, name: 'Sand Genie', hp: 44, damage: 4, defense: 1, speed: 0, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 5, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 21, name: 'Sand Darknut', hp: 60, damage: 5, defense: 3, speed: 1.2, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 7, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 7, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "hammer", weight: 8 }] },
{ id: 22, name: 'Sand Gleeok', hp: 52, damage: 4, defense: 1, speed: 1.5, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 5, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 4, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 23, name: 'Mirage Wizzrobe', hp: 60, damage: 4, defense: 1, speed: 0, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 6, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "keese"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "ice_rod", weight: 5 }] },
{ id: 24, name: 'Dune Moldorm', hp: 28, damage: 4, defense: 1, speed: 2, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 5, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 5, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 25, name: 'Sand Genie', hp: 66, damage: 5, defense: 2, speed: 0, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 7, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 26, name: 'Dune Like Like', hp: 32, damage: 4, defense: 1, speed: 1, colors: { body: "#DAA520", eye: "#8B4513", accent: "#FFD700" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 20 }] },
{ id: 27, name: 'Frost Aquamentus', hp: 72, damage: 5, defense: 2, speed: 0, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 7, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 28, name: 'Frost Manhandla', hp: 40, damage: 4, defense: 1, speed: 1.8, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 4, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 5, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 5, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }] },
{ id: 29, name: 'Glacier Gleeok', hp: 78, damage: 5, defense: 2, speed: 1.5, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 7, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 5, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 8, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 6, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 30, name: 'Frost Agahnim', hp: 60, damage: 6, defense: 3, speed: 0, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 8, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 8, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 8, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 31, name: 'Frost Like Like', hp: 64, damage: 5, defense: 1, speed: 1, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "super_potion", weight: 20 }] },
{ id: 32, name: 'Iron Darknut', hp: 60, damage: 5, defense: 3, speed: 1.2, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 7, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 7, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "hammer", weight: 8 }] },
{ id: 33, name: 'Frost Gleeok', hp: 104, damage: 5, defense: 1, speed: 1.5, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 7, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 8, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 6, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "fire_rod", weight: 5 }] },
{ id: 34, name: 'Frost Wizzrobe', hp: 60, damage: 4, defense: 1, speed: 0, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 5, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 6, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "keese"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "ice_rod", weight: 5 }] },
{ id: 35, name: 'Frost Moldorm', hp: 56, damage: 5, defense: 1, speed: 2, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 7, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 7, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 36, name: 'Ice Genie', hp: 66, damage: 5, defense: 2, speed: 0, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 7, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 37, name: 'Frost Like Like', hp: 64, damage: 5, defense: 1, speed: 1, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "super_potion", weight: 20 }] },
{ id: 38, name: 'Frost Aquamentus', hp: 72, damage: 5, defense: 2, speed: 0, colors: { body: "#4682B4", eye: "#E0E0E0", accent: "#87CEEB" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 7, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 39, name: 'Crystal Manhandla', hp: 80, damage: 5, defense: 1, speed: 1.8, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 7, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 7, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "boomerang", weight: 5 }] },
{ id: 40, name: 'Shadow Ice Gleeok', hp: 104, damage: 5, defense: 1, speed: 1.5, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 7, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 5, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 8, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 6, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "fire_rod", weight: 5 }] },
{ id: 41, name: 'Crystal Agahnim', hp: 115, damage: 7, defense: 3, speed: 0, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 9, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 9, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 9, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 9, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 11, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 9, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "magic_sword", weight: 5 }] },
{ id: 42, name: 'Deep Moldorm', hp: 56, damage: 5, defense: 1, speed: 2, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 7, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 7, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 43, name: 'Deep Darknut', hp: 115, damage: 6, defense: 3, speed: 1.2, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 8, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 8, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 10, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 6, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "hammer", weight: 8 }] },
{ id: 44, name: 'Shadow Gleeok', hp: 104, damage: 5, defense: 1, speed: 1.5, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 7, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 5, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 8, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 6, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "fire_rod", weight: 5 }] },
{ id: 45, name: 'Crystal Wizzrobe', hp: 115, damage: 5, defense: 1, speed: 0, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 8, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "ice_rod", weight: 5 }] },
{ id: 46, name: 'Deep Moldorm', hp: 56, damage: 5, defense: 1, speed: 2, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 7, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 7, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }] },
{ id: 47, name: 'Crystal Genie', hp: 127, damage: 6, defense: 2, speed: 0, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 8, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 9, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3}, { type: "projectile", damage: 8, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "magic_crystal", weight: 10 }] },
{ id: 48, name: 'Deep Like Like', hp: 64, damage: 5, defense: 1, speed: 1, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 5, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "super_potion", weight: 20 }] },
{ id: 49, name: 'Crystal Aquamentus', hp: 138, damage: 6, defense: 2, speed: 0, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 8, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 10, cooldown: 1200, count: 3, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "fire_rod", weight: 5 }] },
{ id: 50, name: 'Crystal Manhandla', hp: 115, damage: 6, defense: 2, speed: 1.8, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 8, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 8, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "boomerang", weight: 5 }] },
{ id: 51, name: 'Shadow Ice Gleeok', hp: 195, damage: 6, defense: 2, speed: 1.5, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 8, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 6, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 9, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 1400, count: 4, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 8, cooldown: 1100, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "fire_rod", weight: 5 }] },
{ id: 52, name: 'Crystal Agahnim', hp: 115, damage: 7, defense: 3, speed: 0, colors: { body: "#4B0082", eye: "#DA70D6", accent: "#9400D3" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 9, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 9, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 9, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 9, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 11, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 9, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "magic_sword", weight: 5 }] },
{ id: 53, name: 'Tide Wizzrobe', hp: 150, damage: 5, defense: 1, speed: 0, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 8, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "ice_rod", weight: 5 }, { item: "magic_potion", weight: 15 }] },
{ id: 54, name: 'Abyssal Darknut', hp: 115, damage: 6, defense: 3, speed: 1.2, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 8, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 8, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 10, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 6, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "hammer", weight: 8 }] },
{ id: 55, name: 'Tidal Gleeok', hp: 150, damage: 6, defense: 2, speed: 1.5, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 8, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 6, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 9, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 7, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "fire_rod", weight: 5 }] },
{ id: 56, name: 'Tide Wizzrobe', hp: 115, damage: 5, defense: 1, speed: 0, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 7, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 8, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "ice_rod", weight: 5 }] },
{ id: 57, name: 'Abyssal Moldorm', hp: 105, damage: 6, defense: 2, speed: 2, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 8, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 8, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "hookshot", weight: 3 }] },
{ id: 58, name: 'Tidal Genie', hp: 127, damage: 6, defense: 2, speed: 0, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 8, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 9, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3}, { type: "projectile", damage: 8, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "magic_crystal", weight: 10 }] },
{ id: 59, name: 'Abyssal Like Like', hp: 92, damage: 6, defense: 2, speed: 1, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "super_potion", weight: 20 }] },
{ id: 60, name: 'Tidal Aquamentus', hp: 180, damage: 6, defense: 2, speed: 0, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 8, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 10, cooldown: 1200, count: 3, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "fire_rod", weight: 5 }] },
{ id: 61, name: 'Abyssal Manhandla', hp: 200, damage: 8, defense: 3, speed: 1.8, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 10, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "boomerang", weight: 5 }] },
{ id: 62, name: 'Abyssal Water Gleeok', hp: 195, damage: 6, defense: 2, speed: 1.5, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 8, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 6, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 9, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 7, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 1400, count: 4, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 8, cooldown: 1100, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "fire_rod", weight: 5 }] },
{ id: 63, name: 'Tidal Agahnim', hp: 200, damage: 10, defense: 4, speed: 0, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 13, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 13, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 13, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 13, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 15, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 13, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "magic_sword", weight: 5 }] },
{ id: 64, name: 'Abyssal Darknut', hp: 150, damage: 6, defense: 3, speed: 1.2, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 8, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 8, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 10, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 6, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "hammer", weight: 8 }, { item: "magic_sword", weight: 3 }] },
{ id: 65, name: 'Abyssal Darknut', hp: 200, damage: 8, defense: 4, speed: 1.2, colors: { body: "#006994", eye: "#00CED1", accent: "#4682B4" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 12, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 8, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hammer", weight: 8 }, { item: "magic_sword", weight: 3 }] },
{ id: 66, name: 'Storm Gleeok', hp: 195, damage: 6, defense: 2, speed: 1.5, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 8, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 6, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 9, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 7, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 3}, { type: "melee", damage: 8, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "fire_rod", weight: 5 }] },
{ id: 67, name: 'Storm Wizzrobe', hp: 200, damage: 6, defense: 2, speed: 0, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 9, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "ice_rod", weight: 5 }, { item: "magic_potion", weight: 15 }] },
{ id: 68, name: 'Cloud Moldorm', hp: 105, damage: 6, defense: 2, speed: 2, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 6, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 8, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 8, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "hookshot", weight: 3 }] },
{ id: 69, name: 'Sky Genie', hp: 165, damage: 6, defense: 2, speed: 0, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 8, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 9, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3}, { type: "projectile", damage: 8, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "magic_crystal", weight: 10 }] },
{ id: 70, name: 'Cloud Like Like', hp: 160, damage: 8, defense: 3, speed: 1, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 10, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "super_potion", weight: 20 }] },
{ id: 71, name: 'Storm Aquamentus', hp: 300, damage: 8, defense: 3, speed: 0, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 10, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 10, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 12, cooldown: 1200, count: 3, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }] },
{ id: 72, name: 'Storm Manhandla', hp: 200, damage: 8, defense: 3, speed: 1.8, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 10, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "boomerang", weight: 5 }] },
{ id: 73, name: 'Sky Ice Gleeok', hp: 325, damage: 8, defense: 3, speed: 1.5, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 10, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 8, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 11, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 9, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 12, cooldown: 1400, count: 4, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 10, cooldown: 1100, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }, { item: "master_sword", weight: 3 }] },
{ id: 74, name: 'Storm Agahnim', hp: 200, damage: 10, defense: 4, speed: 0, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 13, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 13, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 13, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 13, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 15, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 13, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "magic_sword", weight: 5 }] },
{ id: 75, name: 'Storm Gleeok', hp: 325, damage: 8, defense: 3, speed: 1.5, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 10, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 11, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 9, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 12, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 3}, { type: "melee", damage: 10, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }, { item: "master_sword", weight: 3 }] },
{ id: 76, name: 'Sky Darknut', hp: 200, damage: 8, defense: 4, speed: 1.2, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 12, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 8, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hammer", weight: 8 }, { item: "magic_sword", weight: 3 }] },
{ id: 77, name: 'Storm Gleeok', hp: 325, damage: 8, defense: 3, speed: 1.5, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 10, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 11, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 9, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 12, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 3}, { type: "melee", damage: 10, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }, { item: "master_sword", weight: 3 }] },
{ id: 78, name: 'Storm Wizzrobe', hp: 200, damage: 6, defense: 2, speed: 0, colors: { body: "#FFD700", eye: "#FFFACD", accent: "#FF8C00" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 9, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "ice_rod", weight: 5 }, { item: "magic_potion", weight: 15 }] },
{ id: 79, name: 'Shadow Moldorm', hp: 175, damage: 8, defense: 3, speed: 2, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hookshot", weight: 3 }] },
{ id: 80, name: 'Dark Genie', hp: 275, damage: 8, defense: 3, speed: 0, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 10, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 10, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 11, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3}, { type: "projectile", damage: 10, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "magic_crystal", weight: 10 }] },
{ id: 81, name: 'Shadow Like Like', hp: 200, damage: 8, defense: 3, speed: 1, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 10, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "super_potion", weight: 20 }] },
{ id: 82, name: 'Dark Aquamentus', hp: 300, damage: 8, defense: 3, speed: 0, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 10, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 10, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 12, cooldown: 1200, count: 3, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }] },
{ id: 83, name: 'Dark Manhandla', hp: 250, damage: 8, defense: 3, speed: 1.8, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "charge", drawFunction: "drawGeneric", description: "A spinning plant monster with razor-sharp limbs.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 2000, count: 4, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 10, cooldown: 800, count: 4, spreadAngle: 360, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "boomerang", weight: 5 }] },
{ id: 84, name: 'Dark Ice Gleeok', hp: 325, damage: 8, defense: 3, speed: 1.5, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "charge", drawFunction: "drawGleeok", description: "A spectral dragon wielding ice and shadow.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 10, cooldown: 2200, count: 2, spreadAngle: 360, projectileSpeed: 1.5}, { type: "melee", damage: 8, cooldown: 1800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 11, cooldown: 2000, count: 3, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 9, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 12, cooldown: 1400, count: 4, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 10, cooldown: 1100, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }, { item: "master_sword", weight: 3 }] },
{ id: 85, name: 'Void Agahnim', hp: 250, damage: 10, defense: 4, speed: 0, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "stationary", drawFunction: "drawWizzrobe", description: "A dark mage protected by impenetrable barriers.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 13, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 60, attacks: [{ type: "melee", damage: 13, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "projectile", damage: 13, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 30, attacks: [{ type: "projectile", damage: 13, cooldown: 1500, count: 3, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 10, attacks: [{ type: "melee", damage: 15, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 13, cooldown: 1500, count: 4, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "magic_sword", weight: 5 }] },
{ id: 86, name: 'Shadow Moldorm', hp: 175, damage: 8, defense: 3, speed: 2, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hookshot", weight: 3 }] },
{ id: 87, name: 'Shadow Darknut', hp: 250, damage: 8, defense: 4, speed: 1.2, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 12, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 8, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hammer", weight: 8 }, { item: "magic_sword", weight: 3 }] },
{ id: 88, name: 'Dark Gleeok', hp: 325, damage: 8, defense: 3, speed: 1.5, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 10, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 8, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 11, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 9, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 12, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 3}, { type: "melee", damage: 10, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }, { item: "master_sword", weight: 3 }] },
{ id: 89, name: 'Void Wizzrobe', hp: 250, damage: 6, defense: 2, speed: 0, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 8, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 9, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "ice_rod", weight: 5 }, { item: "magic_potion", weight: 15 }] },
{ id: 90, name: 'Shadow Moldorm', hp: 175, damage: 8, defense: 3, speed: 2, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hookshot", weight: 3 }] },
{ id: 91, name: 'Dark Genie', hp: 550, damage: 15, defense: 4, speed: 0, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 20, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 20, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 21, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3}, { type: "projectile", damage: 20, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "magic_crystal", weight: 10 }] },
{ id: 92, name: 'Shadow Like Like', hp: 200, damage: 8, defense: 3, speed: 1, colors: { body: "#1A0A2A", eye: "#9400D3", accent: "#4B0082" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 10, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "super_potion", weight: 20 }] },
{ id: 93, name: 'Infernal Aquamentus', hp: 600, damage: 15, defense: 4, speed: 0, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "stationary", drawFunction: "drawGleeok", description: "A dragon that breathes flames at everything in sight.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 20, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 2}]}, { hpThreshold: 50, attacks: [{ type: "aoe", damage: 20, cooldown: 1800, count: 3, spreadAngle: 120, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 22, cooldown: 1200, count: 3, spreadAngle: 180, projectileSpeed: 3}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }] },
{ id: 94, name: 'Abyssal Like Like', hp: 200, damage: 8, defense: 3, speed: 1, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "chase", drawFunction: "drawGeneric", description: "A voracious worm that steals hearts and shields.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 40, attacks: [{ type: "melee", damage: 10, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "summon", damage: 0, cooldown: 5000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "slime"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "super_potion", weight: 20 }] },
{ id: 95, name: 'Demon Genie', hp: 550, damage: 15, defense: 4, speed: 0, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "stationary", drawFunction: "drawGeneric", description: "A lamp spirit that unleashes devastating elemental blasts.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 20, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 2.5}]}, { hpThreshold: 50, attacks: [{ type: "projectile", damage: 20, cooldown: 1200, count: 2, spreadAngle: 90, projectileSpeed: 2.5}]}, { hpThreshold: 20, attacks: [{ type: "aoe", damage: 21, cooldown: 1000, count: 3, spreadAngle: 120, projectileSpeed: 3}, { type: "projectile", damage: 20, cooldown: 800, count: 2, spreadAngle: 45, projectileSpeed: 3.5}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "magic_crystal", weight: 10 }] },
{ id: 96, name: 'Abyssal Moldorm', hp: 175, damage: 8, defense: 3, speed: 2, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "charge", drawFunction: "drawGeneric", description: "A giant segmented worm that charges relentlessly.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 2000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 10, cooldown: 3000, count: 1, spreadAngle: 360, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hookshot", weight: 3 }] },
{ id: 97, name: 'Dark Wizzrobe', hp: 500, damage: 12, defense: 3, speed: 0, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "teleport", drawFunction: "drawWizzrobe", description: "A teleporting mage who commands destructive magic.", phases: [{ hpThreshold: 100, attacks: [{ type: "projectile", damage: 16, cooldown: 1500, count: 2, spreadAngle: 60, projectileSpeed: 3}]}, { hpThreshold: 40, attacks: [{ type: "projectile", damage: 17, cooldown: 1200, count: 3, spreadAngle: 90, projectileSpeed: 3.5}, { type: "summon", damage: 0, cooldown: 4000, count: 2, spreadAngle: 0, projectileSpeed: 0, enemyType: "stalfos"}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "ice_rod", weight: 5 }, { item: "magic_potion", weight: 15 }] },
{ id: 98, name: 'Demon Darknut', hp: 250, damage: 8, defense: 4, speed: 1.2, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "chase", drawFunction: "drawDarknut", description: "A heavily-armored knight wielding a massive sword.", phases: [{ hpThreshold: 100, attacks: [{ type: "melee", damage: 8, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 50, attacks: [{ type: "melee", damage: 10, cooldown: 800, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "melee", damage: 10, cooldown: 1500, count: 3, spreadAngle: 120, projectileSpeed: 0}]}, { hpThreshold: 25, attacks: [{ type: "melee", damage: 12, cooldown: 600, count: 1, spreadAngle: 0, projectileSpeed: 0}, { type: "aoe", damage: 8, cooldown: 2000, count: 4, spreadAngle: 180, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "hammer", weight: 8 }, { item: "magic_sword", weight: 3 }] },
{ id: 99, name: 'Infernal Gleeok', hp: 650, damage: 15, defense: 4, speed: 1.5, colors: { body: "#8B0000", eye: "#FFD700", accent: "#FF4500" }, behavior: "charge", drawFunction: "drawGleeok", description: "A multi-headed dragon whose fiery breath devastates all.", phases: [{ hpThreshold: 100, attacks: [{ type: "aoe", damage: 20, cooldown: 2000, count: 2, spreadAngle: 360, projectileSpeed: 2}, { type: "melee", damage: 15, cooldown: 1500, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 60, attacks: [{ type: "aoe", damage: 21, cooldown: 1800, count: 3, spreadAngle: 360, projectileSpeed: 2.5}, { type: "melee", damage: 16, cooldown: 1200, count: 1, spreadAngle: 0, projectileSpeed: 0}]}, { hpThreshold: 30, attacks: [{ type: "aoe", damage: 22, cooldown: 1200, count: 4, spreadAngle: 360, projectileSpeed: 3}, { type: "melee", damage: 17, cooldown: 1000, count: 1, spreadAngle: 0, projectileSpeed: 0}]}], drops: [{ item: "heart_piece", weight: 100 }, { item: "healing_potion", weight: 30 }, { item: "super_potion", weight: 15 }, { item: "magic_potion", weight: 10 }, { item: "heart_piece", weight: 20 }, { item: "fire_rod", weight: 5 }, { item: "master_sword", weight: 3 }] },
];
function getBossType(id) {
return BOSS_TYPES.find(b => b.id === id);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { BOSS_TYPES, getBossType };
}

108
src/data/dungeons.js Normal file
View File

@ -0,0 +1,108 @@
'use strict';
const DUNGEONS = [
{ id: 0, name: 'The Stone Chamber', theme: 'stone', difficulty: 1, seed: 0, roomCount: 20, requiredWeapon: null, description: 'A modest stone chamber where adventurers first learn the ways of the sword.' },
{ id: 1, name: 'The Granite Halls', theme: 'stone', difficulty: 1, seed: 12346, roomCount: 21, requiredWeapon: 'Sword', description: 'Massive granite halls guard an ancient passage. A sword is required to break the stone seal.' },
{ id: 2, name: 'The Quarry Depths', theme: 'stone', difficulty: 1, seed: 24694, roomCount: 20, requiredWeapon: null, description: 'An abandoned quarry where echoes of forgotten miners still linger in the dark.' },
{ id: 3, name: 'The Verdant Grove', theme: 'forest', difficulty: 2, seed: 37044, roomCount: 23, requiredWeapon: null, description: 'A living grove where ancient trees shift and rearrange the paths beneath your feet.' },
{ id: 4, name: 'The Mosswood Sanctum', theme: 'forest', difficulty: 2, seed: 49396, roomCount: 25, requiredWeapon: null, description: 'Thick moss carpets every surface in this hidden woodland sanctuary.' },
{ id: 5, name: 'The Ancient Canopy', theme: 'forest', difficulty: 1, seed: 61750, roomCount: 22, requiredWeapon: null, description: 'High above the forest floor, platforms of woven branches form an aerial dungeon.' },
{ id: 6, name: 'The Fernmere Hollow', theme: 'forest', difficulty: 2, seed: 74106, roomCount: 24, requiredWeapon: null, description: 'A sunken hollow choked with ferns and fed by underground streams.' },
{ id: 7, name: 'The Bramblewood Labyrinth', theme: 'forest', difficulty: 1, seed: 86464, roomCount: 23, requiredWeapon: null, description: 'Thorny brambles twist and turn, forming an ever-shifting maze of thorns.' },
{ id: 8, name: 'The Oakheart Stronghold', theme: 'forest', difficulty: 2, seed: 98824, roomCount: 26, requiredWeapon: null, description: 'Built inside a colossal oak, this stronghold is defended by tree spirits.' },
{ id: 9, name: 'The Willowmere Temple', theme: 'forest', difficulty: 2, seed: 111186, roomCount: 27, requiredWeapon: null, description: 'Weeping willows drape over a waterside temple of forgotten forest gods.' },
{ id: 10, name: 'The Thornwood Bastion', theme: 'forest', difficulty: 2, seed: 123550, roomCount: 23, requiredWeapon: null, description: 'A fortified grove where every plant is a weapon and every path a trap.' },
{ id: 11, name: 'The Elderwood Ruins', theme: 'forest', difficulty: 3, seed: 135916, roomCount: 32, requiredWeapon: null, description: 'The crumbling ruins of an ancient forest civilization, overgrown with time.' },
{ id: 12, name: 'The Sylvan Depths', theme: 'forest', difficulty: 2, seed: 148284, roomCount: 25, requiredWeapon: null, description: 'Deep within the forest, roots have carved vast chambers into the earth below.' },
{ id: 13, name: 'The Myconid Caverns', theme: 'forest', difficulty: 3, seed: 160654, roomCount: 34, requiredWeapon: null, description: 'Giant fungal colonies pulse with bioluminescent light in these underground caverns.' },
{ id: 14, name: 'The Treant\'s Lair', theme: 'forest', difficulty: 2, seed: 173026, roomCount: 27, requiredWeapon: null, description: 'The ancient treant guards its hollowed heart, a labyrinth of wood and sap.' },
{ id: 15, name: 'The Sandstone Temple', theme: 'desert', difficulty: 3, seed: 185400, roomCount: 33, requiredWeapon: 'Bow', description: 'An ancient desert temple where only an arrow can open the sealed gateway.' },
{ id: 16, name: 'The Scorch Dunes', theme: 'desert', difficulty: 2, seed: 197776, roomCount: 24, requiredWeapon: null, description: 'Burning dunes hide buried chambers that shift with every shifting sandstorm.' },
{ id: 17, name: 'The Oasis of Mirages', theme: 'desert', difficulty: 2, seed: 210154, roomCount: 24, requiredWeapon: null, description: 'A shimmering oasis that may or may not be real — trust nothing in the desert heat.' },
{ id: 18, name: 'The Dunehowl Fortress', theme: 'desert', difficulty: 3, seed: 222534, roomCount: 35, requiredWeapon: null, description: 'A fortress carved into a dune, its walls howling with desert winds.' },
{ id: 19, name: 'The Sunblasted Ziggurat', theme: 'desert', difficulty: 3, seed: 234916, roomCount: 30, requiredWeapon: null, description: 'A towering ziggurat baked by millennia of relentless sun.' },
{ id: 20, name: 'The Mirage Wastes', theme: 'desert', difficulty: 3, seed: 247300, roomCount: 32, requiredWeapon: null, description: 'Endless wastes where reality distorts and paths lead nowhere but deeper.' },
{ id: 21, name: 'The Glass Desert', theme: 'desert', difficulty: 4, seed: 259686, roomCount: 37, requiredWeapon: null, description: 'A desert where ancient fire turned sand to glass, creating razor-sharp terrain.' },
{ id: 22, name: 'The Serpent\'s Burrow', theme: 'desert', difficulty: 3, seed: 272074, roomCount: 36, requiredWeapon: null, description: 'A vast network of tunnels dug by a legendary desert serpent.' },
{ id: 23, name: 'The Dune Sea', theme: 'desert', difficulty: 4, seed: 284464, roomCount: 39, requiredWeapon: null, description: 'An ocean of sand where buried cities lie beneath the shifting dunes.' },
{ id: 24, name: 'The Obsidian Crater', theme: 'desert', difficulty: 3, seed: 296856, roomCount: 31, requiredWeapon: null, description: 'A volcanic crater of black obsidian, still warm from ancient eruptions.' },
{ id: 25, name: 'The Sunken Caravan', theme: 'desert', difficulty: 4, seed: 309250, roomCount: 41, requiredWeapon: null, description: 'A buried caravan of sandships, swallowed whole by a sudden dune collapse.' },
{ id: 26, name: 'The Scorpion\'s Keep', theme: 'desert', difficulty: 3, seed: 321646, roomCount: 33, requiredWeapon: null, description: 'A fortress shaped like a giant scorpion, its pincers guarding the entrance.' },
{ id: 27, name: 'The Avalanche Peak', theme: 'mountain', difficulty: 4, seed: 334044, roomCount: 38, requiredWeapon: 'Hammer', description: 'A treacherous mountain peak where only a hammer can shatter the ice seals.' },
{ id: 28, name: 'The Summit Shrine', theme: 'mountain', difficulty: 3, seed: 346444, roomCount: 36, requiredWeapon: null, description: 'A shrine perched at the mountain\'s summit, reachable only by daring climbers.' },
{ id: 29, name: 'The Thunderhead Pass', theme: 'mountain', difficulty: 4, seed: 358846, roomCount: 40, requiredWeapon: null, description: 'A mountain pass perpetually shrouded in thunderclouds and lightning.' },
{ id: 30, name: 'The Frostbite Crags', theme: 'mountain', difficulty: 4, seed: 371250, roomCount: 36, requiredWeapon: null, description: 'Frozen crags where the cold itself is a deadly enemy.' },
{ id: 31, name: 'The Cloudspire Tower', theme: 'mountain', difficulty: 5, seed: 383656, roomCount: 47, requiredWeapon: null, description: 'A tower that pierces the clouds, its upper floors lost in mist.' },
{ id: 32, name: 'The Avalanche Caverns', theme: 'mountain', difficulty: 4, seed: 396064, roomCount: 42, requiredWeapon: null, description: 'Underground caverns beneath the peaks, filled with glacial melt and echoing thunder.' },
{ id: 33, name: 'The Geyser Springs', theme: 'mountain', difficulty: 5, seed: 408474, roomCount: 49, requiredWeapon: null, description: 'A network of caverns heated by underground geysers that erupt without warning.' },
{ id: 34, name: 'The Iron Peak', theme: 'mountain', difficulty: 4, seed: 420886, roomCount: 38, requiredWeapon: null, description: 'A mountain rich in iron ore, its mines form an intricate subterranean maze.' },
{ id: 35, name: 'The Stormridge Stronghold', theme: 'mountain', difficulty: 5, seed: 433300, roomCount: 48, requiredWeapon: null, description: 'A fortress on a lightning-battered ridge, built by storm-worshipping ancients.' },
{ id: 36, name: 'The Yeti\'s Den', theme: 'mountain', difficulty: 4, seed: 445716, roomCount: 41, requiredWeapon: null, description: 'A frozen cave system inhabited by the legendary mountain yeti.' },
{ id: 37, name: 'The Glacier\'s Edge', theme: 'mountain', difficulty: 5, seed: 458134, roomCount: 50, requiredWeapon: null, description: 'A labyrinth carved into a massive glacier, its passages shifting with the ice.' },
{ id: 38, name: 'The Thunderpeak Fortress', theme: 'mountain', difficulty: 4, seed: 470554, roomCount: 37, requiredWeapon: null, description: 'A fortress built into a peak struck by lightning every storm season.' },
{ id: 39, name: 'The Underroot Caverns', theme: 'underground', difficulty: 5, seed: 482976, roomCount: 45, requiredWeapon: 'Bomb', description: 'Massive root systems form a subterranean web. Bombs are needed to clear the way.' },
{ id: 40, name: 'The Crystal Grotto', theme: 'underground', difficulty: 5, seed: 495400, roomCount: 47, requiredWeapon: null, description: 'A vast grotto filled with glowing crystals that sing when the wind passes through.' },
{ id: 41, name: 'The Fungal Hollows', theme: 'underground', difficulty: 6, seed: 507826, roomCount: 53, requiredWeapon: null, description: 'Giant mushroom forests grow in the dark, their spores a constant hazard.' },
{ id: 42, name: 'The Deepdelve Mines', theme: 'underground', difficulty: 5, seed: 520254, roomCount: 51, requiredWeapon: null, description: 'Ancient mining tunnels that stretch deeper than any living soul has ventured.' },
{ id: 43, name: 'The Echoing Chasm', theme: 'underground', difficulty: 6, seed: 532684, roomCount: 55, requiredWeapon: null, description: 'A bottomless chasm where every sound reverberates into madness.' },
{ id: 44, name: 'The Burrow Networks', theme: 'underground', difficulty: 5, seed: 545116, roomCount: 46, requiredWeapon: null, description: 'An intricate network of burrows connecting the roots of the entire forest above.' },
{ id: 45, name: 'The Subterranean Lake', theme: 'underground', difficulty: 6, seed: 557550, roomCount: 52, requiredWeapon: null, description: 'A vast underground lake with submerged chambers accessible only by diving.' },
{ id: 46, name: 'The Limestone Vaults', theme: 'underground', difficulty: 5, seed: 569986, roomCount: 49, requiredWeapon: null, description: 'Natural limestone vaults formed by millennia of dripping mineral water.' },
{ id: 47, name: 'The Mole\'s Labyrinth', theme: 'underground', difficulty: 6, seed: 582424, roomCount: 56, requiredWeapon: null, description: 'A mole empire has constructed an impossibly complex labyrinth beneath the earth.' },
{ id: 48, name: 'The Geode Chamber', theme: 'underground', difficulty: 5, seed: 594864, roomCount: 50, requiredWeapon: null, description: 'A massive geode split open to reveal crystalline chambers of breathtaking beauty.' },
{ id: 49, name: 'The Tunnels of Ash', theme: 'underground', difficulty: 6, seed: 607306, roomCount: 54, requiredWeapon: null, description: 'Tunnels filled with volcanic ash that clouds vision and chokes the breath.' },
{ id: 50, name: 'The Darkdelve', theme: 'underground', difficulty: 6, seed: 619750, roomCount: 51, requiredWeapon: null, description: 'The deepest point of the underground realm, where darkness has a will of its own.' },
{ id: 51, name: 'The Basalt Depths', theme: 'underground', difficulty: 7, seed: 632196, roomCount: 62, requiredWeapon: null, description: 'Columns of basalt form a natural maze deep beneath the crust.' },
{ id: 52, name: 'The Core Chamber', theme: 'underground', difficulty: 6, seed: 644644, roomCount: 57, requiredWeapon: null, description: 'The legendary chamber near the earth\'s core, pulsing with raw elemental energy.' },
{ id: 53, name: 'The Coral Reef Temple', theme: 'ocean', difficulty: 7, seed: 657094, roomCount: 64, requiredWeapon: 'Fire Rod', description: 'A temple built into a living coral reef. A fire rod is needed to burn through the kelp gates.' },
{ id: 54, name: 'The Abyssal Trench', theme: 'ocean', difficulty: 6, seed: 669546, roomCount: 53, requiredWeapon: null, description: 'A trench plunging into the deepest ocean, where pressure crushes all but the brave.' },
{ id: 55, name: 'The Tidepool Sanctum', theme: 'ocean', difficulty: 6, seed: 682000, roomCount: 61, requiredWeapon: null, description: 'A network of tide pools that fill and drain with the moon, shifting the dungeon layout.' },
{ id: 56, name: 'The Kraken\'s Lair', theme: 'ocean', difficulty: 6, seed: 694456, roomCount: 55, requiredWeapon: null, description: 'The underwater lair of a legendary kraken, its tentacles blocking every exit.' },
{ id: 57, name: 'The Kelp Forest', theme: 'ocean', difficulty: 7, seed: 706914, roomCount: 67, requiredWeapon: null, description: 'A dense forest of giant kelp that sways and entangles the unwary.' },
{ id: 58, name: 'The Sunken Fortress', theme: 'ocean', difficulty: 6, seed: 719374, roomCount: 63, requiredWeapon: null, description: 'A fortress sunk by a great flood, its flooded halls now home to ocean creatures.' },
{ id: 59, name: 'The Pearl Grotto', theme: 'ocean', difficulty: 6, seed: 731836, roomCount: 60, requiredWeapon: null, description: 'A grotto lined with iridescent pearls, guarded by pearl-diving ogresses.' },
{ id: 60, name: 'The Leviathan\'s Maw', theme: 'ocean', difficulty: 7, seed: 744300, roomCount: 65, requiredWeapon: null, description: 'A dungeon inside the skeletal remains of a colossal leviathan.' },
{ id: 61, name: 'The Brine Caverns', theme: 'ocean', difficulty: 8, seed: 756766, roomCount: 70, requiredWeapon: null, description: 'Salt caves beneath the ocean floor, their walls encrusted with millennia of brine.' },
{ id: 62, name: 'The Tidal Depths', theme: 'ocean', difficulty: 7, seed: 769234, roomCount: 66, requiredWeapon: null, description: 'Chambers that flood and drain with the tides, requiring precise timing to navigate.' },
{ id: 63, name: 'The Shipwreck Cavern', theme: 'ocean', difficulty: 8, seed: 781704, roomCount: 72, requiredWeapon: null, description: 'A cavern filled with the ghostly wrecks of a hundred sunken ships.' },
{ id: 64, name: 'The Nautilus Chamber', theme: 'ocean', difficulty: 7, seed: 794176, roomCount: 68, requiredWeapon: null, description: 'A chamber shaped like a nautilus shell, its spiral passages leading to the center.' },
{ id: 65, name: 'The Abyssal Throne', theme: 'ocean', difficulty: 8, seed: 806650, roomCount: 69, requiredWeapon: null, description: 'The throne room of the ocean king, at the bottom of the deepest abyss.' },
{ id: 66, name: 'The Cloudspire Aerie', theme: 'sky', difficulty: 7, seed: 819126, roomCount: 63, requiredWeapon: 'Ice Rod', description: 'A soaring aerie among the clouds. An ice rod is required to freeze the wind bridges.' },
{ id: 67, name: 'The Stratosphere Temple', theme: 'sky', difficulty: 8, seed: 831604, roomCount: 74, requiredWeapon: null, description: 'A temple floating in the stratosphere, accessible only by enchanted flight.' },
{ id: 68, name: 'The Galewind Fortress', theme: 'sky', difficulty: 7, seed: 844084, roomCount: 68, requiredWeapon: null, description: 'A fortress battered by hurricane winds on every side.' },
{ id: 69, name: 'The Celestial Observatory', theme: 'sky', difficulty: 7, seed: 856566, roomCount: 65, requiredWeapon: null, description: 'An ancient observatory where star-aligned mechanisms guard celestial secrets.' },
{ id: 70, name: 'The Zephyr\'s Roost', theme: 'sky', difficulty: 8, seed: 869050, roomCount: 71, requiredWeapon: null, description: 'The nesting ground of legendary wind spirits, perched on floating islands.' },
{ id: 71, name: 'The Cumulus Citadel', theme: 'sky', difficulty: 9, seed: 881536, roomCount: 82, requiredWeapon: null, description: 'A citadel built entirely from solidified cloud matter, shifting with the weather.' },
{ id: 72, name: 'The Starfall Platform', theme: 'sky', difficulty: 8, seed: 894024, roomCount: 73, requiredWeapon: null, description: 'A platform where fallen stars are collected and stored as powerful weapons.' },
{ id: 73, name: 'The Nimbus Sanctum', theme: 'sky', difficulty: 9, seed: 906514, roomCount: 85, requiredWeapon: null, description: 'A sanctum hidden within a permanent storm cloud, crackling with energy.' },
{ id: 74, name: 'The Thundercloud Tower', theme: 'sky', difficulty: 8, seed: 919006, roomCount: 75, requiredWeapon: null, description: 'A tower wreathed in perpetual lightning, its floors charged with electricity.' },
{ id: 75, name: 'The Aurora Spire', theme: 'sky', difficulty: 9, seed: 931500, roomCount: 84, requiredWeapon: null, description: 'A spire illuminated by eternal auroras, its colors shifting with the dungeon\'s mood.' },
{ id: 76, name: 'The Jetstream Labyrinth', theme: 'sky', difficulty: 8, seed: 943996, roomCount: 70, requiredWeapon: null, description: 'A labyrinth of floating platforms riding the jet stream at impossible altitudes.' },
{ id: 77, name: 'The Zenith Keep', theme: 'sky', difficulty: 9, seed: 956494, roomCount: 88, requiredWeapon: null, description: 'The highest keep in the sky realm, where the boundary between heaven and earth blurs.' },
{ id: 78, name: 'The Firmament Palace', theme: 'sky', difficulty: 8, seed: 968994, roomCount: 72, requiredWeapon: null, description: 'A palace built upon the firmament itself, its walls painted with constellations.' },
{ id: 79, name: 'The Eclipse Fortress', theme: 'shadow', difficulty: 9, seed: 981496, roomCount: 83, requiredWeapon: 'Hookshot', description: 'A fortress born from a solar eclipse. A hookshot is needed to cross the void gaps.' },
{ id: 80, name: 'The Umbral Caverns', theme: 'shadow', difficulty: 9, seed: 994000, roomCount: 80, requiredWeapon: null, description: 'Caverns of pure shadow where light itself is consumed and never returns.' },
{ id: 81, name: 'The Duskwood Labyrinth', theme: 'shadow', difficulty: 9, seed: 1006506, roomCount: 86, requiredWeapon: null, description: 'A labyrinth of eternal dusk where time stands still and hope fades slowly.' },
{ id: 82, name: 'The Nightfall Temple', theme: 'shadow', difficulty: 9, seed: 1019014, roomCount: 84, requiredWeapon: null, description: 'A temple dedicated to the goddess of night, her shadow stretching across every chamber.' },
{ id: 83, name: 'The Phantom\'s Reach', theme: 'shadow', difficulty: 9, seed: 1031524, roomCount: 87, requiredWeapon: null, description: 'A realm where phantoms roam freely and the living are mere intruders.' },
{ id: 84, name: 'The Twilight Abyss', theme: 'shadow', difficulty: 9, seed: 1044036, roomCount: 82, requiredWeapon: null, description: 'An abyss of twilight where the boundary between worlds grows dangerously thin.' },
{ id: 85, name: 'The Shadowmoor', theme: 'shadow', difficulty: 9, seed: 1056550, roomCount: 89, requiredWeapon: null, description: 'A moor of shadow where the ground itself is alive and hungry.' },
{ id: 86, name: 'The Void\'s Edge', theme: 'shadow', difficulty: 9, seed: 1069066, roomCount: 85, requiredWeapon: null, description: 'The edge of the void itself, where reality unravels into nothingness.' },
{ id: 87, name: 'The Darkflame Sanctum', theme: 'shadow', difficulty: 9, seed: 1081584, roomCount: 88, requiredWeapon: null, description: 'A sanctum lit by darkflame torches that burn with cold, shadowy fire.' },
{ id: 88, name: 'The Tenebris Keep', theme: 'shadow', difficulty: 9, seed: 1094104, roomCount: 83, requiredWeapon: null, description: 'A keep named for darkness itself, its walls absorbing all light.' },
{ id: 89, name: 'The Wraith\'s Domain', theme: 'shadow', difficulty: 9, seed: 1106626, roomCount: 90, requiredWeapon: null, description: 'The domain of an ancient wraith, where the dead have long since forgotten they were alive.' },
{ id: 90, name: 'The Abyssal Shadow', theme: 'shadow', difficulty: 9, seed: 1119150, roomCount: 81, requiredWeapon: null, description: 'The deepest shadow in the realm, where even fear cannot penetrate.' },
{ id: 91, name: 'The Obsidian Void', theme: 'shadow', difficulty: 10, seed: 1131676, roomCount: 92, requiredWeapon: null, description: 'A void of obsidian darkness — the penultimate trial before the final confrontation.' },
{ id: 92, name: 'The Endless Night', theme: 'shadow', difficulty: 9, seed: 1144204, roomCount: 86, requiredWeapon: null, description: 'An eternal night with no dawn, a dungeon that never lets you leave easily.' },
{ id: 93, name: 'The Demon\'s Keep', theme: 'final', difficulty: 10, seed: 1156734, roomCount: 95, requiredWeapon: null, description: 'The keep of the great demon. All paths lead to the final battle.' },
{ id: 94, name: 'The Dark Lord\'s Throne', theme: 'final', difficulty: 9, seed: 1169266, roomCount: 97, requiredWeapon: null, description: 'The throne room of the dark lord, where the fate of the world will be decided.' },
{ id: 95, name: 'The Final Bastion', theme: 'final', difficulty: 10, seed: 1181800, roomCount: 98, requiredWeapon: null, description: 'The last bastion between the hero and the source of all evil.' },
{ id: 96, name: 'The World\'s End', theme: 'final', difficulty: 9, seed: 1194336, roomCount: 92, requiredWeapon: null, description: 'The edge of the world, where the fabric of reality tears and chaos reigns.' },
{ id: 97, name: 'The Last Stand', theme: 'final', difficulty: 10, seed: 1206874, roomCount: 100, requiredWeapon: null, description: 'The final confrontation. There is no retreat — only victory or oblivion.' },
{ id: 98, name: 'The Creator\'s Chamber', theme: 'final', difficulty: 9, seed: 1219414, roomCount: 95, requiredWeapon: null, description: 'The chamber of the world\'s creator, holding the secrets of existence itself.' },
{ id: 99, name: 'The Eternal Realm', theme: 'final', difficulty: 10, seed: 1231956, roomCount: 97, requiredWeapon: null, description: 'Beyond the final door lies the eternal realm — where the true nature of everything awaits.' },
];
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DUNGEONS };
}

406
src/data/enemies.js Normal file
View File

@ -0,0 +1,406 @@
'use strict';
const ENEMY_TYPES = [
{
id: 'slime',
name: 'Slime',
hp: 2,
damage: 1,
defense: 0,
behavior: 'patrol',
speed: 1.0,
attackRange: 1,
colors: { body: '#6B8E23', eye: '#000', accent: '#8FBC8F' },
drawFunction: 'drawSlime',
drops: [
{ item: 'rupee_green', weight: 80 },
{ item: 'rupee_blue', weight: 15 },
{ item: 'healing_potion', weight: 5 }
]
},
{
id: 'slime_red',
name: 'Red Slime',
hp: 4,
damage: 2,
defense: 0,
behavior: 'patrol',
speed: 1.2,
attackRange: 1,
colors: { body: '#B22222', eye: '#000', accent: '#FF6347' },
drawFunction: 'drawSlime',
drops: [
{ item: 'rupee_blue', weight: 70 },
{ item: 'rupee_red', weight: 20 },
{ item: 'healing_potion', weight: 10 }
]
},
{
id: 'slime_blue',
name: 'Blue Slime',
hp: 6,
damage: 3,
defense: 1,
behavior: 'patrol',
speed: 0.8,
attackRange: 1,
colors: { body: '#4169E1', eye: '#000', accent: '#87CEEB' },
drawFunction: 'drawSlime',
drops: [
{ item: 'rupee_red', weight: 60 },
{ item: 'rupee_yellow', weight: 30 },
{ item: 'healing_potion', weight: 10 }
]
},
{
id: 'keese',
name: 'Keese',
hp: 1,
damage: 1,
defense: 0,
behavior: 'chase',
speed: 2.0,
attackRange: 1,
colors: { body: '#4B0082', eye: '#FFFF00', accent: '#800080' },
drawFunction: 'drawKeese',
drops: [
{ item: 'rupee_green', weight: 90 },
{ item: 'healing_potion', weight: 10 }
]
},
{
id: 'octorok',
name: 'Octorok',
hp: 3,
damage: 0,
defense: 0,
behavior: 'ranged',
speed: 0.5,
attackRange: 4,
projectileSpeed: 3,
colors: { body: '#8B4513', eye: '#FFF', accent: '#D2691E' },
drawFunction: 'drawOctorok',
drops: [
{ item: 'rupee_green', weight: 70 },
{ item: 'rupee_blue', weight: 20 },
{ item: 'arrow', weight: 10 }
]
},
{
id: 'moblin',
name: 'Moblin',
hp: 8,
damage: 4,
defense: 2,
behavior: 'chase',
speed: 1.5,
attackRange: 1,
colors: { body: '#2E8B57', eye: '#FF0000', accent: '#228B22' },
drawFunction: 'drawMoblin',
drops: [
{ item: 'rupee_blue', weight: 60 },
{ item: 'rupee_red', weight: 25 },
{ item: 'healing_potion', weight: 10 },
{ item: 'sword', weight: 2 },
{ item: 'arrow', weight: 3 }
]
},
{
id: 'stalfos',
name: 'Stalfos',
hp: 5,
damage: 3,
defense: 1,
behavior: 'chase',
speed: 1.8,
attackRange: 1,
colors: { body: '#F5F5DC', eye: '#FF0000', accent: '#DDA0DD' },
drawFunction: 'drawStalfos',
drops: [
{ item: 'rupee_red', weight: 50 },
{ item: 'rupee_yellow', weight: 30 },
{ item: 'healing_potion', weight: 15 },
{ item: 'bomb', weight: 5 }
]
},
{
id: 'goriya',
name: 'Goriya',
hp: 6,
damage: 3,
defense: 2,
behavior: 'chase',
speed: 1.6,
attackRange: 2,
colors: { body: '#DC143C', eye: '#FFF', accent: '#FF6347' },
drawFunction: 'drawGoriya',
drops: [
{ item: 'rupee_red', weight: 50 },
{ item: 'rupee_yellow', weight: 25 },
{ item: 'healing_potion', weight: 15 },
{ item: 'bow', weight: 5 },
{ item: 'arrow', weight: 5 }
]
},
{
id: 'armos',
name: 'Armos Knight',
hp: 10,
damage: 5,
defense: 3,
behavior: 'stationary',
speed: 0,
attackRange: 1,
colors: { body: '#FFD700', eye: '#000080', accent: '#FF8C00' },
drawFunction: 'drawArmos',
drops: [
{ item: 'rupee_yellow', weight: 50 },
{ item: 'healing_potion', weight: 30 },
{ item: 'hammer', weight: 5 },
{ item: 'heart_piece', weight: 2 }
]
},
{
id: 'lynel',
name: 'Lynel',
hp: 20,
damage: 8,
defense: 4,
behavior: 'charge',
speed: 2.5,
attackRange: 2,
colors: { body: '#8B0000', eye: '#FFD700', accent: '#FF4500' },
drawFunction: 'drawLynel',
drops: [
{ item: 'rupee_yellow', weight: 40 },
{ item: 'healing_potion', weight: 30 },
{ item: 'heart_piece', weight: 10 },
{ item: 'sword', weight: 5 },
{ item: 'arrow', weight: 5 },
{ item: 'bomb', weight: 5 },
{ item: 'boomerang', weight: 2 },
{ item: 'magic_potion', weight: 3 }
]
},
{
id: 'like',
name: 'Like Like',
hp: 4,
damage: 2,
defense: 1,
behavior: 'chase',
speed: 1.2,
attackRange: 1,
colors: { body: '#708090', eye: '#FF0000', accent: '#A9A9A9' },
drawFunction: 'drawSlime',
drops: [
{ item: 'rupee_blue', weight: 60 },
{ item: 'rupee_red', weight: 30 },
{ item: 'healing_potion', weight: 10 }
]
},
{
id: 'bub',
name: 'Bub',
hp: 3,
damage: 2,
defense: 0,
behavior: 'patrol',
speed: 1.0,
attackRange: 1,
colors: { body: '#696969', eye: '#FFF', accent: '#808080' },
drawFunction: 'drawKeese',
drops: [
{ item: 'rupee_green', weight: 70 },
{ item: 'rupee_blue', weight: 20 },
{ item: 'healing_potion', weight: 10 }
]
},
{
id: 'reedeath',
name: 'Reedeath',
hp: 3,
damage: 1,
defense: 0,
behavior: 'stationary',
speed: 0,
attackRange: 1,
colors: { body: '#228B22', eye: '#FFF', accent: '#32CD32' },
drawFunction: 'drawStalfos',
drops: [
{ item: 'rupee_green', weight: 80 },
{ item: 'healing_potion', weight: 15 },
{ item: 'bomb', weight: 5 }
]
},
{
id: 'wallmaster',
name: 'Wallmaster',
hp: 2,
damage: 15,
defense: 0,
behavior: 'teleport',
speed: 0,
attackRange: 1,
colors: { body: '#8B0000', eye: '#FFD700', accent: '#4B0082' },
drawFunction: 'drawKeese',
drops: [
{ item: 'rupee_yellow', weight: 60 },
{ item: 'heart_piece', weight: 20 },
{ item: 'healing_potion', weight: 20 }
]
},
{
id: 'manhandla',
name: 'Manhandla',
hp: 12,
damage: 6,
defense: 2,
behavior: 'charge',
speed: 2.0,
attackRange: 1,
colors: { body: '#8B4513', eye: '#FF0000', accent: '#D2691E' },
drawFunction: 'drawSlime',
drops: [
{ item: 'rupee_red', weight: 50 },
{ item: 'rupee_yellow', weight: 30 },
{ item: 'healing_potion', weight: 15 },
{ item: 'heart_piece', weight: 5 }
]
},
{
id: 'tektite',
name: 'Tektite',
hp: 2,
damage: 1,
defense: 0,
behavior: 'patrol',
speed: 1.5,
attackRange: 1,
colors: { body: '#A0522D', eye: '#FFD700', accent: '#CD853F' },
drawFunction: 'drawKeese',
drops: [
{ item: 'rupee_green', weight: 80 },
{ item: 'healing_potion', weight: 15 },
{ item: 'rupee_blue', weight: 5 }
]
},
{
id: 'leever',
name: 'Leever',
hp: 4,
damage: 2,
defense: 1,
behavior: 'charge',
speed: 2.0,
attackRange: 1,
colors: { body: '#DAA520', eye: '#000', accent: '#FFD700' },
drawFunction: 'drawKeese',
drops: [
{ item: 'rupee_blue', weight: 60 },
{ item: 'rupee_red', weight: 25 },
{ item: 'healing_potion', weight: 15 }
]
},
{
id: 'skullwallma',
name: 'Skullwallma',
hp: 5,
damage: 3,
defense: 1,
behavior: 'stationary',
speed: 0,
attackRange: 1,
colors: { body: '#F5F5DC', eye: '#FF0000', accent: '#DDA0DD' },
drawFunction: 'drawStalfos',
drops: [
{ item: 'rupee_red', weight: 50 },
{ item: 'healing_potion', weight: 30 },
{ item: 'bomb', weight: 15 },
{ item: 'heart_piece', weight: 5 }
]
},
{
id: 'ironknuckle',
name: 'Iron Knuckle',
hp: 15,
damage: 7,
defense: 4,
behavior: 'chase',
speed: 1.8,
attackRange: 2,
colors: { body: '#708090', eye: '#FF0000', accent: '#C0C0C0' },
drawFunction: 'drawMoblin',
drops: [
{ item: 'rupee_yellow', weight: 40 },
{ item: 'healing_potion', weight: 30 },
{ item: 'sword', weight: 10 },
{ item: 'heart_piece', weight: 10 },
{ item: 'arrow', weight: 10 }
]
},
{
id: 'darknut',
name: 'Darknut',
hp: 18,
damage: 6,
defense: 5,
behavior: 'chase',
speed: 1.4,
attackRange: 1,
colors: { body: '#2F4F4F', eye: '#FF0000', accent: '#708090' },
drawFunction: 'drawMoblin',
drops: [
{ item: 'rupee_yellow', weight: 40 },
{ item: 'healing_potion', weight: 25 },
{ item: 'heart_piece', weight: 15 },
{ item: 'hammer', weight: 10 },
{ item: 'boomerang', weight: 10 }
]
},
{
id: 'wizzrobe',
name: 'Wizzrobe',
hp: 8,
damage: 5,
defense: 1,
behavior: 'teleport',
speed: 0,
attackRange: 4,
colors: { body: '#4B0082', eye: '#FFFF00', accent: '#800080' },
drawFunction: 'drawStalfos',
drops: [
{ item: 'rupee_yellow', weight: 40 },
{ item: 'healing_potion', weight: 20 },
{ item: 'heart_piece', weight: 15 },
{ item: 'magic_potion', weight: 15 },
{ item: 'fire_rod', weight: 5 },
{ item: 'ice_rod', weight: 5 }
]
},
{
id: 'peahat',
name: 'Peahat',
hp: 2,
damage: 1,
defense: 0,
behavior: 'stationary',
speed: 0,
attackRange: 1,
colors: { body: '#228B22', eye: '#FFD700', accent: '#FF69B4' },
drawFunction: 'drawStalfos',
drops: [
{ item: 'rupee_green', weight: 80 },
{ item: 'healing_potion', weight: 15 },
{ item: 'rupee_blue', weight: 5 }
]
}
];
function getEnemyType(id) {
return ENEMY_TYPES.find(e => e.id === id);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ENEMY_TYPES, getEnemyType };
}

25
src/data/items.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
const ITEMS = [
{ id: 'healing_potion', name: 'Healing Potion', type: 'consumable', description: 'Restores 1 heart.', color: '#FF69B4', stackable: true, value: 0 },
{ id: 'super_potion', name: 'Super Potion', type: 'consumable', description: 'Restores 3 hearts.', color: '#FF1493', stackable: true, value: 0 },
{ id: 'magic_potion', name: 'Magic Potion', type: 'consumable', description: 'Restores magic power.', color: '#9400D3', stackable: true, value: 0 },
{ id: 'heart_piece', name: 'Heart Piece', type: 'consumable', description: 'Permanently increases max HP by 1/4 heart.', color: '#FF0000', stackable: false, value: 0 },
{ id: 'arrow', name: 'Arrow', type: 'material', description: 'Ammunition for the Bow.', color: '#8B4513', stackable: true, value: 0 },
{ id: 'bomb', name: 'Bomb', type: 'material', description: 'Explodes on impact. Destroys walls.', color: '#8B4513', stackable: true, value: 0 },
{ id: 'key', name: 'Key', type: 'key', description: 'Opens locked doors.', color: '#FFD700', stackable: true, value: 0 },
{ id: 'small_key', name: 'Small Key', type: 'key', description: 'Opens chests and small doors.', color: '#DAA520', stackable: true, value: 0 },
{ id: 'rupee_green', name: 'Green Rupee', type: 'currency', description: 'Worth 1 rupee.', color: '#00FF00', stackable: true, value: 1 },
{ id: 'rupee_blue', name: 'Blue Rupee', type: 'currency', description: 'Worth 5 rupees.', color: '#0000FF', stackable: true, value: 5 },
{ id: 'rupee_red', name: 'Red Rupee', type: 'currency', description: 'Worth 20 rupees.', color: '#FF0000', stackable: true, value: 20 },
{ id: 'rupee_yellow', name: 'Yellow Rupee', type: 'currency', description: 'Worth 100 rupees.', color: '#FFD700', stackable: true, value: 100 },
{ id: 'magic_crystal', name: 'Magic Crystal', type: 'material', description: 'Used to power magic weapons.', color: '#E0FFFF', stackable: true, value: 0 }
];
function getItem(id) {
return ITEMS.find(i => i.id === id);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ITEMS, getItem };
}

155
src/data/themes.js Normal file
View File

@ -0,0 +1,155 @@
'use strict';
// === DUNGEON THEMES ===
// Each theme defines color palettes and aesthetic properties for dungeon generation
const THEMES = {
stone: {
name: 'Stone',
floorColor: '#C4A265',
floorDarkColor: '#A8884D',
wallColor: '#505050',
wallTopColor: '#6E6E6E',
wallFrontColor: '#5A5A5A',
wallBottomColor: '#383838',
wallBorderColor: '#404040',
torchGlow: 'rgba(255,140,0,0.15)',
torchStickColor: '#654321',
torchFlameColor: '#FF8C00',
torchCoreColor: '#FFFF00',
cellColor: '#505050',
cellTextColor: '#FFF',
},
forest: {
name: 'Forest',
floorColor: '#4A6741',
floorDarkColor: '#3A5731',
wallColor: '#5D4037',
wallTopColor: '#6D5047',
wallFrontColor: '#5D4037',
wallBottomColor: '#4D3027',
wallBorderColor: '#3D2017',
torchGlow: 'rgba(100,200,100,0.15)',
torchStickColor: '#4A3020',
torchFlameColor: '#00FF00',
torchCoreColor: '#AAFFAA',
cellColor: '#2E5A2E',
cellTextColor: '#AFA',
},
desert: {
name: 'Desert',
floorColor: '#D4A567',
floorDarkColor: '#C49557',
wallColor: '#8B7355',
wallTopColor: '#9B8365',
wallFrontColor: '#8B7355',
wallBottomColor: '#7B6345',
wallBorderColor: '#6B5335',
torchGlow: 'rgba(255,200,100,0.15)',
torchStickColor: '#654321',
torchFlameColor: '#FFD700',
torchCoreColor: '#FFFFAA',
cellColor: '#C49557',
cellTextColor: '#3A2A1A',
},
mountain: {
name: 'Mountain',
floorColor: '#708090',
floorDarkColor: '#607080',
wallColor: '#4A5568',
wallTopColor: '#5A6578',
wallFrontColor: '#4A5568',
wallBottomColor: '#3A4558',
wallBorderColor: '#2A3548',
torchGlow: 'rgba(200,200,255,0.15)',
torchStickColor: '#4A4040',
torchFlameColor: '#87CEEB',
torchCoreColor: '#E0F0FF',
cellColor: '#4A5568',
cellTextColor: '#E0E8F0',
},
underground: {
name: 'Underground',
floorColor: '#3A3A3A',
floorDarkColor: '#2A2A2A',
wallColor: '#2D2D2D',
wallTopColor: '#3D3D3D',
wallFrontColor: '#2D2D2D',
wallBottomColor: '#1D1D1D',
wallBorderColor: '#151515',
torchGlow: 'rgba(255,100,0,0.2)',
torchStickColor: '#3A3020',
torchFlameColor: '#FF6347',
torchCoreColor: '#FFD700',
cellColor: '#2D2D2D',
cellTextColor: '#FFD700',
},
ocean: {
name: 'Ocean',
floorColor: '#4682B4',
floorDarkColor: '#3672A4',
wallColor: '#2F4F6F',
wallTopColor: '#3F5F7F',
wallFrontColor: '#2F4F6F',
wallBottomColor: '#1F3F5F',
wallBorderColor: '#0F2F4F',
torchGlow: 'rgba(100,150,255,0.15)',
torchStickColor: '#3A4050',
torchFlameColor: '#00BFFF',
torchCoreColor: '#E0FFFF',
cellColor: '#2F4F6F',
cellTextColor: '#AFDCF0',
},
sky: {
name: 'Sky',
floorColor: '#E8E0D0',
floorDarkColor: '#D8D0C0',
wallColor: '#F0E8D8',
wallTopColor: '#F8F0E8',
wallFrontColor: '#F0E8D8',
wallBottomColor: '#E0D8C8',
wallBorderColor: '#D0C8B8',
torchGlow: 'rgba(255,255,200,0.15)',
torchStickColor: '#8B7355',
torchFlameColor: '#FFD700',
torchCoreColor: '#FFFACD',
cellColor: '#F0E8D8',
cellTextColor: '#5A4A3A',
},
shadow: {
name: 'Shadow',
floorColor: '#1A0A2A',
floorDarkColor: '#0A001A',
wallColor: '#2A1A3A',
wallTopColor: '#3A2A4A',
wallFrontColor: '#2A1A3A',
wallBottomColor: '#1A0A2A',
wallBorderColor: '#0A0010',
torchGlow: 'rgba(150,0,200,0.2)',
torchStickColor: '#2A1A2A',
torchFlameColor: '#9400D3',
torchCoreColor: '#DA70D6',
cellColor: '#2A1A3A',
cellTextColor: '#D0A0E0',
},
final: {
name: 'Final',
floorColor: '#2A0A0A',
floorDarkColor: '#1A0000',
wallColor: '#3A1A1A',
wallTopColor: '#4A2A2A',
wallFrontColor: '#3A1A1A',
wallBottomColor: '#2A0A0A',
wallBorderColor: '#1A0000',
torchGlow: 'rgba(255,0,0,0.25)',
torchStickColor: '#3A0A0A',
torchFlameColor: '#FF0000',
torchCoreColor: '#FF4500',
cellColor: '#3A0A0A',
cellTextColor: '#FF6347',
},
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = { THEMES };
}

180
src/data/weapons.js Normal file
View File

@ -0,0 +1,180 @@
'use strict';
const WEAPONS = [
{
id: 'sword',
name: 'Sword',
damage: 3,
type: 'melee',
range: 1,
cooldown: 400,
ammo: null,
ammoCost: 1,
color: '#C0C0C0',
effect: null,
drawFunction: 'drawSword',
description: 'A basic sword. Reliable and versatile.'
},
{
id: 'bow',
name: 'Bow',
damage: 2,
type: 'ranged',
range: 8,
cooldown: 800,
ammo: 'arrow',
ammoCost: 1,
color: '#8B4513',
effect: null,
drawFunction: 'drawBow',
description: 'Shoots arrows at distant foes.'
},
{
id: 'boomerang',
name: 'Boomerang',
damage: 2,
type: 'ranged',
range: 5,
cooldown: 1200,
ammo: null,
ammoCost: 1,
color: '#FF8C00',
effect: 'stun',
drawFunction: 'drawBoomerang',
description: 'Returns to hand. Stuns enemies on hit.'
},
{
id: 'hammer',
name: 'Hammer',
damage: 5,
type: 'melee',
range: 2,
cooldown: 1000,
ammo: null,
ammoCost: 1,
color: '#8B4513',
effect: null,
drawFunction: 'drawHammer',
description: 'Heavy weapon. Breaks walls and cracks blocks.'
},
{
id: 'bomb',
name: 'Bomb',
damage: 4,
type: 'consumable',
range: 2,
cooldown: 600,
ammo: 'bomb',
ammoCost: 1,
color: '#8B4513',
effect: 'explode',
drawFunction: 'drawBomb',
description: 'Creates an explosion. Destroys breakable blocks.'
},
{
id: 'fire_rod',
name: 'Fire Rod',
damage: 3,
type: 'ranged',
range: 6,
cooldown: 1000,
ammo: 'magic',
ammoCost: 1,
color: '#FF4500',
effect: 'fire',
drawFunction: 'drawFireRod',
description: 'Shoots fireballs. Burns enemies over time.'
},
{
id: 'ice_rod',
name: 'Ice Rod',
damage: 2,
type: 'ranged',
range: 5,
cooldown: 1200,
ammo: 'magic',
ammoCost: 1,
color: '#87CEEB',
effect: 'ice',
drawFunction: 'drawIceRod',
description: 'Freezes enemies in place.'
},
{
id: 'hookshot',
name: 'Hookshot',
damage: 1,
type: 'ranged',
range: 8,
cooldown: 1500,
ammo: null,
ammoCost: 1,
color: '#C0C0C0',
effect: null,
drawFunction: 'drawHookshot',
description: 'Grapples to targets across gaps.'
},
{
id: 'master_sword',
name: 'Master Sword',
damage: 5,
type: 'melee',
range: 1,
cooldown: 350,
ammo: null,
ammoCost: 1,
color: '#FFD700',
effect: null,
drawFunction: 'drawMasterSword',
description: 'The legendary blade of the hero.'
},
{
id: 'magic_sword',
name: 'Magic Sword',
damage: 6,
type: 'melee',
range: 2,
cooldown: 500,
ammo: 'magic',
ammoCost: 1,
color: '#9400D3',
effect: 'stun',
drawFunction: 'drawMagicSword',
description: 'Deals magic damage and stuns foes.'
},
{
id: 'slingshot',
name: 'Slingshot',
damage: 1,
type: 'ranged',
range: 5,
cooldown: 600,
ammo: null,
ammoCost: 1,
color: '#8B4513',
effect: null,
drawFunction: 'drawSlingshot',
description: 'Simple but effective. No ammo needed.'
},
{
id: 'bug_net',
name: 'Bug Net',
damage: 1,
type: 'melee',
range: 1,
cooldown: 500,
ammo: null,
ammoCost: 1,
color: '#87CEEB',
effect: null,
drawFunction: 'drawBugNet',
description: 'Captures small creatures.'
}
];
function getWeapon(id) {
return WEAPONS.find(w => w.id === id);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { WEAPONS, getWeapon };
}

360
src/game/ai.js Normal file
View File

@ -0,0 +1,360 @@
'use strict';
// === ENEMY AI SYSTEM ===
function updateEnemies(dt) {
// Update regular enemies
for (const enemy of [...gameState.enemies]) {
enemy.moveTimer -= dt;
enemy.attackTimer -= dt;
if (enemy.hitTimer > 0) enemy.hitTimer -= dt;
if (enemy.specialTimer > 0) {
enemy.specialTimer -= dt;
if (enemy.specialTimer <= 0) {
enemy.stunned = false;
enemy.frozen = false;
enemy.onFire = false;
}
}
if (enemy.onFire) {
enemy.attackTimer -= dt * 0.5; // Fire damage
if (enemy.attackTimer <= 0) {
enemy.attackTimer = 1000;
enemy.hp -= 1;
spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#FF4500', 2);
}
}
// Skip AI if stunned or frozen
if (enemy.stunned || enemy.frozen) continue;
// Movement AI
if (enemy.moveTimer <= 0) {
enemy.moveTimer = enemy.moveInterval || ENEMY_MOVE_INTERVAL;
updateEnemyMovement(enemy);
}
// Attack AI
if (manhattanDist(enemy.x, enemy.y, gameState.player.x, gameState.player.y) <= (enemy.attackRange || 1)) {
if (enemy.attackTimer <= 0) {
enemy.attackTimer = enemy.attackInterval || ENEMY_ATTACK_INTERVAL;
enemyAttack(enemy);
}
}
// Special behaviors
if (enemy.behavior === 'ranged' && enemy.projectileTimer !== undefined) {
enemy.projectileTimer -= dt;
if (enemy.projectileTimer <= 0) {
enemy.projectileTimer = enemy.shootInterval || 3000;
enemyShoot(enemy);
}
}
if (enemy.behavior === 'charge') {
if (enemy.chargeTimer !== undefined) {
enemy.chargeTimer -= dt;
if (enemy.chargeTimer <= 0 && !enemy.charging) {
enemy.charging = true;
enemy.chargeDuration = 1000;
}
}
if (enemy.charging) {
enemy.chargeDuration -= dt;
if (enemy.chargeDuration <= 0) {
enemy.charging = false;
enemy.chargeTimer = enemy.chargeCooldown || 5000;
enemyCharge(enemy);
}
}
}
}
// Update boss AI
if (gameState.currentBoss) {
const boss = gameState.currentBoss;
boss.moveTimer -= dt;
boss.attackTimer -= dt;
if (boss.hitTimer > 0) boss.hitTimer -= dt;
if (boss.specialTimer > 0) {
boss.specialTimer -= dt;
if (boss.specialTimer <= 0) {
boss.stunned = false;
boss.frozen = false;
}
}
if (!boss.stunned && !boss.frozen) {
if (boss.moveTimer <= 0) {
boss.moveTimer = boss.moveInterval || ENEMY_MOVE_INTERVAL;
updateBossMovement(boss);
}
if (boss.attackTimer <= 0) {
boss.attackTimer = boss.attackInterval || ENEMY_ATTACK_INTERVAL;
bossAttack(boss);
}
}
}
// Update projectiles
updateProjectiles(dt);
}
function updateEnemyMovement(enemy) {
const px = gameState.player.x;
const py = gameState.player.y;
const dist = manhattanDist(enemy.x, enemy.y, px, py);
// Behavior: chase
if (enemy.behavior === 'chase' || enemy.behavior === 'ranged' || enemy.behavior === 'charge') {
if (enemy.behavior === 'ranged' && dist < (enemy.minRange || 3)) return; // Back away if too close
if (enemy.behavior === 'ranged' && dist > (enemy.maxRange || 8)) {
// Move closer if too far
moveToward(enemy, px, py);
return;
}
moveToward(enemy, px, py);
return;
}
// Behavior: patrol
if (enemy.behavior === 'patrol') {
if (!enemy.patrolPath || enemy.patrolIdx === undefined) {
enemy.patrolIdx = 0;
enemy.patrolPath = generatePatrolPath(enemy);
}
if (enemy.patrolPath) {
const target = enemy.patrolPath[enemy.patrolIdx];
if (target) {
moveToward(enemy, target.x, target.y);
if (enemy.x === target.x && enemy.y === target.y) {
enemy.patrolIdx = (enemy.patrolIdx + 1) % enemy.patrolPath.length;
}
}
}
return;
}
// Behavior: stationary
if (enemy.behavior === 'stationary') {
if (dist <= (enemy.activateRange || 2)) {
moveToward(enemy, px, py);
}
return;
}
// Behavior: teleport
if (enemy.behavior === 'teleport') {
if (enemy.teleportTimer !== undefined) {
enemy.teleportTimer -= dt;
if (enemy.teleportTimer <= 0) {
enemy.teleportTimer = enemy.teleportInterval || 4000;
// Teleport near player
const tx = clamp(px + randInt(-2, 2), 1, COLS - 2);
const ty = clamp(py + randInt(-2, 2), 1, ROWS - 2);
if (isPassable(gameState.layout, tx, ty)) {
spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#8B008B', 6);
enemy.x = tx;
enemy.y = ty;
spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#8B008B', 6);
}
}
}
return;
}
// Behavior: explode
if (enemy.behavior === 'explode') {
moveToward(enemy, px, py);
if (manhattanDist(enemy.x, enemy.y, px, py) === 0) {
// Explode!
spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#FF4500', 12);
enemy.hp = 0;
// Damage player if adjacent
if (manhattanDist(enemy.x, enemy.y, px, py) <= 1 && !gameState.player.invincible) {
gameState.player.hp -= (enemy.explodeDamage || 3);
gameState.player.hitTimer = HIT_FLASH;
showMessage('Explosion!');
}
// Damage in radius
for (const other of gameState.enemies) {
if (other !== enemy && manhattanDist(enemy.x, enemy.y, other.x, other.y) <= (enemy.explodeRadius || 2)) {
dealDamage(other, enemy.explodeDamage || 2, { name: 'explosion' });
}
}
}
}
}
function moveToward(entity, tx, ty) {
const dx = tx - entity.x;
const dy = ty - entity.y;
let bestDir = null;
let bestDist = manhattanDist(entity.x, entity.y, tx, ty);
for (let d = 0; d < 4; d++) {
const nx = entity.x + DIRS[d].x;
const ny = entity.y + DIRS[d].y;
if (!inBounds(nx, ny)) continue;
if (isWall(gameState.layout, nx, ny)) continue;
if (gameState.layout[ny][nx] === TILE_TYPES.LAVA) continue;
if (gameState.rocks.find(r => r.x === nx && r.y === ny)) continue;
if (gameState.enemies.find(e => e !== entity && e.x === nx && e.y === ny)) continue;
if (gameState.chests.find(c => c.x === nx && c.y === ny)) continue;
if (gameState.player.x === nx && gameState.player.y === ny) continue;
if (gameState.currentBoss && gameState.currentBoss.x === nx && gameState.currentBoss.y === ny) continue;
const newDist = manhattanDist(nx, ny, tx, ty);
if (newDist < bestDist) {
bestDist = newDist;
bestDir = d;
}
}
if (bestDir !== null) {
entity.x += DIRS[bestDir].x;
entity.y += DIRS[bestDir].y;
}
}
function updateBossMovement(boss) {
moveToward(boss, gameState.player.x, gameState.player.y);
}
function enemyAttack(enemy) {
if (gameState.player.invincible || gameState.player.invincibleTimer > 0) return;
const damage = enemy.attackDamage || 2;
gameState.player.hp -= damage;
gameState.player.hitTimer = HIT_FLASH;
showMessage(`Hit by ${enemy.name}! (-${damage} HP)`);
}
function bossAttack(boss) {
if (gameState.player.invincible || gameState.player.invincibleTimer > 0) return;
const attacks = boss.attacks || [{ type: 'melee', damage: 3 }];
const attack = attacks[Math.floor((boss.rng ? boss.rng() : Math.random()) * attacks.length)];
if (attack.type === 'melee') {
if (manhattanDist(boss.x, boss.y, gameState.player.x, gameState.player.y) <= (attack.range || 1)) {
gameState.player.hp -= attack.damage;
gameState.player.hitTimer = HIT_FLASH;
showMessage(`Boss attack! (-${attack.damage} HP)`);
}
}
if (attack.type === 'ranged') {
const dir = computeDirection(boss.x, boss.y, gameState.player.x, gameState.player.y);
spawnProjectile(boss.x, boss.y, dir, {
damage: attack.damage,
projSpeed: attack.speed || 3,
projRange: attack.range || 6,
projColor: attack.color || '#FF0000',
name: 'boss_proj',
});
}
if (attack.type === 'aoe') {
spawnParticles(boss.x * TILE + TILE / 2, boss.y * TILE + TILE / 2, attack.color || '#FF00FF', 15);
if (manhattanDist(boss.x, boss.y, gameState.player.x, gameState.player.y) <= (attack.radius || 3)) {
gameState.player.hp -= attack.damage;
gameState.player.hitTimer = HIT_FLASH;
showMessage(`Boss AoE attack! (-${attack.damage} HP)`);
}
}
}
function enemyShoot(enemy) {
const dir = computeDirection(enemy.x, enemy.y, gameState.player.x, gameState.player.y);
spawnProjectile(enemy.x, enemy.y, dir, {
damage: enemy.shootDamage || 1,
projSpeed: enemy.shootSpeed || 3,
projRange: enemy.shootRange || 5,
projColor: enemy.projColor || '#FFFF00',
name: 'enemy_proj',
});
}
function enemyCharge(enemy) {
const dir = computeDirection(enemy.x, enemy.y, gameState.player.x, gameState.player.y);
const speed = enemy.chargeSpeed || 3;
for (let i = 0; i < speed; i++) {
const nx = enemy.x + dir.x;
const ny = enemy.y + dir.y;
if (!isPassable(gameState.layout, nx, ny)) break;
if (gameState.enemies.find(e => e !== enemy && e.x === nx && e.y === ny)) break;
if (gameState.player.x === nx && gameState.player.y === ny) {
if (!gameState.player.invincible) {
gameState.player.hp -= (enemy.chargeDamage || 3);
gameState.player.hitTimer = HIT_FLASH;
showMessage('Charged into you!');
}
break;
}
enemy.x = nx;
enemy.y = ny;
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, '#FFFFFF', 2);
}
}
function computeDirection(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
if (Math.abs(dx) > Math.abs(dy)) {
return { x: dx > 0 ? 1 : -1, y: 0 };
} else {
return { x: 0, y: dy > 0 ? 1 : -1 };
}
}
function generatePatrolPath(enemy) {
const path = [];
const points = enemy.patrolPoints || [];
if (points.length >= 2) return points;
// Generate random patrol points around spawn position
const sx = enemy.spawnX || enemy.x;
const sy = enemy.spawnY || enemy.y;
for (let i = 0; i < 4; i++) {
const px = clamp(sx + randInt(-3, 3), 1, COLS - 2);
const py = clamp(sy + randInt(-3, 3), 1, ROWS - 2);
if (isPassable(gameState.layout, px, py)) {
path.push({ x: px, y: py });
}
}
if (path.length < 2) path.push({ x: sx, y: sy }, { x: clamp(sx + 2, 1, COLS - 2), y: sy });
return path;
}
function createEnemyInstance(def, x, y) {
return {
name: def.name,
id: def.id,
x: x, y: y,
spawnX: x, spawnY: y,
hp: def.hp,
maxHp: def.hp,
defense: def.defense || 0,
attackDamage: def.attackDamage || 1,
moveInterval: def.moveInterval || ENEMY_MOVE_INTERVAL,
attackInterval: def.attackInterval || ENEMY_ATTACK_INTERVAL,
attackRange: def.attackRange || 1,
behavior: def.behavior || 'chase',
moveTimer: ENEMY_MOVE_INTERVAL,
attackTimer: ENEMY_ATTACK_INTERVAL,
hitTimer: 0,
stunned: false,
frozen: false,
onFire: false,
specialTimer: 0,
projectileTimer: def.projectileInterval ? def.projectileInterval : undefined,
color: def.color,
colorLight: def.colorLight,
eyeColor: def.eyeColor,
};
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { updateEnemies, updateEnemyMovement, moveToward, enemyAttack, bossAttack, createEnemyInstance };
}

222
src/game/combat.js Normal file
View File

@ -0,0 +1,222 @@
'use strict';
// === COMBAT SYSTEM ===
function tryAttack() {
const weapon = gameState.player.activeWeapon;
if (!weapon) {
// Try to use first available weapon
if (gameState.player.weapons.length > 0) {
gameState.player.activeWeapon = gameState.player.weapons[0];
} else {
showMessage('You need a weapon!');
return;
}
}
const active = gameState.player.activeWeapon;
if (gameState.player.attackTimer > 0) return;
// Check ammo
if (active.ammoRequired) {
const ammoType = active.ammoType || 'arrows';
const ammoCount = ammoType === 'arrows' ? gameState.player.arrows : gameState.player.bombs;
if (ammoCount <= 0) {
showMessage(`No ${ammoType} left!`);
return;
}
if (ammoType === 'arrows') gameState.player.arrows--;
else gameState.player.bombs--;
}
// Set attack timer
gameState.player.attackTimer = active.cooldown || ATTACK_COOLDOWN;
gameState.player.attackAnim = active.animDuration || ATTACK_ANIM;
const dir = DIRS[gameState.player.dir];
const range = active.range || 1;
// Melee attack
if (active.type === 'melee') {
for (let i = 1; i <= range; i++) {
const ax = gameState.player.x + dir.x * i;
const ay = gameState.player.y + dir.y * i;
if (!inBounds(ax, ay)) break;
// Hit enemies
for (const enemy of gameState.enemies) {
if (enemy.x === ax && enemy.y === ay) {
dealDamage(enemy, active.damage, active);
}
}
// Hit boss
if (gameState.currentBoss && gameState.currentBoss.x === ax && gameState.currentBoss.y === ay) {
dealDamage(gameState.currentBoss, active.damage, active);
}
}
// AoE weapons hit in arc
if (active.aoe) {
for (const enemy of gameState.enemies) {
if (manhattanDist(gameState.player.x, gameState.player.y, enemy.x, enemy.y) <= active.aoeRange) {
dealDamage(enemy, Math.floor(active.damage * 0.5), active);
}
}
}
}
// Ranged attack
if (active.type === 'ranged') {
spawnProjectile(gameState.player.x, gameState.player.y, dir, active);
}
// Special effects
if (active.effect === 'stun') {
for (const enemy of gameState.enemies) {
if (enemy.x === gameState.player.x + dir.x && enemy.y === gameState.player.y + dir.y) {
enemy.stunned = true;
enemy.specialTimer = active.effectDuration || 2000;
}
}
}
if (active.effect === 'freeze') {
for (const enemy of gameState.enemies) {
if (enemy.x === gameState.player.x + dir.x && enemy.y === gameState.player.y + dir.y) {
enemy.frozen = true;
enemy.specialTimer = active.effectDuration || 3000;
}
}
}
if (active.effect === 'fire') {
for (const enemy of gameState.enemies) {
if (enemy.x === gameState.player.x + dir.x && enemy.y === gameState.player.y + dir.y) {
dealDamage(enemy, active.damage, active);
enemy.onFire = true;
enemy.specialTimer = active.effectDuration || 2000;
}
}
}
// Clean up dead enemies
const beforeCount = gameState.enemies.length;
gameState.enemies = gameState.enemies.filter(e => e.hp > 0);
const killed = beforeCount - gameState.enemies.length;
if (killed > 0) {
gameState.enemiesDefeated += killed;
showMessage(`${killed} enemy${killed > 1 ? 'ies' : ''} defeated!`);
}
// Check boss death
if (gameState.currentBoss && gameState.currentBoss.hp <= 0) {
const boss = gameState.currentBoss;
showMessage(`${boss.name} defeated!`);
gameState.currentBoss = null;
gameState.exitOpen = true;
gameState.enemiesDefeated++;
spawnParticles(boss.x * TILE + TILE / 2, boss.y * TILE + TILE / 2, '#FFD700', 20);
}
// If no enemies and no exit, room is clear
if (gameState.enemies.length === 0 && !gameState.exit && !gameState.currentBoss) {
gameState.exitOpen = true;
showMessage('Room cleared!');
}
}
function dealDamage(target, damage, weapon) {
if (target.invincible && target.invincibleTimer > 0) return;
// Apply defense reduction
const def = target.defense || 0;
const actualDamage = Math.max(1, damage - def);
target.hp -= actualDamage;
target.hitTimer = HIT_FLASH;
// Spawn hit particles
spawnParticles(target.x * TILE + TILE / 2, target.y * TILE + TILE / 2, '#FF0000', 4);
// Check for phase change (bosses)
if (target.phases && target.phases[target.phase + 1] && target.hp <= target.phases[target.phase + 1].hpThreshold * target.maxHp / 100) {
target.phase++;
showMessage(`${target.name} enrage${target.phase > 0 ? 's' : ''}! (Phase ${target.phase + 1})`);
target.attackTimer = 0;
target.moveTimer = 0;
}
}
function spawnProjectile(x, y, dir, weapon) {
if (!gameState.projectiles) gameState.projectiles = [];
gameState.projectiles.push({
x: x + dir.x,
y: y + dir.y,
dir: { ...dir },
speed: weapon.projSpeed || 4,
damage: weapon.damage,
range: weapon.projRange || 8,
traveled: 0,
type: weapon.type,
effect: weapon.effect,
effectDuration: weapon.effectDuration,
color: weapon.projColor || '#FFFF00',
moveTimer: 0,
});
}
function updateProjectiles(dt) {
if (!gameState.projectiles) return;
for (let i = gameState.projectiles.length - 1; i >= 0; i--) {
const p = gameState.projectiles[i];
p.moveTimer -= dt;
if (p.moveTimer > 0) continue;
p.moveTimer = 100;
const nx = p.x + p.dir.x;
const ny = p.y + p.dir.y;
p.traveled++;
// Check bounds
if (!inBounds(nx, ny) || p.traveled >= p.range) {
gameState.projectiles.splice(i, 1);
continue;
}
// Check walls
if (isWall(gameState.layout, nx, ny)) {
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, p.color, 3);
gameState.projectiles.splice(i, 1);
continue;
}
// Hit enemies
let hit = false;
for (const enemy of gameState.enemies) {
if (enemy.x === nx && enemy.y === ny) {
dealDamage(enemy, p.damage, { name: 'projectile' });
if (p.effect === 'stun') { enemy.stunned = true; enemy.specialTimer = p.effectDuration || 2000; }
if (p.effect === 'freeze') { enemy.frozen = true; enemy.specialTimer = p.effectDuration || 3000; }
hit = true;
break;
}
}
// Hit boss
if (!hit && gameState.currentBoss && gameState.currentBoss.x === nx && gameState.currentBoss.y === ny) {
dealDamage(gameState.currentBoss, p.damage, { name: 'projectile' });
hit = true;
}
if (hit) {
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, '#FF0000', 5);
gameState.projectiles.splice(i, 1);
continue;
}
p.x = nx;
p.y = ny;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { tryAttack, dealDamage, spawnProjectile, updateProjectiles };
}

148
src/game/items.js Normal file
View File

@ -0,0 +1,148 @@
'use strict';
// === ITEM SYSTEM ===
var Save = typeof require !== 'undefined' ? require('../save/save') : {};
var saveData = Save.saveData || window.saveData;
var saveProgress = Save.saveProgress || window.saveProgress;
var addHeartPiece = Save.addHeartPiece || window.addHeartPiece;
var addWeapon = Save.addWeapon || window.addWeapon;
function openChest(chest) {
chest.opened = true;
const loot = chest.loot;
if (!loot) return;
spawnParticles(chest.x * TILE + TILE / 2, chest.y * TILE + TILE / 2, '#FFD700', 10);
if (loot.type === 'weapon') {
const weaponDef = WEAPONS.find(w => w.name === loot.name);
if (weaponDef && !gameState.player.weapons.find(w => w.name === weaponDef.name)) {
gameState.player.weapons.push({ ...weaponDef });
if (!gameState.player.activeWeapon) {
gameState.player.activeWeapon = gameState.player.weapons[0];
}
addWeapon(weaponDef.name);
showMessage(`Got ${weaponDef.name}!`);
}
}
if (loot.type === 'consumable') {
const itemDef = ITEMS.find(i => i.name === loot.name);
if (itemDef) {
if (itemDef.id === 'potion_small') {
gameState.player.hp = Math.min(gameState.player.hp + 4, gameState.player.maxHp);
showMessage('Small Potion! (+4 HP)');
} else if (itemDef.id === 'potion_medium') {
gameState.player.hp = Math.min(gameState.player.hp + 8, gameState.player.maxHp);
showMessage('Medium Potion! (+8 HP)');
} else if (itemDef.id === 'potion_large') {
gameState.player.hp = Math.min(gameState.player.hp + 12, gameState.player.maxHp);
showMessage('Large Potion! (+12 HP)');
} else if (itemDef.id === 'heart_piece') {
gameState.player.items.push({ ...itemDef });
addHeartPiece();
showMessage(`Heart Piece! (${saveData.heartPieces}/4)`);
} else if (itemDef.id === 'heart_container') {
if (gameState.player.maxHp < MAX_HEARTS * 2) {
gameState.player.maxHp += 2;
gameState.player.hp = gameState.player.maxHp;
showMessage('Heart Container! Max HP increased!');
}
} else if (itemDef.id === 'arrows') {
gameState.player.arrows += (loot.count || 10);
showMessage(`+${loot.count || 10} Arrows!`);
} else if (itemDef.id === 'bombs') {
gameState.player.bombs += (loot.count || 10);
showMessage(`+${loot.count || 10} Bombs!`);
} else if (itemDef.id === 'key') {
gameState.player.keys += (loot.count || 1);
showMessage(`+${loot.count || 1} Key!`);
} else if (itemDef.id === 'bow') {
if (!gameState.player.weapons.find(w => w.name === 'Bow')) {
const bowDef = WEAPONS.find(w => w.name === 'Bow');
if (bowDef) {
gameState.player.weapons.push({ ...bowDef });
addWeapon('Bow');
showMessage('Got Bow!');
}
}
gameState.player.arrows += (loot.count || 10);
showMessage(`+${loot.count || 10} Arrows!`);
} else {
gameState.player.items.push({ ...itemDef, count: loot.count || 1 });
showMessage(`Got ${itemDef.name}!`);
}
}
}
if (loot.type === 'upgrade') {
if (loot.name === 'sword_upgrade' && gameState.player.activeWeapon) {
gameState.player.activeWeapon.damage += 1;
showMessage('Sword upgraded! (+1 damage)');
}
}
saveProgress();
}
function collectFloorItem(item) {
item.collected = true;
const itemDef = ITEMS.find(i => i.id === item.id);
if (!itemDef) return;
spawnParticles(item.x * TILE + TILE / 2, item.y * TILE + TILE / 2, itemDef.color || '#FFD700', 5);
if (itemDef.type === 'potion') {
gameState.player.hp = Math.min(gameState.player.hp + (itemDef.healAmount || 4), gameState.player.maxHp);
showMessage(`${itemDef.name}! (+${itemDef.healAmount} HP)`);
} else if (itemDef.id === 'heart_piece') {
addHeartPiece();
showMessage(`Heart Piece! (${saveData.heartPieces}/4)`);
} else if (itemDef.id === 'arrows') {
gameState.player.arrows += (item.count || 5);
showMessage(`+${item.count || 5} Arrows!`);
} else if (itemDef.id === 'bombs') {
gameState.player.bombs += (item.count || 5);
showMessage(`+${item.count || 5} Bombs!`);
} else if (itemDef.id === 'key') {
gameState.player.keys++;
showMessage('+1 Key!');
} else {
gameState.player.items.push({ ...itemDef });
showMessage(`Got ${itemDef.name}!`);
}
}
function useItem(itemIdx) {
if (!gameState.player.items[itemIdx]) return;
const item = gameState.player.items[itemIdx];
const def = ITEMS.find(i => i.id === item.id);
if (!def || def.type !== 'consumable') return;
if (def.id.startsWith('potion')) {
gameState.player.hp = Math.min(gameState.player.hp + (def.healAmount || 4), gameState.player.maxHp);
showMessage(`Used ${def.name}! (+${def.healAmount} HP)`);
spawnParticles(gameState.player.x * TILE + TILE / 2, gameState.player.y * TILE + TILE / 2, '#00FF00', 8);
} else {
showMessage(`Can't use ${def.name} here`);
return;
}
item.count = (item.count || 1) - 1;
if (item.count <= 0) {
gameState.player.items.splice(itemIdx, 1);
}
}
function switchWeapon() {
if (gameState.player.weapons.length <= 1) return;
const currentIdx = gameState.player.weapons.findIndex(w => w === gameState.player.activeWeapon);
const nextIdx = (currentIdx + 1) % gameState.player.weapons.length;
gameState.player.activeWeapon = gameState.player.weapons[nextIdx];
showMessage(`Switched to ${gameState.player.activeWeapon.name}`);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { openChest, collectFloorItem, useItem, switchWeapon };
}

239
src/game/movement.js Normal file
View File

@ -0,0 +1,239 @@
'use strict';
// === PLAYER MOVEMENT ===
function tryMove(dx, dy) {
if (gameState.player.invincibleTimer > 0 && gameState.player.invincibleTimer < 1000) return;
const px = gameState.player.x;
const py = gameState.player.y;
const nx = px + dx;
const ny = py + dy;
if (dx === 0 && dy === 0) return;
// Update facing direction
if (dy === 1 && gameState.player.dir !== 0) gameState.player.dir = 0;
else if (dy === -1 && gameState.player.dir !== 1) gameState.player.dir = 1;
else if (dx === -1 && gameState.player.dir !== 2) gameState.player.dir = 2;
else if (dx === 1 && gameState.player.dir !== 3) gameState.player.dir = 3;
if (!inBounds(nx, ny)) return;
// Check exit before wall check
if (gameState.exit && gameState.exit.x === nx && gameState.exit.y === ny) {
tryExit();
return;
}
// Check walls
if (isWall(gameState.layout, nx, ny)) return;
// Check lava/water (not passable without item)
if (gameState.layout[ny][nx] === TILE_TYPES.LAVA) return;
if (gameState.layout[ny][nx] === TILE_TYPES.WATER) {
if (!gameState.player.items.find(i => i.id === 'flippers' || i.id === 'power_boomerang')) return;
}
// Check traps
const trap = gameState.traps.find(t => t.x === nx && t.y === ny && !t.triggered);
if (trap) {
trap.triggered = true;
if (!gameState.player.invincible) {
gameState.player.hp -= trap.damage;
gameState.player.hitTimer = HIT_FLASH;
showMessage(`Trap! (-${trap.damage} HP)`);
}
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, trap.color || '#FF0000', 8);
}
// Push rocks
const rock = gameState.rocks.find(r => r.x === nx && r.y === ny);
if (rock) {
const rnx = nx + dx;
const rny = ny + dy;
if (!inBounds(rnx, rny)) return;
if (isWall(gameState.layout, rnx, rny)) return;
if (gameState.rocks.find(r => r.x === rnx && r.y === rny)) return;
if (gameState.enemies.find(e => e.x === rnx && e.y === rny)) return;
if (gameState.exit && gameState.exit.x === rnx && gameState.exit.y === rny) return;
if (gameState.chests.find(c => c.x === rnx && c.y === rny)) return;
if (gameState.layout[rny][rnx] === TILE_TYPES.LAVA) return;
rock.x = rnx;
rock.y = rny;
spawnParticles(rnx * TILE + TILE / 2, rny * TILE + TILE / 2, COLORS.rock, 3);
}
// Check enemies (can't walk through)
if (gameState.enemies.find(e => e.x === nx && e.y === ny)) return;
if (gameState.currentBoss && gameState.currentBoss.x === nx && gameState.currentBoss.y === ny) return;
// Move player
gameState.player.x = nx;
gameState.player.y = ny;
// Check chests
const chest = gameState.chests.find(c => c.x === nx && c.y === ny && !c.opened);
if (chest) {
openChest(chest);
return;
}
// Check floor items
const floorItem = gameState.floorItems.find(i => i.x === nx && i.y === ny && !i.collected);
if (floorItem) {
collectFloorItem(floorItem);
}
// Check switches
const sw = gameState.switches.find(s => s.x === nx && s.y === ny && !s.activated);
if (sw) {
sw.activated = true;
showMessage(sw.message || 'Switch activated!');
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, '#FFD700', 6);
if (sw.targetDoor) {
const door = gameState.doors.find(d => d.id === sw.targetDoor);
if (door) {
door.opened = !door.opened;
showMessage(door.opened ? 'Door opened!' : 'Door closed!');
}
}
if (sw.openExit) {
gameState.exitOpen = true;
showMessage('The exit has opened!');
}
}
// Check levers
const lever = gameState.levers.find(l => l.x === nx && l.y === ny && !l.activated);
if (lever) {
lever.activated = true;
showMessage(lever.message || 'Lever pulled!');
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, COLORS.lever, 6);
if (lever.effect === 'open_exit') {
gameState.exitOpen = true;
showMessage('The exit has opened!');
}
if (lever.effect === 'drop_item') {
gameState.player.items.push({ ...lever.item });
showMessage(`Got ${lever.item.name}!`);
}
if (lever.effect === 'remove_wall') {
for (const [wx, wy] of lever.wallsToRemove) {
if (inBounds(wx, wy)) gameState.layout[wy][wx] = TILE_TYPES.FLOOR;
}
}
}
// Check doors
const door = gameState.doors.find(d => d.x === nx && d.y === ny);
if (door && !door.opened) {
if (door.requiredKey && gameState.player.keys > 0) {
gameState.player.keys--;
door.opened = true;
showMessage('Door unlocked!');
} else if (door.requiredKey) {
showMessage('Need a key!');
} else {
door.opened = true;
}
}
// Check soft walls (breakable)
if (isSoftWall(gameState.layout, nx, ny)) {
const weapon = gameState.player.activeWeapon;
if (weapon && (weapon.canBreakWalls || weapon.name === 'bomb' || weapon.name === 'hammer')) {
gameState.layout[ny][nx] = TILE_TYPES.FLOOR;
spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, COLORS.softwall, 6);
showMessage('Wall destroyed!');
} else {
showMessage('Need bombs or hammer!');
}
return;
}
// Check puzzle
checkPuzzle();
}
function tryInteract() {
const px = gameState.player.x;
const py = gameState.player.y;
const dir = DIRS[gameState.player.dir];
const tx = px + dir.x;
const ty = py + dir.y;
// Try doors in facing direction
const door = gameState.doors.find(d => d.x === tx && d.y === ty && !d.opened);
if (door) {
if (door.requiredKey && gameState.player.keys > 0) {
gameState.player.keys--;
door.opened = true;
showMessage('Door unlocked!');
} else if (door.requiredKey) {
showMessage('Need a key!');
} else {
door.opened = true;
showMessage('Door opened!');
}
return;
}
// Try chests in facing direction
const chest = gameState.chests.find(c => c.x === tx && c.y === ty && !c.opened);
if (chest) {
openChest(chest);
return;
}
}
function tryExit() {
if (!gameState.exit) return;
if (!gameState.exitOpen) {
showMessage('The door is locked!');
return;
}
// Check if this is the last room
if (gameState.currentRoomIdx >= (gameState.dungeonRooms ? gameState.dungeonRooms.length - 1 : 0)) {
completeDungeon();
return;
}
// Move to next room
gameState.currentRoomIdx++;
loadDungeonRoom(gameState.currentDungeon, gameState.currentRoomIdx);
}
function completeDungeon() {
const dungeon = DUNGEONS[gameState.currentDungeon];
showMessage(`${dungeon.name} cleared!`);
// Save progress
markDungeonComplete(gameState.currentDungeon);
addStats(gameState.roomsCleared, gameState.enemiesDefeated);
gameState.dungeonsCompleted++;
saveProgress();
// Check if all dungeons completed
if (gameState.currentDungeon >= DUNGEONS.length - 1) {
switchScreen(SCREEN_GAME_COMPLETE);
} else {
switchScreen(SCREEN_DUNGEON_COMPLETE);
}
}
function checkPuzzle() {
if (gameState.targets.length === 0) return;
const allSolved = gameState.targets.every(t => gameState.rocks.some(r => r.x === t.x && r.y === t.y));
if (allSolved) {
gameState.exitOpen = true;
showMessage('Puzzle solved! The door has opened!');
spawnParticles(gameState.exit.x * TILE + TILE / 2, gameState.exit.y * TILE + TILE / 2, '#FFD700', 12);
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { tryMove, tryInteract, tryExit, completeDungeon, checkPuzzle };
}

146
src/game/state.js Normal file
View File

@ -0,0 +1,146 @@
'use strict';
// === GAME STATE ===
let gameState;
function initGameState() {
gameState = window.gameState = {
// Screen/dungeon state
currentDungeon: null,
currentRoomIdx: 0,
dungeonRooms: null,
currentRoom: null,
// Player
player: {
x: 0, y: 0,
hp: 12, maxHp: 12,
dir: 0, // 0=down, 1=up, 2=left, 3=right
moveTimer: 0,
attackTimer: 0,
attackAnim: 0,
hitTimer: 0,
invincibleTimer: 0,
invincible: false,
weapons: [],
activeWeapon: null,
items: [],
bombs: 0,
arrows: 0,
keys: 0,
},
// Room state
layout: null,
rocks: [],
targets: [],
chests: [],
enemies: [],
exit: null,
exitOpen: false,
torches: [],
doors: [],
levers: [],
switches: [],
traps: [],
softwalls: [],
currentBoss: null,
floorItems: [],
// Messages
message: '',
messageTimer: 0,
// Particles
particles: [],
// Stats
roomsCleared: 0,
enemiesDefeated: 0,
dungeonsCompleted: 0,
};
}
function resetPlayer() {
gameState.player.hp = gameState.player.maxHp;
gameState.player.x = 0;
gameState.player.y = 0;
gameState.player.dir = 0;
gameState.player.moveTimer = 0;
gameState.player.attackTimer = 0;
gameState.player.attackAnim = 0;
gameState.player.hitTimer = 0;
gameState.player.invincibleTimer = 1000;
}
function showMessage(text) {
gameState.message = text;
gameState.messageTimer = MESSAGE_DURATION;
}
// === ROOM LOADING ===
function loadDungeonRoom(dungeonIdx, roomIdx) {
const rooms = gameState.dungeonRooms;
if (!rooms || roomIdx >= rooms.length) return;
gameState.currentDungeon = dungeonIdx;
gameState.currentRoomIdx = roomIdx;
const room = rooms[roomIdx];
// BSP generator produces room.tiles; legacy generators produce room.layout
gameState.layout = room.layout ? cloneLayout(room.layout) : (room.tiles ? cloneLayout(room.tiles) : createEmptyLayout());
// Compute player start position
if (room.playerStart) {
gameState.player.x = room.playerStart.x;
gameState.player.y = room.playerStart.y;
} else {
// Default: entrance starts at left-center, boss at left-center, others at center
if (room.type === 'entrance') {
gameState.player.x = 2;
gameState.player.y = Math.floor(ROWS / 2);
} else if (room.type === 'boss') {
gameState.player.x = 2;
gameState.player.y = Math.floor(ROWS / 2);
} else {
gameState.player.x = Math.floor(COLS / 2);
gameState.player.y = Math.floor(ROWS / 2);
}
}
gameState.rocks = room.rocks ? room.rocks.map(r => ({ ...r })) : [];
gameState.targets = room.targets ? room.targets.map(t => ({ ...t })) : [];
gameState.chests = room.chests ? room.chests.map(c => ({ ...c, opened: false })) : [];
gameState.enemies = room.enemies ? room.enemies.map(e => ({
...e,
moveTimer: ENEMY_MOVE_INTERVAL,
attackTimer: ENEMY_ATTACK_INTERVAL,
hitTimer: 0,
projectileTimer: 0,
specialTimer: 0,
phase: 0,
stunned: false,
frozen: false,
})) : [];
gameState.exit = room.exit ? { ...room.exit } : (room.hasBoss ? { x: COLS - 1, y: Math.floor(ROWS / 2), dir: 3 } : null);
gameState.exitOpen = room.exitOpen || false;
gameState.torches = room.torches ? room.torches.map(t => [...t]) : [];
gameState.doors = room.doors ? room.doors.map(d => ({ ...d })) : [];
gameState.levers = room.levers ? room.levers.map(l => ({ ...l })) : [];
gameState.switches = room.switches ? room.switches.map(s => ({ ...s, activated: false })) : [];
gameState.traps = room.traps ? room.traps.map(t => ({ ...t, triggered: false })) : [];
gameState.softwalls = room.softwalls ? room.softwalls.map(s => [...s]) : [];
gameState.currentRoom = room;
gameState.currentBoss = room.boss ? { ...room.boss, moveTimer: ENEMY_MOVE_INTERVAL, attackTimer: ENEMY_ATTACK_INTERVAL, hitTimer: 0, phase: 0, rng: createRNG((gameState.currentDungeon + 1) * 1000 + roomIdx) } : null;
gameState.floorItems = room.floorItems ? room.floorItems.map(i => ({ ...i, collected: false })) : [];
gameState.particles = [];
gameState.message = room.name || `Room ${roomIdx + 1}`;
gameState.messageTimer = MESSAGE_DURATION;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { initGameState, loadDungeonRoom, showMessage, resetPlayer };
}

34
src/game/transitions.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
// === SCREEN TRANSITIONS ===
// Transitions are handled in app.js updateTransition()
// This file provides helper functions for transition effects
function startTransition(target) {
if (transitioning) return;
transitioning = true;
targetScreen = target;
transitionProgress = 1;
}
// Screen fade effect
function getTransitionAlpha() {
if (!transitioning) return 0;
return 1 - transitionProgress;
}
// Check if transition is complete
function isTransitionComplete() {
return !transitioning && transitionProgress <= 0;
}
// Room transition message
function onRoomTransition(roomName) {
gameState.message = roomName;
gameState.messageTimer = MESSAGE_DURATION;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { startTransition, getTransitionAlpha, isTransitionComplete };
}

287
src/generator/dungeon.js Normal file
View File

@ -0,0 +1,287 @@
'use strict';
// Browser: use globals directly; Node: use require
const _C3 = typeof require !== 'undefined' ? require('../core/constants') : { COLS, ROWS, TILE_TYPES };
const RNG = typeof require !== 'undefined' ? require('../core/rng') : { createRNG };
const Puzzles = typeof require !== 'undefined' ? require('./puzzles') : { generatePuzzles };
const Placement = typeof require !== 'undefined' ? require('./placement') : { generatePlacement };
// Abstract grid for BSP layout — scales with room count
function getLayoutSize(targetCount) {
const area = Math.ceil(targetCount * 36 * 1.5);
const w = Math.ceil(Math.sqrt(area * 1.33));
const h = Math.ceil(area / w);
return { cols: Math.ceil(w / 4) * 4, rows: Math.ceil(h / 4) * 4 };
}
function generateDungeon(dungeon) {
const rng = RNG.createRNG(dungeon.seed);
const targetCount = dungeon.roomCount;
const rooms = generateBSPLayout(targetCount, rng);
if (rooms.length === 0) return [];
connectRooms(rooms, rng);
ensureConnectivity(rooms, rng);
assignRoomTypes(rooms, dungeon, rng);
// Each room gets its own COLS x ROWS tile grid for rendering
for (const room of rooms) {
room.tiles = createRoomTileGrid(room, rng);
room.decorations = generateDecorations(room, rng);
}
// Generate puzzles (modifies room tiles and puzzles arrays)
Puzzles.generatePuzzles(rooms, dungeon, rng);
// Place enemies, items, bosses (needs tile grid from above)
Placement.generatePlacement(rooms, dungeon, rng);
return rooms;
}
function generateBSPLayout(targetCount, rng) {
const layout = getLayoutSize(targetCount);
const maxDepth = Math.max(6, Math.ceil(Math.log2(targetCount)) + 2);
const root = createBSP(0, 0, layout.cols, layout.rows, 0, 0, targetCount, maxDepth, rng);
const leaves = [];
collectLeaves(root, leaves);
// Sort by y then x for consistent ordering
leaves.sort((a, b) => (a.cy - b.cy) * layout.cols + (a.cx - b.cx));
const rooms = [];
for (const leaf of leaves) {
rooms.push({
id: rooms.length,
x: leaf.cx,
y: leaf.cy,
width: leaf.width,
height: leaf.height,
type: 'normal',
connections: [],
enemies: [],
items: [],
puzzles: [],
decorations: [],
hasBoss: false,
hasExit: false,
hasEntrance: false,
tiles: [],
});
}
return rooms;
}
function createBSP(x, y, width, height, depth, roomCount, targetCount, maxDepth, rng) {
const node = { x, y, width, height, depth, cx: x + Math.floor(width / 2), cy: y + Math.floor(height / 2), left: null, right: null };
if (roomCount >= targetCount) return node;
if (depth >= maxDepth) return node;
const minRegion = 5;
const minSplit = minRegion * 2;
const canH = height >= minSplit;
const canV = width >= minSplit;
if (!canH && !canV) return node;
let dir;
if (canH && canV) {
dir = width > height ? 'v' : (height > width ? 'h' : (rng() < 0.5 ? 'h' : 'v'));
} else {
dir = canH ? 'h' : 'v';
}
let splitPos;
if (dir === 'h') {
const lo = Math.max(minRegion, Math.ceil(height * 0.35));
const hi = Math.min(height - minRegion, Math.floor(height * 0.65));
if (lo > hi) return node;
splitPos = Math.floor(rng() * (hi - lo + 1)) + lo;
} else {
const lo = Math.max(minRegion, Math.ceil(width * 0.35));
const hi = Math.min(width - minRegion, Math.floor(width * 0.65));
if (lo > hi) return node;
splitPos = Math.floor(rng() * (hi - lo + 1)) + lo;
}
if (dir === 'h') {
node.left = createBSP(x, y, width, splitPos, depth + 1, roomCount, targetCount, maxDepth, rng);
const lc = countLeaves(node.left);
node.right = createBSP(x, y + splitPos, width, height - splitPos, depth + 1, roomCount + lc, targetCount, maxDepth, rng);
} else {
node.left = createBSP(x, y, splitPos, height, depth + 1, roomCount, targetCount, maxDepth, rng);
const lc = countLeaves(node.left);
node.right = createBSP(x + splitPos, y, width - splitPos, height, depth + 1, roomCount + lc, targetCount, maxDepth, rng);
}
return node;
}
function countLeaves(node) {
if (!node.left && !node.right) return 1;
let c = 0;
if (node.left) c += countLeaves(node.left);
if (node.right) c += countLeaves(node.right);
return c;
}
function collectLeaves(node, leaves) {
if (!node.left && !node.right) {
leaves.push({ cx: node.cx, cy: node.cy, width: node.width, height: node.height });
return;
}
if (node.left) collectLeaves(node.left, leaves);
if (node.right) collectLeaves(node.right, leaves);
}
// Connect rooms using MST (Prim's algorithm)
function connectRooms(rooms, rng) {
if (rooms.length < 2) return;
const connected = new Set([0]);
const unconnected = new Set(rooms.slice(1).map(r => r.id));
while (unconnected.size > 0) {
let bestDist = Infinity, bestFrom = -1, bestTo = -1;
for (const fromId of connected) {
for (const toId of unconnected) {
const d = Math.abs(rooms[fromId].x - rooms[toId].x) + Math.abs(rooms[fromId].y - rooms[toId].y);
if (d < bestDist) { bestDist = d; bestFrom = fromId; bestTo = toId; }
}
}
if (bestTo < 0) break;
rooms[bestFrom].connections.push(bestTo);
rooms[bestTo].connections.push(bestFrom);
connected.add(bestTo);
unconnected.delete(bestTo);
}
// Extra connections for non-linear paths
for (let i = 0; i < rooms.length; i++) {
for (let j = i + 1; j < rooms.length; j++) {
if (rooms[i].connections.includes(j) || rooms[j].connections.includes(i)) continue;
const d = Math.abs(rooms[i].x - rooms[j].x) + Math.abs(rooms[i].y - rooms[j].y);
if (d < 20 && rng() < 0.25) {
rooms[i].connections.push(j);
rooms[j].connections.push(i);
}
}
}
}
// BFS from room 0, connect unreachable rooms to nearest connected room
function ensureConnectivity(rooms, rng) {
if (rooms.length < 2) return;
const visited = new Set([0]);
const queue = [0];
while (queue.length > 0) {
const rid = queue.shift();
for (const cid of rooms[rid].connections) {
if (!visited.has(cid)) { visited.add(cid); queue.push(cid); }
}
}
for (const room of rooms) {
if (visited.has(room.id)) continue;
let nearest = null, nearestDist = Infinity;
for (const other of rooms) {
if (!visited.has(other.id)) continue;
const d = Math.abs(room.x - other.x) + Math.abs(room.y - other.y);
if (d < nearestDist) { nearestDist = d; nearest = other; }
}
if (nearest) {
room.connections.push(nearest.id);
nearest.connections.push(room.id);
visited.add(room.id);
const q2 = [room.id];
while (q2.length > 0) {
const rid = q2.shift();
for (const cid of rooms[rid].connections) {
if (!visited.has(cid)) { visited.add(cid); q2.push(cid); }
}
}
}
}
}
function assignRoomTypes(rooms, dungeon, rng) {
if (rooms.length === 0) return;
rooms[0].type = 'entrance';
rooms[0].hasEntrance = true;
const last = rooms[rooms.length - 1];
last.type = 'boss';
last.hasBoss = true;
last.bossId = dungeon.id;
// Treasure (~20%)
const tc = Math.max(1, Math.floor(rooms.length * 0.2));
const tcands = rooms.filter((r, i) => i > 0 && i < rooms.length - 1 && r.type === 'normal');
shuffleRng(tcands, rng);
for (let i = 0; i < Math.min(tc, tcands.length); i++) tcands[i].type = 'treasure';
// Puzzle (~15%)
const pc = Math.max(1, Math.floor(rooms.length * 0.15));
const pcands = rooms.filter(r => r.type === 'normal');
shuffleRng(pcands, rng);
for (let i = 0; i < Math.min(pc, pcands.length); i++) pcands[i].type = 'puzzle';
// Trap (~10%)
const trapc = Math.max(0, Math.floor(rooms.length * 0.1));
const trapcands = rooms.filter(r => r.type === 'normal');
shuffleRng(trapcands, rng);
for (let i = 0; i < Math.min(trapc, trapcands.length); i++) trapcands[i].type = 'trap';
}
function shuffleRng(arr, rng) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
function createRoomTileGrid(room, rng) {
const tiles = [];
for (let y = 0; y < _C3.ROWS; y++) {
tiles[y] = [];
for (let x = 0; x < _C3.COLS; x++) {
if (y === 0 || y === _C3.ROWS - 1 || x === 0 || x === _C3.COLS - 1) tiles[y][x] = _C3.TILE_TYPES.WALL;
else tiles[y][x] = _C3.TILE_TYPES.FLOOR;
}
}
// Doors on walls based on connections
for (const connId of room.connections) {
const side = Math.floor(rng() * 4);
let dx, dy;
if (side === 0) { dx = Math.floor(rng() * (_C3.COLS - 2)) + 1; dy = 0; }
else if (side === 1) { dx = Math.floor(rng() * (_C3.COLS - 2)) + 1; dy = _C3.ROWS - 1; }
else if (side === 2) { dx = 0; dy = Math.floor(rng() * (_C3.ROWS - 2)) + 1; }
else { dx = _C3.COLS - 1; dy = Math.floor(rng() * (_C3.ROWS - 2)) + 1; }
if (tiles[dy][dx] === _C3.TILE_TYPES.WALL) tiles[dy][dx] = _C3.TILE_TYPES.DOOR;
}
if (room.hasBoss) tiles[Math.floor(_C3.ROWS / 2)][_C3.COLS - 1] = _C3.TILE_TYPES.EXIT;
if (room.hasEntrance) tiles[Math.floor(_C3.ROWS / 2)][_C3.COLS - 1] = _C3.TILE_TYPES.DOOR;
return tiles;
}
function generateDecorations(room, rng) {
const decorations = [];
if (rng() < 0.7) decorations.push({ type: 'torch', x: 2, y: 2, lit: true });
if (rng() < 0.5) decorations.push({ type: 'torch', x: _C3.COLS - 3, y: 2, lit: true });
if (rng() < 0.5) decorations.push({ type: 'torch', x: 2, y: _C3.ROWS - 3, lit: true });
if (rng() < 0.7) decorations.push({ type: 'torch', x: _C3.COLS - 3, y: _C3.ROWS - 3, lit: true });
return decorations;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { generateDungeon };
}

16
src/generator/index.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
// Browser: use globals directly; Node: use require
const DungeonMod = typeof require !== 'undefined' ? require('./dungeon') : { generateDungeon };
const RoomsMod = typeof require !== 'undefined' ? require('./rooms') : { generateRoomLayout };
const PuzzlesMod = typeof require !== 'undefined' ? require('./puzzles') : { generatePuzzles };
const PlacementMod = typeof require !== 'undefined' ? require('./placement') : { generatePlacement };
const generateDungeon = DungeonMod.generateDungeon;
const generateRoomLayout = RoomsMod.generateRoomLayout;
const generatePuzzles = PuzzlesMod.generatePuzzles;
const generatePlacement = PlacementMod.generatePlacement;
if (typeof module !== 'undefined' && module.exports) {
module.exports = { generateDungeon, generateRoomLayout, generatePuzzles, generatePlacement };
}

320
src/generator/placement.js Normal file
View File

@ -0,0 +1,320 @@
'use strict';
// Browser: use window globals; Node: use require
const _C2 = typeof require !== 'undefined' ? require('../core/constants') : { TILE_TYPES, ROWS, COLS, DIR_NAMES };
const _E = typeof require !== 'undefined' ? require('../data/enemies') : { ENEMY_TYPES, getEnemyType };
const _B = typeof require !== 'undefined' ? require('../data/bosses') : { BOSS_TYPES };
const _I = typeof require !== 'undefined' ? require('../data/items') : { ITEMS, getItem };
const _DIR_NAMES = _C2.DIR_NAMES;
// Enemy pools by difficulty tier (cumulative)
const ENEMY_TIERS = {
1: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite'],
2: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite'],
3: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma'],
4: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma'],
5: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma', 'moblin', 'stalfos', 'goriya', 'armos', 'manhandla'],
6: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma', 'moblin', 'stalfos', 'goriya', 'armos', 'manhandla'],
7: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma', 'moblin', 'stalfos', 'goriya', 'armos', 'manhandla', 'wizzrobe', 'ironknuckle', 'darknut'],
8: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma', 'moblin', 'stalfos', 'goriya', 'armos', 'manhandla', 'wizzrobe', 'ironknuckle', 'darknut'],
9: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma', 'moblin', 'stalfos', 'goriya', 'armos', 'manhandla', 'wizzrobe', 'ironknuckle', 'darknut', 'lynel'],
10: ['slime', 'keese', 'peahat', 'reedeath', 'bub', 'tektite', 'octorok', 'like', 'leever', 'skullwallma', 'moblin', 'stalfos', 'goriya', 'armos', 'manhandla', 'wizzrobe', 'ironknuckle', 'darknut', 'lynel'],
};
// Room type -> max enemy count
const ENEMY_COUNT_BY_TYPE = {
normal: 3,
treasure: 1,
puzzle: 1,
trap: 2,
boss: 2,
entrance: 1,
corridor: 0,
};
function findFloorPosition(room, rng) {
const candidates = [];
for (let y = 1; y < _C2.ROWS - 1; y++) {
for (let x = 1; x < _C2.COLS - 1; x++) {
if (room.tiles[y][x] === _C2.TILE_TYPES.FLOOR) {
candidates.push({ x, y });
}
}
}
if (candidates.length === 0) return null;
return candidates[Math.floor(rng() * candidates.length)];
}
function findFloorPositionExcluding(room, rng, occupied) {
const candidates = [];
for (let y = 1; y < _C2.ROWS - 1; y++) {
for (let x = 1; x < _C2.COLS - 1; x++) {
if (room.tiles[y][x] === _C2.TILE_TYPES.FLOOR) {
const taken = occupied.some(p => p.x === x && p.y === y);
if (!taken) {
candidates.push({ x, y });
}
}
}
}
if (candidates.length === 0) return null;
return candidates[Math.floor(rng() * candidates.length)];
}
function getEnemyPool(difficulty) {
const tier = Math.max(1, Math.min(10, difficulty));
const pool = ENEMY_TIERS[tier];
if (!pool) return ENEMY_TIERS[1];
return pool;
}
function getRupeeType(difficulty, rng) {
const r = rng();
if (difficulty <= 2) {
return r < 0.85 ? 'rupee_green' : 'rupee_blue';
} else if (difficulty <= 4) {
if (r < 0.5) return 'rupee_green';
if (r < 0.9) return 'rupee_blue';
return 'rupee_red';
} else if (difficulty <= 6) {
if (r < 0.3) return 'rupee_green';
if (r < 0.65) return 'rupee_blue';
if (r < 0.95) return 'rupee_red';
return 'rupee_yellow';
} else {
if (r < 0.15) return 'rupee_green';
if (r < 0.4) return 'rupee_blue';
if (r < 0.75) return 'rupee_red';
return 'rupee_yellow';
}
}
function getRandomDirection(rng) {
return _DIR_NAMES[Math.floor(rng() * 4)];
}
function getEnemyCountForRoom(roomType, difficulty, rng) {
const maxCount = ENEMY_COUNT_BY_TYPE[roomType] || 0;
if (maxCount === 0) return 0;
const chance = 0.6 + (difficulty / 10) * 0.3;
if (rng() < (1 - chance)) return 0;
return Math.floor(rng() * (maxCount + 1));
}
function createEnemy(typeId, x, y, rng) {
const def = _E.getEnemyType(typeId);
if (!def) return null;
return {
id: def.id,
x,
y,
hp: def.hp,
maxHp: def.hp,
alive: true,
behavior: def.behavior,
state: 'idle',
direction: getRandomDirection(rng),
timer: 0,
damage: def.damage,
defense: def.defense,
speed: def.speed,
attackRange: def.attackRange,
};
}
function createItem(typeId, x, y, count) {
return {
id: typeId,
x,
y,
collected: false,
count: count || 1,
};
}
function placeEnemies(rooms, dungeon, rng) {
const difficulty = dungeon.difficulty || 1;
const pool = getEnemyPool(difficulty);
for (const room of rooms) {
if (room.type === 'corridor') continue;
const count = getEnemyCountForRoom(room.type, difficulty, rng);
const occupied = [];
for (let i = 0; i < count; i++) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (!pos) break;
const typeId = pool[Math.floor(rng() * pool.length)];
const enemy = createEnemy(typeId, pos.x, pos.y, rng);
if (enemy) {
room.enemies.push(enemy);
occupied.push({ x: pos.x, y: pos.y });
}
}
}
}
function placeItems(rooms, dungeon, rng) {
const difficulty = dungeon.difficulty || 1;
const hasBow = dungeon.playerHasBow || false;
for (const room of rooms) {
if (room.type === 'corridor') continue;
const occupied = room.enemies.map(e => ({ x: e.x, y: e.y }));
// Rupees: 30% chance per normal room, 0-3 rupees
if (room.type === 'normal' || room.type === 'entrance') {
if (rng() < 0.3) {
const count = 1 + Math.floor(rng() * 3);
for (let i = 0; i < count; i++) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (!pos) break;
const rupeeType = getRupeeType(difficulty, rng);
room.items.push(createItem(rupeeType, pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
}
}
// Healing potions: 15% chance per room
if (rng() < 0.15) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
room.items.push(createItem('healing_potion', pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
}
// Super potions: 5% chance (difficulty 3+)
if (difficulty >= 3 && rng() < 0.05) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
room.items.push(createItem('super_potion', pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
}
// Magic potions: 3% chance (difficulty 5+)
if (difficulty >= 5 && rng() < 0.03) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
room.items.push(createItem('magic_potion', pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
}
// Arrows: 20% chance if player has bow
if (hasBow && rng() < 0.2) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
const arrowCount = 3 + Math.floor(rng() * 5);
room.items.push(createItem('arrow', pos.x, pos.y, arrowCount));
occupied.push({ x: pos.x, y: pos.y });
}
}
// Bombs: 15% chance per room
if (rng() < 0.15) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
const bombCount = 1 + Math.floor(rng() * 4);
room.items.push(createItem('bomb', pos.x, pos.y, bombCount));
occupied.push({ x: pos.x, y: pos.y });
}
}
// Small keys: placed in treasure rooms or random rooms
if (room.type === 'treasure') {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
room.items.push(createItem('small_key', pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
} else if (rng() < 0.08) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
room.items.push(createItem('small_key', pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
}
// Heart pieces: 2% chance per room (rare)
if (rng() < 0.02) {
const pos = findFloorPositionExcluding(room, rng, occupied);
if (pos) {
room.items.push(createItem('heart_piece', pos.x, pos.y, 1));
occupied.push({ x: pos.x, y: pos.y });
}
}
}
}
function placeBoss(bossRoom, bossType, rng) {
const def = _B.BOSS_TYPES[bossType];
if (!def) return null;
const x = Math.floor(_C2.COLS / 2);
const y = Math.floor(_C2.ROWS / 2);
const boss = {
id: def.id,
name: def.name,
x,
y,
hp: def.hp,
maxHp: def.hp,
damage: def.damage,
defense: def.defense,
behavior: def.behavior,
phases: def.phases ? def.phases.map(p => ({ ...p, attacks: p.attacks.map(a => ({ ...a })) })) : [],
alive: true,
drops: def.drops ? [...def.drops] : [],
currentState: {
phase: 0,
attackTimer: {},
stunned: false,
frozen: false,
},
direction: getRandomDirection(rng),
timer: 0,
state: 'idle',
};
return boss;
}
function generatePlacement(rooms, dungeon, rng) {
placeEnemies(rooms, dungeon, rng);
placeItems(rooms, dungeon, rng);
// Place boss in the boss room
const bossRoom = rooms.find(r => r.type === 'boss');
if (bossRoom && dungeon.bossId !== undefined) {
const boss = placeBoss(bossRoom, dungeon.bossId, rng);
if (boss) {
bossRoom.boss = boss;
bossRoom.hasBoss = true;
}
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
generatePlacement,
placeEnemies,
placeItems,
placeBoss,
findFloorPosition,
getEnemyPool,
getRupeeType,
};
}

420
src/generator/puzzles.js Normal file
View File

@ -0,0 +1,420 @@
'use strict';
// Browser: use window globals; Node: use require
const _C = typeof require !== 'undefined' ? require('../core/constants') : { TILE_TYPES, ROWS, COLS };
function findFloorPosition(room, rng, exclude) {
const candidates = [];
for (let y = 1; y < _C.ROWS - 1; y++) {
for (let x = 1; x < _C.COLS - 1; x++) {
if (room.tiles[y][x] === _C.TILE_TYPES.FLOOR) {
if (!exclude || !exclude.some(p => p.x === x && p.y === y)) {
candidates.push({ x, y });
}
}
}
}
if (candidates.length === 0) return null;
const idx = Math.floor(rng() * candidates.length);
return candidates[idx];
}
function findWallPosition(room, rng, edge, exclude) {
const candidates = [];
if (edge === 'bottom') {
for (let x = 1; x < _C.COLS - 1; x++) {
const y = _C.ROWS - 1;
if (room.tiles[y][x] === _C.TILE_TYPES.WALL) {
if (!exclude || !exclude.some(p => p.x === x && p.y === y)) {
candidates.push({ x, y });
}
}
}
} else if (edge === 'top') {
for (let x = 1; x < _C.COLS - 1; x++) {
const y = 0;
if (room.tiles[y][x] === _C.TILE_TYPES.WALL) {
if (!exclude || !exclude.some(p => p.x === x && p.y === y)) {
candidates.push({ x, y });
}
}
}
} else if (edge === 'left') {
for (let y = 1; y < _C.ROWS - 1; y++) {
const x = 0;
if (room.tiles[y][x] === _C.TILE_TYPES.WALL) {
if (!exclude || !exclude.some(p => p.x === x && p.y === y)) {
candidates.push({ x, y });
}
}
}
} else if (edge === 'right') {
for (let y = 1; y < _C.ROWS - 1; y++) {
const x = _C.COLS - 1;
if (room.tiles[y][x] === _C.TILE_TYPES.WALL) {
if (!exclude || !exclude.some(p => p.x === x && p.y === y)) {
candidates.push({ x, y });
}
}
}
}
if (candidates.length === 0) return null;
const idx = Math.floor(rng() * candidates.length);
return candidates[idx];
}
function generateRockPushPuzzle(room, rng) {
const difficulty = room.difficulty || 1;
const rockCount = Math.min(3, difficulty);
const elements = [];
const occupied = [];
let solutions = [];
for (let i = 0; i < rockCount; i++) {
let attempts = 0;
let rockPos, targetPos;
while (attempts < 50) {
rockPos = findFloorPosition(room, rng, occupied);
if (!rockPos) break;
targetPos = findFloorPosition(room, rng, [...occupied, rockPos]);
if (!targetPos) {
attempts++;
continue;
}
const dx = targetPos.x - rockPos.x;
const dy = targetPos.y - rockPos.y;
const pushDirX = -Math.sign(dx);
const pushDirY = -Math.sign(dy);
const behindX = rockPos.x + pushDirX;
const behindY = rockPos.y + pushDirY;
if (behindX < 1 || behindX >= _C.COLS - 1 ||
behindY < 1 || behindY >= _C.ROWS - 1 ||
occupied.some(p => p.x === behindX && p.y === behindY)) {
attempts++;
continue;
}
if (room.tiles[behindY][behindX] !== _C.TILE_TYPES.FLOOR) {
attempts++;
continue;
}
room.tiles[rockPos.y][rockPos.x] = _C.TILE_TYPES.ROCK;
room.tiles[targetPos.y][targetPos.x] = _C.TILE_TYPES.TARGET;
const rock = {
type: 'rock',
x: rockPos.x,
y: rockPos.y,
pushable: true,
requiredPosition: { x: targetPos.x, y: targetPos.y },
};
const target = {
type: 'target',
x: targetPos.x,
y: targetPos.y,
active: false,
};
elements.push(rock);
elements.push(target);
occupied.push(rockPos);
occupied.push(targetPos);
occupied.push({ x: behindX, y: behindY });
solutions.push(`Push rock at (${rockPos.x}, ${rockPos.y}) to (${targetPos.x}, ${targetPos.y})`);
break;
}
}
return {
type: 'rock_push',
elements,
solution: solutions.join('. '),
solved: false,
reward: elements.length >= 4 ? 'heart_piece' : 'small_key',
};
}
function generateLeverPuzzle(room, rng) {
const elements = [];
const occupied = [];
const leverPos = findFloorPosition(room, rng, occupied);
if (!leverPos) return null;
occupied.push(leverPos);
room.tiles[leverPos.y][leverPos.x] = _C.TILE_TYPES.LEVER;
elements.push({
type: 'lever',
x: leverPos.x,
y: leverPos.y,
activated: false,
});
const targetPos = findFloorPosition(room, rng, occupied);
if (!targetPos) return null;
occupied.push(targetPos);
room.tiles[targetPos.y][targetPos.x] = _C.TILE_TYPES.TARGET;
const targetEffect = rng() < 0.6 ? 'softwall' : 'door';
elements.push({
type: 'target',
x: targetPos.x,
y: targetPos.y,
active: false,
effect: targetEffect,
});
if (targetEffect === 'softwall') {
let softwallPos = null;
for (let attempts = 0; attempts < 30; attempts++) {
softwallPos = findFloorPosition(room, rng, occupied);
if (!softwallPos) break;
if (room.tiles[softwallPos.y][softwallPos.x] === _C.TILE_TYPES.FLOOR) {
break;
}
}
if (softwallPos) {
room.tiles[softwallPos.y][softwallPos.x] = _C.TILE_TYPES.SOFTWALL;
occupied.push(softwallPos);
elements.push({
type: 'softwall',
x: softwallPos.x,
y: softwallPos.y,
breakable: true,
linkedToLever: true,
});
}
} else if (targetEffect === 'door') {
const edges = ['top', 'bottom', 'left', 'right'];
const edge = edges[Math.floor(rng() * edges.length)];
const doorPos = findWallPosition(room, rng, edge, occupied);
if (doorPos) {
room.tiles[doorPos.y][doorPos.x] = _C.TILE_TYPES.DOOR;
occupied.push(doorPos);
elements.push({
type: 'door',
x: doorPos.x,
y: doorPos.y,
locked: true,
keyRequired: 'small_key',
linkedToLever: true,
});
}
}
return {
type: 'lever',
elements,
solution: `Activate lever at (${leverPos.x}, ${leverPos.y}) to ${targetEffect === 'softwall' ? 'remove softwall' : 'unlock door'}`,
solved: false,
reward: 'small_key',
};
}
function generateKeyDoorPuzzle(room, rng) {
const elements = [];
const occupied = [];
const edges = ['top', 'bottom', 'left', 'right'];
const edge = edges[Math.floor(rng() * edges.length)];
const doorPos = findWallPosition(room, rng, edge, occupied);
if (!doorPos) return null;
room.tiles[doorPos.y][doorPos.x] = _C.TILE_TYPES.DOOR;
occupied.push(doorPos);
elements.push({
type: 'door',
x: doorPos.x,
y: doorPos.y,
locked: true,
keyRequired: 'small_key',
});
const keyRoom = {
type: 'key',
x: -1,
y: -1,
keyType: 'small_key',
collected: false,
roomId: -1,
};
elements.push(keyRoom);
return {
type: 'key_door',
elements,
solution: `Find small key in another room, then unlock door at (${doorPos.x}, ${doorPos.y})`,
solved: false,
reward: 'chest_unlock',
keyRoomId: keyRoom.roomId,
};
}
function generateSwitchPuzzle(room, rng) {
const elements = [];
const occupied = [];
const switchPos = findFloorPosition(room, rng, occupied);
if (!switchPos) return null;
occupied.push(switchPos);
room.tiles[switchPos.y][switchPos.x] = _C.TILE_TYPES.SWITCH;
elements.push({
type: 'switch',
x: switchPos.x,
y: switchPos.y,
activated: false,
});
const puzzleVariant = rng() < 0.7 ? 'softwall' : 'door';
if (puzzleVariant === 'softwall') {
let softwallPos = null;
for (let attempts = 0; attempts < 30; attempts++) {
softwallPos = findFloorPosition(room, rng, occupied);
if (!softwallPos) break;
if (room.tiles[softwallPos.y][softwallPos.x] === _C.TILE_TYPES.FLOOR) {
break;
}
}
if (softwallPos) {
room.tiles[softwallPos.y][softwallPos.x] = _C.TILE_TYPES.SOFTWALL;
occupied.push(softwallPos);
elements.push({
type: 'softwall',
x: softwallPos.x,
y: softwallPos.y,
breakable: true,
linkedToSwitch: true,
});
}
} else {
const edges = ['top', 'bottom', 'left', 'right'];
const edge = edges[Math.floor(rng() * edges.length)];
const doorPos = findWallPosition(room, rng, edge, occupied);
if (doorPos) {
room.tiles[doorPos.y][doorPos.x] = _C.TILE_TYPES.DOOR;
occupied.push(doorPos);
elements.push({
type: 'door',
x: doorPos.x,
y: doorPos.y,
locked: true,
keyRequired: null,
linkedToSwitch: true,
});
}
}
return {
type: 'switch',
elements,
solution: `Step on switch at (${switchPos.x}, ${switchPos.y}) to ${puzzleVariant === 'softwall' ? 'break softwall' : 'unlock door'}`,
solved: false,
reward: 'small_key',
};
}
function generatePuzzles(rooms, dungeon, rng) {
const difficulty = dungeon.difficulty || 1;
const puzzleRooms = rooms.filter(r => r.type === 'puzzle');
for (const room of puzzleRooms) {
room.difficulty = difficulty;
const puzzleTypes = ['rock_push', 'lever', 'switch'];
if (room.width >= 6 && room.height >= 6) {
puzzleTypes.push('key_door');
}
const chosenType = puzzleTypes[Math.floor(rng() * puzzleTypes.length)];
let puzzle;
switch (chosenType) {
case 'rock_push':
puzzle = generateRockPushPuzzle(room, rng);
break;
case 'lever':
puzzle = generateLeverPuzzle(room, rng);
break;
case 'switch':
puzzle = generateSwitchPuzzle(room, rng);
break;
case 'key_door':
puzzle = generateKeyDoorPuzzle(room, rng);
break;
default:
puzzle = generateSwitchPuzzle(room, rng);
break;
}
if (puzzle) {
room.puzzles.push(puzzle);
if (puzzle.type === 'key_door' && puzzle.keyRoomId !== undefined) {
const keyRoom = rooms.find(r => r.id === puzzle.keyRoomId);
if (keyRoom) {
const keyPos = findFloorPosition(keyRoom, rng, null);
if (keyPos) {
const keyElement = puzzle.elements.find(e => e.type === 'key');
if (keyElement) {
keyElement.x = keyPos.x;
keyElement.y = keyPos.y;
keyElement.roomId = keyRoom.id;
room.tiles[keyPos.y][keyPos.x] = _C.TILE_TYPES.ITEM;
}
}
}
}
}
}
for (const room of puzzleRooms) {
for (const puzzle of room.puzzles) {
if (puzzle.type === 'key_door' && puzzle.keyRoomId === -1) {
const otherRooms = rooms.filter(r => r.id !== room.id && r.type !== 'boss');
if (otherRooms.length > 0) {
const keyRoom = otherRooms[Math.floor(rng() * otherRooms.length)];
puzzle.keyRoomId = keyRoom.id;
const keyPos = findFloorPosition(keyRoom, rng, null);
if (keyPos) {
const keyElement = puzzle.elements.find(e => e.type === 'key');
if (keyElement) {
keyElement.x = keyPos.x;
keyElement.y = keyPos.y;
keyElement.roomId = keyRoom.id;
keyRoom.tiles[keyPos.y][keyPos.x] = _C.TILE_TYPES.ITEM;
keyRoom.items.push({
type: 'key',
keyType: 'small_key',
x: keyPos.x,
y: keyPos.y,
collected: false,
});
}
}
}
}
}
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { generatePuzzles };
}

399
src/generator/rooms.js Normal file
View File

@ -0,0 +1,399 @@
'use strict';
// null = FLOOR (default), explicit values for special tiles
// Outer ring = WALL perimeter
const NORMAL_TEMPLATES = [
// Empty room
[
[1, 1, 1, 1, 1],
[1, null, null, null, 1],
[1, null, null, null, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
// With rock
[
[1, 1, 1, 1, 1],
[1, null, null, null, 1],
[1, null, null, null, 1],
[1, null, null, 11, 1],
[1, 1, 1, 1, 1],
],
// With softwall
[
[1, 1, 1, 1, 1],
[1, null, null, null, 1],
[1, null, 2, null, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
// Rock + torch
[
[1, 1, 1, 1, 1],
[1, 5, null, null, 1],
[1, null, null, null, 1],
[1, null, null, 11, 1],
[1, 1, 1, 1, 1],
],
];
const ENTRANCE_TEMPLATES = [
// EXIT at bottom
[
[1, 1, 1, 1, 1],
[1, null, null, null, 1],
[1, null, 6, null, 1],
[1, null, null, null, 1],
[1, 1, 14, 1, 1],
],
// EXIT at top
[
[1, 1, 14, 1, 1],
[1, null, null, null, 1],
[1, null, 6, null, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
// EXIT at left
[
[1, 1, 1, 1, 1],
[14, null, null, null, 1],
[1, null, null, 6, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
// EXIT at right
[
[1, 1, 1, 1, 1],
[1, null, null, null, 14],
[1, null, 6, null, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
];
const BOSS_TEMPLATES = [
// Open arena, corner torches, EXIT bottom center
[
[1, 1, 1, 1, 1, 1, 1],
[1, 5, null, null, null, 5, 1],
[1, null, null, null, null, null, 1],
[1, null, null, null, null, null, 1],
[1, null, null, null, null, null, 1],
[1, 5, null, null, null, 5, 1],
[1, 1, 1, 14, 1, 1, 1],
],
// Arena with center softwall
[
[1, 1, 1, 1, 1, 1, 1],
[1, 5, null, null, null, 5, 1],
[1, null, null, null, null, null, 1],
[1, null, null, 2, null, null, 1],
[1, null, null, null, null, null, 1],
[1, 5, null, null, null, 5, 1],
[1, 1, 1, 14, 1, 1, 1],
],
// Arena with two softwalls
[
[1, 1, 1, 1, 1, 1, 1],
[1, 5, null, null, null, 5, 1],
[1, null, null, 2, null, null, 1],
[1, null, null, null, null, null, 1],
[1, null, null, 2, null, null, 1],
[1, 5, null, null, null, 5, 1],
[1, 1, 1, 14, 1, 1, 1],
],
];
const TREASURE_TEMPLATES = [
// Chest in center, clean
[
[1, 1, 1, 1, 1],
[1, null, null, null, 1],
[1, null, 12, null, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
// Chest with traps
[
[1, 1, 1, 1, 1],
[1, 9, null, 9, 1],
[1, null, 12, null, 1],
[1, null, null, null, 1],
[1, 1, 1, 1, 1],
],
// Chest with traps + rock
[
[1, 1, 1, 1, 1],
[1, 9, null, 9, 1],
[1, null, 12, null, 1],
[1, null, 11, null, 1],
[1, 1, 1, 1, 1],
],
// Chest surrounded by traps
[
[1, 1, 1, 1, 1],
[1, 9, 9, 9, 1],
[1, 9, 12, 9, 1],
[1, 9, 9, 9, 1],
[1, 1, 1, 1, 1],
],
];
const PUZZLE_TEMPLATES = [
// Lever + target
[
[1, 1, 1, 1, 1, 1],
[1, null, null, 7, null, 1],
[1, null, null, null, null, 1],
[1, null, null, null, null, 1],
[1, null, 10, null, null, 1],
[1, 1, 1, 1, 1, 1],
],
// Rock + target
[
[1, 1, 1, 1, 1, 1],
[1, null, null, null, 11, 1],
[1, null, null, null, null, 1],
[1, null, null, null, null, 1],
[1, null, 10, null, null, 1],
[1, 1, 1, 1, 1, 1],
],
// Switch + target
[
[1, 1, 1, 1, 1, 1],
[1, null, null, 8, null, 1],
[1, null, null, null, null, 1],
[1, null, null, null, null, 1],
[1, null, 10, null, null, 1],
[1, 1, 1, 1, 1, 1],
],
// Two levers + target
[
[1, 1, 1, 1, 1, 1],
[1, null, 7, null, null, 1],
[1, null, null, null, null, 1],
[1, null, null, null, null, 1],
[1, null, 10, null, 7, 1],
[1, 1, 1, 1, 1, 1],
],
];
const TRAP_TEMPLATES = [
// 2 traps
[
[1, 1, 1, 1, 1],
[1, null, 9, null, 1],
[1, null, null, null, 1],
[1, null, null, 9, 1],
[1, 1, 1, 1, 1],
],
// 3 traps
[
[1, 1, 1, 1, 1],
[1, 9, null, null, 1],
[1, null, 9, null, 1],
[1, null, null, 9, 1],
[1, 1, 1, 1, 1],
],
// 4 traps
[
[1, 1, 1, 1, 1],
[1, 9, null, null, 1],
[1, null, null, null, 1],
[1, null, null, 9, 1],
[1, 1, 9, 9, 1],
],
// Lava traps
[
[1, 1, 1, 1, 1],
[1, null, 3, null, 1],
[1, null, null, null, 1],
[1, null, 3, null, 1],
[1, 1, 1, 1, 1],
],
];
function getRoomLayoutTemplate(roomType, rng) {
let templates;
switch (roomType) {
case 'entrance':
templates = ENTRANCE_TEMPLATES;
break;
case 'boss':
templates = BOSS_TEMPLATES;
break;
case 'treasure':
templates = TREASURE_TEMPLATES;
break;
case 'puzzle':
templates = PUZZLE_TEMPLATES;
break;
case 'trap':
templates = TRAP_TEMPLATES;
break;
default:
templates = NORMAL_TEMPLATES;
break;
}
const idx = Math.floor(rng() * templates.length);
return templates[idx].map(row => [...row]);
}
function generateRoomLayout(room, theme, rng) {
const template = getRoomLayoutTemplate(room.type, rng);
const tW = template[0].length;
const tH = template.length;
const rW = room.width;
const rH = room.height;
// Initialize room grid: walls on perimeter, floor inside
const grid = [];
for (let y = 0; y < rH; y++) {
grid[y] = [];
for (let x = 0; x < rW; x++) {
if (y === 0 || y === rH - 1 || x === 0 || x === rW - 1) {
grid[y][x] = TILE_TYPES.WALL;
} else {
grid[y][x] = TILE_TYPES.FLOOR;
}
}
}
// Map template tiles to room grid
for (let ty = 0; ty < tH; ty++) {
for (let tx = 0; tx < tW; tx++) {
const tile = template[ty][tx];
if (tile === null) continue;
let rx, ry;
// Interior tile
if (ty > 0 && ty < tH - 1 && tx > 0 && tx < tW - 1) {
rx = Math.round((tx - 1) * (rW - 2) / (tW - 2)) + 1;
ry = Math.round((ty - 1) * (rH - 2) / (tH - 2)) + 1;
}
// Top edge
else if (ty === 0 && tx > 0 && tx < tW - 1) {
rx = Math.round((tx / (tW - 1)) * (rW - 1));
ry = 0;
}
// Bottom edge
else if (ty === tH - 1 && tx > 0 && tx < tW - 1) {
rx = Math.round((tx / (tW - 1)) * (rW - 1));
ry = rH - 1;
}
// Left edge
else if (tx === 0 && ty > 0 && ty < tH - 1) {
rx = 0;
ry = Math.round((ty / (tH - 1)) * (rH - 1));
}
// Right edge
else if (tx === tW - 1 && ty > 0 && ty < tH - 1) {
rx = rW - 1;
ry = Math.round((ty / (tH - 1)) * (rH - 1));
}
// Corners (wall overlaps, skip unless special)
else {
continue;
}
if (rx >= 0 && rx < rW && ry >= 0 && ry < rH) {
grid[ry][rx] = tile;
}
}
}
// Add corner torches based on RNG
if (rng() < 0.6) {
grid[1][1] = TILE_TYPES.TORCH;
}
if (rng() < 0.4) {
grid[1][rW - 2] = TILE_TYPES.TORCH;
}
if (rng() < 0.4) {
grid[rH - 2][1] = TILE_TYPES.TORCH;
}
if (rng() < 0.6) {
grid[rH - 2][rW - 2] = TILE_TYPES.TORCH;
}
// Add theme-appropriate floor decorations
if (theme && theme.floorHazards) {
for (const hazard of theme.floorHazards) {
if (rng() < hazard.chance) {
const hx = 1 + Math.floor(rng() * (rW - 2));
const hy = 1 + Math.floor(rng() * (rH - 2));
if (grid[hy][hx] === TILE_TYPES.FLOOR) {
grid[hy][hx] = hazard.tileType;
}
}
}
}
room.tiles = grid;
applyThemeToRoom(room, theme);
return grid;
}
function applyThemeToRoom(room, theme) {
if (!theme) return;
room.theme = theme;
room.floorColor = theme.floorColor;
room.floorDarkColor = theme.floorDarkColor;
room.wallColor = theme.wallColor;
room.wallTopColor = theme.wallTopColor;
room.wallFrontColor = theme.wallFrontColor;
room.wallBottomColor = theme.wallBottomColor;
room.wallBorderColor = theme.wallBorderColor;
for (const deco of room.decorations) {
switch (deco.type) {
case 'torch':
deco.glowColor = theme.torchGlow;
deco.stickColor = theme.torchStickColor;
deco.flameColor = theme.torchFlameColor;
deco.coreColor = theme.torchCoreColor;
break;
case 'rock':
deco.color = theme.rockColor || '#888888';
deco.darkColor = theme.rockDarkColor || '#666666';
deco.lightColor = theme.rockLightColor || '#AAAAAA';
break;
case 'chest':
deco.closedColor = theme.chestClosedColor || '#8B4513';
deco.goldColor = theme.chestGoldColor || '#DAA520';
deco.openColor = theme.chestOpenColor || '#A0522D';
break;
case 'lever':
deco.color = theme.leverColor || '#B8860B';
break;
case 'switch':
deco.color = theme.switchFloorColor || '#DAA520';
break;
case 'target':
deco.color = theme.targetColor || '#FFD700';
deco.glowColor = theme.targetGlow || 'rgba(255,215,0,0.3)';
break;
case 'trap':
deco.color = theme.trapColor || '#8B008B';
break;
case 'lava':
deco.color = theme.lavaColor || '#FF4500';
deco.lightColor = theme.lavaLightColor || '#FF6347';
break;
case 'water':
deco.color = theme.waterColor || '#4169AA';
deco.lightColor = theme.waterLightColor || '#5F9EA0';
break;
}
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { generateRoomLayout, getRoomLayoutTemplate, applyThemeToRoom };
}

29
src/input/actions.js Normal file
View File

@ -0,0 +1,29 @@
'use strict';
// === ACTION DEFINITIONS ===
// Maps action names to key bindings. Actual key detection is in keyboard.js.
const ACTIONS = {
move: { keys: ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'], type: 'hold' },
attack: { keys: ['Space', 'KeyZ'], type: 'press' },
interact: { keys: ['KeyE', 'Enter'], type: 'press' },
menu: { keys: ['KeyI'], type: 'press' },
back: { keys: ['Escape', 'KeyQ'], type: 'press' },
switchWeapon: { keys: ['KeyX'], type: 'press' },
useItem: { keys: ['KeyR'], type: 'press' },
bomb: { keys: ['KeyB'], type: 'press' },
};
// Menu navigation actions
const MENU_ACTIONS = {
up: { keys: ['ArrowUp', 'KeyW'] },
down: { keys: ['ArrowDown', 'KeyS'] },
left: { keys: ['ArrowLeft', 'KeyA'] },
right: { keys: ['ArrowRight', 'KeyD'] },
select: { keys: ['Enter', 'Space'] },
back: { keys: ['Escape', 'KeyQ'] },
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ACTIONS, MENU_ACTIONS, initActions: () => {}, getActionState: () => {}, getAction: () => {}, getActiveActions: () => {} };
}

83
src/input/keyboard.js Normal file
View File

@ -0,0 +1,83 @@
'use strict';
// === KEYBOARD INPUT SYSTEM ===
const inputSystem = {
keys: {},
justPressed: {},
actions: {},
init() {
document.addEventListener('keydown', (e) => {
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space', 'Enter', 'ShiftLeft', 'ShiftRight', 'KeyE', 'KeyI', 'KeyQ', 'Tab'].includes(e.code)) {
e.preventDefault();
}
if (!this.keys[e.code]) {
this.justPressed[e.code] = true;
}
this.keys[e.code] = true;
});
document.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
},
clearKeys() {
this.keys = {};
this.justPressed = {};
},
update(dt) {
// Process actions
this.actions = {
move: this.getMove(),
attack: this.justPressed['Space'] || this.justPressed['KeyZ'],
interact: this.justPressed['KeyE'] || this.justPressed['Enter'],
menu: this.justPressed['KeyI'],
back: this.justPressed['Escape'] || this.justPressed['KeyQ'],
switchWeapon: this.justPressed['KeyX'],
useItem: this.justPressed['KeyR'],
bomb: this.justPressed['KeyB'],
};
// Clear justPressed for next frame
this.justPressed = {};
},
getMove() {
let dx = 0, dy = 0;
if (this.keys['ArrowUp'] || this.keys['KeyW']) dy = -1;
else if (this.keys['ArrowDown'] || this.keys['KeyS']) dy = 1;
if (this.keys['ArrowLeft'] || this.keys['KeyA']) dx = -1;
else if (this.keys['ArrowRight'] || this.keys['KeyD']) dx = 1;
if (dx !== 0 || dy !== 0) return { dx, dy };
return null;
},
getAction(name) {
return this.actions[name] || false;
},
isKeyDown(code) {
return !!this.keys[code];
},
wasKeyPressed(code) {
return !!this.justPressed[code];
},
};
function initKeyboard() {
inputSystem.init();
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { inputSystem, initKeyboard, isKeyDown: (code) => inputSystem.isKeyDown(code),
isKeyJustPressed: (code) => inputSystem.wasKeyPressed(code),
setKeyHandlers: () => {},
resetKeys: () => inputSystem.clearKeys(),
getMove: () => inputSystem.getMove(),
getAction: (name) => inputSystem.getAction(name),
clearKeys: () => inputSystem.clearKeys(),
update: (dt) => inputSystem.update(dt) };
}

195
src/menu/dungeon-select.js Normal file
View File

@ -0,0 +1,195 @@
'use strict';
// === DUNGEON SELECT SCREEN ===
function renderDungeonSelect(container, save, onSelected) {
container.innerHTML = '';
container.style.background = 'linear-gradient(180deg, #0a0a1a 0%, #1a1a2d 50%, #0a0a1a 100%)';
const title = document.createElement('div');
title.className = 'menu-title';
title.textContent = 'SELECT DUNGEON';
container.appendChild(title);
// Filter tabs
const filterContainer = document.createElement('div');
filterContainer.className = 'filter-tabs';
filterContainer.style.pointerEvents = 'auto';
const themeNames = ['All', ...Object.keys(THEMES)];
let activeFilter = 'all';
for (const theme of themeNames) {
const tab = document.createElement('button');
tab.className = 'filter-tab' + (theme === 'All' ? ' active' : '');
tab.textContent = theme === 'All' ? 'All' : theme;
tab.dataset.theme = theme === 'All' ? 'all' : theme.toLowerCase();
tab.addEventListener('click', () => {
filterContainer.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
activeFilter = tab.dataset.theme;
renderGrid(activeFilter);
});
filterContainer.appendChild(tab);
}
container.appendChild(filterContainer);
// Dungeon grid
const grid = document.createElement('div');
grid.className = 'dungeon-grid';
grid.id = 'dungeon-grid';
function renderGrid(filter) {
grid.innerHTML = '';
const filtered = filter === 'all'
? DUNGEONS.map((d, i) => ({ ...d, idx: i }))
: DUNGEONS.map((d, i) => ({ ...d, idx: i })).filter(d => d.theme === filter);
for (const dungeon of filtered) {
const cell = document.createElement('div');
cell.className = 'dungeon-cell';
const themeData = THEMES[dungeon.theme];
cell.style.background = themeData ? themeData.cellColor || '#333' : '#333';
cell.style.color = themeData ? themeData.cellTextColor || '#FFF' : '#FFF';
cell.textContent = dungeon.idx + 1;
cell.dataset.idx = dungeon.idx;
const unlocked = isDungeonUnlocked(dungeon.idx);
const completed = isDungeonCompleted(dungeon.idx);
if (!unlocked) cell.classList.add('locked');
if (completed) cell.classList.add('completed');
if (unlocked) {
cell.addEventListener('click', () => {
grid.querySelectorAll('.dungeon-cell').forEach(c => c.classList.remove('selected'));
cell.classList.add('selected');
showInfoPanel(dungeon);
});
cell.addEventListener('dblclick', () => {
if (unlocked) onSelected(dungeon.idx);
});
}
grid.appendChild(cell);
}
}
renderGrid('all');
container.appendChild(grid);
// Info panel
const infoPanel = document.createElement('div');
infoPanel.className = 'info-panel';
infoPanel.id = 'info-panel';
infoPanel.style.display = 'none';
container.appendChild(infoPanel);
function showInfoPanel(dungeon) {
infoPanel.style.display = 'block';
const themeData = THEMES[dungeon.theme] || {};
infoPanel.innerHTML = `
<div class="name">${dungeon.name}</div>
<div class="theme">Theme: ${dungeon.theme}</div>
<div class="difficulty">Difficulty: ${'★'.repeat(dungeon.difficulty)}${'☆'.repeat(10 - dungeon.difficulty)}</div>
<div class="rooms">Rooms: ${dungeon.roomCount}</div>
${dungeon.requiredWeapon ? `<div style="color:#FF6347">Requires: ${dungeon.requiredWeapon}</div>` : ''}
<div style="margin-top:4px; color:#AAA">${dungeon.description || ''}</div>
<div style="margin-top:6px; color:#888">Double-click to enter</div>
`;
}
// Enter button
const enterBtn = document.createElement('button');
enterBtn.className = 'menu-button';
enterBtn.textContent = 'Enter Dungeon';
enterBtn.id = 'enter-btn';
enterBtn.style.marginTop = '10px';
enterBtn.style.display = 'none';
enterBtn.addEventListener('click', () => {
const selected = grid.querySelector('.dungeon-cell.selected');
if (selected && !selected.classList.contains('locked')) {
onSelected(parseInt(selected.dataset.idx));
}
});
container.appendChild(enterBtn);
// Back button
const backBtn = document.createElement('button');
backBtn.className = 'menu-button';
backBtn.textContent = 'Back';
backBtn.style.marginTop = '6px';
backBtn.addEventListener('click', () => { switchScreen(SCREEN_TITLE); });
container.appendChild(backBtn);
// Listen for cell selection to show enter button
const origRenderGrid = renderGrid;
grid.addEventListener('click', () => {
const selected = grid.querySelector('.dungeon-cell.selected');
enterBtn.style.display = selected && !selected.classList.contains('locked') ? 'block' : 'none';
});
// Keyboard navigation
let selectedCell = null;
container.addEventListener('keydown', (e) => {
const cells = [...grid.querySelectorAll('.dungeon-cell:not(.locked)')];
if (!cells.length) return;
if (!selectedCell) {
selectedCell = cells[0];
selectedCell.classList.add('selected');
const idx = parseInt(selectedCell.dataset.idx);
showInfoPanel(DUNGEONS[idx]);
enterBtn.style.display = 'block';
return;
}
const idx = cells.indexOf(selectedCell);
if (e.code === 'ArrowRight' || e.code === 'KeyD') {
e.preventDefault();
selectedCell.classList.remove('selected');
selectedCell = cells[Math.min(idx + 1, cells.length - 1)];
selectedCell.classList.add('selected');
const i = parseInt(selectedCell.dataset.idx);
showInfoPanel(DUNGEONS[i]);
}
if (e.code === 'ArrowLeft' || e.code === 'KeyA') {
e.preventDefault();
selectedCell.classList.remove('selected');
selectedCell = cells[Math.max(idx - 1, 0)];
selectedCell.classList.add('selected');
const i = parseInt(selectedCell.dataset.idx);
showInfoPanel(DUNGEONS[i]);
}
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
e.preventDefault();
selectedCell.classList.remove('selected');
selectedCell = cells[Math.min(idx + 10, cells.length - 1)];
selectedCell.classList.add('selected');
const i = parseInt(selectedCell.dataset.idx);
showInfoPanel(DUNGEONS[i]);
}
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
selectedCell.classList.remove('selected');
selectedCell = cells[Math.max(idx - 10, 0)];
selectedCell.classList.add('selected');
const i = parseInt(selectedCell.dataset.idx);
showInfoPanel(DUNGEONS[i]);
}
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
if (selectedCell && !selectedCell.classList.contains('locked')) {
onSelected(parseInt(selectedCell.dataset.idx));
}
}
if (e.code === 'Escape' || e.code === 'KeyQ') {
e.preventDefault();
switchScreen(SCREEN_TITLE);
}
});
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderDungeonSelect };
}

75
src/menu/hud-overlay.js Normal file
View File

@ -0,0 +1,75 @@
'use strict';
// === INVENTORY / HUD OVERLAY ===
function renderInventoryOverlay(container, state) {
container.innerHTML = '';
container.style.background = 'rgba(0,0,0,0.85)';
const title = document.createElement('div');
title.className = 'menu-title';
title.textContent = 'INVENTORY';
title.style.fontSize = '24px';
container.appendChild(title);
// Weapons
const weaponsDiv = document.createElement('div');
weaponsDiv.style.cssText = 'font-family: monospace; font-size: 12px; color: #CCC; margin: 8px 0; max-width: 400px;';
weaponsDiv.innerHTML = '<div style="color:#FFD700; margin-bottom:4px;">WEAPONS</div>';
if (state.player.weapons.length === 0) {
weaponsDiv.innerHTML += '<div style="color:#666;">No weapons</div>';
} else {
for (const w of state.player.weapons) {
const isActive = w === state.player.activeWeapon;
const line = document.createElement('div');
line.style.cssText = `color: ${isActive ? '#FFD700' : '#AAA'}; padding: 2px 0;`;
line.textContent = `${isActive ? '> ' : ' '}${w.name} (DMG:${w.damage})`;
weaponsDiv.appendChild(line);
}
}
container.appendChild(weaponsDiv);
// Items
const itemsDiv = document.createElement('div');
itemsDiv.style.cssText = 'font-family: monospace; font-size: 12px; color: #CCC; margin: 8px 0; max-width: 400px;';
itemsDiv.innerHTML = '<div style="color:#FFD700; margin-bottom:4px;">ITEMS</div>';
if (state.player.items.length === 0) {
itemsDiv.innerHTML += '<div style="color:#666;">No items</div>';
} else {
for (const item of state.player.items) {
const def = ITEMS.find(i => i.id === item.id);
const line = document.createElement('div');
line.style.cssText = 'color: #90EE90; padding: 2px 0;';
line.textContent = `${def ? def.name : item.id}${(item.count || 1) > 1 ? ` x${item.count}` : ''}`;
itemsDiv.appendChild(line);
}
}
container.appendChild(itemsDiv);
// Resources
const resourcesDiv = document.createElement('div');
resourcesDiv.style.cssText = 'font-family: monospace; font-size: 12px; color: #CCC; margin: 8px 0; max-width: 400px;';
resourcesDiv.innerHTML = `
<div style="color:#FFD700; margin-bottom:4px;">RESOURCES</div>
<div>Arrows: ${state.player.arrows}</div>
<div>Bombs: ${state.player.bombs}</div>
<div>Keys: ${state.player.keys}</div>
`;
container.appendChild(resourcesDiv);
// Stats
const statsDiv = document.createElement('div');
statsDiv.style.cssText = 'font-family: monospace; font-size: 11px; color: #888; margin-top: 10px; max-width: 400px; text-align: center;';
statsDiv.textContent = `Dungeons: ${state.dungeonsCompleted} Rooms: ${state.roomsCleared} Enemies: ${state.enemiesDefeated}`;
container.appendChild(statsDiv);
// Controls hint
const hint = document.createElement('div');
hint.style.cssText = 'font-family: monospace; font-size: 11px; color: #666; margin-top: 10px; text-align: center;';
hint.textContent = 'X: Switch Weapon R: Use Potion I/Escape: Close';
container.appendChild(hint);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderInventoryOverlay };
}

244
src/menu/screens.js Normal file
View File

@ -0,0 +1,244 @@
'use strict';
// === DEATH / WIN / COMPLETION SCREENS ===
function renderDeathScreen(container, state) {
container.innerHTML = '';
container.style.background = 'rgba(20,0,0,0.9)';
const title = document.createElement('div');
title.className = 'menu-title';
title.textContent = 'YOU DIED';
title.style.color = '#FF0000';
container.appendChild(title);
const dungeon = DUNGEONS[state.currentDungeon];
if (dungeon) {
const subtitle = document.createElement('div');
subtitle.className = 'menu-subtitle';
subtitle.textContent = `${dungeon.name} - Room ${state.currentRoomIdx + 1}`;
container.appendChild(subtitle);
}
const stats = document.createElement('div');
stats.style.cssText = 'font-family: monospace; font-size: 12px; color: #888; margin: 15px 0; text-align: center; line-height: 1.8;';
stats.innerHTML = `
Rooms Cleared: ${state.roomsCleared}<br>
Enemies Defeated: ${state.enemiesDefeated}<br>
Dungeons Completed: ${state.dungeonsCompleted}
`;
container.appendChild(stats);
// Retry button
const retryBtn = createMenuButton('Try Again', 0);
retryBtn.addEventListener('click', () => {
state.roomsCleared = 0;
state.enemiesDefeated = 0;
resetPlayer();
state.player.invincibleTimer = 1000;
// Restore saved weapons
for (const wName of saveData.weapons) {
const wDef = WEAPONS.find(w => w.name === wName);
if (wDef && !state.player.weapons.find(w => w.name === wName)) {
state.player.weapons.push({ ...wDef });
}
}
if (state.player.weapons.length > 0 && !state.player.activeWeapon) {
state.player.activeWeapon = state.player.weapons[0];
}
state.currentRoomIdx = 0;
state.dungeonRooms = generateDungeon(DUNGEONS[state.currentDungeon]);
loadDungeonRoom(state.currentDungeon, 0);
switchScreen(SCREEN_PLAYING);
});
container.appendChild(retryBtn);
// Dungeon select button
const selectBtn = createMenuButton('Dungeon Select', 1);
selectBtn.addEventListener('click', () => { switchScreen(SCREEN_DUNGEON_SELECT); });
container.appendChild(selectBtn);
// Title button
const titleBtn = createMenuButton('Title Screen', 2);
titleBtn.addEventListener('click', () => { switchScreen(SCREEN_TITLE); });
container.appendChild(titleBtn);
// Keyboard navigation
const buttons = [retryBtn, selectBtn, titleBtn];
let selectedIndex = 0;
buttons[0].classList.add('selected');
container.addEventListener('keydown', (e) => {
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
e.preventDefault();
buttons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex + 1) % buttons.length;
buttons[selectedIndex].classList.add('selected');
}
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
buttons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex - 1 + buttons.length) % buttons.length;
buttons[selectedIndex].classList.add('selected');
}
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
buttons[selectedIndex].click();
}
});
}
function renderDungeonCompleteScreen(container, state) {
container.innerHTML = '';
container.style.background = 'rgba(0,10,0,0.9)';
const title = document.createElement('div');
title.className = 'menu-title';
title.textContent = 'DUNGEON CLEARED!';
title.style.color = '#00FF00';
container.appendChild(title);
const dungeon = DUNGEONS[state.currentDungeon];
if (dungeon) {
const subtitle = document.createElement('div');
subtitle.className = 'menu-subtitle';
subtitle.textContent = `${dungeon.name} conquered!`;
container.appendChild(subtitle);
}
const stats = document.createElement('div');
stats.style.cssText = 'font-family: monospace; font-size: 12px; color: #AAA; margin: 15px 0; text-align: center; line-height: 1.8;';
stats.innerHTML = `
Rooms Cleared: ${state.roomsCleared}<br>
Enemies Defeated: ${state.enemiesDefeated}<br>
HP Remaining: ${state.player.hp}/${state.player.maxHp}
`;
container.appendChild(stats);
const nextIdx = state.currentDungeon + 1;
const hasNext = nextIdx < DUNGEONS.length;
if (hasNext) {
const nextDungeon = DUNGEONS[nextIdx];
const nextInfo = document.createElement('div');
nextInfo.style.cssText = 'font-family: monospace; font-size: 12px; color: #FFD700; margin: 8px 0; text-align: center;';
nextInfo.textContent = `Next: ${nextDungeon.name} (Difficulty: ${'★'.repeat(nextDungeon.difficulty)})`;
container.appendChild(nextInfo);
const nextBtn = createMenuButton('Next Dungeon', 0);
nextBtn.addEventListener('click', () => {
state.currentDungeon = nextIdx;
state.currentRoomIdx = 0;
state.roomsCleared = 0;
state.dungeonRooms = generateDungeon(DUNGEONS[nextIdx]);
resetPlayer();
switchScreen(SCREEN_PLAYING);
});
container.appendChild(nextBtn);
}
const selectBtn = createMenuButton('Dungeon Select', hasNext ? 1 : 0);
selectBtn.addEventListener('click', () => { switchScreen(SCREEN_DUNGEON_SELECT); });
container.appendChild(selectBtn);
const titleBtn = createMenuButton('Title Screen', hasNext ? 2 : 1);
titleBtn.addEventListener('click', () => { switchScreen(SCREEN_TITLE); });
container.appendChild(titleBtn);
// Keyboard navigation
const buttons = container.querySelectorAll('.menu-button');
let selectedIndex = 0;
if (buttons.length) buttons[0].classList.add('selected');
container.addEventListener('keydown', (e) => {
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
e.preventDefault();
buttons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex + 1) % buttons.length;
buttons[selectedIndex].classList.add('selected');
}
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
buttons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex - 1 + buttons.length) % buttons.length;
buttons[selectedIndex].classList.add('selected');
}
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
buttons[selectedIndex].click();
}
});
}
function renderGameCompleteScreen(container, state) {
container.innerHTML = '';
container.style.background = 'linear-gradient(180deg, #0a0a00 0%, #1a1a00 50%, #0a0a00 100%)';
const title = document.createElement('div');
title.className = 'menu-title';
title.textContent = 'ALL DUNGEONS CLEARED!';
title.style.color = '#FFD700';
title.style.fontSize = '24px';
container.appendChild(title);
const subtitle = document.createElement('div');
subtitle.className = 'menu-subtitle';
subtitle.textContent = 'You are the true Hero of Dungeons!';
container.appendChild(subtitle);
const sword = document.createElement('div');
sword.style.cssText = 'margin: 10px 0 20px; font-size: 36px; color: #FFD700;';
sword.textContent = '\u2694 \u2694 \u2694';
container.appendChild(sword);
const stats = document.createElement('div');
stats.style.cssText = 'font-family: monospace; font-size: 13px; color: #DDD; margin: 10px 0; text-align: center; line-height: 2;';
stats.innerHTML = `
Dungeons Conquered: 100<br>
Total Rooms Cleared: ${saveData.totalRoomsCleared}<br>
Total Enemies Slain: ${saveData.totalEnemiesDefeated}<br>
Weapons Found: ${saveData.weapons.length}/${WEAPONS.length}<br>
Heart Pieces: ${saveData.heartPieces}
`;
container.appendChild(stats);
const trophy = document.createElement('div');
trophy.style.cssText = 'font-size: 48px; margin: 15px 0;';
trophy.textContent = '\uD83C\uDFC6';
container.appendChild(trophy);
const titleBtn = createMenuButton('Title Screen', 0);
titleBtn.addEventListener('click', () => { switchScreen(SCREEN_TITLE); });
container.appendChild(titleBtn);
const selectBtn = createMenuButton('Dungeon Select', 1);
selectBtn.addEventListener('click', () => { switchScreen(SCREEN_DUNGEON_SELECT); });
container.appendChild(selectBtn);
const buttons = [titleBtn, selectBtn];
let selectedIndex = 0;
buttons[0].classList.add('selected');
container.addEventListener('keydown', (e) => {
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
e.preventDefault();
buttons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex + 1) % buttons.length;
buttons[selectedIndex].classList.add('selected');
}
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
buttons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex - 1 + buttons.length) % buttons.length;
buttons[selectedIndex].classList.add('selected');
}
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
buttons[selectedIndex].click();
}
});
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderDeathScreen, renderDungeonCompleteScreen, renderGameCompleteScreen };
}

131
src/menu/title-screen.js Normal file
View File

@ -0,0 +1,131 @@
'use strict';
// === TITLE SCREEN ===
function renderTitleScreen(container) {
container.innerHTML = '';
container.style.background = 'linear-gradient(180deg, #1a0a00 0%, #2d1810 50%, #1a0a00 100%)';
const title = document.createElement('div');
title.className = 'menu-title';
title.textContent = 'ZELDA DUNGEON QUEST';
title.style.fontSize = '28px';
container.appendChild(title);
const subtitle = document.createElement('div');
subtitle.className = 'menu-subtitle';
subtitle.textContent = '100 Dungeons \u00B7 22 Enemies \u00B7 12 Weapons';
container.appendChild(subtitle);
// Decorative sword
const swordDiv = document.createElement('div');
swordDiv.style.cssText = 'margin: 10px 0 20px; font-size: 24px; color: #C0C0C0;';
swordDiv.textContent = '\u2694';
container.appendChild(swordDiv);
// Menu buttons
const buttonsContainer = document.createElement('div');
buttonsContainer.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 6px;';
const startBtn = createMenuButton('Start Adventure', 0);
startBtn.addEventListener('click', () => { switchScreen(SCREEN_DUNGEON_SELECT); });
buttonsContainer.appendChild(startBtn);
const controlsBtn = createMenuButton('Controls', 1);
const controlsDiv = document.createElement('div');
controlsDiv.style.cssText = 'display: none; font-family: monospace; font-size: 11px; color: #AAA; background: rgba(0,0,0,0.7); border: 1px solid #555; padding: 10px 14px; margin: 8px 0; max-width: 360px; text-align: left; line-height: 1.6;';
controlsDiv.innerHTML = `
<span style="color:#FFD700">Movement:</span> Arrow Keys / WASD<br>
<span style="color:#FFD700">Attack:</span> Space / Z<br>
<span style="color:#FFD700">Interact:</span> E / Enter<br>
<span style="color:#FFD700">Switch Weapon:</span> X<br>
<span style="color:#FFD700">Use Potion:</span> R<br>
<span style="color:#FFD700">Place Bomb:</span> B<br>
<span style="color:#FFD700">Menu/Inventory:</span> I<br>
<span style="color:#FFD700">Back:</span> Escape / Q<br>
<span style="color:#FFD700">Debug:</span> F1 (overlay) F2 (skip) F3 (god) F4 (spawn)
`;
controlsBtn.addEventListener('click', () => {
controlsDiv.style.display = controlsDiv.style.display === 'none' ? 'block' : 'none';
});
buttonsContainer.appendChild(controlsBtn);
buttonsContainer.appendChild(controlsDiv);
container.appendChild(buttonsContainer);
// Save info
const summary = getSaveSummary();
const lastDungeon = saveData.completedDungeons.length > 0
? saveData.completedDungeons[saveData.completedDungeons.length - 1] + 1
: 0;
if (summary.dungeonsCompleted > 0) {
const saveInfo = document.createElement('div');
saveInfo.className = 'menu-subtitle';
saveInfo.style.marginTop = '15px';
saveInfo.style.fontSize = '12px';
saveInfo.textContent = `Progress: ${summary.dungeonsCompleted}/100 dungeons \u00B7 ${summary.enemiesDefeated} enemies slain`;
container.appendChild(saveInfo);
const resumeBtn = createMenuButton('Resume', 2);
if (lastDungeon < DUNGEONS.length) {
resumeBtn.addEventListener('click', () => {
gameState.currentDungeon = lastDungeon;
gameState.currentRoomIdx = 0;
gameState.dungeonRooms = generateDungeon(DUNGEONS[lastDungeon]);
resetPlayer();
// Restore saved weapons
for (const wName of saveData.weapons) {
const wDef = WEAPONS.find(w => w.name === wName);
if (wDef && !gameState.player.weapons.find(w => w.name === wName)) {
gameState.player.weapons.push({ ...wDef });
}
}
if (gameState.player.weapons.length > 0 && !gameState.player.activeWeapon) {
gameState.player.activeWeapon = gameState.player.weapons[0];
}
switchScreen(SCREEN_PLAYING);
});
container.appendChild(resumeBtn);
}
}
// Keyboard navigation for title screen
let selectedIndex = 0;
const allButtons = [startBtn];
if (summary.dungeonsCompleted > 0 && lastDungeon < DUNGEONS.length) allButtons.unshift(
document.querySelector('.menu-button[data-index="2"]')
);
container.addEventListener('keydown', (e) => {
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
e.preventDefault();
allButtons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex + 1) % allButtons.length;
allButtons[selectedIndex].classList.add('selected');
}
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
allButtons[selectedIndex].classList.remove('selected');
selectedIndex = (selectedIndex - 1 + allButtons.length) % allButtons.length;
allButtons[selectedIndex].classList.add('selected');
}
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
allButtons[selectedIndex].click();
}
});
allButtons[0].classList.add('selected');
}
function createMenuButton(text, index) {
const btn = document.createElement('button');
btn.className = 'menu-button';
btn.textContent = text;
btn.dataset.index = index;
return btn;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderTitleScreen, createMenuButton };
}

View File

@ -0,0 +1,261 @@
'use strict';
// === BOSS DRAWING ===
function renderBoss(boss) {
if (!boss) return;
const px = boss.x * TILE;
const py = boss.y * TILE;
const isHit = boss.hitTimer > 0;
ctx.save();
if (isHit) {
ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.02) * 0.5;
}
// Boss is drawn larger (2x2 tiles)
const def = BOSS_TYPES.find(b => b.id === boss.id || b.name === boss.name);
if (def && def.drawFunction && typeof window[def.drawFunction] === 'function') {
window[def.drawFunction](ctx, px, py, boss, isHit);
} else {
drawGenericBoss(ctx, px, py, boss, isHit);
}
// Boss HP bar (wider, at top of screen)
const barW = 200;
const barH = 8;
const barX = (CANVAS_W - barW) / 2;
const barY = 4;
const hpRatio = boss.hp / boss.maxHp;
ctx.fillStyle = '#333333';
ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = hpRatio > 0.5 ? '#00FF00' : hpRatio > 0.25 ? '#FFAA00' : '#FF0000';
ctx.fillRect(barX, barY, barW * hpRatio, barH);
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
// Boss name
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
ctx.fillText(boss.name, CANVAS_W / 2, barY + barH + 10);
ctx.textAlign = 'left';
// Phase indicator
if (boss.phases && boss.phase > 0) {
ctx.fillStyle = '#FF4444';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText(`Phase ${boss.phase + 1}`, CANVAS_W / 2, barY + barH + 20);
ctx.textAlign = 'left';
}
ctx.restore();
}
function drawGenericBoss(ctx, px, py, boss, isHit) {
const size = TILE * 2;
const color = isHit ? '#FFFFFF' : (boss.color || '#8B0000');
// Large body
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(px - TILE / 2, py - TILE / 2, size, size, 6);
ctx.fill();
// Details
ctx.fillStyle = boss.colorLight || '#CC2222';
ctx.beginPath();
ctx.roundRect(px - TILE / 2 + 4, py - TILE / 2 + 4, size - 8, size * 0.4, 4);
ctx.fill();
// Eyes
ctx.fillStyle = boss.eyeColor || '#FFD700';
ctx.beginPath();
ctx.arc(px - 6, py - 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + 6, py - 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(px - 6, py - 4, 2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + 6, py - 4, 2, 0, Math.PI * 2);
ctx.fill();
}
// Specific boss draw functions
function drawGleeok(ctx, px, py, boss, isHit) {
const size = TILE * 2;
const color = isHit ? '#FFFFFF' : (boss.color || '#2F4F4F');
const heads = boss.heads || 3;
const aliveHeads = boss.aliveHeads || heads;
// Body
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(px, py, size * 0.45, size * 0.35, 0, 0, Math.PI * 2);
ctx.fill();
// Scales pattern
ctx.fillStyle = boss.colorLight || '#3F5F5F';
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2;
ctx.beginPath();
ctx.ellipse(px + Math.cos(angle) * 12, py + Math.sin(angle) * 8, 4, 3, angle, 0, Math.PI * 2);
ctx.fill();
}
// Heads
for (let i = 0; i < heads; i++) {
const angle = (i / heads) * Math.PI * 2 - Math.PI / 2;
const hx = px + Math.cos(angle) * 24;
const hy = py + Math.sin(angle) * 18 - 8;
const alive = i < aliveHeads;
if (!alive) {
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(hx, hy, 5, 0, Math.PI * 2);
ctx.fill();
continue;
}
const headBob = Math.sin(Date.now() * 0.005 + i) * 2;
ctx.fillStyle = boss.headColor || '#006400';
ctx.beginPath();
ctx.ellipse(hx, hy + headBob, 6, 5, 0, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(hx - 2, hy + headBob, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(hx + 2, hy + headBob, 1.5, 0, Math.PI * 2);
ctx.fill();
// Fire breath
if (boss.attacking && i < aliveHeads) {
ctx.fillStyle = '#FF4500';
const dirX = Math.cos(angle);
const dirY = Math.sin(angle);
ctx.beginPath();
ctx.moveTo(hx + dirX * 6, hy + headBob + dirY * 6);
ctx.lineTo(hx + dirX * 14, hy + headBob + dirY * 14);
ctx.lineTo(hx + dirX * 6 + dirY * 3, hy + headBob + dirY * 6 - dirX * 3);
ctx.closePath();
ctx.fill();
}
}
}
function drawDarknut(ctx, px, py, boss, isHit) {
const size = TILE * 2;
const color = isHit ? '#FFFFFF' : (boss.color || '#708090');
const armorBroken = boss.armorBroken || 0;
// Armor body
ctx.fillStyle = color;
ctx.fillRect(px - 10, py - 12, 20, 28);
// Helmet
ctx.fillStyle = isHit ? '#FFFFFF' : '#606060';
ctx.beginPath();
ctx.arc(px, py - 8, 8, 0, Math.PI * 2);
ctx.fill();
// Visor
ctx.fillStyle = boss.eyeColor || '#FF0000';
ctx.fillRect(px - 5, py - 10, 10, 3);
// Armor plates (break with phases)
ctx.fillStyle = isHit ? '#FFFFFF' : '#808080';
if (armorBroken < 1) {
ctx.fillRect(px - 12, py - 4, 24, 6); // Chest plate
}
if (armorBroken < 2) {
ctx.fillRect(px - 12, py + 8, 24, 6); // Waist plate
}
// Weapon (two-handed)
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(px + 12, py - 16, 3, 32);
ctx.fillStyle = '#8B4513';
ctx.fillRect(px + 10, py + 6, 7, 4);
// Speed increases with armor broken
if (armorBroken > 0) {
ctx.fillStyle = 'rgba(255,0,0,0.2)';
ctx.fillRect(px - 14, py - 14, 28, 32);
}
}
function drawWizzrobe(ctx, px, py, boss, isHit) {
const robeColor = isHit ? '#FFFFFF' : (boss.color || '#4B0082');
const teleporting = boss.teleporting || false;
if (teleporting) {
ctx.globalAlpha = 0.3 + Math.sin(Date.now() * 0.05) * 0.3;
}
// Robe
ctx.fillStyle = robeColor;
ctx.beginPath();
ctx.moveTo(px, py - 14);
ctx.lineTo(px - 10, py + 16);
ctx.lineTo(px + 10, py + 16);
ctx.closePath();
ctx.fill();
// Hood
ctx.beginPath();
ctx.arc(px, py - 10, 8, 0, Math.PI * 2);
ctx.fill();
// Face opening
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(px, py - 10, 4, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#FF00FF';
ctx.beginPath();
ctx.arc(px - 2, py - 11, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + 2, py - 11, 1.5, 0, Math.PI * 2);
ctx.fill();
// Magic orb
if (boss.castSpell) {
ctx.fillStyle = '#FF00FF';
ctx.globalAlpha = 0.7;
ctx.beginPath();
ctx.arc(px + 14, py - 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
// Register boss draw functions
const BOSS_DRAW_FUNCTIONS = {
gleeok: drawGleeok,
darknut: drawDarknut,
wizzrobe: drawWizzrobe,
generic: drawGenericBoss,
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderBoss, drawGenericBoss, drawGleeok, drawDarknut, drawWizzrobe };
}

View File

@ -0,0 +1,362 @@
'use strict';
// === ENEMY DRAWING FUNCTIONS ===
function renderEnemies() {
for (const enemy of gameState.enemies) {
drawEnemy(enemy);
}
}
function drawEnemy(enemy) {
const px = enemy.x * TILE;
const py = enemy.y * TILE;
const isHit = enemy.hitTimer > 0;
const isStunned = enemy.stunned;
const isFrozen = enemy.frozen;
const isOnFire = enemy.onFire;
ctx.save();
if (isHit) {
ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.02) * 0.5;
}
if (isStunned) {
ctx.globalAlpha = 0.6;
}
if (isFrozen) {
ctx.filter = 'hue-rotate(180deg) brightness(1.2)';
}
if (isOnFire) {
ctx.filter = 'saturate(2) hue-rotate(-30deg)';
}
// Draw based on enemy type
const def = ENEMY_TYPES.find(e => e.id === enemy.id || e.name === enemy.name);
if (def && def.drawFunction && typeof window[def.drawFunction] === 'function') {
window[def.drawFunction](ctx, px, py, enemy, isHit);
} else {
// Default goblin-style drawing
const bodyColor = isHit ? '#FFFFFF' : (enemy.color || COLORS.goblin);
ctx.fillStyle = bodyColor;
ctx.beginPath();
ctx.roundRect(px + 3, py + 4, TILE - 6, TILE - 8, 3);
ctx.fill();
ctx.fillStyle = enemy.colorLight || COLORS.goblinLight;
ctx.beginPath();
ctx.roundRect(px + 4, py + 5, (TILE - 6) * 0.6, (TILE - 8) * 0.4, 2);
ctx.fill();
ctx.fillStyle = enemy.eyeColor || COLORS.goblinEye;
ctx.beginPath();
ctx.arc(px + TILE / 2 - 4, py + 10, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 4, py + 10, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 4, py + 10, 1.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 4, py + 10, 1.2, 0, Math.PI * 2);
ctx.fill();
}
// HP bar
const barW = TILE - 4;
const hpRatio = enemy.hp / enemy.maxHp;
ctx.fillStyle = '#333333';
ctx.fillRect(px + 2, py - 4, barW, 4);
ctx.fillStyle = hpRatio > 0.5 ? '#00FF00' : hpRatio > 0.25 ? '#FFAA00' : '#FF0000';
ctx.fillRect(px + 2, py - 4, barW * hpRatio, 4);
// Stun indicator
if (isStunned) {
ctx.fillStyle = '#FFFF00';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText('?', px + TILE / 2, py - 6);
ctx.textAlign = 'left';
}
// Freeze indicator
if (isFrozen) {
ctx.fillStyle = '#00BFFF';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText('❄', px + TILE / 2, py - 6);
ctx.textAlign = 'left';
}
ctx.restore();
}
// Individual enemy draw functions
function drawSlime(ctx, px, py, enemy, isHit) {
const bounce = Math.sin(Date.now() * 0.008 + enemy.x * 2) * 2;
const color = isHit ? '#FFFFFF' : (enemy.color || '#00CC44');
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(px + TILE / 2, py + TILE / 2 + bounce, TILE * 0.35, TILE * 0.25, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = isHit ? '#FFFFFF' : '#00FF66';
ctx.beginPath();
ctx.ellipse(px + TILE / 2, py + TILE / 2 + bounce - 2, TILE * 0.15, TILE * 0.1, 0, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 3, py + TILE / 2 + bounce, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 3, py + TILE / 2 + bounce, 1.5, 0, Math.PI * 2);
ctx.fill();
}
function drawKeese(ctx, px, py, enemy, isHit) {
const flap = Math.sin(Date.now() * 0.015 + enemy.x * 3) * 3;
const color = isHit ? '#FFFFFF' : (enemy.color || '#4A3040');
// Body
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(px + TILE / 2, py + TILE / 2 + flap, TILE * 0.2, TILE * 0.15, 0, 0, Math.PI * 2);
ctx.fill();
// Wings
ctx.fillStyle = isHit ? '#FFFFFF' : '#3A2030';
ctx.beginPath();
ctx.ellipse(px + TILE / 2 - 8, py + TILE / 2 + flap, 8, 4, -0.3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(px + TILE / 2 + 8, py + TILE / 2 + flap, 8, 4, 0.3, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = enemy.eyeColor || '#FF0000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 2, py + TILE / 2 + flap, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 2, py + TILE / 2 + flap, 1.5, 0, Math.PI * 2);
ctx.fill();
}
function drawOctorok(ctx, px, py, enemy, isHit) {
const color = isHit ? '#FFFFFF' : (enemy.color || '#8B008B');
// Body
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2, TILE * 0.35, 0, Math.PI * 2);
ctx.fill();
// Legs
ctx.fillStyle = isHit ? '#FFFFFF' : (enemy.colorLight || '#A020A0');
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2 + Date.now() * 0.001;
const lx = px + TILE / 2 + Math.cos(angle) * TILE * 0.4;
const ly = py + TILE / 2 + Math.sin(angle) * TILE * 0.4;
ctx.fillRect(lx - 1, ly - 1, 3, 3);
}
// Eyes
ctx.fillStyle = enemy.eyeColor || '#FFFF00';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 4, py + TILE / 2 - 2, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 4, py + TILE / 2 - 2, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 4, py + TILE / 2 - 2, 1, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 4, py + TILE / 2 - 2, 1, 0, Math.PI * 2);
ctx.fill();
}
function drawMoblin(ctx, px, py, enemy, isHit) {
const color = isHit ? '#FFFFFF' : (enemy.color || '#2F4F2F');
// Body (larger than goblin)
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(px + 2, py + 2, TILE - 4, TILE - 4, 4);
ctx.fill();
ctx.fillStyle = enemy.colorLight || '#3F5F3F';
ctx.beginPath();
ctx.roundRect(px + 3, py + 3, (TILE - 4) * 0.5, (TILE - 4) * 0.4, 2);
ctx.fill();
// Snout
ctx.fillStyle = isHit ? '#FFFFFF' : '#5F3F1F';
ctx.fillRect(px + TILE / 2 - 3, py + 14, 6, 6);
// Eyes
ctx.fillStyle = enemy.eyeColor || '#FF4400';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 5, py + 8, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 5, py + 8, 2.5, 0, Math.PI * 2);
ctx.fill();
// Spear
ctx.fillStyle = '#8B4513';
ctx.fillRect(px + TILE - 4, py + 6, 2, TILE - 10);
ctx.fillStyle = '#808080';
ctx.fillRect(px + TILE - 6, py + 4, 6, 4);
}
function drawStalfos(ctx, px, py, enemy, isHit) {
const color = isHit ? '#FFFFFF' : (enemy.color || '#E8E8E8');
// Skeleton body
ctx.fillStyle = color;
ctx.fillRect(px + TILE / 2 - 2, py + 4, 4, TILE - 10);
// Skull
ctx.beginPath();
ctx.arc(px + TILE / 2, py + 6, 5, 0, Math.PI * 2);
ctx.fill();
// Eye sockets
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 2, py + 5, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 2, py + 5, 1.5, 0, Math.PI * 2);
ctx.fill();
// Eye glow
ctx.fillStyle = enemy.eyeColor || '#FF0000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 2, py + 5, 0.8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 2, py + 5, 0.8, 0, Math.PI * 2);
ctx.fill();
// Arms
ctx.fillStyle = color;
ctx.fillRect(px + 2, py + 10, TILE - 4, 2);
// Sword
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(px + TILE - 4, py + 4, 2, 12);
}
function drawGoriya(ctx, px, py, enemy, isHit) {
const color = isHit ? '#FFFFFF' : (enemy.color || '#4169AA');
// Body
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(px + 3, py + 4, TILE - 6, TILE - 8, 3);
ctx.fill();
// Mask
ctx.fillStyle = enemy.colorLight || '#5F9EA0';
ctx.fillRect(px + TILE / 2 - 5, py + 6, 10, 8);
// Eyes
ctx.fillStyle = enemy.eyeColor || '#FFFF00';
ctx.fillRect(px + TILE / 2 - 3, py + 8, 2, 2);
ctx.fillRect(px + TILE / 2 + 1, py + 8, 2, 2);
// Boomerang
ctx.fillStyle = '#DAA520';
ctx.beginPath();
ctx.arc(px + TILE - 2, py + 10, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#B8860B';
ctx.fillRect(px + TILE - 4, py + 8, 2, 6);
}
function drawArmos(ctx, px, py, enemy, isHit) {
const color = isHit ? '#FFFFFF' : (enemy.color || '#DAA520');
const activated = enemy.activated || false;
// Statue body
ctx.fillStyle = activated ? '#FFD700' : color;
ctx.fillRect(px + 4, py + 2, TILE - 8, TILE - 4);
// Triangle eyes
ctx.fillStyle = activated ? '#FF0000' : '#B8860B';
ctx.beginPath();
ctx.moveTo(px + TILE / 2 - 4, py + 8);
ctx.lineTo(px + TILE / 2 - 2, py + 12);
ctx.lineTo(px + TILE / 2 - 6, py + 12);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(px + TILE / 2 + 4, py + 8);
ctx.lineTo(px + TILE / 2 + 2, py + 12);
ctx.lineTo(px + TILE / 2 + 6, py + 12);
ctx.closePath();
ctx.fill();
// Sword
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(px + TILE - 2, py + 4, 2, 10);
}
function drawLynel(ctx, px, py, enemy, isHit) {
const color = isHit ? '#FFFFFF' : (enemy.color || '#8B0000');
// Large body
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(px, py + 2, TILE, TILE - 4, 4);
ctx.fill();
// Mane
ctx.fillStyle = enemy.colorLight || '#CC2222';
ctx.fillRect(px + 2, py + 2, TILE - 4, 6);
// Eyes
ctx.fillStyle = enemy.eyeColor || '#FFD700';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 5, py + 8, 3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 5, py + 8, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(px + TILE / 2 - 5, py + 8, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(px + TILE / 2 + 5, py + 8, 1.5, 0, Math.PI * 2);
ctx.fill();
}
// Register draw functions
const ENEMY_DRAW_FUNCTIONS = {
slime: drawSlime,
keese: drawKeese,
octorok: drawOctorok,
moblin: drawMoblin,
stalfos: drawStalfos,
goriya: drawGoriya,
armos: drawArmos,
lynel: drawLynel,
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderEnemies, drawEnemy, drawSlime, drawKeese, drawOctorok, drawMoblin, drawStalfos, drawGoriya, drawArmos, drawLynel };
}

View File

@ -0,0 +1,199 @@
'use strict';
// === WEAPON DRAWING ===
function drawWeaponHeld(weapon, px, py, dir) {
const def = WEAPONS.find(w => w.name === weapon.name || w.id === weapon.id);
if (!def || !def.drawFunction) return;
const drawFn = window[def.drawFunction];
if (typeof drawFn !== 'function') return;
drawFn(ctx, px, py, dir);
}
function drawWeaponInHUD(weapon, x, y) {
const def = WEAPONS.find(w => w.name === weapon.name || w.id === weapon.id);
if (!def || !drawWeaponIcon) return;
drawWeaponIcon(ctx, def, x, y);
}
// Sword
function drawSwordHeld(ctx, px, py, dir) {
ctx.fillStyle = COLORS.swordBlade;
const sx = px + TILE / 2 + DIRS[dir].x * 10;
const sy = py + TILE / 2 + DIRS[dir].y * 10;
ctx.fillRect(sx - 2, sy - 2, 4, 4);
ctx.fillStyle = COLORS.swordHandle;
const hx = px + TILE / 2 + DIRS[dir].x * 5;
const hy = py + TILE / 2 + DIRS[dir].y * 5;
ctx.fillRect(hx - 1.5, hy - 1.5, 3, 3);
}
function drawMasterSwordHeld(ctx, px, py, dir) {
ctx.fillStyle = '#E8E8FF';
const sx = px + TILE / 2 + DIRS[dir].x * 12;
const sy = py + TILE / 2 + DIRS[dir].y * 12;
ctx.fillRect(sx - 2, sy - 2, 4, 4);
// Aura
ctx.fillStyle = 'rgba(100,100,255,0.3)';
ctx.beginPath();
ctx.arc(sx, sy, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#DAA520';
const hx = px + TILE / 2 + DIRS[dir].x * 5;
const hy = py + TILE / 2 + DIRS[dir].y * 5;
ctx.fillRect(hx - 2, hy - 2, 4, 4);
}
// Bow
function drawBowHeld(ctx, px, py, dir) {
ctx.fillStyle = '#8B4513';
const bx = px + TILE / 2 + DIRS[dir].x * 6;
const by = py + TILE / 2 + DIRS[dir].y * 6;
ctx.beginPath();
if (dir === 0 || dir === 1) {
ctx.arc(bx, by, 6, dir === 0 ? -0.8 : 0.8, dir === 0 ? 0.8 : 2.4);
} else {
ctx.arc(bx, by, 6, dir === 2 ? 0.3 : 2.9, dir === 2 ? 1.1 : 3.7);
}
ctx.strokeStyle = '#8B4513';
ctx.lineWidth = 2;
ctx.stroke();
// String
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(bx + DIRS[dir].x * 2, by + DIRS[dir].y * 2);
ctx.lineTo(bx - DIRS[dir].x * 2, by - DIRS[dir].y * 2);
ctx.stroke();
}
// Boomerang
function drawBoomerangHeld(ctx, px, py, dir) {
ctx.fillStyle = '#FFD700';
const bx = px + TILE / 2 + DIRS[dir].x * 8;
const by = py + TILE / 2 + DIRS[dir].y * 8;
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(bx + DIRS[dir].x * 4, by + DIRS[dir].y * 2);
ctx.lineTo(bx + DIRS[dir].x * 2, by + DIRS[dir].y * 4);
ctx.closePath();
ctx.fill();
}
// Bombs
function drawBombHeld(ctx, px, py, dir) {
ctx.fillStyle = '#333';
const bx = px + TILE / 2 + DIRS[dir].x * 6;
const by = py + TILE / 2 + DIRS[dir].y * 6;
ctx.beginPath();
ctx.arc(bx, by, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FF0000';
ctx.fillRect(bx - 1, by - 6, 2, 3);
}
// Hammer
function drawHammerHeld(ctx, px, py, dir) {
ctx.fillStyle = '#8B4513';
const hx = px + TILE / 2 + DIRS[dir].x * 8;
const hy = py + TILE / 2 + DIRS[dir].y * 8;
ctx.fillRect(hx - 1, hy - 1, 3, 10);
ctx.fillStyle = '#666';
ctx.fillRect(hx - 4, hy - 5, 8, 6);
}
// Fire Rod
function drawFireRodHeld(ctx, px, py, dir) {
ctx.fillStyle = '#8B4513';
const rx = px + TILE / 2 + DIRS[dir].x * 6;
const ry = py + TILE / 2 + DIRS[dir].y * 6;
ctx.fillRect(rx - 1, ry - 1, 3, 8);
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(rx + DIRS[dir].x * 2, ry + DIRS[dir].y * 2, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FF4500';
ctx.beginPath();
ctx.arc(rx + DIRS[dir].x * 2, ry + DIRS[dir].y * 2, 2, 0, Math.PI * 2);
ctx.fill();
}
// Ice Rod
function drawIceRodHeld(ctx, px, py, dir) {
ctx.fillStyle = '#8B4513';
const rx = px + TILE / 2 + DIRS[dir].x * 6;
const ry = py + TILE / 2 + DIRS[dir].y * 6;
ctx.fillRect(rx - 1, ry - 1, 3, 8);
ctx.fillStyle = '#B0E0E6';
ctx.beginPath();
ctx.arc(rx + DIRS[dir].x * 2, ry + DIRS[dir].y * 2, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#00BFFF';
ctx.beginPath();
ctx.arc(rx + DIRS[dir].x * 2, ry + DIRS[dir].y * 2, 2, 0, Math.PI * 2);
ctx.fill();
}
// Slingshot
function drawSlingshotHeld(ctx, px, py, dir) {
ctx.fillStyle = '#8B4513';
const sx = px + TILE / 2 + DIRS[dir].x * 6;
const sy = py + TILE / 2 + DIRS[dir].y * 6;
ctx.beginPath();
ctx.moveTo(sx - 3, sy);
ctx.lineTo(sx, sy - 4);
ctx.lineTo(sx + 3, sy);
ctx.strokeStyle = '#8B4513';
ctx.lineWidth = 2;
ctx.stroke();
ctx.strokeStyle = '#DEB887';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(sx - 3, sy);
ctx.lineTo(sx + 3, sy);
ctx.stroke();
}
// Hookshot
function drawHookshotHeld(ctx, px, py, dir) {
ctx.fillStyle = '#C0C0C0';
const hx = px + TILE / 2 + DIRS[dir].x * 6;
const hy = py + TILE / 2 + DIRS[dir].y * 6;
ctx.fillRect(hx - 1, hy - 1, 3, 8);
ctx.fillStyle = '#808080';
ctx.beginPath();
ctx.arc(hx + DIRS[dir].x * 2, hy + DIRS[dir].y * 2, 3, 0, Math.PI);
ctx.strokeStyle = '#808080';
ctx.lineWidth = 2;
ctx.stroke();
}
// Register weapon draw functions
const WEAPON_DRAW = {
sword: { held: drawSwordHeld },
master_sword: { held: drawMasterSwordHeld },
bow: { held: drawBowHeld },
boomerang: { held: drawBoomerangHeld },
bomb: { held: drawBombHeld },
hammer: { held: drawHammerHeld },
fire_rod: { held: drawFireRodHeld },
ice_rod: { held: drawIceRodHeld },
slingshot: { held: drawSlingshotHeld },
hookshot: { held: drawHookshotHeld },
};
function drawWeaponIcon(ctx, weapon, x, y) {
const drawFn = WEAPON_DRAW[weapon.id];
if (drawFn && drawFn.held) {
ctx.save();
drawFn.held(ctx, x, y, 0);
ctx.restore();
} else {
ctx.fillStyle = weapon.color || '#C0C0C0';
ctx.fillRect(x - 3, y - 8, 6, 16);
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { drawWeaponHeld, drawWeaponInHUD, drawSwordHeld, drawMasterSwordHeld, drawBowHeld, drawBoomerangHeld, drawBombHeld, drawHammerHeld, drawFireRodHeld, drawIceRodHeld, drawSlingshotHeld, drawHookshotHeld, drawWeaponIcon };
}

77
src/rendering/effects.js vendored Normal file
View File

@ -0,0 +1,77 @@
'use strict';
// === PARTICLE EFFECTS ===
function spawnParticles(x, y, color, count) {
if (!gameState.particles) gameState.particles = [];
if (gameState.particles.length > MAX_PARTICLES) {
gameState.particles.splice(0, gameState.particles.length - MAX_PARTICLES + count);
}
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 3;
gameState.particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
gravity: 0.05,
color: color,
size: 1 + Math.random() * 2,
life: 300 + Math.random() * 400,
maxLife: 700,
maxAlpha: 0.8 + Math.random() * 0.2,
alpha: 1,
});
}
}
function spawnHitParticles(x, y) {
spawnParticles(x, y, '#FF0000', 4);
}
function spawnDefeatParticles(x, y) {
spawnParticles(x, y, '#FFD700', 12);
spawnParticles(x, y, '#FFFFFF', 6);
}
function spawnExplosionParticles(x, y) {
spawnParticles(x, y, '#FF4500', 15);
spawnParticles(x, y, '#FFD700', 8);
spawnParticles(x, y, '#FFFFFF', 4);
}
function renderParticles() {
if (!gameState.particles) return;
for (const p of gameState.particles) {
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// Draw projectiles
function renderProjectiles() {
if (!gameState.projectiles) return;
for (const p of gameState.projectiles) {
ctx.fillStyle = p.color || '#FFFF00';
ctx.beginPath();
ctx.arc(p.x * TILE + TILE / 2, p.y * TILE + TILE / 2, 4, 0, Math.PI * 2);
ctx.fill();
// Trail
ctx.fillStyle = (p.color || '#FFFF00') + '40';
ctx.beginPath();
ctx.arc(p.x * TILE + TILE / 2 - p.dir.x * 6, p.y * TILE + TILE / 2 - p.dir.y * 6, 3, 0, Math.PI * 2);
ctx.fill();
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { spawnParticles, spawnHitParticles, spawnDefeatParticles, spawnExplosionParticles, renderParticles, renderProjectiles };
}

107
src/rendering/hud.js Normal file
View File

@ -0,0 +1,107 @@
'use strict';
// === HUD RENDERING ===
function renderHUD() {
// Hearts
const totalHearts = Math.ceil(gameState.player.maxHp / 2);
const fullHearts = Math.floor(gameState.player.hp / 2);
const halfHeart = gameState.player.hp % 2 === 1;
for (let i = 0; i < totalHearts; i++) {
const hx = 12 + i * 22;
const hy = 12;
drawHeart(ctx, hx, hy, 14, i < fullHearts);
if (i === fullHearts && halfHeart) {
ctx.save();
ctx.beginPath();
ctx.rect(hx - 7, hy - 7, 14, 14);
ctx.clip();
drawHeart(ctx, hx, hy, 14, true);
ctx.restore();
}
}
// Active weapon icon
if (gameState.player.activeWeapon) {
drawWeaponIcon(ctx, gameState.player.activeWeapon, CANVAS_W - 24, 14);
}
// Ammo counts
if (gameState.player.arrows > 0) {
ctx.fillStyle = '#8B4513';
ctx.fillRect(CANVAS_W - 50, 10, 2, 12);
ctx.fillStyle = '#808080';
ctx.fillRect(CANVAS_W - 52, 10, 6, 3);
ctx.fillStyle = '#FFFFFF';
ctx.font = '9px monospace';
ctx.textAlign = 'right';
ctx.fillText(gameState.player.arrows, CANVAS_W - 54, 22);
ctx.textAlign = 'left';
}
if (gameState.player.bombs > 0) {
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(CANVAS_W - 38, 14, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFFFFF';
ctx.font = '9px monospace';
ctx.textAlign = 'right';
ctx.fillText(gameState.player.bombs, CANVAS_W - 42, 22);
ctx.textAlign = 'left';
}
if (gameState.player.keys > 0) {
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(CANVAS_W - 30, 30, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FFFFFF';
ctx.font = '9px monospace';
ctx.textAlign = 'right';
ctx.fillText(gameState.player.keys, CANVAS_W - 34, 38);
ctx.textAlign = 'left';
}
// Weapon inventory (bottom right)
if (gameState.player.weapons.length > 1) {
let wx = CANVAS_W - 16;
for (let i = gameState.player.weapons.length - 1; i >= 0; i--) {
const w = gameState.player.weapons[i];
const isActive = w === gameState.player.activeWeapon;
ctx.fillStyle = isActive ? '#FFD700' : '#555';
ctx.fillRect(wx - 10, CANVAS_H - 20, 12, 16);
ctx.strokeStyle = isActive ? '#FFD700' : '#333';
ctx.lineWidth = isActive ? 1.5 : 0.5;
ctx.strokeRect(wx - 10, CANVAS_H - 20, 12, 16);
drawWeaponIcon(ctx, w, wx - 4, CANVAS_H - 12);
wx -= 14;
}
}
// Room name / dungeon info
const dungeon = DUNGEONS[gameState.currentDungeon];
if (dungeon) {
ctx.fillStyle = '#CCCCCC';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText(`${dungeon.name} - Room ${gameState.currentRoomIdx + 1}/${gameState.dungeonRooms ? gameState.dungeonRooms.length : '?'}`, CANVAS_W / 2, CANVAS_H - 8);
ctx.textAlign = 'left';
}
// Controls hint (fades out)
if (gameState.messageTimer <= 0 && gameState.currentRoomIdx === 0) {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(CANVAS_W / 2 - 100, CANVAS_H - 28, 200, 18);
ctx.fillStyle = '#888';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('Arrows/WASD: Move Space: Attack E: Interact X: Switch', CANVAS_W / 2, CANVAS_H - 15);
ctx.textAlign = 'left';
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderHUD };
}

559
src/rendering/renderer.js Normal file
View File

@ -0,0 +1,559 @@
'use strict';
// === MAIN RENDERER ===
// Draws the game world: floor, walls, torches, doors, etc.
function renderFloor(theme) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y][x] === TILE_TYPES.FLOOR) {
const shade = ((x + y) % 2 === 0) ? (theme.floorColor || COLORS.floor) : (theme.floorDarkColor || COLORS.floorDark);
ctx.fillStyle = shade;
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
}
}
}
}
function renderWalls(theme) {
const wc = getWallColors(theme);
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y][x] === TILE_TYPES.WALL) {
const px = x * TILE;
const py = y * TILE;
ctx.fillStyle = wc.wall;
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = wc.top;
ctx.fillRect(px, py, TILE, 3);
ctx.fillStyle = wc.front;
ctx.fillRect(px, py + 3, TILE, TILE - 6);
ctx.fillStyle = wc.bottom;
ctx.fillRect(px, py + TILE - 3, TILE, 3);
ctx.strokeStyle = theme.wallBorderColor || '#404040';
ctx.lineWidth = 0.5;
ctx.strokeRect(px + 0.5, py + 0.5, TILE - 1, TILE - 1);
}
}
}
}
function renderTorches(theme) {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y][x] === TILE_TYPES.TORCH) {
const cx = x * TILE + TILE / 2;
const cy = y * TILE + TILE / 2;
// Glow
ctx.fillStyle = theme.torchGlow || COLORS.torchGlow;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 1.2, 0, Math.PI * 2);
ctx.fill();
// Floor tile under torch
ctx.fillStyle = theme.floorColor || COLORS.floor;
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
// Stick
ctx.fillStyle = theme.torchStickColor || COLORS.torchStick;
ctx.fillRect(cx - 2, cy + 2, 4, TILE * 0.4);
// Flame
ctx.fillStyle = theme.torchFlameColor || COLORS.torchFlame;
ctx.beginPath();
ctx.ellipse(cx, cy - 4, 4, 6, 0, 0, Math.PI * 2);
ctx.fill();
// Flame core
ctx.fillStyle = theme.torchCoreColor || '#FFFF00';
ctx.beginPath();
ctx.ellipse(cx, cy - 3, 2, 3, 0, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
function renderDoors(theme) {
for (const door of gameState.doors) {
const px = door.x * TILE;
const py = door.y * TILE;
ctx.fillStyle = COLORS.doorFrame;
ctx.fillRect(px - 1, py - 1, TILE + 2, TILE + 2);
if (door.opened) {
ctx.fillStyle = (theme && theme.floorColor) || COLORS.floor;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
} else {
ctx.fillStyle = COLORS.door;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.strokeStyle = '#8B7355';
ctx.lineWidth = 1;
ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.fillStyle = COLORS.doorKnob;
ctx.beginPath();
const knobX = door.dir === 3 ? px + TILE - 7 : px + 7;
ctx.arc(knobX, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
}
}
}
function renderSoftwalls() {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y][x] === TILE_TYPES.SOFTWALL) {
const px = x * TILE;
const py = y * TILE;
ctx.fillStyle = COLORS.softwall;
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = COLORS.softwallDark;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
// Crack pattern
ctx.strokeStyle = '#3A2818';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px + 4, py + 4);
ctx.lineTo(px + TILE / 2, py + TILE / 2);
ctx.lineTo(px + TILE - 4, py + 6);
ctx.stroke();
}
}
}
}
function renderLava() {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y][x] === TILE_TYPES.LAVA) {
const px = x * TILE;
const py = y * TILE;
const t = Date.now() * 0.003;
ctx.fillStyle = COLORS.lava;
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = COLORS.lavaLight;
const offset = Math.sin(t + x * 0.5 + y * 0.3) * 3;
ctx.fillRect(px + 4 + offset, py + 6, TILE - 8, TILE - 10);
// Bubbles
ctx.fillStyle = '#FF6347';
ctx.beginPath();
const bx = px + TILE / 2 + Math.sin(t * 1.5 + x) * 4;
const by = py + TILE / 2 + Math.cos(t * 1.2 + y) * 3;
ctx.arc(bx, by, 2, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
function renderWater() {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (gameState.layout[y][x] === TILE_TYPES.WATER) {
const px = x * TILE;
const py = y * TILE;
const t = Date.now() * 0.002;
ctx.fillStyle = COLORS.water;
ctx.fillRect(px, py, TILE, TILE);
ctx.fillStyle = COLORS.waterLight;
const offset = Math.sin(t + x * 0.8 + y * 0.4) * 2;
ctx.fillRect(px + 3 + offset, py + 8, TILE - 6, 3);
ctx.fillRect(px + 6 - offset, py + 16, TILE - 10, 2);
}
}
}
}
function renderTraps() {
for (const trap of gameState.traps) {
if (trap.triggered) continue;
const px = trap.x * TILE;
const py = trap.y * TILE;
// Hidden trap - draw subtle indicator
ctx.fillStyle = (trap.color || COLORS.trap) + '40';
ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8);
// Spike pattern
ctx.fillStyle = trap.color || COLORS.trap;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
ctx.beginPath();
const sx = px + 6 + i * 9;
const sy = py + TILE - 6 - j * 9;
ctx.moveTo(sx, sy - 4);
ctx.lineTo(sx - 2, sy);
ctx.lineTo(sx + 2, sy);
ctx.closePath();
ctx.fill();
}
}
}
}
function renderLevers() {
for (const lever of gameState.levers) {
if (lever.activated) continue;
const px = lever.x * TILE;
const py = lever.y * TILE;
// Base
ctx.fillStyle = '#666';
ctx.fillRect(px + TILE / 2 - 2, py + TILE / 2 - 2, 4, 4);
// Handle
ctx.fillStyle = COLORS.lever;
ctx.fillRect(px + TILE / 2 - 1, py + 4, 3, TILE / 2 - 4);
// Knob
ctx.fillStyle = '#DAA520';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + 4, 3, 0, Math.PI * 2);
ctx.fill();
}
}
function renderSwitches() {
for (const sw of gameState.switches) {
const px = sw.x * TILE;
const py = sw.y * TILE;
if (sw.activated) {
ctx.fillStyle = COLORS.switchFloor;
ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8);
ctx.fillStyle = '#00FF00';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillStyle = COLORS.switchFloor;
ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8);
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
}
}
}
function renderTargets() {
for (const target of gameState.targets) {
const cx = target.x * TILE + TILE / 2;
const cy = target.y * TILE + TILE / 2;
const occupied = gameState.rocks.some(r => r.x === target.x && r.y === target.y);
ctx.fillStyle = COLORS.targetGlow;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = occupied ? '#00FF00' : COLORS.target;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 0.3, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = occupied ? '#00AA00' : '#CC9900';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, TILE * 0.3, 0, Math.PI * 2);
ctx.stroke();
}
}
function renderRocks() {
for (const rock of gameState.rocks) {
const px = rock.x * TILE;
const py = rock.y * TILE;
const inset = 3;
ctx.fillStyle = COLORS.rock;
ctx.beginPath();
ctx.roundRect(px + inset, py + inset, TILE - inset * 2, TILE - inset * 2, 4);
ctx.fill();
ctx.fillStyle = COLORS.rockLight;
ctx.beginPath();
ctx.roundRect(px + inset + 1, py + inset + 1, (TILE - inset * 2) * 0.5, (TILE - inset * 2) * 0.4, 2);
ctx.fill();
ctx.fillStyle = COLORS.rockDark;
ctx.beginPath();
ctx.roundRect(px + inset + (TILE - inset * 2) * 0.4, py + inset + (TILE - inset * 2) * 0.5, (TILE - inset * 2) * 0.4, (TILE - inset * 2) * 0.4, 2);
ctx.fill();
ctx.strokeStyle = '#555555';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(px + inset, py + inset, TILE - inset * 2, TILE - inset * 2, 4);
ctx.stroke();
}
}
function renderChests() {
for (const chest of gameState.chests) {
const px = chest.x * TILE;
const py = chest.y * TILE;
if (chest.opened) {
ctx.fillStyle = COLORS.chestOpen;
ctx.fillRect(px + 2, py + 10, TILE - 4, TILE - 12);
ctx.fillStyle = COLORS.chestGold;
ctx.fillRect(px + 4, py + 12, TILE - 8, TILE - 14);
ctx.fillStyle = COLORS.chestClosed;
ctx.fillRect(px + 2, py + 2, TILE - 4, 6);
ctx.fillStyle = COLORS.chestGold;
ctx.fillRect(px + 4, py + 3, TILE - 8, 4);
} else {
ctx.fillStyle = COLORS.chestClosed;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.fillStyle = COLORS.chestGold;
ctx.fillRect(px + 4, py + 4, TILE - 8, TILE - 8);
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#B8860B';
ctx.lineWidth = 1;
ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4);
}
}
}
function renderItems() {
for (const item of gameState.floorItems) {
if (item.collected) continue;
const px = item.x * TILE;
const py = item.y * TILE;
const def = ITEMS.find(i => i.id === item.id);
if (!def) continue;
const bob = Math.sin(Date.now() * 0.005 + item.x + item.y) * 2;
if (item.id === 'heart_piece') {
drawHeart(ctx, px + TILE / 2, py + TILE / 2 + bob, 10, true);
} else if (item.id.startsWith('potion')) {
ctx.fillStyle = def.color || '#FF0000';
ctx.fillRect(px + 10, py + 8 + bob, 12, 16);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(px + 12, py + 6 + bob, 8, 4);
} else if (item.id === 'arrows') {
ctx.fillStyle = '#8B4513';
ctx.fillRect(px + 12, py + 4 + bob, 2, 24);
ctx.fillStyle = '#808080';
ctx.fillRect(px + 10, py + 4 + bob, 6, 4);
} else if (item.id === 'bombs') {
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2 + bob, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FF0000';
ctx.fillRect(px + TILE / 2 - 1, py + 2 + bob, 2, 4);
} else if (item.id === 'key') {
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(px + 10, py + 10 + bob, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(px + 14, py + 9 + bob, 10, 3);
ctx.fillRect(px + 20, py + 9 + bob, 3, 5);
} else {
ctx.fillStyle = def.color || '#FFD700';
ctx.beginPath();
ctx.arc(px + TILE / 2, py + TILE / 2 + bob, 5, 0, Math.PI * 2);
ctx.fill();
}
}
}
// Draw exit door
function renderExit() {
if (!gameState.exit) return;
const px = gameState.exit.x * TILE;
const py = gameState.exit.y * TILE;
ctx.fillStyle = COLORS.doorFrame;
ctx.fillRect(px - 1, py - 1, TILE + 2, TILE + 2);
if (gameState.exitOpen) {
ctx.fillStyle = '#1A1A1A';
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
} else {
ctx.fillStyle = COLORS.door;
ctx.fillRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.strokeStyle = '#8B7355';
ctx.lineWidth = 1;
ctx.strokeRect(px + 2, py + 2, TILE - 4, TILE - 4);
ctx.fillStyle = COLORS.doorKnob;
ctx.beginPath();
const knobX = gameState.exit.dir === 3 ? px + TILE - 7 : px + 7;
ctx.arc(knobX, py + TILE / 2, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'center';
const arrowX = gameState.exit.dir === 3 ? px + TILE + 10 : px - 10;
ctx.fillText(
gameState.exit.dir === 3 ? '\u2192' :
gameState.exit.dir === 2 ? '\u2190' :
gameState.exit.dir === 0 ? '\u2193' : '\u2191',
arrowX, py + TILE / 2 + 4
);
ctx.textAlign = 'left';
}
// Draw player character
function renderPlayer() {
const p = gameState.player;
const px = p.x * TILE;
const py = p.y * TILE;
const isHit = p.hitTimer > 0;
const isInvincible = p.invincibleTimer > 0;
ctx.save();
if (isHit) {
ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.03) * 0.5;
}
if (isInvincible) {
ctx.globalAlpha = Math.sin(Date.now() * 0.01) > 0 ? 1 : 0.3;
}
const bodyColor = isHit ? '#FF6666' : COLORS.playerBody;
// Body
ctx.fillStyle = bodyColor;
ctx.fillRect(px + 6, py + 14, TILE - 12, TILE - 16);
// Tunic detail
ctx.fillStyle = COLORS.playerTunic;
ctx.fillRect(px + 8, py + 16, TILE - 16, TILE - 20);
// Head
ctx.fillStyle = COLORS.playerHead;
ctx.beginPath();
ctx.arc(px + TILE / 2, py + 10, 7, 0, Math.PI * 2);
ctx.fill();
// Hair
ctx.fillStyle = COLORS.playerHair;
ctx.beginPath();
ctx.arc(px + TILE / 2, py + 8, 7, Math.PI, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = COLORS.playerEyes;
const eyeOffX = DIRS[p.dir].x * 2;
const eyeOffY = DIRS[p.dir].y * 2;
ctx.fillRect(px + TILE / 2 - 3 + eyeOffX, py + 9 + eyeOffY, 2, 2);
ctx.fillRect(px + TILE / 2 + 1 + eyeOffX, py + 9 + eyeOffY, 2, 2);
// Active weapon indicator
if (p.activeWeapon) {
drawWeaponHeld(p.activeWeapon, px, py, p.dir);
} else if (p.weapons.length > 0) {
drawWeaponHeld(p.weapons[0], px, py, p.dir);
}
ctx.restore();
}
// Draw attack animation
function renderAttackAnim() {
const p = gameState.player;
if (p.attackAnim <= 0 || !p.activeWeapon) return;
const px = p.x * TILE;
const py = p.y * TILE;
const dir = DIRS[p.dir];
ctx.save();
ctx.globalAlpha = p.attackAnim / (p.activeWeapon.animDuration || ATTACK_ANIM);
const ax = px + TILE / 2 + dir.x * TILE * 0.6;
const ay = py + TILE / 2 + dir.y * TILE * 0.6;
// Weapon-specific attack visual
if (p.activeWeapon.type === 'melee') {
ctx.fillStyle = p.activeWeapon.attackColor || COLORS.swordBlade;
if (dir.x !== 0) {
ctx.fillRect(ax - 3, ay - TILE * 0.4, 6, TILE * 0.8);
} else {
ctx.fillRect(ax - TILE * 0.4, ay - 3, TILE * 0.8, 6);
}
}
// Slash effect
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 2;
ctx.globalAlpha = p.attackAnim / (p.activeWeapon.animDuration || ATTACK_ANIM) * 0.7;
if (dir.x !== 0) {
ctx.beginPath();
ctx.moveTo(ax, ay - TILE * 0.5);
ctx.lineTo(ax + dir.x * 8, ay);
ctx.lineTo(ax, ay + TILE * 0.5);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(ax - TILE * 0.5, ay);
ctx.lineTo(ax, ay + dir.y * 8);
ctx.lineTo(ax + TILE * 0.5, ay);
ctx.stroke();
}
ctx.restore();
}
// Draw messages
function renderMessages() {
if (gameState.messageTimer <= 0) return;
ctx.save();
ctx.globalAlpha = Math.min(1, gameState.messageTimer / 300);
ctx.fillStyle = 'rgba(0,0,0,0.7)';
const textW = ctx.measureText(gameState.message).width;
const boxW = textW + 20;
const boxX = (CANVAS_W - boxW) / 2;
const boxY = CANVAS_H / 2 - 30;
ctx.fillRect(boxX, boxY, boxW, 24);
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 1;
ctx.strokeRect(boxX, boxY, boxW, 24);
ctx.fillStyle = COLORS.msgText;
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText(gameState.message, CANVAS_W / 2, boxY + 17);
ctx.textAlign = 'left';
ctx.restore();
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { renderFloor, renderWalls, renderTorches, renderDoors, renderSoftwalls, renderLava, renderWater, renderTraps, renderLevers, renderSwitches, renderTargets, renderRocks, renderChests, renderItems, renderExit, renderPlayer, renderAttackAnim, renderMessages };
}

120
src/save/save.js Normal file
View File

@ -0,0 +1,120 @@
'use strict';
// === SAVE SYSTEM ===
const SAVE_KEY = 'zelda_dungeon_save_v1';
let saveData = {
completedDungeons: [],
weapons: [],
heartPieces: 0,
totalRoomsCleared: 0,
totalEnemiesDefeated: 0,
dungeonsBeaten: 0,
playTime: 0,
};
function loadSaveData() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (raw) {
saveData = JSON.parse(raw);
console.log('Save loaded:', saveData);
}
} catch (e) {
console.warn('Failed to load save:', e);
}
// Check for electron API (for Electron builds)
if (window.electronAPI && window.electronAPI.loadSave) {
try {
const electronSave = window.electronAPI.loadSave();
if (electronSave) saveData = electronSave;
} catch (e) { /* fallback to localStorage */ }
}
}
function saveProgress() {
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
console.log('Save written:', saveData);
} catch (e) {
console.warn('Failed to save:', e);
}
if (window.electronAPI && window.electronAPI.saveData) {
try {
window.electronAPI.saveData(saveData);
} catch (e) { /* fallback to localStorage */ }
}
}
function markDungeonComplete(dungeonIdx) {
if (!saveData.completedDungeons.includes(dungeonIdx)) {
saveData.completedDungeons.push(dungeonIdx);
saveData.dungeonsBeaten++;
}
}
function addWeapon(name) {
if (!saveData.weapons.includes(name)) {
saveData.weapons.push(name);
}
}
function addHeartPiece() {
saveData.heartPieces++;
// Every 4 heart pieces = 1 new heart (2 HP)
if (saveData.heartPieces % 4 === 0) {
if (gameState && gameState.player) {
if (gameState.player.maxHp < MAX_HEARTS * 2) {
gameState.player.maxHp += 2;
gameState.player.hp = gameState.player.maxHp;
showMessage('Heart Container! Max HP increased!');
}
}
}
}
function addStats(rooms, enemies) {
saveData.totalRoomsCleared += rooms;
saveData.totalEnemiesDefeated += enemies;
}
function isDungeonCompleted(dungeonIdx) {
return saveData.completedDungeons.includes(dungeonIdx);
}
function isDungeonUnlocked(dungeonIdx) {
if (dungeonIdx === 0) return true;
return saveData.completedDungeons.includes(dungeonIdx - 1);
}
function getSaveSummary() {
return {
dungeonsCompleted: saveData.completedDungeons.length,
dungeonsBeaten: saveData.dungeonsBeaten,
weapons: saveData.weapons.length,
heartPieces: saveData.heartPieces,
roomsCleared: saveData.totalRoomsCleared,
enemiesDefeated: saveData.totalEnemiesDefeated,
};
}
function clearSave() {
saveData = {
completedDungeons: [],
weapons: [],
heartPieces: 0,
totalRoomsCleared: 0,
totalEnemiesDefeated: 0,
dungeonsBeaten: 0,
playTime: 0,
};
localStorage.removeItem(SAVE_KEY);
console.log('Save cleared');
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { saveData, loadSaveData, saveProgress, markDungeonComplete,
addWeapon, addHeartPiece, addStats, isDungeonCompleted, isDungeonUnlocked,
getSaveSummary, clearSave };
}

110
test-full-node.js Normal file
View File

@ -0,0 +1,110 @@
// Full browser simulation test
const fs = require('fs');
const vm = require('vm');
// Create browser context
const context = {
window: null,
document: {
currentScript: { src: '', getAttribute: function(a) { return null; } },
getElementsByTagName: function() { return []; },
},
location: { origin: 'http://localhost:3000' },
console: console,
Math: Math, JSON: JSON, Array: Array, Object: Object,
String: String, Number: Number, Function: Function,
Error: Error, RegExp: RegExp, Set: Set, Map: Map,
Promise: Promise, setTimeout: setTimeout,
localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} },
};
context.window = context;
vm.createContext(context);
var results = [];
function assert(name, cond) { results.push((cond ? 'PASS' : 'FAIL') + ': ' + name); }
function setScript(src) { context.document.currentScript = { src: 'http://localhost:3000' + src, getAttribute: () => null }; }
function load(name, code) {
setScript(name);
try {
vm.runInContext(code, context);
console.log(' Loaded:', name);
} catch(e) {
console.error(' ERROR in', name + ':', e.message);
}
}
// Load all scripts in order
load('/src/browser-polyfill.js', fs.readFileSync('src/browser-polyfill.js', 'utf8'));
load('/src/core/constants.js', fs.readFileSync('src/core/constants.js', 'utf8'));
load('/src/core/rng.js', fs.readFileSync('src/core/rng.js', 'utf8'));
load('/src/core/utils.js', fs.readFileSync('src/core/utils.js', 'utf8'));
load('/src/data/themes.js', fs.readFileSync('src/data/themes.js', 'utf8'));
load('/src/data/dungeons.js', fs.readFileSync('src/data/dungeons.js', 'utf8'));
load('/src/data/enemies.js', fs.readFileSync('src/data/enemies.js', 'utf8'));
load('/src/data/weapons.js', fs.readFileSync('src/data/weapons.js', 'utf8'));
load('/src/data/bosses.js', fs.readFileSync('src/data/bosses.js', 'utf8'));
load('/src/data/items.js', fs.readFileSync('src/data/items.js', 'utf8'));
load('/src/generator/rooms.js', fs.readFileSync('src/generator/rooms.js', 'utf8'));
load('/src/generator/puzzles.js', fs.readFileSync('src/generator/puzzles.js', 'utf8'));
load('/src/generator/placement.js', fs.readFileSync('src/generator/placement.js', 'utf8'));
load('/src/generator/dungeon.js', fs.readFileSync('src/generator/dungeon.js', 'utf8'));
// Core tests
assert('module exists', typeof context.module !== 'undefined');
assert('require is function', typeof context.require === 'function');
assert('TILE=32', context.window.TILE === 32);
assert('COLS=32', context.window.COLS === 32);
assert('ROWS=24', context.window.ROWS === 24);
assert('TILE_TYPES object', context.window.TILE_TYPES && typeof context.window.TILE_TYPES === 'object');
assert('COLORS object', context.window.COLORS && typeof context.window.COLORS === 'object');
assert('createRNG function', typeof context.createRNG === 'function');
assert('createEmptyLayout function', typeof context.createEmptyLayout === 'function');
assert('createBorderLayout function', typeof context.createBorderLayout === 'function');
assert('isPassable function', typeof context.isPassable === 'function');
// Data tests
assert('THEMES object', context.window.THEMES && typeof context.window.THEMES === 'object');
assert('DUNGEONS array', Array.isArray(context.window.DUNGEONS));
assert('DUNGEONS count=100', context.window.DUNGEONS && context.window.DUNGEONS.length === 100);
assert('ENEMY_TYPES array', Array.isArray(context.window.ENEMY_TYPES));
assert('ENEMY_TYPES count=22', context.window.ENEMY_TYPES && context.window.ENEMY_TYPES.length === 22);
assert('WEAPONS array', Array.isArray(context.window.WEAPONS));
assert('WEAPONS count=12', context.window.WEAPONS && context.window.WEAPONS.length === 12);
assert('BOSS_TYPES array', Array.isArray(context.window.BOSS_TYPES));
assert('BOSS_TYPES count=100', context.window.BOSS_TYPES && context.window.BOSS_TYPES.length === 100);
assert('ITEMS array', Array.isArray(context.window.ITEMS));
assert('getItem function', typeof context.getItem === 'function');
assert('getEnemyType function', typeof context.getEnemyType === 'function');
assert('getBossType function', typeof context.getBossType === 'function');
assert('getWeapon function', typeof context.getWeapon === 'function');
// Generator tests
assert('generateDungeon function', typeof context.generateDungeon === 'function');
assert('generateRoomLayout function', typeof context.generateRoomLayout === 'function');
assert('generatePuzzles function', typeof context.generatePuzzles === 'function');
assert('generatePlacement function', typeof context.generatePlacement === 'function');
// Require resolution tests
context.document.currentScript = { src: 'http://localhost:3000/src/generator/placement.js', getAttribute: () => null };
var roomsMod = context.require('./rooms');
assert('require ./rooms generateRoomLayout', roomsMod.generateRoomLayout !== undefined);
var enemiesMod = context.require('../data/enemies');
assert('require ../data/enemies ENEMY_TYPES', Array.isArray(enemiesMod.ENEMY_TYPES));
// Dungeon generation test
var dungeon0 = context.window.DUNGEONS[0];
var rooms = context.generateDungeon(dungeon0);
assert('generateDungeon returns rooms', Array.isArray(rooms) && rooms.length > 0);
assert('dungeon #0 rooms ~20', rooms.length >= 17 && rooms.length <= 25);
assert('entrance assigned', rooms[0].type === 'entrance');
assert('boss assigned', rooms[rooms.length - 1].type === 'boss');
assert('all rooms connected', rooms.every(function(r) { return r.connections && r.connections.length >= 1; }));
assert('rooms have tiles', rooms.every(function(r) { return Array.isArray(r.tiles); }));
console.log('\n=== Results ===');
results.forEach(r => console.log(r));
var passed = results.filter(r => r.startsWith('PASS')).length;
var failed = results.filter(r => r.startsWith('FAIL')).length;
console.log('\n' + passed + ' passed, ' + failed + ' failed');
process.exit(failed > 0 ? 1 : 0);

117
test-polyfill-node.js Normal file
View File

@ -0,0 +1,117 @@
// Browser simulation test for polyfill
const fs = require('fs');
const path = require('path');
const vm = require('vm');
// Create a realistic browser context
const context = {
// Global browser objects
window: null, // will be set below
document: {
currentScript: { src: '', getAttribute: function(a) { return null; } },
getElementsByTagName: function() { return []; },
},
location: { origin: 'http://localhost:3000' },
console: console,
Math: Math,
JSON: JSON,
Array: Array,
Object: Object,
String: String,
Number: Number,
Function: Function,
Error: Error,
RegExp: RegExp,
Set: Set,
Map: Map,
Promise: Promise,
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearInterval: clearInterval,
// Browser APIs used by the code
localStorage: {
getItem: function() { return null; },
setItem: function() {},
removeItem: function() {},
},
};
// window IS the global object in browsers
context.window = context;
vm.createContext(context);
const polyfillCode = fs.readFileSync('src/browser-polyfill.js', 'utf8');
const constantsCode = fs.readFileSync('src/core/constants.js', 'utf8');
const rngCode = fs.readFileSync('src/core/rng.js', 'utf8');
const utilsCode = fs.readFileSync('src/core/utils.js', 'utf8');
var results = [];
function assert(name, condition) {
results.push((condition ? 'PASS' : 'FAIL') + ': ' + name);
}
// Track which script is loading
function setScript(src) {
context.document.currentScript = { src: 'http://localhost:3000' + src, getAttribute: function(a) { return null; } };
}
// Load polyfill
try {
vm.runInContext(polyfillCode, context);
assert('module exists', typeof context.module !== 'undefined');
assert('require is function', typeof context.require === 'function');
} catch(e) { console.error('Polyfill error:', e.message); }
// Load constants.js
setScript('/src/core/constants.js');
try {
vm.runInContext(constantsCode, context);
assert('TILE on window', context.window.TILE === 32);
assert('COLS on window', context.window.COLS === 32);
assert('ROWS on window', context.window.ROWS === 24);
assert('TILE_TYPES is object', context.window.TILE_TYPES && typeof context.window.TILE_TYPES === 'object');
assert('TILE_TYPES.FLOOR=0', context.window.TILE_TYPES && context.window.TILE_TYPES.FLOOR === 0);
assert('TILE_TYPES.WALL=1', context.window.TILE_TYPES && context.window.TILE_TYPES.WALL === 1);
assert('T_FLOOR', context.T_FLOOR === 0);
assert('T_WALL', context.T_WALL === 1);
assert('COLORS exists', context.COLORS && typeof context.COLORS === 'object');
assert('SCREEN_TITLE', context.SCREEN_TITLE === 'title');
} catch(e) { console.error('constants error:', e.message); }
// Load rng.js
setScript('/src/core/rng.js');
try {
vm.runInContext(rngCode, context);
assert('createRNG is function', typeof context.createRNG === 'function');
assert('mulberry32 is function', typeof context.mulberry32 === 'function');
} catch(e) { console.error('rng error:', e.message); }
// Load utils.js (uses require('./constants'))
setScript('/src/core/utils.js');
try {
vm.runInContext(utilsCode, context);
assert('createEmptyLayout is function', typeof context.createEmptyLayout === 'function');
assert('createBorderLayout is function', typeof context.createBorderLayout === 'function');
assert('isPassable is function', typeof context.isPassable === 'function');
} catch(e) { console.error('utils error:', e.message); }
// Test require resolution
try {
context.document.currentScript = { src: 'http://localhost:3000/src/test.js', getAttribute: function() { return null; } };
var consts = context.require('./core/constants');
assert('require constants TILE=32', consts.TILE === 32);
assert('require constants COLS=32', consts.COLS === 32);
assert('require constants ROWS=24', consts.ROWS === 24);
assert('require constants TILE_TYPES', consts.TILE_TYPES && consts.TILE_TYPES.FLOOR === 0);
var rng = context.require('./core/rng');
assert('require createRNG', typeof rng.createRNG === 'function');
} catch(e) { console.error('require error:', e.message); }
console.log('\n=== Results ===');
results.forEach(r => console.log(r));
var passed = results.filter(r => r.startsWith('PASS')).length;
var failed = results.filter(r => r.startsWith('FAIL')).length;
console.log('\n' + passed + ' passed, ' + failed + ' failed');
process.exit(failed > 0 ? 1 : 0);

96
test-polyfill.html Normal file
View File

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Polyfill Test</title></head>
<body>
<pre id="output"></pre>
<script src="src/browser-polyfill.js"></script>
<script src="src/core/constants.js"></script>
<script src="src/core/rng.js"></script>
<script src="src/core/utils.js"></script>
<script src="src/data/themes.js"></script>
<script src="src/data/dungeons.js"></script>
<script src="src/data/enemies.js"></script>
<script src="src/data/weapons.js"></script>
<script src="src/data/bosses.js"></script>
<script src="src/data/items.js"></script>
<script src="src/generator/dungeon.js"></script>
<script src="src/generator/rooms.js"></script>
<script src="src/generator/puzzles.js"></script>
<script src="src/generator/placement.js"></script>
<script>
var output = document.getElementById('output');
var lines = [];
function assert(name, condition) {
var result = condition ? 'PASS' : 'FAIL';
lines.push(result + ': ' + name);
}
// Test polyfill created globals
assert('module exists', typeof module !== 'undefined');
assert('require exists', typeof require === 'function');
// Test constants.js exposed to window
assert('TILE on window', window.TILE === 32);
assert('COLS on window', window.COLS === 32);
assert('ROWS on window', window.ROWS === 24);
assert('TILE_TYPES exists', window.TILE_TYPES && typeof window.TILE_TYPES === 'object');
assert('TILE_TYPES.FLOOR', window.TILE_TYPES && window.TILE_TYPES.FLOOR === 0);
assert('TILE_TYPES.WALL', window.TILE_TYPES && window.TILE_TYPES.WALL === 1);
// Test backward-compat constants
assert('T_FLOOR', window.T_FLOOR === 0);
assert('T_WALL', window.T_WALL === 1);
// Test rng.js exposed to window
assert('createRNG exists', typeof createRNG === 'function');
assert('mulberry32 exists', typeof mulberry32 === 'function');
// Test utils.js require worked
assert('C from utils (via require)', typeof createEmptyLayout === 'function');
assert('createBorderLayout exists', typeof createBorderLayout === 'function');
assert('isPassable exists', typeof isPassable === 'function');
// Test data files exposed to window
assert('THEMES exists', window.THEMES && typeof window.THEMES === 'object');
assert('DUNGEONS exists', Array.isArray(window.DUNGEONS));
assert('DUNGEONS.length', window.DUNGEONS && window.DUNGEONS.length === 100);
assert('ENEMY_TYPES exists', Array.isArray(window.ENEMY_TYPES));
assert('ENEMY_TYPES.length', window.ENEMY_TYPES && window.ENEMY_TYPES.length === 22);
assert('WEAPONS exists', Array.isArray(window.WEAPONS));
assert('BOSS_TYPES exists', Array.isArray(window.BOSS_TYPES));
assert('ITEMS exists', Array.isArray(window.ITEMS));
// Test generator files
assert('generateDungeon exists', typeof generateDungeon === 'function');
assert('generateRoomLayout exists', typeof generateRoomLayout === 'function');
assert('generatePuzzles exists', typeof generatePuzzles === 'function');
assert('generatePlacement exists', typeof generatePlacement === 'function');
// Test require resolution
var constantsFromRequire = require('./core/constants');
assert('require constants TILE', constantsFromRequire.TILE === 32);
assert('require constants COLS', constantsFromRequire.COLS === 32);
assert('require constants ROWS', constantsFromRequire.ROWS === 24);
var rngFromRequire = require('./core/rng');
assert('require createRNG', typeof rngFromRequire.createRNG === 'function');
// Test dungeon generation
var dungeon = window.DUNGEONS[0];
var rooms = generateDungeon(dungeon);
assert('generateDungeon returns rooms', Array.isArray(rooms) && rooms.length > 0);
assert('dungeon #0 room count ~20', rooms.length >= 18 && rooms.length <= 26);
assert('entrance room assigned', rooms[0].type === 'entrance');
assert('boss room assigned', rooms[rooms.length - 1].type === 'boss');
assert('rooms connected', rooms.every(function(r) { return r.connections && r.connections.length > 0; }));
lines.push('');
lines.push('Total: ' + lines.filter(function(l) { return l.startsWith('PASS'); }).length + ' passed, ' + lines.filter(function(l) { return l.startsWith('FAIL'); }).length + ' failed');
output.textContent = lines.join('\n');
</script>
</body>
</html>