Inspiration
Tower defence is usually about where you put things and whether you can afford the next one. I wanted the interesting decision to be somewhere else, so I started from a sentence rather than a genre: your towers are lights, and your enemies are shadows. Once light is the weapon, a real constraint falls out of the fiction for free — every lamp on a road draws from the same grid, and the grid is never big enough. The question stops being "what do I build" and becomes "what am I willing to leave dark".
What it does
You are the night janitor of a coastal substation. Shadows walk down one road toward it, and anything standing in light burns. You place lamps, beacons, prisms and mirrors on sockets along the verge, then spend the night moving a handful of power slots between them as the pressure moves. Reach dawn and the city wakes up still lit; lose four districts and it goes dark for good.
Power is the whole game. A light at zero power still stands there, dark and useless, and moving a slot costs a second and a half of filament warm-up — long enough that every reallocation is a bet on where the next threat lands rather than a free correction.
You read your position off the board instead of a readout: the width of a lit pool, the segments in the power column, the skyline going out district by district behind you.
How we built it
Portrait, one thumb, Three.js and GSAP — both vendored locally, because the build has to run with the network switched off.
The game is developed across 30 ES modules and concatenated into a single
readable index.html by a build step, which is what the competition asks for.
Nothing is minified; the code is meant to be read.
The board is a painted plate rather than geometry, so the hard part was making light land on it convincingly. The plate is read once at load into a four-channel field: two channels for what can receive light at all (ground, road, verge, rock — not sea, not sky) and two carrying a surface normal recovered from the painting's own luminance with a Sobel. That normal is what turned a mask into lighting: without it a lamp lights a fence as a rectangle, and with it the fence is a row of posts with a bright side and a dark one.
The balance is measured, not guessed. There is a headless harness that drives the game's own modules with a seeded PRNG and four scripted player skill levels, so any change can be checked against 120 runs per policy in a few seconds. Every difficulty number in the build was chosen by running it.
Challenges we ran into
A black band across the lit road. Hiding the smoke, hiding the cast shadows and zeroing the shading all left it there. Rendering the light-mask itself as an image showed a gash straight through it. The cause was in the painting: the test that separates ground from water asks "blue, and free of red", and the deep shade painted across the tarmac measures r/b 0.09 against the sea's 0.05 — the same colour. No hue test can ever separate those. What separates them is that the sea touches the edge of the frame and a shadow on the road does not, so the fix floods inward from the border and reclaims anything the flood never reaches.
A rectangle behind every creature. Two passes of softening edges did not fix it, because three separate edge fades still meet at corners and the eye finds a corner faster than anything else in a soft image. Imposing one elliptical falloff instead — a shape rather than a fade — did.
Mirroring that never happened. More on that below.
Accomplishments that we're proud of
The lights obey the painting. A lamp on the clifftop lights the road and the fence and stops at the cliff edge; it does not light the sea fifty metres below or the skyline a mile across the water, and it models the fence posts as it passes them.
And the ending. When the last district falls, the road goes dark from the substation back up the hill — so the final light to go out is usually the first one you ever placed — and only then does the skyline follow, right to left, starting on the bright core you have been watching all night.
What we learned
Verify the thing that draws, not the thing that configures. Fixtures on the
wrong side of the road were facing away from it. I "fixed" it twice by changing
a facing flag and confirming the flag had changed. It had. But three.js renders
sprites through a shader that recovers on-screen size with length() on the
model matrix, and length() cannot return a negative number — so a negative x
scale is discarded before it reaches the geometry. No fixture had ever been
mirrored. It looked right on seven of ten sockets only because that is how many
have the road on their right.
Measure the render, not the intent. Highlighting the selected light by lifting its colour 16% moved 2551 pixels by a mean of 4.7 of 255 — about six percent — because sRGB compresses the top of the range and the art was already near white. Six percent is not a cue. The fix was to make the fixture swell instead.
A finished mechanic nobody can see is not finished. The screen-clearing Flash worked perfectly and was fired by tapping an object whose visuals had been switched off in an earlier pass. The game was asking for a tap on a blank patch of ground, with nothing to say the power existed. It now has a meter that fills where you can watch it.
And instruments lie too. Adding the Flash cascade dropped the measured win rate from 78% to 33% and I nearly reverted it. The game was fine — the test harness still expected a return value the function no longer had, and was quietly computing NaN.
What's next for Lumen
A campaign of coast towns, each a hand-painted board with its own road and its own geometry for light to land on. Lights and shadows both gain behaviours, and the single shared pool becomes a network you route power through rather than a number you split — so the decision that makes this prototype work gets a whole game's worth of room.
Log in or sign up for Devpost to join the conversation.