A tower defense game where the towers are units and the map keeps rearranging itself. Lanes spawn, run for a few waves, and retire somewhere else, so there is no build to settle into. The whole game is deciding which of your towers should be which type, and where.
It started as a MonoGameJam entry and has been rebuilt since. The jam version is the video at the bottom.
Movement is the resource. A tower stops firing the moment you pick it up, and any tower that lands on a hex it was not already on gets Overcharge: a few seconds at double fire rate. Standing still is the weak state.
Change gets telegraphed. A new lane draws in as a dashed ghost for three seconds before it goes live. Anything still standing on it when it does gets shoved to the nearest free hex, which counts as landing, so the knock hands you Overcharge. Nothing ever silently stops working.
Read the wave, not a tooltip. Three minion types carry three different defenses, and each one has an answer:
| Minion | What it has | The answer |
|---|---|---|
| Swarm | nothing, and there are a lot of them | Shotgun, whose cone clears a pack |
| Armored | flat damage reduction | Sniper, since shotgun pellets tick for 1 |
| Shielded | a shield that regrows and blocks direct damage | Flamethrower, since burn goes through it |
A wrong hit is a small grey tick, a right one is a big amber crunch. Retyping is free and costs about a second of downtime, so which tower is which type is a question every wave.
Four cores instead of a life counter. Lanes run to one of them. A leak damages the one that lane was headed for, a core that runs out dies, and its lanes redirect to whatever is left. The tower budget grows with the number of lanes and stays one ahead of it, so you cannot cover everything and you are picking which core takes the hit.
Mining. Charge nodes appear out where the lanes are not and pay only while a tower is standing on one, for about eleven seconds. Pulling a tower off the fight is the decision, and the trip back lands it in Overcharge. Charge buys one of three upgrades offered after each wave, or a lane or a pack dropped on an opponent. One number, two uses.
Drag a tower where you want it, or click it and click again. Both do the same thing, so it works on touch.
| Input | What it does |
|---|---|
| Left drag on empty ground | Box select |
Shift click |
Add to the selection |
| Double click a tower | Select every tower of that type |
Q W E |
Retype the selection to shotgun, sniper, flamethrower |
| Right click a tower | Cycle its type |
Ctrl + 1-6 |
Bind a control group, the number alone recalls it |
Shift (hold) |
Slow time to about a fifth while you issue moves, spending Focus |
Space |
Pause |
R |
Restart |
O |
Open the versus lobby |
Esc |
Cancel a drag, then clear the selection, then quit |
F12 |
Screenshot next to the executable |
Hover a tower to see its range as a hex ring. The sniper's dead zone is drawn as a red hole, since it is a rule you otherwise only meet by losing a shot to it.
Two boards, one relay. Each player runs their own simulation, so nothing has to stay in sync and there is no lockstep and no rollback. Only discrete events cross the wire:
msg send <kind> a lane or a pack, headed to the other board
msg state <cores> <wave> <score> about 2 Hz, for the opponent strip
msg over <score> this board is done
The relay in Server/ never reads a game message. It pairs clients into rooms and forwards
lines, and it deploys to Cloudflare Workers from .github/workflows/relay.yml. Desktop
reads the address out of Settings.json next to the executable, and the browser uses
whatever is compiled into Game/Settings.cs, since it has nowhere to keep a file.
Game/ holds all the code. Each folder under Platforms/ is a host that links it in.
| Platform | Framework | Notes |
|---|---|---|
DesktopGL |
MonoGame | Windows, macOS, Linux. This is the one the release workflow ships. |
WindowsDX |
MonoGame | Windows only. |
BlazorGL.KNI |
KNI | Runs in the browser, and it's what goes to itch.io as an HTML5 build. |
Drawing is one ShapeBatch from Apos.Shapes,
with no SpriteBatch and no render target. Text is drawn from the font's own outlines in
the same batch, so the content pipeline only handles audio.
The simulation in World.cs has no drawing in it and no MonoGame types beyond Vector2,
which is what lets a headless harness run thousands of games for balance without a window.
- ProjectCore on itch.
dotnet run --project Platforms/DesktopGL
dotnet run --project Platforms/WindowsDX
The browser build serves itself:
dotnet run --project Platforms/BlazorGL.KNI
Setting PROJECTCORE_SHOTS to a comma separated list of seconds writes a png at each of
them and exits, which is how the game gets looked at without a person driving it.
PROJECTCORE_SEED fixes the board so two runs are comparable:
PROJECTCORE_SEED=7 PROJECTCORE_SHOTS=4,30,90 dotnet run --project Platforms/DesktopGL
dotnet publish Platforms/DesktopGL -c Release -r win-x64 --self-contained --output artifacts/windows
dotnet publish Platforms/DesktopGL -c Release -r linux-x64 --self-contained --output artifacts/linux
macOS ships as an .app bundle instead, built by a script that has to run on a Mac:
./Platforms/DesktopGL/package-osx.sh 2.0.0 artifacts/osx
The bundle carries a full tree per architecture and a shell launcher that picks between
them, because a self-contained .NET app ships framework assemblies compiled per
architecture and lipo can't merge those. Every Mach-O in it gets ad-hoc signed, which is
the part that can't happen anywhere but macOS: Apple Silicon kills any process whose images
aren't signed, so an unsigned build is one that never starts. The publish also turns
NetCoreBeauty off for osx-*, since it patches hostfxr and a patched Mach-O is invalid.
The web build lands in artifacts/web/wwwroot, with the index.html at its root. That
folder is what goes to itch.io:
dotnet publish Platforms/BlazorGL.KNI -c Release --output artifacts/web
Pushing a v* tag builds all four and sends them to itch.io on the windows, osx,
linux, and html5 channels. Running the workflow by hand from the Actions tab builds
everything and publishes nothing, which is the way to check a change to it without minting
a tag.
Draws the icon at every size it ships in, for both desktop platforms, the macOS bundle and the web favicon:
python Tools/make-icons.py
Nothing is upscaled. Three lanes converging on a core reads at 512 and turns to mush at 16, so the small sizes drop them and keep the hex alone.
This game was created by Lightning2x and Apos. Follow us on Twitter:
