Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

25 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸͺ± Fable Worms

a tiny tale of tremendous artillery β€” a browser-based artillery game in the spirit of Team17's Worms, told across four storybook lands.

Built with Phaser 3 + TypeScript + Vite. All art is procedural (canvas-generated textures, hand-drawn vector characters). Sound is a hybrid: a curated CC0 sample pack (Kenney SFX + FreePD music, see public/audio/CREDITS.md) carries explosions, footsteps, UI and the per-biome soundtrack, while WebAudio synthesis still voices the characterful cues (charge hum, sheep baa, ceremony chimes) and covers every sample while it loads β€” the game is never silent, even offline.

Play

npm install
npm --prefix server install
npm run dev:all   # client β†’ http://localhost:5173 Β· game server β†’ :9770

npm run dev runs just the client (local/hotseat play works without the server); npm run dev:server runs just the game server. There is no separate database or docker service β€” the server embeds SQLite (node:sqlite, file created on boot). Node β‰₯ 22.5 is required for the server.

Production build: npm run build (output in dist/, servable from any static host β€” point it at your server with VITE_SERVER_URL in .env.local).

Native simulator

Start the game server in one terminal, then launch the native simulator build from another:

# iOS Simulator
npm run dev:server:ios:local
npm run cap:run:ios:local
npm run verify:mobile:native:ios:local

# Android Emulator
npm run dev:server:android:local
npm run cap:run:android:local
npm run verify:mobile:native:android:local

Local native bundles are platform-specific because the iOS Simulator reaches the server through http://localhost:9770, while the Android Emulator reaches the host through http://10.0.2.2:9770. Use npm run cap:sync:ios:local or npm run cap:sync:android:local before the matching scoped verifier when you switch simulator platforms.

The Android npm run/build helpers resolve JDK 21 and common Android SDK locations before invoking Gradle or Capacitor. If you run Gradle directly, set JAVA_HOME and ANDROID_HOME / ANDROID_SDK_ROOT, or create Android Studio's ignored android/local.properties with sdk.dir=....

Guest login, hosted lobbies, quick match, and bot skirmish work without Discord secrets. Discord login on a simulator also needs DISCORD_CLIENT_ID and DISCORD_CLIENT_SECRET in the server environment, plus the matching local redirect URL registered in Discord: http://localhost:9770/auth/discord/callback for iOS Simulator or http://10.0.2.2:9770/auth/discord/callback for Android Emulator.

How it plays

The Sprouts face The Nightcaps β€” against the AI or a friend in 2-player hotseat β€” taking alternating turns on a fully destructible island. Last team wiggling wins.

The lands

  • πŸŒ„ Dusk Meadow β€” fireflies, flowers, fable trees and a low apricot sun
  • ❄️ Frozen Tundra β€” snowfall, ice crystals, snowy pines, aurora and floating ice shelves
  • πŸŒ‹ Ember Hollow β€” charred spires, magma veins, old bones, and a sea of lava
  • πŸ„ Glimmer Glen β€” a bioluminescent night glen: giant glowing mushrooms, drifting spores
  • 🎲 Random β€” surprise me

Clicking a land on the menu repaints the world behind it, live. Each map also rolls a landform archetype β€” rolling hills, a valley with flanking cliffs, terraced mesas, or jagged spires β€” so no two islands feel alike.

The arsenal (20 weapons β€” 14 in the starter kit, 6 earned in the Locker)

Nine skills ride on the hotbar (keys 1–9); everything else lives in the backpack (key 0, or the πŸŽ’ slot at the end of the dock). The backpack dialog lets you stow hotbar skills and equip packed ones mid-battle, and the Locker β†’ Armory arranges the same dock between battles β€” your loadout is remembered between sessions. On desktop the dock stands upright in the bottom-left corner; on phones it lies flat along the bottom.

Everyone starts with the classic fourteen:

# #
1 πŸš€ Bazooka wind-riding rocket 8 πŸ”« Shotgun two quick shots
2 πŸ•ŠοΈ Homing Pigeon seeks the nearest foe 9 ⚑ Uzi 14-round steerable spray
3 πŸ’£ Grenade 3s fuse, bouncy 0 🧨 Dynamite drop it and RUN
4 πŸ‡ Cluster Bomb pops into five Β· 🎱 Mine arms itself, holds a grudge
5 ✨ Holy Grenade hallelujah… Β· ✈️ Airstrike click a target
6 πŸ‘ Sheep hops merrily; fire again to boom Β· πŸ”₯ Napalm Strike rains burning fire
7 🏏 Baseball Bat point-blank home run Β· πŸŒ€ Teleport click: blink away

Six more are earned exactly like cosmetics β€” XP levels, rank tiers, or acorns:

unlock
🎯 Marksman Rifle one pinpoint 34-damage crack across the island level 7
πŸ’₯ Mortar an impact shell that pops into four bomblets level 10
🍾 Molotov a warm hello that leaves burning patches level 14
🍌 Banana Bomb five more where that came from Ember rank
πŸ‘‰ The Prod one point of damage, one enormous shove 🌰 200
πŸŒͺ️ Minigun 24 rounds of overkill 🌰 600

Mines chain-react to explosions, napalm and molotovs leave burning patches that hurt over time, and the sheep answers to no one after launch. Online, each team fights with its own unlocked arsenal (crates only ever gift weapons you own), so your opponent's banana is no business of yours β€” yet.

Match options

  • Foe β€” πŸ€– vs AI or πŸ‘₯ 2-player hotseat (the dock switches sides each turn)
  • Worms β€” 3, 4 or 5 per team
  • Sudden death β€” from round 8 the sea rises every turn; high ground or bust
  • Difficulty β€” Cozy (aim preview, gentle AI), Classic, Brutal (deadeye AI, 30s turns)

Wind changes every turn, crates parachute in with health or bonus weapons, fallen worms leave gravestones, and the match ends in a full prize ceremony: the verdict slams in letter by letter under a slow sunburst, paper confetti rains for the victor, the scrapbook stat cards slap down and the MVP takes a bow. One tap anywhere skips to the settled ledger.

Damage lands the classic Worms way: a hit flings the worm first, and only once it tumbles to a rest does the camera lean in, the number pop, and the health drain (multiple victims get a little camera tour each). Worms are solid to each other β€” walk into a teammate and you stop, drop onto one and you stand on their head (knocked worms still sail through, and blast landings get gently nudged apart). And when a worm finally goes, it goes its own way: the equipped farewell animation plays and its chosen gravestone marks the spot β€” both cosmetic slots in the Locker.

The Locker

LOCKER on the menu opens your inventory, in two panes sharing one unlock language (starter kit β†’ XP levels β†’ rank tiers β†’ the shop):

  • πŸ‘’ Wardrobe β€” the live worm mannequin (the real in-game renderer, pupils following your pointer) and rarity-framed racks across eight slots: hats, eyes (buttons, sleepy lids, hearts, a cyclops, the Will-o'-Glow…), specs, whiskers, bling, auras, gravestones (wooden cross, glowing glen cap, a rubber ducky, the gilded acorn…) and farewells β€” the death animation itself (sky ascent, restless spirit, grand-finale firework, supernova…). The grave rack poses the stone beside the mannequin; the farewell rack plays a full live-flicker-boom rehearsal on the stage. Hover to try on, click to dress the whole team.
  • βš”οΈ Armory β€” every weapon on the same racks, with a spotlight card (damage, ammo, unlock story) and your 9-slot dock to arrange between battles. Locked weapons hang in shadow with their price of admission.

Shop items are bought with acorns 🌰 β€” earned by finishing matches (8 + 0.1/damage + 18 for the win, ranked Γ—1.15, bots Γ—0.35), stored on your account, spent with one tap on the price tag. Purchases, like every other unlock, are validated server-side (POST /me/buy).

Online multiplayer

PLAY ONLINE on the menu opens the hub. Play as a guest (just a name) or sign in with Discord β€” accounts earn XP / levels, a rank, and acorns for the Locker shop.

  • βš”οΈ Quick Match β€” casual 1v1 matchmaking, first come first fought
  • πŸ† Ranked Match β€” Elo matchmaking with a widening rating window; climbs the tiers (Seedling β†’ Sprout β†’ Bramble β†’ Nightcap β†’ Ember β†’ Tempest β†’ Mythic). Needs a Discord account.
  • πŸ€– Bot Skirmish β€” the server-side AI at cozy/classic/brutal. Counts for XP while signed in, at a reduced rate (Γ—0.35).
  • 🏝️ Browse / Host an Island β€” public lobby browser, or host with your own settings (land, worms, turn time, sudden death, public or code-only) and share the room code.

Profiles & friends

The menu wears an identity chip (avatar, level, rank crest) and a Friends button in the top-right. The chip opens your profile page: the live worm mannequin in your full loadout keeping vigil beside your chosen gravestone, your rank crest with rating progress to the next tier, the XP bar, nine lifetime stat tiles (battles, win rate, ranked record, damage dealt & taken, biggest hit, worms KO'd & lost, shots fired) and your recent match history β€” every online match with an account aboard is recorded.

The Friends drawer (accounts only) searches islanders by name, sends and accepts friend requests (pings arrive live, with a badge on the button), shows presence for every friend β€” online, in a lobby, in queue, in battle β€” and lets you tap any name for their profile page, where you can compare stats side by side or invite them to battle. Invites ride a persistent social channel: the inviter's lobby (or a freshly auto-hosted code-only island) reaches the friend as a floating Join pop wherever they are in the menus. Lobby seats are tappable too β€” size up your opponent's profile before the horn sounds.

Matches are fully server-authoritative: the server runs the entire simulation (movement, physics, all 20 weapons, wind, crates, sudden death) at 30 Hz and clients only send intents β€” walk, aim, charge, fire. Each team's in-match arsenal is derived from its account on the server (level, rank, purchases) β€” never from anything the client claims. Clients render 15 Hz snapshots with interpolation and replay a discrete event stream for effects. The destructible terrain is regenerated from the match seed on each client and kept in sync by carve events, so both players (and the server) agree on every pixel. A dropped player gets a 60-second reconnection window (the sim pauses); quitting mid-match forfeits.

XP: base 30 + 0.4/damage + 70 for the win (+15% ranked, Γ—0.35 vs bots). Acorns follow the same shape at roughly a quarter scale. Ranked rating: Elo, K=32, starting at 1000.

Signed in, the end ceremony tallies the night on a kraft Field Report: the XP bar genuinely fills (crossing a level flashes the bar, rolls the badge and bursts), acorns click into the pouch, and the rating odometer spins β€” crossing a tier line stages a whole rank promotion, crest swap, ribbon and all. Every weapon or cosmetic whose level/rank threshold was crossed this match is dealt face-down and flipped as a rarity-glowing prize card (the unlock rules are the same shared catalog the server enforces, so the cards can never lie), and a "road ahead" strip teases the nearest thresholds still to chase. Guests earn nothing β€” the ceremony shows them the exact XP and acorns that washed away with the tide, next to a Discord sign-in button.

Controls

Input Action
A / D (or arrows) walk
W / double-tap S jump / backflip
Mouse aim
Hold click / Space, release charge & fire
Click fire instant weapons Β· detonate the sheep Β· place targeted strikes
1–9, Tab, or click the dock switch weapon
0 open the backpack (stow & equip skills)
Wheel / right-drag / F zoom, pan, re-focus camera
M / Esc mute / pause

On a phone

The game is installable as a PWA (manifest + service worker) and insists on landscape: a rotate prompt covers portrait, and starting a match requests fullscreen + an orientation lock where the platform allows it. Touch play: tap-hold to aim & charge, release to fire, on-screen pads walk and jump/backflip, pinch to zoom, and the dock + backpack are all tappable.

Architecture

src/
  const.ts            world dimensions, teams, difficulty, match settings,
                      THEMES (palette, sky, hills, sea, ambient), render depths
  utils.ts            math, seeded RNG, 1-D value noise
  audio.ts            hybrid SFX engine: CC0 samples (public/audio/sfx) with
                      pitch-jittered round-robin variants + WebAudio synthesis
                      for bespoke cues and as an instant fallback
  music.ts            crossfading BGM (menu + one track per biome) and
                      synthesized ambience beds (wind / cave drips / lava)
  terrain.ts          destructible pixel terrain: Uint8Array mask + painted
                      CanvasTexture + tinted silhouette for the cartoon outline;
                      landform archetypes (rolling / valley / mesa / spires /
                      cliffs / skyisles / cavern β€” the last fully roofed, where
                      airstrikes splat on the rock), ledge notches, jutting
                      shelves, worm tunnels and hidden pockets; theme-driven
                      paint with 2D-noise mottling, strata, mineral veins,
                      per-seed palette drift, cave-moss floors and hanging
                      ceiling fringe (roots / icicles / magma drips / crystals),
                      buried inclusions & decorations, from grass tufts up to
                      trees, pines, bone arches and giant glow-mushrooms
  background.ts       theme-driven sky, stars, aurora, sun, parallax hills,
                      clouds, sea or lava, fireflies / snow / embers; textures
                      repaint in place so theme swaps are live
  fx.ts               particle emitters, explosions, flames, feathers, splashes,
                      damage text, wind streaks
  weapons.ts          weapon table, firing (charged / hitscan / melee / placed),
                      the one true explode() with mine chain-reactions
  ai.ts               enemy brain β€” simulates real trajectories over an
                      angleΓ—power grid, scores impacts, knows when to bat,
                      drop dynamite, spray the uzi or release the pigeon
  ui.ts               DOM chrome: menu with match options, HUD, the 9-slot
                      hotbar dock + backpack dialog (loadout persisted in
                      localStorage), touch controls, fullscreen/orientation
                      helpers, round/sudden-death pill; hands the match
                      verdict to endscreen.ts
  endscreen.ts        the post-match prize ceremony: skippable timeline
                      (letter-slam verdict, confetti, staged reveals), the
                      Field Report (XP bar with level-up bursts, acorn
                      ticker, rating odometer + rank promotions), prize
                      cards for every unlock threshold crossed this match
                      (diffed with the shared catalogs), the road-ahead
                      teaser and the guest nudge
  entities/           worm (mask physics + worm-vs-worm collision + deferred
                      damage that lands once the worm settles, the upright
                      Team17-style character standing on a coiled tail,
                      slope-aware posture, and eight cosmetic skin slots β€”
                      hat / eyes / glasses / mustache / earring / aura /
                      gravestone / farewell β€” rolled from the worm's name or
                      dressed by the wardrobe), wormdraw (the figure renderer,
                      shared with the wardrobe preview), gravedraw (every
                      equippable resting place, same GfxLike trick), deathfx
                      (the farewell animations as stateless frame painters β€”
                      identical in-game and on the wardrobe stage),
                      projectile (incl. homing steering), crate, gravestone,
                      strike plane, mine, sheep
  wardrobe.ts         the equipped loadout: localStorage + account sync
  inventory.ts        the client's view of ownership: acorn balance, shop
                      purchases, which weapons this account may field, /me/buy
  wardrobe-ui.ts      the Locker dialog: Wardrobe pane (live mannequin, rarity
                      racks) + Armory pane (weapon cards, spotlight, dock
                      shelf) sharing one unlock/purchase grammar
  profile-ui.ts       the profile page: live mannequin + gravestone, rank
                      crest & tier progress, stat tiles, match history,
                      friend / invite / stat-compare actions
  social-ui.ts        the people layer: menu identity chip, friends drawer
                      (search, requests, presence), floating game-invite pops
  gfx2d.ts            canvas-2D adapter so wormdraw renders in the DOM
  scenes/GameScene.ts turn state machine, sudden-death sea, fire patches,
                      camera direction (incl. the damage-number zoom-in tour),
                      input, aim UI
  net/                online play:
    config.ts           server URL (VITE_SERVER_URL)
    auth.ts             token store, Discord OAuth return, guest login, /me
    client.ts           @colyseus/sdk wrapper (create/join/queue/reserve)
    social.ts           the social channel: friends cache, presence, invites,
                        profile fetches (REST + the persistent social room)
    netui.ts            online hub, lobby browser, lobby room, queue overlay;
                        feeds the match result + progression report to the
                        end ceremony (endscreen.ts)
    NetGameScene.ts     renders online matches: sends intents, interpolates
                        snapshots into puppet entities, replays the event stream
shared/               dependency-free code run by BOTH client and server
  sconst.ts             physics/weapon/terrain-gen constants (single source)
  srng.ts               deterministic RNG + noise
  mask.ts               seed β†’ identical terrain mask everywhere
  sim.ts                the authoritative match simulation (headless port of
                        the whole game: worms, weapons, mines, sheep, planes,
                        fires, turn machine, sudden death)
  sai.ts                the bot brain, running server-side
  protocol.ts           every websocket message type
  progression.ts        XP curve, match XP, Elo, rank tiers
  cosmetics.ts          the wardrobe catalog: items, rarities, unlock rules
                        (levels / rank tiers / acorn shop / future lootbox) +
                        the loadout validator both client and server run
  arsenal.ts            the weapon unlock catalog + per-account arsenal builder
  economy.ts            acorn payouts, shop prices, owned-keys validation
server/               Colyseus + Express game server
  src/index.ts          HTTP + websocket entry, /health, /lobbies
  src/auth.ts           Discord OAuth2 code flow, guest JWTs, /me, /me/buy
  src/profile.ts        public profile pages, player search, friends CRUD
  src/social.ts         in-memory presence + push hub (friend pings, invites)
  src/db.ts             SQLite (node:sqlite) accounts, progression, acorns,
                        owned items, lifetime battle stats, match history,
                        friendships
  src/rooms/MatchRoom   lobby phase β†’ 30 Hz authoritative sim β†’ results;
                        reconnection grace, forfeits, XP/Elo persistence +
                        lifetime stats & match_log rows
  src/rooms/QueueRoom   casual & ranked matchmaking (widening Elo window)
  src/rooms/SocialRoom  the always-on social channel: presence, game invites
  src/simtest.ts        headless bot-vs-bot smoke test (npm run simtest)
scripts/verify*.mjs   headless Playwright smoke tests (needs Chrome installed)

The terrain is the heart of it: a Uint8Array solidity mask mirrored by a painted canvas texture. Explosions carve both (plus a scorch rim) and re-upload the texture; worms, projectiles, sheep and the AI's trajectory simulation all sample the same mask.

The sea level (WORLD.waterY) is mutable β€” sudden death raises it turn by turn and everything (drowning, splashes, AI pathing, the painted sea) reads the live value.

Server setup (secrets)

Copy server/.env.example to server/.env and fill it in:

Variable What
JWT_SECRET long random string (openssl rand -hex 32) β€” signs session tokens
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET from the Discord developer portal
SERVER_URL public URL of the game server (dev: http://localhost:9770)
CLIENT_ORIGIN allowed client origins, comma-separated (dev: http://localhost:5173)

In the Discord application, under OAuth2 β†’ Redirects, add:

http://localhost:9770/auth/discord/callback        (dev)
https://<your-server-domain>/auth/discord/callback (prod)

Only the identify scope is used. Without the Discord secrets the server still runs β€” guest play, lobbies, quick match and bot matches all work; only Discord sign-in (and therefore XP/rank persistence) is disabled.

Verification scripts

With the dev server running and Google Chrome installed (screenshots land in shots/):

node scripts/verify.mjs          # menu, live theme previews, a shot, AI turn
node scripts/verify-weapons.mjs  # grenade, airstrike, real victory flow
node scripts/verify-arsenal.mjs  # sheep/uzi/dynamite/pigeon/napalm/mine/
                                 # teleport/bat + hotseat + sudden death
node scripts/verify-deep.mjs     # explosions, drowning, death, endscreen, fps
node scripts/verify-ux.mjs       # close-ups, pause, quit-to-menu, replay
node scripts/verify-online.mjs   # needs dev:all β€” guest auth, bot skirmish,
                                 # host+join lobby, PvP turns, forfeit flow
node scripts/verify-wardrobe.mjs # wardrobe pane: tabs, rarities, locks,
                                 # equip + hover try-on, loadout in a match
node scripts/verify-wardrobe-online.mjs  # needs dev:all β€” loadout validated
                                 # by the server and worn in a bot skirmish
node scripts/verify-locker.mjs   # the Locker: armory cards, lock states, buy
                                 # tags, dock arranging, and the new weapons
                                 # fired in a real match (dev unlock-all)
node scripts/verify-endscreen.mjs # the end ceremony: full jackpot rehearsal
                                 # (level-up, rank-up, prize cards, road
                                 # ahead), tap-to-skip, guest nudge, and a
                                 # real local victory rolling the ceremony
npm run simtest                  # headless server sim: full bot-vs-bot matches

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages