A top-down Zelda-like action-adventure prototype built with Phaser 4 (RC) and bundled with Vite. (See PHASER4_MIGRATION.md for migration details.)
Move around a procedurally generated overworld, fight goblins with your sword, collect hearts, and survive.
| Input | Action |
|---|---|
| WASD / Arrow keys | Move (8-directional, animations snap to 4 cardinal) |
| Space | Sword attack (directional, 1 s cooldown) |
| R | Restart (on game over or victory screen) |
| Touch (mobile) | Left 70 % of screen = virtual joystick, right 30 % = attack |
# Install dependencies
npm install
# Start the dev server (hot-reload)
npm run devOpen http://localhost:5173 in your browser.
npm run build # outputs to dist/
npm run preview # serves dist/ at http://localhost:4173Both the dev and preview servers bind to 0.0.0.0, so they're accessible from other machines on the network (useful inside containers or VMs).
The project is now fully TypeScript (*.ts sources with tsconfig.json; Vite transpiles seamlessly). To protect against regressions:
npm run type-check # tsc --noEmit
npm test # vitest watch mode
npm run test:run # CI-style run
npm run test:coverage # Generates V8 coverage report (console + HTML/JSON in ./coverage/; ~69% now, utils high via extracts; excludes tests/mocks)
npm run test:ui # Vitest UI dashboard-
File convention:
<target>.test.ts(e.g.,gameSceneUtils.test.tstestsgameSceneUtils.ts). -
Refactors: Extracted non-UI logic to
gameSceneUtils.ts(effects/AI/math for readability/tests) +types.ts(shared Phaser types); GameScene logic further split intosystems/(player, combat, enemies, UI, collectibles) for maintainability and testability. -
Mocks: Vitest/jsdom + setup for Phaser (enables unit tests on UI-heavy code).
-
Coverage: Use report to prioritize (e.g., GameScene next).
wild-adventure/
├── index.html Vite entry point
├── package.json
├── vite.config.ts
├── tsconfig.json TypeScript config
├── tsconfig.node.json For Vite config
├── public/ Static assets (drop PNGs here)
└── src/
├── main.ts Phaser boot & game config
├── constants.ts Tunable gameplay constants & sprite frame map
├── types.ts Shared TS types (GameEnemy, PlayerIntent, etc.)
├── map.ts Procedural 50×50 tilemap generator
├── worldFactory.ts World generation (terrain, chests, structures)
├── fallbacks/ Auto-generated textures when PNGs are missing
├── systems/ Decoupled game systems
│ ├── playerController.ts Movement, animation, damage
│ ├── combatSystem.ts Attack timing, sword hitbox
│ ├── enemySystem.ts Spawning, AI, projectiles
│ ├── uiSystem.ts HUD, overlays
│ ├── collectiblesSystem.ts Unified pickup pipeline
│ └── inputSources.ts Device-agnostic input (keyboard, touch)
├── gameSceneUtils.ts Extracted non-UI utils (effects/AI/math)
├── style.css Minimal fullscreen styles
└── scenes/
├── StartScene.ts Title/loading screen
└── GameScene.ts Main game scene (orchestrates systems)
The game auto-generates coloured placeholder textures at runtime, so no image files are required to play. If you want to swap in real pixel art, drop the following PNGs into public/:
| File | Size | Description |
|---|---|---|
player_sheet.png |
512×512 | Spritesheet — 32×32 frames, 16 columns (see constants.ts for the frame map) |
grass.png |
32×32 | Grass tile |
tree.png |
32×32 | Tree tile (impassable) |
rock.png |
32×32 | Rock tile (impassable) |
goblin.png |
32×32 | Goblin sprite |
wizrobe.png |
32×32 | Wizrobe sprite |
lynel.png |
32×32 | Lynel sprite |
gel.png |
32×32 | Gel sprite |
heart_full.png |
16×16 | Full heart icon |
heart_empty.png |
16×16 | Empty heart icon |
compass.png |
16×16 | Compass collectible |
compass_arrow.png |
16×16 | Enemy-tracking arrow indicator |
- "Pixel art 32×32 top-down hero sprite sheet, 16 columns, walk/idle/attack in 4 directions, transparent background, Zelda LTTP style"
- "Pixel art 32×32 goblin sprite, top-down, green skin, red eyes, Zelda style"
- "Pixel art 32×32 seamless grass/tree/rock tiles, top-down RPG"
- "Pixel art 16×16 heart icon, red fill, black outline"
- Overworld: 50×50 tile map (~80 % grass, ~12 % trees, ~8 % rocks) with carved cross-paths and a tree border wall.
- Player: 4 hearts (96 HP). Takes 24 damage per enemy touch. 0.8 s invincibility after each hit with knockback.
- Enemies: The world contains a mix of goblins, wizrobes, gels, and a lynel miniboss. Each enemy type has distinct AI behavior:
- Goblins: Patrol randomly and chase the player within 128 px; die in one sword hit and drop a heart pickup (+24 HP).
- Wizrobes: Chase the player and periodically fire ranged projectiles; vulnerable to sword attacks.
- Gels: Small, fast enemies that split into two smaller gels when killed (unless already small).
- Lynel: A tough miniboss that pursues aggressively with both melee and ranged attacks; requires multiple hits to defeat.
- The exact composition and number of each enemy type may vary from game to game.
- Enemy AI is implemented using a strategy pattern in
src/systems/enemyBehaviors/for easy extension.
- Chests: 4 chests are scattered across the map containing:
- 3 Triforce pieces (Courage, Wisdom, Power) — collect all 3 to upgrade HP to 8 hearts!
- 1 Compass — once collected, a red arrow appears at the top of the screen pointing toward the closest enemy, helping you track them down.
- Win condition: Defeat all 5 enemies.
- Lose condition: HP reaches 0.
MIT
