Inspiration
Reddit's "Games with a Hook" brief kept circling back to one word: retention. Not "can you build a fun game" but "can you build a reason to open Reddit tomorrow." That reframed the whole project for us. A high score is a moment; a streak is a promise you made to yourself yesterday.
I also wanted to push back against two defaults we kept seeing in game jams: cute pixel art, and combat that rewards spamming input. Nightwatch is the opposite of both — a near-black, silhouette-horror shooter where the only way to survive is precision, and where missing hurts more than any enemy could.
The visual language came from Limbo and Inside — flat paper-cut silhouettes against a luminous sky — crossed with a specific idea: what if the monsters were the only source of color, and that color was something your own weapon deliberately doesn't share? Aliens glow violet. Your pistol glows teal. Two species of light that never touch.
What it does
Nightwatch is a 60-second first-person shooter that runs natively inside a Reddit post via Devvit Web.
A mothership hangs silently over the treeline. Alien creatures — bulbous, tentacled, bioluminescent things with no eyes, only a pulsing violet heart — drop from its underbelly and drift toward you on unpredictable paths: weaving, zigzagging, flanking from the sides. You tap to fire a bolt of energy from a two-handed alien pistol. Hit, and the creature dissolves into light. Miss — or let one reach you — and your streak resets to zero.
That streak is the hook: it survives between sessions. End tonight on a streak of 14, and tomorrow's first shot starts at 14. You get two watches a night, so both have to count. It turns an arcade shooter into something closer to a habit loop — the game a player carries in their head between visits, not just during them.
How we built it
Client: Three.js + TypeScript, bundled with Vite. Nothing in the scene is a downloaded asset — every tree, boulder, alien, and gun barrel is procedural geometry built at runtime (LatheGeometry for the alien's bell and the gun's barrel, recursive branching for dead trees, merged BufferGeometry for the fence so it's one draw call). We wrote a custom fresnel ShaderMaterial for the aliens: a near-black opaque body with a violet rim that only blooms at glancing angles, plus a sin-lattice fragment shader for scattered bioluminescent freckles across the skin.
Post-processing: a custom EffectComposer pipeline — selective bloom via HDR color boosting (materials multiply their emissive color above 1.0, and the bloom threshold sits at exactly 1.0, so only the things that should glow do), ACES tone mapping, and a whisper-level film grain + vignette tuned by eye until it stopped reading as a blurry old TV and started reading as atmosphere.
Server: Hono running on Devvit's serverless runtime, with Redis as the only persistence layer — a sorted set for the all-time leaderboard, a hash per player for lifetime stats and the streak carry, and an expiring counter for the daily play cap. Identity never comes from the client; every request resolves the player via Devvit's own reddit.getCurrentUsername(), so a score can only ever land on the account that actually played.
The streak carry was the trickiest piece of state to get right: the server hands out the current streak at the start of a run, the client plays on top of it, and at the end the server validates the submitted result against the exact carry it gave out — zero misses means the math must check out exactly, or the submission is rejected as forged.
I also wired up GitHub Actions: every PR upload-builds to a dev playtest subreddit and a bot comments the live test link; every merge to main type-checks, builds, uploads, and files a publish request to the production subreddit — so shipping a change is a git push, not a checklist.
Challenges we ran into
The speed-penalty spiral. Letting aliens reach you was supposed to speed up the next spawns as a difficulty ramp. My first implementation accumulated the penalty multiplicatively on every miss — three misses in a row and the game became mathematically unwinnable within seconds. The fix was simple once we found it: cap the penalty linearly against a hard maximum instead of letting it compound.
Silhouettes that wouldn't stay silhouettes. My first alien material used additive blending for that classic "glowing ghost" look — but against the pale moonlit fog, additive blending made the whole creature translucent and washed-out instead of a crisp cutout. Switching to normal blending with depthWrite: true, plus a manual fog calculation in the fragment shader decoupled from the scene's actual fog density, is what finally made the aliens read as solid dark shapes at gameplay distance instead of pale smudges.
Green-glowing hands. When I first added the hand-and-pistol rig, the energy light placed for the gun's glow was bright enough — and close enough to bare "skin" material — that the player's own hands lit up sickly green under the teal light. The fix was two-pronged: dark leather gloves instead of skin, and dialing the light intensity down by a third.
A CI pipeline that only breaks once. Devvit's publish command insists on an interactive first-run consent prompt for uploading your source zip — which hangs forever in a headless GitHub Actions runner. Rather than pre-seed hidden local state, we dug into the CLI's own source and found it's gated by a simple environment variable (DEVVIT_ALLOW_SOURCE_UPLOAD), so CI could skip the prompt outright instead of needing a human to have run it once by hand.
Accomplishments that we're proud of
- A fully procedural world — every asset, from the alien's tentacles to the pistol's cocking serrations, is generated code, not an imported model.
- A streak mechanic that's provably fair: the server validates every submission against the exact carry it issued, so a "forged" high streak isn't possible even from a modified client.
- A post-processing pipeline that turns flat geometry into something that reads as genuinely atmospheric, not just dark.
- A CI/CD setup that takes a change from
git pushto a live, playable update on the production subreddit with zero manual steps. - Closing the loop on player state: a run that's abandoned mid-watch — closing the tab, losing signal — still gets recorded via a
pagehidekeepalive, so no one loses a play they already spent.
What we learned
That restraint is a design tool, not a limitation. Every rule in this project — one violet, one teal, never eyes on the aliens, fog that fades to haze instead of black — existed specifically to make the few bright things mean something. And that a retention mechanic doesn't need progression trees or dailies bolted on top; sometimes it's one number that only goes up if you're careful, and the fear of losing it is the whole game.
What's next for Nightwatch
- Weekly leaderboards alongside the all-time board, so the competition resets and newer players have a reason to climb.
- Alternate weapon behaviors — a charge shot or a piercing bolt — introduced as streak milestones rather than currency purchases, keeping the loop skill-first.
- Difficulty variants for players who've mastered the base 60 seconds, rather than pushing the base game harder for everyone.
Log in or sign up for Devpost to join the conversation.