Inspiration

I love arena survivor roguelites — Brotato, Vampire Survivors — where you only steer, the game does the shooting, and every run turns a pile of small choices into a wildly different build. I wanted to see how far I could get building one from scratch with an AI-native workflow: me directing, Claude Code writing most of the TypeScript, Magnific AI generating the weapon and item icons, across 13 sessions in about a month.

What it does

A top-down zombie survivor for mobile (portrait, one-thumb controls). You move with a floating virtual joystick; your weapons aim and fire themselves at the nearest threat. Survive 20 timed waves of aliens and zombies that scale in count and stats, harvest materials from kills, and spend them in a shop between waves — buy and combine weapons across 6 tiers, stack stat upgrades, and pick items with special effects like chain lightning and meteors. Wave 20 drops a boss on you: kill it and the run is won, die anywhere along the way and it's over.

Under the hood it's fully data-driven: every enemy, weapon, hero, and wave curve lives in JSON, so balance changes never touch code.

How we built it

Cocos Creator 3.8.7 + TypeScript, with Claude Code as the main pair. The build order was deliberate — each session shipped one system on top of the last:

  1. App shell — five scenes (splash → loading → lobby → game) and a config layer that loads and caches all JSON data.
  2. Movement — joystick driving a physics-based character, direct 1:1 response with no acceleration curve.
  3. Enemy framework — an Entity hub, a finite state machine (spawn / detect / charge / attack / flee / dead), and composable core components (movement, stats, damage receiver, death, animation). A new enemy is a JSON row plus a state combo, not a new class.
  4. Wave spawning — 20 waves of ring-spawned, pooled enemies, every spawn telegraphed by a warning circle that fills for 1 second before the enemy appears.
  5. Weapons — auto-targeting melee and ranged weapons; both deliver hits differently but funnel into one DamageData application point.
  6. Damage, death, drops → stats → economy → shop → items → boss — one session each, always playable end-to-end before moving on.

On the art side, we split the work by what each tool does best: characters, enemies, environments, and UI are hand-drawn by our artist — sprite flipbook animation for enemies and FX, Spine skeletal animation for the boss. The one exception is weapon and item icons: with dozens of them to cover, we generated those with Magnific AI and cleaned them up to match the style, keeping the artist's time on the creatures and screens players stare at all run.

The workflow lesson that stuck: feed Claude Code the architecture doc and an explicit state list every session. Give it the diagram, not the vibe, and the codebase stays consistent for 13 sessions straight.

Challenges we ran into

  • The bug that healed enemies. With one hero (Brawler, -50 Ranged Damage), enemies simply refused to die — while damage numbers kept popping. The damage formula computed 12 + (-50) = -38 per shot, and health.decrease(-38) was healing the enemy 38 HP per bullet. Fixed in two layers: weapon damage clamps to a minimum of 1, and the damage receiver rejects negative amounts entirely. The scariest part: the on-screen numbers looked correct the whole time.
  • State leaking between runs. Singleton managers meant run 2 started with run 1's stats, items, and shop round. Half my "balance problems" were actually leaks. One centralized reset at match start fixed it — you can't balance a game that doesn't reset cleanly.
  • Frame-based melee hitboxes. The first enemy melee used an animation-toggled weapon collider; it kept clipping the player on approach and interrupted swings. Replaced with a deterministic check at animation-finish: damage lands only if the attack animation completes while you're still in contact.
  • Pool ghosts. Recycled enemies came back with disabled colliders and stale animation callbacks. The fix was philosophical: re-initialize everything on the spawn path instead of trusting despawn-side cleanup.
  • AI version drift. Claude Code kept mixing Cocos 2.x and 3.x APIs until I pinned the engine version and API conventions into its working context.

Accomplishments that we're proud of

  • A complete, winnable roguelite loop — 20 waves, shop, builds, boss — playable end-to-end, built in 13 sessions.
  • An enemy framework where a new enemy type genuinely costs one JSON row and a state combination.
  • Fair-feeling pressure: every single spawn is telegraphed, spawns ring the player so threat comes from all sides, and difficulty ramps in readable steps instead of one exponential explosion.
  • A single ordered modifier stack (Flat → PercentAdd → PercentMult, cached with dirty flags) that powers heroes, level-ups, items, and weapon scaling — one system to balance instead of four.
  • Performance discipline from day one: everything pooled, a hard cap of 100 live enemies, shared enemy-position snapshots for weapon targeting.

What we learned

  • Any number that flows through modifiers needs a clamp at the point of application. Don't trust the pipeline to stay in range.
  • "Pool bugs" are almost always incomplete resets; put the full re-init in one place.
  • Death has two moments — "stops participating" and "finishes animating" — and they must be separated.
  • Juice is not optional: recoil tweens, impact FX, and crit-colored damage numbers changed how the game felt more than any stat tweak.
  • With AI pair-programming, specificity wins: explicit state lists, pinned versions, and an architecture doc produce dramatically better code than open-ended asks.

What's next for Pota vs Zombies

The full game would expand into multiple levels with new enemies and bosses, while adding heroes, weapons, and items that deepen build variety and give players more to discover. A meta-progression layer between runs would let players permanently upgrade their heroes and weapons, so every player's build feels like their own.

Built With

  • claude
  • cocos
  • magnific
Share this project:

Updates

Submission history