Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Free Vampire Survivors–style roguelite with Pokémon, in the browser. Unofficial fan game.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗A free Vampire Survivors–style roguelite with Pokémon, playable in the browser.
Pick one of 27 starters (or hatch eggs for 55 more Pokémon, 82 in all), survive 15 minutes of wild hordes, learn moves, evolve, and take down Mewtwo.
▶ Play at pokemonsurvivors.com
[!NOTE] Pokémon Survivors is an unofficial, non-commercial fan game. It is not affiliated with, endorsed, sponsored or approved by Nintendo, The Pokémon Company, GAME FREAK or Creatures. See Legal.
The game is in Brazilian Portuguese 🇧🇷, English 🇺🇸, Spanish 🌎, French 🇫🇷 and Traditional Chinese 🇹🇼, picked from your browser language and switchable in Settings. Pokémon, move and item names use their official English names.
27 starters from every region, each with a full 3-stage evolution line, its own stats, signature move and learnset.
Eggs and 54 more playable Pokémon. Bosses, locked chests and rifts sometimes leave an egg. It waits in a 3-slot incubator and hatches with the steps you take in runs, unlocking one of 54 families from every type and generation (Pichu, Eevee, Pidgey, Geodude, Gastly, Abra, Magikarp, Riolu, Zorua… and the rarest Dratini, Larvitar, Beldum, Bagon, Gible, Deino, Dreepy, Slakoth and Feebas), each with its own move, special and mid-run evolution. Hatching plays a little scene with the cry, 1% of hatchlings are shiny for good, and duplicates become Poké Dollars. A 55th family, Munchlax → Snorlax, only hatches from the Friendship Egg, which comes from inviting friends. Four ghost families (Litwick, Phantump, Drifloon and Sandygast) only hatch from the Haunted Egg, which Duskull's stall sells for ₽ or candies during Halloween Night (STALLS in data/visitors.ts, game/stalls.ts). Four more only hatch from the Kecleon Egg, the Showcase's fixed card, one a day (buyKecleonEgg in game/shop.ts): Porygon (Porygon2 needs the Up-Grade and Porygon-Z the Dubious Disc, EvolutionStage.item), Exeggcute (Exeggutor needs the Leaf Stone), Scraggy and Smeargle, which never evolves and copies the nearest foe's move with Sketch (game/sketch.ts).
Invites and Mystery Gifts. Every link the game shares carries your handle. A friend who signs up through it gets a Friendship Egg, you follow each other, and once they've played 3 ranked runs you get a Mystery Gift: a Friendship Egg for the first friend, the Ambassador title and an exclusive card frame at 3, a shiny Munchlax at 10. Profiles show how many friends you brought.
Supporters. The game is free with nothing for sale; donating (Pix, cards, Apple Pay or Google Pay, on the support page) with an account brings a cosmetic thank-you as Mystery Gifts: the Supporter title and card frame, a ♥ badge by your @ on the boards and profile, a shiny of your choice (any starter, or an egg family, which unlocks it), a companion that follows you in runs, your name in the Hall of Fame and the victory's credits, and a vote on the next egg family. R$ 50 or US$ 10 in all makes it Gold: a golden card background, PC Box wallpaper and badge. A donation made without an account comes with a code to redeem in the game. Nothing changes a run or the ranking.
Ideas board. The lobby's Ideas tile (or ?ideias) lists what players want next: anyone reads it, an account votes each idea up or down, and once you have a handle you can post your own (3 a day). Ideas that ship move to the Shipped tab.
196 moves and 32 held items. Every family has its own signature move (Brave Bird, Weather Ball, Spore, Fiery Dance, Gigaton Hammer, Moonblast…) and a learnset that grows at each evolution, with more moves than the 6 slots, so no two runs build the same. Max out a regular move, pair it with the right item, and it evolves (Ember + Charcoal → Flamethrower; 48 move evolutions), and evolved moves keep leveling up to Lv 5.
Level-ups are decisions. Every base move forks into two paths at Lv 4 and Lv 7 that change how it plays (bounce, explode, trail, echo…). Cards roll a rarity (rare: +2 levels; shiny, 1 in 64: a free pick), the Forget button bans an offer for the rest of the run, and each run opens with a draft for the 2nd move.
91 Abilities and branching evolutions. At the first evolution you pick 1 of 3 Abilities (the third is the Hidden one). Eevee picks among its 8 eeveelutions (the weather and place favor one), and Kirlia, Snorunt, Poliwhirl, Scyther and Charcadet branch too, each form with its own type, special, moves and Abilities.
Moves from outside the learnset. Beaten wave bosses drop a TM (Rock Slide, Yawn, Ice Beam, Psystrike…), a Move Tutor tent teaches types you lack, and each rift legendary offers its own legendary move (Sacred Fire, Origin Pulse, Roar of Time…). With 6 moves, learning asks which to forget ("1, 2 and… Poof!").
Duo moves and cursed items. Two maxed moves whose types form a recipe fuse into a duo move (Fire Pledge, Water Pledge, Grass Pledge, Fusion Flare, Fusion Bolt, Ion Deluge, Freeze-Dry, Nightmare; one per run), and locked chests may hide a cursed item (Life Orb, Choice Band, Flame Orb, Iron Ball): strong, at a price.
Real type matchups, burn, poison, paralysis, freeze, confusion and more, and a manual special attack for each of the 18 types that grows stronger at the final evolution: Blast Burn, Hydro Cannon, Frenzy Plant, Bolt Strike, Draco Meteor, Astral Barrage, Precipice Blades and more.
Readable damage numbers: hits on the same target add up into one growing number, a few per target at most, with super-effective hits kept apart. On a crowded screen only the biggest hits show (or all of them, or none: it's a setting).
415 wild Pokémon from all 9 generations, Dragon types included, in 5 tiers by base stat total. Kanto, Johto and Hoenn are complete: every species but the legendaries and mythicals (and Unown and Castform, whose look depends on a form) shows up, as a wild Pokémon or as a starter, egg or boss. Every run draws its own cast, a few new faces per stage, from basic Pokémon early to evolved ones late, and each biome pulls the draw towards its types (more Grass and Bug in the forest, Rock and Ground in the cave).
Bosses with telegraphed attack patterns: Onix, Snorlax, a legendary trio drawn per run (the Kanto birds, the Johto beasts or the Hoenn Regis, at 7:00, 9:30 and 12:00, with the same stats per slot whichever trio comes) and Mewtwo. Sometimes a rare mythical joins the finale: Mew, Celebi or Jirachi. Each one switches between rushing in with area slams and hanging back to aim at you, and enrages under half HP (faster, quicker attacks, sometimes two in a row). The horde thins for each boss's first minute, and each boss has until the next one arrives, then it flees with its reward; Mewtwo blasts the horde away and thins it further, so the finale is a duel. Bosses also aim at the path you're taking, leap at you when you keep away, unlock a new attack when enraged and get an extra one on Nightmare; on Hard and Nightmare Mewtwo has a second phase.
Elites, runners, spitters and Alphas: when the horde nearly fills, new arrivals come in as red-outlined elites (more HP, EXP and damage; none during the Mewtwo duel), and one new species each stage runs, aiming where you're going to be. From 3:00 (2:00 on Nightmare) one species per stage spits: it keeps its distance, glows and fires slow shots flavored by its type that pass through your aura (every enemy shot wears a red rim, so it stands out from your own), so you have to step aside. From about 4:00, Alpha Pokémon join the horde: giants with a red glow that shrug off knockback and charge along a red lane they aim first; beat one for EXP, ₽ and the Alpha's prize, a level-up of rare cards.
Legendary rifts: three times per run a rift opens on the map. Step in to duel Lugia, Ho-Oh, Kyogre, Groudon, Rayquaza, Dialga, Latias, Latios or Deoxys in an arena of its own (shallow water, pillars, a rising tide, lava rivers, a crumbling edge, an island ringed by surf, a meteor in space) for big rewards, before it collapses.
Places to explore: a few times a run the entrance to a place shows up on the map, with an arrow on the screen edge. Stand in its doorway to step in: the world outside waits, and the place has its own Pokémon, a goal and a time limit. The Dark Cave is a Mystery Dungeon: floors of rooms and corridors in the dark (only a circle of light around you, wider for Fire and Electric types; wild Pokémon in the dark show only their eyes), stairs down, a Monster House guarding a chest, and the PMD wind that blows you out. The Haunted Graveyard holds only ghosts, rising from the graves; every one you defeat sends its spirit into the Odd Keystone, and with enough of them Spiritomb breaks out, before dawn. Above the Clouds is for Flying types only: an updraft takes you up where only flying Pokémon come, flocks cross the sky and wings drift in on the wind, each raising a stat for the rest of the run.
Landmarks: points of interest stand on the map, placed from the run's seed like the rest of the terrain and dressed by each biome: an abandoned cottage without its roof (walk in through the door; a crate in the corner), a mine tunnel through a long ridge (stand in one mouth to come out of the other while the horde loses track of you), a hedge maze with a chest at its heart, Unown ruins whose altar lights the glyphs up and spills Exp. Candy, and an abandoned camp with its fire still burning and a backpack left behind. Standing at a treasure for a moment opens it, once. A wooden sign names each one as you come close. Between them, bushes, fallen logs and lone rocks and trees.
Mega Evolution: the first rift legendary you beat with a Mega line drops its Mega Stone. At the final evolution, the MEGA button plays the transformation and the Mega form lasts 20 s (60 s recharge), with its own stats, special and ability. 38 Megas across 33 lines, from Mega Venusaur to Mega Greninja; Charizard, Raichu, Garchomp and Lucario have two, and the rift drops both stones so the player keeps the one picked up. Megas SpriteCollab hasn't drawn yet wear their final stage with a rainbow outline.
Endless mode: after Mewtwo, keep going while the horde scales forever and Arceus, the reaper, hunts you from 20:00. Time survived is the score.
Three difficulty levels, picked on the select screen and all open from the start: Normal, Hard (faster, denser hordes that hit harder) and Nightmare (brutal, fast and dense hordes and half of every heal). Each run records its level for the ranking. A pressure director watches how easily you're winning: while nothing hurts you and the horde melts at the screen edge, spawns come faster and closer, more of them elite, with extra rings and swarms (gently on Normal, hard on Nightmare), and it eases off once you get hurt. Later in a run the map itself strikes where you stand (Thunder in the rain, hail in the snow, Rock Slide and Stealth Rock…), always marked on the ground first, and late hordes stiffen against knockback and hit harder, so standing still stops working.
Today's outbreak: every day (Brasília time) one wild species takes over every run, the same for everyone. It's the most common Pokémon of the run from the first minute to the last, so its shiny shows up about twice a run. The title screen announces it, and a daily mission asks you to defeat it.
Halloween Night (Oct 24 to Nov 2, 2026, Brasília time): every run is haunted. Night falls, an extra ghost joins each stage, each day has a haunted outbreak (Gastly on the 31st) and jack-o'-lanterns stand among the breakables: trick or treat, candies or a ring of ghosts. Ghost-type Pokémon, bosses and Alphas drop candies that fill a jar paying a Pumpkin frame, a Halloween background and PC wallpaper, a Rare Egg, the Pumpkin King title and a shiny Gastly. Night bosses (Mimikyu, Gengar, Chandelure and nine more) take Onix's and Snorlax's slots, drawn per run; beating all 12 (the Ghost Hunt) pays the Exorcist title and the Spectral frame. The lobby dresses for the night too (src/data/halloween.ts, src/game/halloween.ts).
Daily Challenge and ribbons: one run a day that is the same for everyone (Brasília time): the same seed, Pokémon (any playable one, egg families you haven't hatched yet included) and difficulty, with no Poké Mart upgrades, Lure, Incense or Shiny Charm, and the day's outbreak on. Only your first finished run of the day goes on the challenge board; the rest are practice. A few hours after the day closes, everyone on it gets a ribbon as a Mystery Gift: gold for the top 10%, silver for the top half, bronze for taking part. Ribbons show on your profile and Trainer Card, and an official attempt's card shows its place. Guests can play and practice it too, off the board.
Shinies: any wild Pokémon can be a shiny elite (1/450, down to 1/225 with the Shiny Charm), and your starter has a 1/64 chance to be shiny for the whole run. Rolling it unlocks that Pokémon's shiny for good. Any shiny you own (rolled, hatched or picked as a supporter) plays Normal or Shiny, as picked on the select screen. Every shiny you catch lives in the PC Box, 30 to a box like the games' PC.
Share cards: the results screen draws a pixel art card of the run (your Pokémon, time, level, the shinies you caught, the legendaries you beat) to share, copy or download, and the PC Box and your profile draw your Trainer Card. Shared links carry your handle (?ref=).
In-game Pokédex of 580 species: every one you've seen, how many you defeated and which you caught. A defeated shiny is yours, and so are the Pokémon you hatch and every form you play. Every Pokémon that beat Mewtwo gets a trophy there and on the partner select, bronze, silver or gold for the hardest difficulty it cleared. The end of a run shows what's new.
Five biomes per run: Route 1, Viridian Forest, Mt. Moon, Mt. Silver and Lavender Tower, each with its own ground, breakables and theme, cross-dissolving with no loading screen.
Weather for everyone: harsh sunlight (Fire ×1.5, Water ×0.75), rain (the opposite), sandstorm and snow (wear down non-immune types) and fog (short sight).
Items on the map: break item balls, berry trees and tall grass for Potions, Rare Candies, Max Repels, a Voltorb that explodes, and more.
Map events that make you pick where to go: buff altars, healing fountains, locked chests (stand in the ring while an ambush closes in), spike and bomb traps that hurt the horde too, and a fleeing Meowth that drops coins. Off-screen ones get an arrow on the screen edge. Field missions pull you across the map for ₽ and a level-up of rare cards: defend an egg in its nest, gather lost Poké Balls against the clock or lead a lost Togepi home.
Meta progression: hatch eggs into new partners, collect Poké Dollars and spend them in the Kecleon Shop, a plaza in the lobby where Kecleon talks to you: today's Showcase (the same for everyone: a free daily gift, the Kecleon Egg's fixed card, a 30% deal, consumables, the day's egg and a style, every card back within 7 days, a wishlist from the Tent on), the Bag (9 consumables, from Lure and Incense to Rare Candy, Max Revive and the Everstone, armed for the next run here or at the Pokémon select), the purple brother's TMs (owned for good and armed before a run, its first TM discs teach them; 10 shop exclusives: Pay Day, Thief, Protect, Hyper Beam, Rain Dance, Sunny Day, Swords Dance, Drain Punch, Thunder Wave and Fly) and evolution items (a branch that needs an item in the official games needs it in a run, owned or dropped by a boss for that run: no Dawn Stone, no Gallade; without items Eevee becomes Vaporeon, Jolteon or Flareon at random, and Scyther and Charcadet stay unevolved at +15%), Training (HP Up, Protein, Exp. Charm… and the Shiny Charm, refundable, plus Bottle Caps: +3% to every stat per level for one Pokémon), Style (card frames, backgrounds and PC Box wallpapers) and Expansion (invest to grow the shop through 5 levels, its building changing on the plaza; the Department Store brings the Tycoon title, a gold Kecleon frame and a golden statue). Meowth's caravan parks on the plaza Friday to Sunday from the Kecleon Mart (an ultra egg, the Gold Bottle Cap, his frame and exclusive TMs, one of each per weekend, 20% off for regulars who haggle), the loyalty card stamps every day you visit (a rare egg, a frame, a title, the statue and a Kecleon Egg sure to hatch shiny at 7, 14, 30, 60 and 100 stamps), and seasonal stalls stand on the plaza during events.
Missions and achievements: three daily and two weekly missions, drawn from the date so everyone gets the same ones, pay ₽ (some weeklies also give an egg, and some missions a Lure or an Incense). 63 achievements earn bronze, silver and gold medals (gold ones bring an egg too), and some grant a title (Shiny Hunter, Pokémon Professor…) shown on the title and results screens.
Original chiptune soundtrack, synthesized live in WebAudio (no audio files): a village theme in the menus, one theme per biome, and a boss theme while a boss is alive.
Ranking and public profiles: signed-in runs go to a board per difficulty, for today, this week and all time, plus the Daily Challenge's board for today and yesterday (days and weeks turn over in Brasília time), filterable by Pokémon and by friends (one-way follows). Each trainer picks a display name and a unique handle and gets a shareable profile (/?u=handle) with their title, records, challenge ribbons, Pokédex progress and recent runs. The server only takes a run that fits the wall clock since its start token.
Optional accounts: sign in with Google to keep your progress in the cloud and continue on any device. Guest play stays the default; signing in brings the guest progress along (the collection joins, each upgrade and record keeps the higher value) and it syncs at the end of every run, offline-safe.
Settings (the gear, top right of the menus), in four tabs: Sound (music and effects volume, mute), Game (language, damage numbers: all, reduced or off, screen shake, and flashes: reduced drops the white screen flashes, swaps the white hit flash for a dim red tint and stops fast flicker, and UI size: Auto grows the menus and HUD with the world on big screens, or 100% to 200%, clamped so phones and small windows stay at 100%; ui/uiScale.ts zooms #ui with CSS zoom), Controls (a touch joystick that follows the thumb or stays fixed in the bottom-left corner, and remappable controls: a key and a gamepad button per action) and About (Install the app, shown only where it can be installed: Chrome and Edge get their own install prompt, iPhones and iPads a Share → Add to Home Screen how-to; ui/install.ts; then legal, credits and the source). Saved in the browser. Each language page has its own manifest (public/manifest*.webmanifest), so the app starts on the page it was installed from (/, /en/, /es/); there is no service worker, Chrome no longer needs one to install.
Trainer Card: the Trainer chip at the top left of the title (your Pokémon, name, title and sync state; a Poké Ball for guests) opens your card: runs, wins, Pokédex, shinies, medals and ribbons, with editing your profile, sharing the card image, missions and signing in or out one tap away.
Pixel-perfect: the world renders at about 300px tall and is integer-upscaled, so sprites, effects and particles share one pixel grid.
| Keyboard / gamepad | Touch | |
|---|---|---|
| Move | WASD / arrows / D-pad / left stick | Drag anywhere (floating joystick), or the fixed joystick in the bottom-left corner (Settings → Controls) |
| Special attack | Space / A (✕) | Round button, bottom right |
| Mega Evolution | E / Y (△) | MEGA button, above the special |
| Arena: Move 1, Move 2, Unite Move | Q, E, R / RB, RT, LT (aimed by the Aim setting; the basic attack is automatic) | The right-thumb cluster: tap to cast with assisted aim, drag to aim, slide back onto the button to cancel |
| Arena: lock or switch target | Click a Pokémon (the ground lets go), Tab / right stick (flick or click) | Tap a Pokémon, or the basic attack button |
| Arena: quick chat, ping, scoreboard | T (then 1–8), G (or Alt+click, middle click, a click on the mini map; then 1–4), hold ` / View (8) for the scoreboard | Chat and ping buttons beside the mini map (or hold the mini map); tap the score for the scoreboard |
| Arena: surrender vote | Match menu from half-time; Y / N while a vote runs | Match menu; the vote's buttons |
| Pause | Esc / P / Start | Pause button, top right |
| Pick an upgrade | 1–3 | Tap it |
| Mute | M | Settings, on the title screen or in the pause menu |
| Menus | Arrows / D-pad, Enter / A, Esc / B back | Tap |
| Switch language | Settings → Game (← → on the language) | Settings (gear, top right) → Game |
| Trainer Card | C (title screen) | The Trainer chip, top left of the title screen |
On a gamepad, the D-pad and left stick move your Pokémon and drive every menu; A confirms in menus and fires the special in a run. Every action's key and gamepad button can be remapped in Settings → Controls (src/engine/bindings.ts); the arrows, Esc and the left stick stay fixed. Once a pad is pressed, the hints and the HUD show its buttons instead of keys, named after its layout (Xbox, PlayStation or Nintendo, from Gamepad.id; src/ui/inputLabels.ts), and the Controls screen names the pads the browser sees. Presses are edge-detected pad by pad, so a second pad holding a button can't hide it on the one in use.
Requires Node.js 22+.
npm install
npm run dev # http://localhost:5173
npm test # focused rules tests (vitest)
npm run build # typecheck + production build
npm run roster # rebuild the wild roster from PokeAPI + SpriteCollab (data/roster.generated.json)
npm run assets # re-download sprites/cries (already committed in public/)
npm run credits # regenerate CREDITS.md from SpriteCollab
In dev builds the browser console exposes ps.run() (current run state), ps.startRun('squirtle'), ps.theme() (music theme playing), ps.biome('cave'), ps.weather('rain'), ps.terrain('cliff') (walks you to the nearest rock, tree, cliff, tower or lake), ps.spawn('chansey', 3) (ps.spawn('pidgey', 1, true) for a shiny), ps.dex() (saved Pokédex), ps.pc() (the PC Box), ps.card() (the Trainer Card's share preview), ps.unlockShiny('charmander') (as if its 1/64 roll hit) and ps.event('chest') (places a map event next to you), plus ps.trio('beasts') (the run's legendary trio) and ps.boss('entei') (a wave boss now) to stage boss fights, ps.rift(), ps.portal(), ps.victory(), ps.endless() and ps.reaper() to stage rifts and the endless mode, ps.landmark('maze') (walks you to the nearest landmark of a kind), ps.place('cave') (an entrance next to you), ps.enter('graveyard') (straight in, the sky for any Pokémon), ps.floor() (the cave's stairs) and ps.spirits(19) (the keystone's spirits) to stage places, ps.mega() ('ready', 'stone', and a Mega id like ps.mega('now', 'charizardY')) to stage a Mega Evolution, and ps.egg('rare'), ps.walk(500), ps.hatch(), ps.hatch('gible', true, 2.8), ps.unlockFamily() and ps.incubator() for eggs. For moves: ps.learn('roarOfTime', 5) gives the run any move at a level, ps.sig('dig') stages a signature move, ps.kit('sweep') a move-kit primitive, ps.fork('swift'), ps.path('swift', 8, 'AB'), ps.offers('shiny') and ps.banishes(3) stage the level-up's forks, rarity and Forget, ps.evolve('vaporeon') and ps.ability('blaze') the evolution picks, ps.duo('firePledge') a duo fusion, ps.curse('lifeOrb') a cursed chest, and ps.tm('iceBeam'), ps.lesson() and ps.event('tutor') the TMs and the Move Tutor. For missions: ps.missions() lists them, ps.mission(i, n) sets one's progress (one short of done by default), , moves the missions' clock, and . For the outbreak and the Kecleon Shop: holds an outbreak instead of the day's (no argument goes back), fills the Bag, sets the shop's level, shows a level's building, stages the empty counter, brings Meowth's caravan, sets the loyalty stamps, puts up a seasonal stall, adds ₽ and owns every style. For invites and Mystery Gifts: lands as a guest invited by @ash (the title shows the invite banner, without asking the API) and lands a gift as if the account brought it (, , , , ; is an inviter's handle or a count of friends).
Vite + TypeScript and Canvas 2D, with no game engine. Everything is plain data plus functions (no classes). The UI is DOM with pixel fonts (Press Start 2P / VT323).
src/
engine/ math, deterministic math (dmath), seeded rng, spatial hash, input, WebAudio sfx and music synth, pixel-art helpers
data/ pure game data: starters, egg families (hatchlings), enemies, waves, biomes, weather, held items, move evolutions, type chart, music scores
game/ simulation (fixed 60 Hz tick): game.ts orchestrates the systems · players.ts (who acts, who world systems target) · coop.ts + lockstep.ts (co-op rules and the tick driver)
moves.ts (weapons) · attacks.ts (hitboxes) · enemies.ts · spawner.ts
combat.ts · pickups.ts · leveling.ts · effects.ts · special.ts (manual special attack) · mega.ts (Mega Evolution)
status.ts (pure condition rules) · statusSystem.ts (DoT ticks, particles)
consumables.ts (floor items, buffs) · props.ts (breakable item sources) · terrain.ts (rocks, trees, bushes, logs, mountains, towers, lakes, landmarks) · scenery.ts (painted pieces from `public/assets/scenery`)
landmarks.ts (treasures, the tunnel, naming) · landmarkLayout.ts (their pieces and the maze, pure)
mapEvents.ts (event director, altars, chests, traps, Meowth) · mapEventRules.ts (their pure rules)
bosses.ts (boss attack patterns, enemy hazards) · bossFight.ts (stances and rage: pure rules)
hazards.ts (the world's own strikes at the player, by difficulty, weather and biome)
horde.ts (elites and runners) · shiny.ts · biomes.ts · weather.ts
dex.ts (Pokédex rules and save format) · sightings.ts (who's on screen)
rift.ts (rift rules) · riftPortal.ts (portal on the map) · riftArena.ts (the arena) · room.ts (what rift arenas and places share)
place.ts (places' rules) · placeEntrance.ts (entrances on the map) · placeInterior.ts (the inside)
cave.ts (the Mystery Dungeon) · dungeon.ts (its floors, pure) · graveyard.ts (ghosts, the keystone, Spiritomb) · sky.ts (flight, wings)
endless.ts (endless rules) · endlessSystem.ts (horde events, Arceus)
tension.ts (pressure director rules) · tensionSystem.ts (reach, harm, close spawns)
eggs.ts (egg drops, incubator, hatching: pure rules)
gifts.ts (Mystery Gifts: parsing and applying them) · referral.ts (invites: landing, who counts, milestones)
tally.ts (run counters and summary) · missions.ts · achievements.ts (missions, medals, titles)
difficulty.ts (Normal, Hard, Nightmare as multipliers)
moveKit.ts · moveHooks.ts · mods.ts (move primitives, per-move state, fork mods) · learnsets.ts (per stage)
abilities.ts · branch.ts (branching evolutions) · evoGate.ts (evolution items in a run) · tms.ts (TMs, Move Tutor) · duoRules.ts · curses.ts
snapshot.ts (snapshot, restore and hash of the whole state) · stateDiff.ts (where two states differ)
determinism/ scripted bot runs for the snapshot proofs and the cross-engine harness (npm run determinism)
coop/ co-op netcode, DOM-free (the soak's bots run the same code in Node): client.ts (socket, rooms, reconnects) · runner.ts (lockstep)
hold.ts (adaptive input hold) · outbox.ts (coalesced stick, reliable picks) · predict.ts + view.ts (draw-only prediction and interpolation)
bank.ts (banking a run once) · entries.ts + start.ts (the wire's inputs and loadouts) · simBuild.ts (the build id) · bot.ts · driver.ts
lobby.ts (the room's rules for the lobby screens: seat states, readiness, invites)
net/ protocol.ts: the co-op wire protocol, shared with the relay (relay/, Cloudflare Worker + Durable Objects) · friends.ts (friends, presence heartbeats and co-op invites through the API)
render/ PMD sprite loader, code-drawn pixel art, procedural biome ground, weather overlays, map events, title scene and logo, frame renderer
ui/ title screen, HUD, Pokémon select (with the egg box), incubator and hatching scene, level-up, pause, settings, results, missions and achievements, toasts, Pokédex, the menus' shared shell (`ui/shell.ts`: top bar, Trainer chip, wallet, gear, action bar) and the Trainer Card
co-op outside a run: coopRoom.ts (the hub: create or join; the room: seats, ready, the host's start) · friends.ts (Amigos and the invite banner)
co-op in a run: teamLayer.ts → teamHud.ts (teammates' cards), coopHud.ts (connection pip, reconnect veil), teamScreens.ts (team waits, spectator, endless vote, end screen), teamAwards.ts; render/team.ts + teamCamera.ts draw teammates and follow them
i18n/ every player-facing string: pt.ts (reference), en.ts, es.ts, fr.ts and zh.ts; site/ has the website copy
site/ static pages rendered at build time from data/: Pokédex, guide, changelog, legal
account.ts optional accounts: Clerk loaded on demand, save sync with the API
analytics.ts events and error reports: PostHog (loaded once the game is up, through the /ingest proxy)
coopShell.ts co-op's browser glue: the tab's client, the room link (`?sala=`), banking a run once, dev bots
ranking.ts ranked runs (start token, offline queue), boards, profiles and follows through the API
server/ accounts API (Hono + Postgres, Clerk tokens), deployed with Docker: see server/README.md
relay/ co-op relay: a Cloudflare Worker with one Durable Object per room, no game code (imports only net/protocol.ts): see relay/README.md
Game.players holds everyone in the run (one solo) and every per-player field lives on Player (moves, items, picks and their phase, the special, the Mega form, the ability, curses, charges). Per-player code reads g.player, the player acting: the step runs walking, moves, picks and damage taken as each player in turn (actAs, eachPlayer in game/players.ts), and every hitbox acts as its caster (Attack.owner). World code asks which player it's about: nearestPlayer (the horde, bosses, Alphas, traits), anchorPlayer (spawns, rings, map events, portals take turns), playerWithin (traps, altars, chests, portals), onAnyScreen (spawns and despawns stay off every screen). A pick belongs to its player (Player.phase, setPhase, phaseOf): the world waits while anyone picks. Solo, every helper gives the one player without touching the rng, so solo runs stay bit-identical; determinism.test.ts pins a few golden runs. The shell reads its own player with localPlayer(g).createCoopGame(seed, loadouts) builds a 2–4 player run (g.coop set; null solo, so solo never takes these paths) and stepTick(g, entries) (game/lockstep.ts) drives it: each tick applies the players' TickEntrys (the stick, held in g.coop.held; presses; picks quoting their screen's round, so late ones drop; away/back/leave; the endless vote), holds the world only for level-ups (shared EXP levels the whole team on the same tick, so everyone picks at once; a timer and away players pick for themselves, autoPick) and steps. A player's own moments (their evolution and the Ability or branch after it, a TM, a Poké Ball's prize, a cursed item, a Mega Evolution) wrap them up, untargetable, while the team plays on. A fainted player lies as a Poké Ball until a teammate stands by them; the run ends when nobody can play on. The horde, bosses and ₽ scale with the active players (data/coop.ts), every Exp. Candy reaches the whole team (the same EXP for everyone), bosses and rifts drop each player's own TM, Poké Ball, Alpha prize and Mega Stone (Pickup.owner), conditions remember who put them on (Affliction.by), and coopPayout splits the team's ₽ by the players who started the run.step(game, input, dt) mutates the state and pushes GameEvents. drains them each frame to play sounds and cries and show toasts. Toasts and banners go through one queue (, rules in ): banners show one at a time, toasts stack in a column whose CSS keeps it clear of the Pokémon, and both wait out menus with their clocks stopped, so nothing is covered or lost.Bug reports and ideas are very welcome: open an issue. Pull requests are welcome too. Please read CONTRIBUTING.md first and follow the Code of Conduct.
Pokémon Survivors is an unofficial, free, non-commercial fan project made by fans for fans. It is not affiliated with, endorsed, sponsored or approved by Nintendo, The Pokémon Company, GAME FREAK inc., Creatures Inc. or Spike Chunsoft.
Pokémon and Pokémon character names are trademarks of Nintendo. Pokémon © Nintendo, Creatures Inc. and GAME FREAK inc. All other trademarks belong to their respective owners. The game has no ads, no paid content and no in-app purchases. Poké Dollars exist only inside the game.
The MIT license covers only the original source code of this repository. It grants no rights to Pokémon characters, names, sounds or artwork. The sprites and portraits are distributed under CC BY-NC 4.0, so the game as a whole cannot be used commercially.
If you hold rights to any material used here and have concerns, please open an issue or contact @pedrobarretto. It will be addressed promptly.
🇧🇷 Pokémon Survivors é um fan game não oficial e sem fins lucrativos. Não tem nenhum vínculo com a Nintendo, The Pokémon Company, GAME FREAK ou Creatures, nem é endossado por elas. Pokémon e os nomes dos personagens são marcas registradas da Nintendo.
ps.completeMission(i)ps.date('2026-10-19')ps.unlockAchievement(id?)ps.resetProgress()ps.outbreak('gastly')ps.item('lure', 3)ps.shopLevel(3)ps.plaza(3)ps.surprise()ps.caravan(true)ps.stamps(7)ps.stall('halloween')ps.money(20000)ps.unlockStyles()ps.invite('ash', 'Ash')ps.gift(kind, arg?, why?)ps.gift('egg', 'friend', 'ash')ps.gift('coins', '500')ps.gift('title')ps.gift('style', 'ambassador', 3)ps.gift('shinyHatch', 'munchlax')whymain.tsui/toasts.tsui/toastQueue.tsmax-heightfire(game, stats) function. MOVES (game/moves.ts) spreads in tables kept in their own modules (signatureMoves.ts, eggMoves2.ts, tmMoves.ts, verbEvolutions.ts, duoMoves.ts, legendaryMoves.ts), whose casts build on the move kit (moveKit.ts: sweep, strike, return, pull, trailer, guard, burrow, throw, drain; moveHooks.ts runs its per-move state). Upgrades are stat deltas, and every base move forks at Lv 4 and Lv 7 into two branches, each with a behavior mod (mods.ts: split, bounce, echo, trail, burstOnKill…). Player modifiers (might, area, cooldown…) are applied in moveStats. Each hitbox is a generic Attack with a motion (linear, homing, orbit, boomerang, follow, static, return) and a visual. Evolutions are data/evolutions.ts; signature, TM, legendary and duo moves never evolve (neverEvolves). Learnsets are per evolution stage (game/learnsets.ts), and the level-up rolls rarity, forks, Esquecer (banish) and the opening draft in leveling.ts.data/abilities.ts holds each Ability as a typed list of effects plus the family and branch sets, game/abilities.ts applies them (stat deltas in recomputeStats, hooks in hurtPlayer, damageEnemy and killEnemy). data/branches.ts lists the branching evolutions and game/branch.ts resolves a run's form (formAt, playerForm) and where an evolution may go with the run's evolution items (evolutionOffers: a branch an item names needs it, Eevee draws one of its free three). game/evoGate.ts holds the items found in a run (a boss's drop) and the evolution held without any. Both picks are offers, recorded in the replay like a level-up pick.data/tms.ts maps each boss and legendary to its TM, game/tms.ts drops and teaches them (the forget flow too) and the Tutor is a map event; game/duoRules.ts holds the duo recipes and game/curses.ts the cursed items' rules.data/waves.ts: spawn stages by time, each a list of tier slots, plus events (flyby swarms and closing rings by tier, bosses, optionally rare via chance). A boss event names a fixed boss, a slot of the run's legendary trio (TRIOS: the slot owns the stats, so every trio is as hard) or the rare mythical (MYTHICALS); the roster draws both with the run's seed, so replays meet the same bosses. The horde keeps turning over (game/renewal.ts): at the cap each new spawn takes the place of the oldest regular enemy off-screen, and off-screen enemies older than a minute retire. Bosses, shinies, swarms and anything on screen are never touched. Past ELITE.fill of the cap new spawns are elites (not while the finale boss lives), and each stage's runner species (drawn with the roster) spawns fast and aims ahead of the player (game/horde.ts). From SPITTERS[difficulty].from a share of spawns are the stage's spitter species, drawn with the roster from outside its cast (game/spitters.ts): they hold a band of distance, glow and spit slow shots flavored by type (enemy hazards, so the aura doesn't stop them), and hold fire during the finale. For the first BOSS_FIGHT.duration s of a wave boss near the player, spawns drop to BOSS_FIGHT.rate. On top, the pressure director (game/tension.ts) raises a 0..1 tension (its ceiling climbing from 1:30 to 10:00, so the early game stays a warm-up) while the player goes unhurt (faster when regulars die near the screen edge) and drops it when they get hurt or run low; it scales spawn rate, spawn closeness, pace, tense elites and extra rings and swarms (splitting the same EXP among them) up to each difficulty's TENSION_PEAK, and holds still while the finale boss lives.npm run roster checks a curated list of species against SpriteCollab and pulls names, types and base stats from PokeAPI into data/roster.generated.json. data/roster.ts assigns each a tier by base stat total and derives its game stats. game/roster.ts draws each run's cast (seeded, weighted by biomeWeight towards the types of the biome each stage plays in), keeps species with a trait to at most one per stage from minute 1, and the shell, having let go of the lobby's sheets, loads only the players' families and the first stage before the run. From then on it streams the current and next stages' species and frees the ones behind (render/spriteNeeds.ts, sweepSprites), so a phone's memory stays flat over a run: a rift's legendary loads from its omen and each of its forms one battle phase ahead, a Mega once its stone and the final stage are in hand, a place's dwellers from its entrance, a shiny when one shows up. Images load at most two at a time (DECODES), the most urgent first, so a burst lands as a trickle. Hit flashes, status tints and outlines are per-frame silhouettes packed into a few capped atlas pages (render/sprites.ts).data/bosses.ts (telegraphed strikes, projectile rings, lingering trails). They spawn Hazards, which hurt the player. Wave bosses tag their patterns with a stance (close or ranged), swap stance every few seconds and enrage under half HP (BOSS_RAGE; rules in game/bossFight.ts); rift legendaries and Arceus keep flat patterns. The world strikes too (game/hazards.ts, data/hazards.ts): from a minute set per difficulty it drops telegraphed volleys, lines across the player's path and lingering patches where they stand, flavored by the weather and biome, through the same Hazards; quiet in rifts and boss entrances, cut while a boss fights. To add a legendary, add it to ENEMIES (legendary: true), add a boss event and its patterns, and add its dex to scripts/dex.mjs.MAP_EVENTS (game/mapEvents.ts): each kind has a spawn rule for the event director (earliest time, weight, max alive, lifetime, distance), a create for its state, an update with the interaction and reward, and optional enter/expire hooks. To add one, add its state to MapEventStates, its definition, its art in render/mapEvents.ts (plus an arrow in MARKER if it's important) and, if it talks to the player, a MapEventNotice handled in ui/mapEvents.ts.data/terrain.ts, game/terrain.ts): each world cell holds at most one feature (a rock pile, a grove, bushes, a fallen log, a mountain, a lake, a tower or a landmark) and a few lone bushes, rocks and trees strewn clear of it, placed from the run's seed so replays rebuild the same map, kept off the cell's edges so neighbors never close a passage, and never near the start. Footprints are unions of ellipses from data, the same in every biome: render/terrain.ts dresses them per biome (the meadow's hill is the cave's rock wall), sculpting the sprites in code with render/sculpt.ts, except towers, trees, rocks, bushes and a few landmark pieces, which are painted PNGs (made with PixelLab) picked per biome in render/scenery.ts, with the sculpted piece standing in until the picture loads. Solids push walkers out (enemies slide along them toward the player) and break enemy shots; lakes slow walkers, or make the player slide on ice in the snow. Flyers, ghosts, bosses and the player's attacks ignore terrain. Tall pieces go see-through where the player walks behind them.data/landmarks.ts) are terrain: one block of 3×3 cells in LANDMARK.odds holds one, in a cell drawn from the seed, so they never crowd. game/landmarkLayout.ts lays out each one's pieces around its anchor (pure: the cottage's walls as chains of ellipses, the maze carved depth-first from its heart and cut into one solid per row of hedges so the player walks in front of the ones north of them and behind the rest, the ruins' ring, the tunnel's ridge and mouths). game/landmarks.ts names them (spot events), opens treasures once (looted remembers the cell when the map rebuilds it) and carries the player through tunnels (transit: foes lose track, the player's walking and collision wait). The art lives in render/landmarkPieces.ts (walls and hedges raised from boxes by extrude, windows cut through to the ground behind; the ridge is a mountain in the biome's skin with mine mouths) and render/landmarks.ts (the campfire, glints, the dark underground).data/places.ts) work like rifts, on their own schedule (placeDue: a slot every ~4 min, after rifts and bosses): placeEntrance.ts places an entrance just off screen (terrain-like solids from PLACE_FEET, so it collides and props make room), placeInterior.ts steps the player in through room.ts (the stash and floor-keeping rifts also use) and runs the place's rules: cave.ts lays out Mystery Dungeon floors (dungeon.ts: rooms on a grid, Z-shaped corridors, stairs in the farthest room; dungeonNav steers walkers through the corridors, hooked into updateEnemies), graveyard.ts turns fainted ghosts into spirits for the Odd Keystone and wakes Spiritomb, sky.ts drifts wings across (they add stats through wingDeltas in recomputeStats). A dungeon floor's title card holds step() still (updatePlaceIntro) while the HUD shows it. Everything is seeded, so replays re-simulate it. The art lives in render/placePieces.ts (the entrances, sculpted; the cave's mouth is carved into a hill in the biome's skin) and render/places.ts (floors, the cave's dark, the sky, wings, the arrow).riftPortal.ts: a portal opens, the player stands in it) apart from the arena (riftArena.ts). Entering sets the world aside (horde, hazards, pickups, props; map events, weather and the biome schedule pause) and leaving puts it back; the run clock keeps going. The portal stays out of MAP_EVENTS: it runs on a fixed schedule (riftDue: a slot every ~4 min, waiting up to 45 s for a living boss to fall) that the event director's random pacing can't promise. Legendaries, arena colors and timings live in data/rifts.ts; each arena's floor plan (shape, scenery, shallow water, lava and bridges, the rising tide or the crumbling edge) in data/arenas.ts, its geometry in game/arenaFloor.ts (pure, tested) and its look in render/rift.ts. The arena's scenery collides through game/terrain.ts like the map's.continueEndless). endlessPressure raises the spawn rate past 15:00 and endlessSystem.ts runs rings, swarms and the reaper.game/alphas.ts, knobs per difficulty in ALPHA_RULES): from about 4:00 an oversized member of the stage's top tier joins the horde every minute or so. It is a boss to the combat rules (no knockback, executes or long holds) but takes no HP bar slot, never flees for the next wave and isn't a boss kill. It aims a charge (a red lane on the ground) and dashes along it; on Nightmare the dash ends in a shockwave and it enrages under half HP. Beaten, it drops EXP, ₽ and a prize that opens a level-up of rare cards. The regular horde also stiffens against knockback as the minutes pass (hordeGrit in game/horde.ts).shinyBoost (each Shiny Charm rank adds a third, a Lure doubles it). Shiny sprites come from SpriteCollab's 0000/0001 form, stored in public/assets/pokemon/<dex>/shiny/. The starter's 1/64 roll is always drawn; a shiny picked on the select screen (shinyStarters in the save, game/shinyStarters.ts) only reaches the run as ReplayStart.shiny, so it changes the colors and nothing else, and the shinyStarter medal counts only the roll (player.luckyShiny).game/outbreak.ts draws one species per Brasília day (game/day.ts) from the plain tier-2 species with a shiny sprite, walking seeded cycles so each gets its day. withOutbreak adds it to every stage of the drawn roster at the weight of the stage's heaviest species (never as a runner), after the draw, so the rest of the cast stays the same. Its shinies come from the volume (measured: ~1,070 spawns and ~2.4 shinies a 15-min run, on the regular odds). It's a run input (ReplayStart.outbreak), and the API fails a replay whose outbreak isn't the one of the day its start token was made, or the day before (outbreakFits).SHELVES) in ui/shop.ts, Kecleon's lines in ui/kecleon.ts. Upgrades are UPGRADES in game/meta.ts (an optional growth per upgrade; the Shiny Charm doubles). The Kecleon Shop's catalog is data (data/shop.ts: five shop levels, eight Bag consumables, the Vitrine's pool) and its rules live in game/shop.ts: the Vitrine do dia is drawn from the Brasília day (a free gift, the Kecleon Egg's fixed card outside the rotation, a 30% deal, the day's cards in a rotation that brings each back within 7 days, the egg's tier, the day's style), the same for everyone, with a daily stock per card (shop in the save, with the level and the wishlist); higher levels see more of the same Vitrine. Investing in a level is for good and counts in the fortune like the ranks. Bought items go to the Bag (game/bag.ts: per item a got and a used counter, so a merge never brings a used item back), and the run uses the armed ones up as it starts (useArmed, as many different ones as the level's bagSlots), recording them as ReplayStart.bag; each does its effect in the run. Styles are priced in data/style.ts, drawn from render/cosmetics.ts and owned and worn in the save's style (game/style.ts). Everything bought on the Vitrine or the style shelf adds to the save's spent, which the sync counts as part of the fortune. The shop is a place too: the lobby's camera rests at the partner's home and the Kecleon plaza stands east of it (render/plaza.ts: cobblestones and the round stone pad drawn in code, the PixelLab building of the shop's level and its props, Kecleon and its purple brother recolored from the same sheets). Passear (the paw button beside the partner) puts the lobby's cards away (inert while away, as on a trip, so taps reach the meadow and not the hidden buttons) and leaves the partner out to walk the meadow and the plaza, a tap or a drag steering it as in the lobby (exploreMeadow); a talk there opens its sheet, and closing it walks on. The Loja tile (L), a tap on a shopkeeper, E beside one and ?loja go there: visitPlaza (render/titleScene.ts) pans the camera while the lobby's cards slide out, the shop's panel opens over the plaza (a side panel, or a bottom sheet on portrait phones) and Kecleon talks beside it in the dialogue box (ui/dialogue.ts): what it says follows a priority of triggers (pickTrigger: a first visit, the wishlist's item back, a win against Mewtwo, a daily streak kept in the save's shop.visit, a heavy wallet…). Investing swaps the plaza's building live with a celebration. Visitors come by too (data/visitors.ts): Meowth's weekend caravan (game/caravan.ts: a stock drawn from the ISO week, one of each per weekend in the save's caravan, a haggle for loyal customers), the loyalty card (game/loyalty.ts: shop.stamps, one per Brasília day with a visit, its milestones paid as the game's own Mystery Gifts, game/gameGifts.ts) and the seasonal stalls (game/stalls.ts), each drawn on its own stone pad. Around a Festival da Praça the fountain stands in front of the shop (PlazaFestival in render/plaza.ts, from the pot src/festival.ts already fetched): while it runs the plaza becomes the Lantern Ring, a curbed round square with its own cobbled approach (RING in render/plaza.ts), dressed by the pot's stage (the fountain, the Togepi float's spot and the iron gate down its axis, six lamps with lantern arms around the fountain, the wishing tree on the west, Lanturn's Lantern Market on the east, then wish tags, bunting, a wheel of lantern strings, and at the end a warm dusk, the Togepi float, Volbeat and Illumise and sky lanterns; the float turns shiny at ₽150M), the donors' plaque stands beside it, and a talk at the fountain, the plaque or Lanturn opens the festival's sheet (ui/festival.ts).game/training.ts, Game.training, null in every other run): stepTraining in game/game.ts runs only the players, their moves and the dummies (wild species standing still at their posts with 10M HP topped up every tick, data/training.ts), so the clock, the horde, bosses, the map and the weather wait and nothing levels up or hurts. The shell changes it directly (setMove, setFork, setItem, setForm, setAbility, setDummies): a training run is never ranked, banked or reported as a run (one dojo_end event instead). Its panel (ui/training.ts) reads Player.damageByMove into a DPS meter (readMeter). Builds (game/builds.ts, the save's builds, one per family, the newer device's winning) are what the player saved there; the level-up, evolution and ability screens mark the cards that follow one (planned, ui/buildMark.ts) and never pick for the player. The Dojo is a place too, west of home (render/dojo.ts: a yard drawn in code, the PixelLab hall, posts and banners from public/assets/dojo/, Machoke at the door and Wobbuffet in the yard), reached by the Dojo tile (T), a talk with Machoke on a walk or ps.dojo().render/card.ts lays each card out on a 270×338 canvas (code art, PMD sprites at whole scales, the game fonts thresholded to hard pixels) and blows it up ×4 into a 1080×1352 PNG. The look comes from render/cosmetics.ts (card frames, backgrounds and PC Box wallpapers as data, plus the defaults), so a new style is a new entry there. ui/share.ts builds the card data from the run or the save and shows the preview (share sheet on phones, clipboard on computers, download otherwise). Links carry the sharer's handle as ?ref=; on boot captureRef (ranking.ts) keeps the first handle a device lands with in ps:ref (never its own; rules in game/referral.ts).gifts table). On every sync account.ts fetches the unclaimed gifts, applies each to the save with applyGift (game/gifts.ts: an egg, ₽, a gift title, a style, or a family hatched shiny on the spot; an egg waits while the incubator is full) and claims them; the save keeps the applied ids (gifts, a union on merge), so a gift never lands twice. ui/gifts.ts shows the "Presente Misterioso!" banner and main.ts plays a shiny hatch's scene out of a run. Invites: the device's first sync for an account sends the stored ps:ref before the save goes up (POST /referral); the API records it only for a new account (canRefer in game/referral.ts: no save yet, created in the last 48 h, not credited before, not oneself), makes both follow each other and sends the newcomer a Friendship Egg. After each finished run gifts.credit checks whether the newcomer reached REFERRAL.runs, credits the inviter once and sends the gifts of the milestone their count hits (FRIEND_MILESTONES). Gift titles (GIFT_TITLES in data/achievements.ts) live in the save's titles next to the medal titles, and gift-only styles have a null price in data/style.ts (the shelf lists them once owned).server/src/webhooks.ts) and is credited once to the account of its handle, or gets a code to redeem (server/src/supporters.ts); each tier the total reaches sends its gifts (TIER_GIFTS in game/supporters.ts). A shinyPick gift leaves a ticket in the save (shinyPicks, merged by id) that ui/supporters.ts spends on any starter or egg family (pickShiny in game/shinyPicks.ts). The companion (companion in the save, a supporter's) is drawn only, by render/companion.ts, so the simulation and its replay never see it. The lobby's Hall card opens the Hall da Fama (ui/supporters.ts): the Lendas (gold) on pedestals with the Pokémon the wall reads from their synced save (hallPokemon in game/trainer.ts), the other supporters as chips, and the call to action with the cosmetic perks and the support page with the player's handle; a supporter's perks become their actions there (the shiny picker, the companion, the poll, code redemption, showing themselves).status: { kind, chance, duration, power } in data (burn, poison, paralysis, slow, sleep, freeze, mark, confuse, seed, weaken; the Poké Flute inflicts sleep too). Burn and poison deal damage over time, burn halves contact damage, poison stacks up to 5x, paralysis stuns and stutters (shorter on bosses), freeze holds a foe until the next hit shatters it, marks stack extra damage taken, confused foes hurt each other, seeds drain to heal you and jump on a faint, and weaken cuts contact damage.special.ts per Pokémon type (with optional per-species overrides). Fire, Water and Grass have bespoke forms; the other types pick one of six shared forms (bolt, nova, rain, ring, wave, trail) plus a color and a status, drawn by render/specialFx.ts. Per-species overrides (BY_SPECIES, per stage if needed) give the agile lines a chain dash (a lunge into the nearest foe; a press within 0.5 s of a hit jumps to the next, and the cooldown starts only when the chain ends) and Aegislash its Stance Change (stance.ts: Shield/Blade multipliers on the final stats). Input reaches step() as data ({ move, special, mega }), so a chain's follow-up presses replay like any other press.data/megas.ts holds each Mega as data (line, asset form, Mega Stone, stats, special, ability effect: stats, an aura status or a weather), game/mega.ts the rules. A beaten rift legendary drops the line's stones (riftArena.ts; two to choose from, which the magnet leaves alone, for the lines with two Megas) and the one picked up sets mega.stone; at the final stage a press starts the transformation, during which step() holds the world still (updateMegaIntro), then the form adds its stats in recomputeStats and gameSpecial swaps in its special until MEGA.duration runs out and MEGA.recharge starts. Mega sprites are alternate forms (FORMS in scripts/dex.mjs, only for the Megas SpriteCollab has drawn; the others fall back to the final stage plus an outline) and Mega cries come from PokeAPI (FORM_CRIES); the art lives in render/megaFx.ts.damageText.ts groups them per target (a short merge window, a cap per target, fixed slot offsets) as a pure rule. Which ones get drawn is presentation: render/damageNumbers.ts applies the setting, and reduced (the default) keeps the 8 biggest on screen plus the boss's, shown a bit shorter.PT | EN | ES | FR | 中文, or I) and in Settings (pause menu). Text lives in src/i18n: pt.ts is the reference and its inferred type forces en.ts, es.ts, fr.ts and zh.ts to match it, down to one line per move upgrade. Data files keep only the official English names. Toast strings use light markup (*bold*, _accent_) so each language can put names where its grammar wants them.! mean super effective.data/music.ts holds the scores in a small tracker notation (engine/score.ts) and engine/music.ts plays them on two pulse channels, a triangle bass and a noise kit. The shell picks the theme each frame (playTheme). Music and effects have separate buses in engine/audio.ts, so music always sits under effects and cries.data/biomes.ts holds the biome order (equal slices of the run), each biome's weather odds, theme and affinity (the types that feel at home there, which weight the enemy draw through biomeWeight). game/biomes.ts follows the schedule and game/weather.ts rolls weather spells and applies their rules (data/weather.ts): weatherBoost/weatherPower multiply damage by attack type in combat.ts (player moves, enemy bodies by their types, boss hazards by HAZARD_TYPES), and sandstorm/snow wear down anyone not immune. The ground and breakables are drawn per biome in render/world.ts and render/props.ts, the overlays in render/weather.ts.localStorage. game/meta.ts holds the pure rules.game/difficulty.ts holds the levels as plain multipliers (DIFFICULTIES; Normal's multipliers are all 1s, plus the i-frames each level grants after a hit). Each one is applied in a single place: enemy HP in spawnEnemy (enemyScale), the damage the player takes and the i-frames after it in hurtPlayer (armor takes 1 off per point, up to ARMOR_CAP of a hit), spawns in the spawner, the time slope through scaling's ramp and every heal through healPlayer. The run keeps its difficulty and summarizeRun records it, so the ranking can keep a board per level.account.ts loads Clerk when the player signs in (or on boot, on a device that did) and pushes the save at sign in, on boot and at the end of every run. game/sync.ts, shared with server/, holds the rules: mergeSaves joins two saves keeping the larger side of everything (unions for the collection and the styles, max for upgrades, records, missions and the bag's counters, the larger fortune for ₽, spending included), so merging again changes nothing; applyPush stores a push made on top of the latest revision as is (so spending and hatching stick) and merges anything else (a first sign in, two devices). A failed sync never blocks the game: the save stays local and syncs next time.game/ranking.ts, shared with server/, holds the rules: what a run reports (RunResult), the checks it must pass (parseRunResult for plausible numbers, clockAllows for the wall clock since its start token, with RANKED.slack for a late token) and the handle and display name rules. A signed-in run asks for its token when it starts loading (startRanked in ranking.ts), reports at a faint or a victory and again on the way out if it went on into endless mode; the result waits in ps:runs until the API takes it, so a run played offline still lands later, and nothing waits on the network. The boards keep each player's best run (server/src/ranking.ts); days and weeks follow BOARD_TIME_ZONE (America/Sao_Paulo). The ranking and profile screens are DOM screens in the game (ui/ranking.ts, ui/profile.ts), and a shared link /?u=handle opens the game straight on that profile.data/bodies.ts, measured from the PNGs by npm run bodies, so they don't depend on when a sprite downloaded) and engine/dmath instead of Math.sin, Math.atan2, Math.hypot, Math.exp and **, which differ in the last bit between V8 and JavaScriptCore, and of Math.round, which JavaScriptCore's JITs compiled to floor(x + 0.5) until 2026 (engine/dmathGuard.test.ts fails on any of them in the simulation's modules). Every run records a replay (game/replay.ts): its start (seed, Pokémon, Poké Mart ranks, difficulty, the day's outbreak, the bag items used, screen size) plus the stick per tick (32 directions × 7 speeds, run-length encoded; the run itself plays with that quantized stick) and the rare events (special, level-up picks, rerolls, skips, endless mode, screen resizes). A ranked run sends it with its result and shows on the boards at once. The API re-simulates the runs on show (the tops and the challenge's official attempts, plus a sample of the rest) on a worker thread after answering (server/src/checks.ts): the run is verified when it ends on the same time, faints and level, and leaves the boards otherwise.src/coop): the client joins a room on the relay its code names (coop/relays.ts: Cloudflare's, relay/, VITE_COOP_URL, dev builds default to the dev relay; or a VPS relay, relay/node on Dokploy: São Paulo for codes starting with 2 to 5, VITE_COOP_URL_SA; a host's room goes there when it answers within 70 ms) as a signed-in player (a fresh Clerk token on every connect) or a guest (an id kept on the device), and keeps the seat secret by room code in localStorage, so any tab opened from the link (?sala=K7QF2) takes the same seat. The runner keeps an adaptive hold of ticks against jitter (2–10, the 98th percentile of the last 10 s of arrivals plus one; up at once, down slowly; stalls left out), slows 10% when the buffer runs thin and catches up 25–100% faster when it's fat, within a CPU budget; far behind (a hidden tab, a fresh join) it syncs as fast as the budget allows behind a progress bar. It sends the state hash every second and, when the relay asks, a deflated snapshot every 10 s (taken at once, compressed off the frame); a diverged client restores the agreed snapshot and replays the short tail. Reconnects back off with jitter and resume from the last known tick; a TCP stall shows the reconnecting veil after 1 s and a dead socket is replaced after 6 s. Upstream it sends at most one message a display frame (a batch: Cloudflare bills the relay per message received): the stick goes out when it changed enough (stickPace.ts: at once for a start, a stop or a turn, a wobble once its drift adds up, at most 30/s), the hashes, the ping and an anchor for prediction ride along, and picks are reliable (resent until seen stamped; a duplicate quotes a stale round and drops). The local player is drawn where the stick and the buttons already took it: the real walking step, the special (a press starts its charge, dash or chain jumps at once when the timers say it's ready), the lunges and rolls under way (a hitstop never holds co-op, so it never holds the copy either), against the real terrain, on a copy (predict.ts); a misprediction glides back, and only a teleport snaps; the world draws from a copy too, teammates and enemies between ticks (view.ts). A run banks once per player (bank.ts, by room and run, before the wallet moves). npm run soak plays bot rooms through a chaos proxy against wrangler dev.ui/coopRoom.ts): create a room or join by code; a link (?sala=K7QF2) joins from anywhere. Guests get "Entre para jogar junto" first: co-op needs an account with a @handle (coopAccess). The room screen redraws on the client's room events: seats in their colors, ready, the host's difficulty, kick and start (enabled once everyone online is ready, the relay's own check in coop/lobby.ts). The run's run event replaces it, and the end screen's Voltar ao lobby banks and comes back to it. Co-op plays the normal mode (COOP_LOCKS, data/coop.ts), so the partner select's co-op mode (starterSelect(..., { coop: true })) has no Lendários tab, Bag, TMs, Bottle Caps or Poké Mart, and a legend partner comes as the first starter (coopStarter). A newer build waits while the player is in a room (inCoopRoom in main.ts), as it waits for a run: leaving the room reaches the title, which reloads. Friends, presence and invites (net/friends.ts, ui/friends.ts) need an account and fail soft; presence beats for every signed-in player in a build with co-op.__SIM_BUILD__, a hash of the simulation's source only (coop/simBuild.ts, computed in vite.config.ts): every module reachable from the lockstep, the snapshots, the co-op rules and the netcode's entries and loadouts, plus RULES_VERSION and PROTOCOL. A deploy that changes the UI, the site or the copy keeps the id, so a reload rejoins the room; one that touches the simulation gets an "update to play together".game/snapshot.ts writes the whole simulation state as bytes (snapshot), brings it back (restore) and hashes it (stateHash), walking the state structurally: plain objects, arrays, Maps and Sets with their order, exact number bits, shared references and the run's rng (SeededRng, whose mulberry32 state is plain data on it); the spatial grid and the terrain's cells are caches it leaves out and rebuilds. Anything else in the state (a function, a class instance) makes it throw, naming the path. Co-op peers compare stateHash and heal from snapshots; nothing in the game imports it. npm run determinism bundles determinism/harness.ts once and plays the same bot runs (four long ones with bosses, rifts, Megas, a branching evolution and endless mode, then every playable for 3 minutes) in Node, Chromium, Firefox and WebKit, comparing hashes every 60 ticks; on a mismatch it finds the first differing tick, saves both snapshots in determinism-out/ and prints the differing paths (npm run determinism -- diff a.bin b.bin reads two snapshots back). CI runs it as its own job.dexLog (species on screen via sightings.ts, defeats and caught shinies in killEnemy); the shell folds it into the save's dex field when the run is banked. game/dex.ts holds the pure rules: a compact [defeated, flags] mark per dex number, markCaught (defeated shinies, hatched eggs via markHatch, and the forms the player played, logged in dexLog.own) and dexCounts for anything that reads progress, like the Pokédex achievements.game/eggs.ts holds the rules (drop odds per source, the per-run cap, steps per tier, the incubator, hatching, duplicates). A run counts player.walked; the shell banks steps and found eggs into the save (eggs, hatched) like ₽, and ready eggs hatch when the player leaves the run (ui/incubator.ts). The playable families live in data/hatchlings.ts with the starters' shape (PlayableDef), and PLAYABLE merges both. grantEgg gives an egg from outside a run (mission rewards).tally (kills by type, weather and biome, shinies, bosses, map event notices), bumped only when something faints or a map event speaks. When the run ends the shell takes a summarizeRun snapshot and recordRun adds it to the missions (game/missions.ts, drawn from the local date via a seeded rng) and the lifetime records behind the achievements (game/achievements.ts). Recording works on the difference from the last snapshot, so a victory that goes on into endless mode is never counted twice. Eggs hatch outside runs, so a hatch is recorded on its own (recordHatch: hatch missions and the egg achievements), and the Pokédex achievements read the save's dex. Weekly missions with an egg and gold medals (MEDAL_EGG) give an egg through grantEgg; at the end of a run those share the run's EGGS.perRun with the eggs it found. The catalogs live in data/missions.ts and data/achievements.ts.