๐Ÿฑ๐Ÿ’ฅ๐Ÿถ Tanker Street A cozy artillery duel where a cat and a dog lob whimsical ordnance across a cherry-blossom night sky.

๐Ÿ’ก Inspiration We grew up on Pocket Tanks, Worms, and Gunbound โ€” that perfect loop of tweak the angle, sweat the wind, fire, and watch the dirt fly. But every modern take we tried was either a hardcore desktop sim or a free-to-play slot machine. We wanted the opposite: something warm, instantly playable, and adorable โ€” a one-thumb mobile game you could open on a bus and feel something in five seconds.

So the pitch became a sentence: "Pocket Tanks, but it's a kitten and a puppy slinging fireworks at each other under cherry trees." The cat (P1) and dog (P2) gave us a built-in rivalry, a readable color language (cool blue vs. warm red), and a tone that let us name weapons things like Healing Tulip, Acorn Mine, and Cozy Cloud without anyone rolling their eyes.

๐ŸŽฎ What it does Tanker Street is a turn-based artillery game built on a real physics sandbox:

Slingshot aiming โ€” pull back to set angle + power, with a live dashed trajectory that teaches ballistics instead of hiding them. Fully destructible terrain โ€” every shot carves the world; tanks slide and settle on the new surface. A growing arsenal of physics-driven weapons (bouncers, diggers, drifters, mines, healing flowers), each with its own gravity, wind behavior, and impact rules. Worlds, an economy, and a weapon store โ€” win levels, earn coins and diamonds, unlock and buy new ordnance. One codebase, three homes โ€” it ships to the web on itch.io, to Android via Capacitor, and to CrazyGames with full SDK integration (cloud saves, session analytics, monetization) โ€” each degrading gracefully when a platform's features aren't there. ๐Ÿ› ๏ธ How we built it The stack is deliberately lean: Phaser 3.90 + Vite, vanilla JS, a synchronous localStore over a single JSON blob, and Web Audio for sound.

Sound, synthesized Rather than ship megabytes of audio, most SFX are procedurally generated with oscillators and noise buffers at runtime โ€” different timbres per weapon class โ€” so they cost almost nothing to download.

๐Ÿง— Challenges we ran into

  1. Phones can't hear sub-bass. Our first firing sounds were rich, punchyโ€ฆ and silent on phone speakers. We measured the spectra with an OfflineAudioContext and found ~88% of the energy sat below 500 Hz, where a phone speaker simply can't reproduce it. We re-synthesized every weapon around the 600 Hzโ€“3 kHz band a phone can push, and verified ~47% of energy moved above 500 Hz. Lesson: audio is only "loud" if the hardware can radiate it.

  2. Cross-origin iframe audio. On CrazyGames the game runs in a sandboxed iframe where the AudioContext starts suspended and a parent-frame click doesn't unlock it โ€” the gesture and resume() must happen inside the iframe. We also hit a brutal class of bug where a fragile mute "latch" could pin the master gain at $0$ for the whole session, looking exactly like an autoplay failure. The fix was deterministic baselines โ€” unmuted โ‰ก gain 1, no capture-and-restore โ€” so a missed edge can never strand the game silent.

  3. 98 MB โ†’ 15 MB. CrazyGames rewards builds under 20 MB (and caps initial download at 50 MB). We were at 98 MB. We wrote a one-off sharp pipeline to palette-compress 46 PNGs in place โ€” โˆ’85%, zero code changes, visually identical โ€” stripped 14 MB of stray .bak files, and deleted dead duplicate spritesheets. Final build: 15 MB, verified pixel-for-pixel against the original menus and gameplay.

  4. The platform maze. We learned the hard way that on CrazyGames Full Launch is earned, not toggled โ€” you ship Basic Launch first, accrue engagement metrics, and get invited. We chased a "SDK not detected" ghost (the detector is event-driven โ€” it only sees gameplayStart() once you actually enter a match), fixed a muteAudio property-path bug (SDK.game.settings.muteAudio, not SDK.game.muteAudio), and got a submission rejected for "ads detected" in Basic โ€” which forced a clean ADS_ENABLED master switch that gates every single ad request behind one flag (verified with a spy: requestAd is never called) and flips on the day we reach Full Launch.

  5. A backslash that broke everything. PowerShell's Compress-Archive writes ZIP entry paths with \, and Linux static hosts serve those as literal filenames โ€” every asset 403'd on itch and CrazyGames. We replaced it with a manual ZipArchive loop that rewrites paths to forward slashes, and now verify each package has index.html at the root with zero backslashes before it ships.

๐Ÿ“š What we learned Verification beats vibes. Spectral analysis for "the sound is too quiet," a requestAd spy for "are ads really off," a 1-px miss check for "does the solver work." Every claim got an observation behind it. Constraints are platform-shaped. "Make it smaller," "make it audible," "make it pass review" all turned out to be physics or policy problems with exact thresholds, not vague polish. Graceful degradation is a design, not an afterthought. One build, three platforms, each detecting what it can do and quietly skipping what it can't โ€” Android never loads the ad SDK, itch self-disables it, CrazyGames lights it up. Read requests narrowly. "Remove the button" meant just the button โ€” keep the feature and auto-trigger it. A small lesson that saved a feature from deletion. ๐Ÿš€ What's next Pass Basic Launch, earn our way to Full Launch, and flip the one flag that turns the cherry-blossom battlefield into a living, monetized world โ€” then keep filling the armory with the silliest physics weapons we can dream up. ๐ŸŒธ

Built With

Share this project:

Updates

Submission history