A year in a grove that remembers everything.
▶ Play now: https://kunskapshunger.github.io/emberwild/ — host a grove and send friends the invite link.
Emberwild is a peer-to-peer elemental arena for 1–6 wardens. You fight over one living grove through four seasons. Nothing in it is scripted. Each spell changes the world, and the world's systems decide what happens next: fire, water, wind, gas, lightning, weather, ecology and creatures. The map carries over between seasons, and the Grove, which narrates the match in its own voice, reacts to what you did to it.
npm install
npm run dev # → http://localhost:5173- Play solo starts a match against AI wardens ("echoes"). They use the same combos you can.
- Host a grove gives you a 5-letter code and an invite link. Friends connect straight to your browser.
- Join takes a code. Other tabs in the same browser connect instantly, and everyone else connects over WebRTC.
Add
?net=onlineto force WebRTC when testing on one machine.
| Input | Action |
|---|---|
| WASD / arrows | move |
| mouse | aim |
| left click (hold) | cast selected spell |
| right click | quick Gale |
| 1–6, Q/E, wheel | choose spell |
| Space | dash |
| Tab | scoreboard |
| M | mute |
| System | What it does | Emerges as… |
|---|---|---|
| Fire | Spreads by fuel, moisture², wind direction and heat. Pre-dries fuel ahead of the front. Wind throws embers downwind. | Summer wildfires, fire walls blown toward enemies, spot fires across the stream |
| Moisture | Every cell drifts toward the season's climate. Water and mud keep neighbours damp. Rain soaks, drought parches. | The same meadow is safe in spring and a tinderbox in summer |
| Gases | Smoke, steam and bog gas move with the wind and diffuse. Bog gas seeps from mushrooms and autumn mud and pools in hollows. | Chain explosions rippling through a swamp when one ember lands |
| Electricity | Floods through connected water, mud and rune floors, and through wet bodies. | Tide + Spark combos, storms turning ponds lethal, self-electrocution |
| Ice & snow | Frost freezes water into slippery bridges. Winter nights freeze ponds from the edges. Fire melts ice into puddles. | Skating escapes, bridges that melt under a pursuer |
| Ecology | Ash regrows into grass. Grass flowers, brush thickens and trees spread. Mossbacks graze, and moths pollinate. | Burnt zones heal into fresh fuel. The map evolves across the year |
| Creatures | Mossbacks herd, graze and stampede from fire, spreading it. Glowmoths swarm light and catch fire. Bog slimes take on the element they touch. Hollow wolves hunt at night and fear fire. | A burning sheep running into your enemy's forest |
| Weather | Seasonal fronts: rain, storms (dry lightning in summer), drought, gales and snow. Day/night cycle. | The battlefield changes under you every ~30 s |
| The Grove's mood | Destruction vs. regrowth each season. A wrathful grove brings storms, wolves and slimes. A blooming one brings wisps, mana and growth. | Players who burn everything make the next season harder for everyone |
| Attribution | Every burning cell, charged puddle and gas blast remembers who started it. | "Mira's wildfire consumed Tomas", credited to Mira |
Scoring: kill +3, wisp +1 (wisps rise from burnt trees and the fallen), undone by your own magic −1.
index.html ── src/main.js ─┬─ net/session.js HostSession (Web Worker + PeerJS + BroadcastChannel) / join
├─ client/ GameClient (prediction, interpolation), Renderer, tiles, lighting, particles, sprites
├─ audio/audio.js procedural SFX, adaptive generative music, narrator voice bus
└─ ui/ HUD, menus (DOM, textContent only)
server/worker.js ── server/server.js authoritative GameServer (20 Hz)
├─ sim.js cellular automaton: fire, gases, charge, moisture, ecology
├─ climate.js seasons, weather, wind, day/night, Grove mood
├─ spells.js 6 spells + projectiles + storm strikes
├─ creatures.js mossbacks, moths, slimes, wolves
├─ combat.js status effects, damage, kill attribution
├─ bots.js AI wardens
└─ mapgen.js procedural grove
shared/ constants, codec (16-bit cell encoding, run-length grid diffs, binary framing), physics, rng
- Topology. The network is a star over WebRTC. The host's browser runs the authoritative server in a Web Worker, so the grove keeps simulating when the host's tab is in the background. PeerJS's public broker is used only for the handshake; game traffic goes peer-to-peer.
- Replication. The 192×112 grid is sent as 16-bit visual cells: material, fire, moisture, gas, charge. Each tick sends a run-length diff, and new joiners get a full frame. Entities are sent as compact JSON snapshots and interpolated about 110 ms behind.
- Movement. Your own warden is predicted client-side against the replicated grid. The host validates the positions it receives and pushes impulses and teleports back.
There is no bundler. The game is native ES modules plus a vendored peerjs.min.js, and there are no native npm
dependencies. The dev server, build script and tests are plain Node.
npm run build # → dist/ (static; host anywhere: GitHub Pages, Netlify, Vercel, itch.io)
npm run preview # serve dist/ locallynpm test # node:test, 39 tests including a full headless 4-season match with 6 bots
npm run test:coverageThe Grove's 34 lines were generated with Google's Gemini 3.8 TTS (gemini-3.8-flash-lite-tts, voice Algenib)
and live in public/voice/. Subtitles work without audio. To regenerate them:
GEMINI_API_KEY=... npm run voice # only missing lines
GEMINI_API_KEY=... npm run voice -- --force # all lines
TTS_MODEL=gemini-3.8-flash-tts GEMINI_API_KEY=... npm run voice -- --force # flagship modelThe key is read from the environment only and is never shipped. Free-tier quotas are about 10 requests/min and 100/day per model; the script throttles itself and stops cleanly when the daily quota runs out.