# EXALT

A *Realm of the Mad God*-style 2D bullet hell: dodge sprays of projectiles, level a Wizard, and bring down the Titans. (Yes, the repo is called **BulletHellGame** — the game's actual name is **EXALT**.)

EXALT is a browser game — vanilla JS ES modules + Canvas 2D, no engine, no frameworks, no build step. **The web version is the canonical, maintained implementation.** It began life as a Java Swing app, which stays in the repo as a frozen reference and receives no further changes.

**Now remastered.** The web port ships a full presentation overhaul on top of the byte-identical simulation: procedurally generated realm floors, bullets redrawn as glowing energy orbs with trails, particles and floating combat text, screen shake, hit flashes, shadows, boss health bars, a minimap, an animated title screen, a pause menu, a game-over run report with restart, and synthesized sound effects. Every sprite is enhanced *at load time, in memory* — no asset file was modified, and no original gameplay rule changed (see the remaster section of [`ARCHITECTURE.md`](ARCHITECTURE.md)).

**And expanded.** The realm is a descent through the Greek underworld, changing **region** as you survive — the Elysian Fields, the Asphodel Wastes, the Shores of Cocytus, the Phlegethon Barrens, the Pit of Tartarus — and the Titans drop **dungeon portals** when they fall. Press `E` to descend into a **procedurally generated dungeon**: every portal rolls its own theme, name, floor layout, wave count, monster mix and keeper, so no two runs go to the same place. *CATACOMB OF ASPHODEL. STRAIT OF THE WAILING TIDE. BREACH OF CHAINED TITANS.* Original pixel-art creatures (Keres, Spartos Wardens, Boread Harpies, Rime Tritons, Kobaloi, Khalkotauroi, Eidola, Hekatoncheires) guard the keepers — **Cerberus**, **Scylla**, **Enceladus** and **Porphyrion** — each rolled with a stat affix (*the Ravenous*, *the Bronze-Clad*, *the Unchained*…) and randomized attack timings, and each dropping the best loot in the game. Deep enough into a run, a portal can open onto the Abyss itself, where **Typhon** waits.

Music is **contextual**: every biome, dungeon and boss has its own instrumental, with sung tracks reserved for the menu and the death screen. [`MUSIC.md`](MUSIC.md) has generation-ready briefs for all of them.

**Video demo:** https://www.youtube.com/watch?v=aaFkC9SpMhk

## Play it

The web port runs from any static file server — nothing to install, nothing to build:

```bash
python3 -m http.server 8000
# then open http://localhost:8000/
```

To deploy (or to run the deployable shape locally), the repo is also a Next.js
app that serves the same three pages — a React landing page at `/`, the game at
`/web/`, the classic build at `/legacy/classic-web/`:

```bash
npm install
npm run dev     # or: npm run build && npm start
```

`npm run dev`/`npm run build` first run `scripts/sync-public.mjs`, which
assembles the deployable subset (the game, the classic build and the legacy
images it borrows, these docs) into the gitignored `public/` — the game itself
stays untouched, unbundled static ES modules. On [Vercel](https://vercel.com),
import the repo and deploy with no overrides; the framework is auto-detected.

The landing page links the two playable builds:

| | |
|---|---|
| [`/web/`](web/) | **EXALT** — the current game, and the only one that gets new work. |
| [`/legacy/classic-web/`](legacy/classic-web/) | **Classic** — the port as it originally looked, kept playable for nostalgia. Flat green map, plain square bullets, the original HUD; same simulation underneath, none of the remaster. |

Click **Enter the Realm**, and survive as long as you can. When you fall, the game-over report shows your run (rounds, kills, gods felled, damage, time) and a click returns you to the title; the pause menu (`ESC`) can restart or abandon a run at any time.

## Controls

| Input | Action |
|---|---|
| `W` `A` `S` `D` | Move |
| **Left click** | Shoot / pick up loot / equip item |
| **Right click** | Drop item |
| `Space` | Spell bomb — a 12-bullet burst that detonates **at your cursor** (RotMG Wizard style) |
| `F` | Drink a health potion |
| `V` | Drink a mana potion |
| `E` | Enter a dungeon portal you're standing on |
| `Esc` | Pause menu (resume / restart / abandon) |
| `B` | *(debug, web only)* spawn 500 bullets — stress test |
| `F3` | *(debug, web only)* FPS / entity-count overlay |

## What you're up against

- **Escalating rounds** of raiding packs — Hellhounds, Telchines and Centaur skirmishers whose health and damage scale with the round number.
- **A named monster every fifth round**, and never the same one twice in a row: the **Gorgon sisters**, the **Chimera**, the **Lernaean Hydra**, the **Minotaur**, the **Manticore** and the **Nemean Lion** take turns, each with its own attack pattern (except when a Titan takes the stage instead).
- **Three Titan bosses** — **Hyperion, Titan of the Blazing Sun** (round 10), **Oceanus, Titan of the Endless Tide** (round 20), and **Cronus, Titan of the Devouring Hour** (round 30) — each with aimed volleys, 13-way bullet rings, and summoned minions. Survive further and they come back in pairs (rounds 40 and 50), then **all three at once** on round 60: the Titanomachy.
- **Five regions** that open as the rounds climb — each Titan drags you a stage deeper, from the Elysian Fields down to the Pit of Tartarus.
- **Procedurally generated dungeons** behind Titan-dropped portals: a fresh theme, name, floor, wave count, monster mix and affixed keeper every single time, with the best loot odds in the game.
- **Thirteen tiers of loot** from brown, blue, cyan, and white bags — tier 1 through tier 13 of staffs, robes and spells, plus 112 stat rings, each tier with its own pixel art and rarity color. You start in a Patched Robe with a Whittled Staff — the tier-0 kit, which is yours alone and never drops — and the Divine Sundering and the Divine Raiment are waiting somewhere past round 39. What drops climbs with the round you're on, and dungeon keepers pay a tier better than the realm does.
- **RotMG-style progression**: eight stats (HP, MP, Attack, Defense, Speed, Dexterity, Vitality, Wisdom), levels from XP, and stat potions to push your caps. Dexterity raises your fire rate; Vitality and Wisdom drive HP/MP regen.
- **RotMG combat rules**, including defense mitigation with the 20% minimum-damage floor.

Twelve long-standing gameplay bugs (tracked as GitHub issues #3–#8 and #10–#15 — a boss soft-lock, diagonal speed boost, stat-stacking loot, swapped level-up bonuses, and more) were recently fixed in the web port, with browser tests pinning each fix. The frozen Java reference intentionally keeps the original behaviors.

## The Java original (frozen reference)

EXALT began as a Java Swing app. That version lives in `legacy/java/` as a **frozen reference** — the behavioral specimen the port was validated against — and receives no fixes or features, but it still runs. There's no build system, and its asset paths are relative to the working directory, so compile and run it from its own directory:

```bash
cd legacy/java
javac -d out $(find bags enemies equipment game pots sprite status -name '*.java')
java -cp out game.Application
```

Entry point: `game.Application`. The packages are `game/`, `sprite/`, `enemies/`, `equipment/`, `pots/`, `bags/`, `status/`; note that `legacy/java/src/` holds the original *assets* (images/sounds), not Java code, and `web/assets/` is the web port's served copy of them.

## Repository layout

```
index.html            landing page linking both playable builds
web/                  the canonical game — all new work happens here
  src/                main.js + core/ entities/ content/ presentation/
  assets/  tests/
legacy/java/          the frozen Java Swing original
legacy/classic-web/   the pre-remaster port, kept playable for nostalgia
```

`ARCHITECTURE.md` documents how the game is built — the arena and camera, the responsive canvas, dungeons and tier ladders, the enemy AI layer, the remaster presentation layer. `PORTING.md` is the port record: the Java-class → JS-module map and the ledger of original bugs, which are fixed and which are reproduced on purpose. `web/src/README.md` explains the module grouping.

## Testing & development

The web port has a browser test suite that drives the real game in Chromium (via Playwright) with a deterministic, frame-pumped game loop — no `npm install` needed in this repo:

```bash
node web/tests/run.js            # run everything
node web/tests/run.js issue-13   # run a single issue's tests
```

For poking at a live game, the page exposes a debug handle:

```js
window.EXALT.getBoard()   // live Board: player, enemies, projectiles, bags, step()
window.EXALT.getScreen()  // 'loading' | 'title' | 'game'
```

**Before changing gameplay code, read [`PORTING.md`](PORTING.md).** It carries the catalog of original quirks the port preserves **on purpose** — if a behavior looks wrong, it's probably deliberate — plus the full Java-class → JS-module map and the timing model (Java's 10 ms Swing tick and timer threads → a fixed 60 Hz step with deterministic game-clock timers). Fixes land in the JS only; the Java is frozen, and a deliberate divergence is recorded by striking through its quirk entry. For how the game's own systems work, read [`ARCHITECTURE.md`](ARCHITECTURE.md).

Under the hood, the web port is plain ES modules with a performance-minded core: object pools for projectiles and popups, a preallocated spatial grid for collisions, one offscreen sprite atlas, and zero steady-state allocations on the hot path.

## Known issues

Honest list — these are real, currently-true behaviors:

- Loot bags despawn 10 seconds after they're created — **including bags you drop your own items into**. Don't use the ground as storage.
- One player bullet that overlaps several enemies damages **all** of them in the same tick.
- Holding `Space` re-triggers the spell bomb at your OS key-repeat rate.
- Enemies can spawn underneath the right-hand UI panel (spawn range spans the full window; the playfield is 1620 px wide).
- Web port: walking animations show only the first frame of each directional GIF (a Canvas 2D limitation, noted in `PORTING.md`); the remaster adds a procedural walk sway on top.
- Java original: no restart after game over — relaunch the app. (The web port's remaster added a full restart flow.)
- Java original: as a frozen reference it keeps all its original bugs (the twelve fixed issues above, backwards title-screen labels, tooltip misalignment), and performance dips with many entities on screen — the web port's pooling and collision grid are port-only additions.

The complete catalog of preserved original quirks lives in `PORTING.md`.

## Ideas for later

- Status effects (inert `Slow`/`Status` classes already exist in both implementations)
- Bigger maps with a moving camera
- Dungeons
- A sound-settings page (hit/death/level-up SFX landed with the remaster; volume control didn't yet)
- Replacing the five soundtrack slots with the generated score described in `MUSIC.md`

## License

MIT — see [`LICENSE`](LICENSE).
