Today was part polish sprint, part bug fixing, part architectural experiment. 20+ commits, most of them small refinements that individually seem trivial but collectively transform the title screen from “functional” to “feels right.”
Multiplayer hang fix
A critical bug: the game hung when a second player joined an existing session. Player 1 presses Start, dungeon generates, everything works. Player 2 joins, and the server locks up.
The root cause was in the IPC (inter-process communication) between server and client nodes. The RegionManager was tracking global state — pendingTransition, dungeonGenerated, etc. — as single booleans. When Player 1’s Start signal set pendingTransition = true and Player 2 joined mid-transition, the IPC handler for Player 2’s session tried to read state that was mid-mutation for Player 1.
The fix was making the IPC handlers stateless for per-player operations. Each player’s Start sequence runs independently without touching shared flags. Shared dungeon state (the actual layout) is separate from per-player state (which region they’re viewing, whether they’re in a transition).
This isn’t fully solved — multiplayer join handling is on our longer-term TODO list — but the game no longer hangs.
MiniMap fixes
Two issues that surfaced during playtesting:
- Race condition — The minimap tried to render geometry that hadn’t been generated yet. Region loads are async, and the minimap’s “region changed” handler fired before the geometry builder finished. Fixed by pre-building minimap geometry during the region load phase, before the minimap is notified.
- Portal count — Entering a new region showed all previously discovered portals from prior regions, not just the current region’s portals. An accumulator that should have been reset wasn’t.
Pixel font evolution
The 8x8 spritesheet-based pixel font from yesterday had a rendering bug on PS4 — color was bleeding between adjacent characters in the spritesheet. This is a known issue with Roblox’s image rendering on certain platforms: when using ImageRectOffset to select sprite regions, sub-pixel sampling can bleed colors from neighboring sprites.
Rather than fight the spritesheet approach (adding padding between characters, using different sampling modes), we switched to Roblox’s built-in Arcade font (Enum.Font.Arcade). It gives us the pixel aesthetic natively without the per-pixel frame rendering overhead. The visual result is nearly identical, and it works on every platform.
The long tail of polish
Fifteen commits of title screen refinement:
- Selection UX — Replaced a color-wave highlight with
[ bracket ]selection borders. Cleaner, more readable, and more retro. Added a blinking effect on the active button for visual emphasis. - Menu panel — Buttons moved onto a shared semi-transparent background panel. Five commits tuning the panel: sizing on large screens, border fading, spacing (from 25% reduction to 2px final — three commits just on spacing).
- Footer — Went through panel-style footer → anchored footer → plain text. Sometimes the simplest option is the right one.
This is the kind of work that doesn’t show up in feature lists. Nobody will ever think “wow, the button spacing is exactly 2 pixels.” But they’d notice if it was wrong.
The 3D diorama
The day’s finale: a procedurally generated 3D diorama behind the title screen. Instead of a flat background, the title screen shows a miniature dungeon — three predefined rooms built through the exact same geometry pipeline that generates gameplay dungeons.
This is the framework philosophy in action. We define three room volumes, feed them into enrich() (which adds doors, trusses, lights, and teleport pads), then call instantiate() to build the geometry. Same pipeline, same code paths, different input. The title screen background isn’t artwork — it’s the game engine running on predefined data.
Late at night, we started the GeometryDef pipeline — a composable, DOM-like structure for layout data where generation becomes a series of enrichment stages. It compiled, it ran, but the terrain didn’t render. We committed with “TERRAIN NOT RENDERING” and went to bed. Tomorrow’s problem.