I have a habit of writing one of these every few years. In 2017 it was a 7DRL experiment merging a roguelike with an FPS, and in 2019 it was ascii characters embedded into a 3d map. This year’s entry is different: DET-33, the bullet-hell roguelike I built in JS and Phaser over the last few months, is feature-complete and plays great. So naturally I’m rewriting it from scratch in Godot 4 and C#.

Why throw away a working game
I’m not, really. The JS version works beautifully on a desktop. The problem is the Steam Deck. Two things go wrong under heavy combat:
- The garbage collector. A bullet-hell game allocates constantly, and the JS GC pauses to clean up at the worst possible moments, causing recurring hitches exactly when the screen is busiest.
- The enemy ceiling. The browser runtime hits a hard wall on entity count well before the number the design actually wants on screen.
A C# rewrite on Godot solves both. The runtime GC is far less intrusive, and a scene tree with node pooling can push entity counts an order of magnitude higher. That’s worth a rewrite.

Porting against a living reference
The rule I set for myself is that the JS project is the source of truth and I never modify it during the port. The Godot repo follows it one-to-one wherever that makes sense. When a feature has to be implemented temporarily or differently (usually because some dependency it relies on hasn’t been ported yet) it gets a TODO(tag) comment at the code site and a matching entry in a docs/TODO.md. Then grep TODO(item-system) finds every call site tied to that deferred work. It keeps the half-finished bits visible instead of rotting quietly in some commit from three weeks ago.
Letting the engine be the architecture
The biggest mental shift porting from JS is resisting the urge to recreate my manager classes. In the JS version I had the usual pile of singletons routing everything. Godot already has a structure for that: the scene tree. Systems are nodes attached to the tree, signals are local to the node that owns them, and only the genuinely cross-cutting concerns (global events, run state, meta-progression, the spatial index for enemy queries, input routing) live as autoload singletons. Letting the tree be the backbone instead of fighting it with a flat manager layer is most of what “writing it the Godot way” turns out to mean.

A month in, the vertical slice is already running more enemies than the JS build ever could. The next post is going to be about the rendering trick that makes that possible, because it’s the strangest and most fun part of the whole port.
