Case study / Roblox gameplay systems
Nightmart
A published Roblox horror game built around one question: is the person at your window who their license says they are? This page explains the systems I wrote so the server can own the answer and the player can still check it for themselves.
- Game
- Nightmart, published on Roblox by @Jetvak009
- Developer
- Parker Fawcett
- Scope
- 27 Luau scripts, about 5,300 lines, plus the open-source ProfileStore library
- Stack
- Luau, Roblox Studio, RemoteEvents, ViewportFrame and WorldModel rendering, ProfileStore
- Credits
- ProfileStore (MAD STUDIO, Apache-2.0) provides session locking and autosave. Base customer and store meshes were generated with Roblox Studio's AI tools. Sound effects come from Roblox's Creator Store audio library.
01 / The game
An observation-horror loop
You work alone at a night window. Each round a customer arrives with a license. You compare their face with the license, review the entry camera tape, serve the people who are who they say they are, and close the window on anything that is not. You rise from trainee to keeper, take on stranger closing duties, and try to keep your mistakes low. The first round is untimed so new players can learn the rules, and gear is earned through play.
02 / The engineering problem
One customer, several kinds of evidence
The game only works if a customer is the same person everywhere the player looks: moving at the window, in the still license photo, on the entry camera, and again in the review shown after a mistake. At the same time, each anomaly has to be visible in the right place. A twitching neck cannot show up in a still photo, and a printed-sideways eye cannot show up in live movement. And none of it can be decided by the client: the server has to own the answer, the progression, and the money.
03 / System one
Server-owned rounds and staged rewards
Every player gets a server-side session that owns the current customer, which clues are real, the round phase, and the rewards earned so far. The client sends small, constrained actions; the server checks them against the round state and the player's position before changing anything, then publishes a snapshot that drives what the player sees.
Rewards are escrowed. Money earned during a round sits in a pending balance and is only committed to the player's saved cash when the round succeeds and the required store work is finished. A failed round discards what was never committed. Saved cash, gear and captures are kept separate from pending round rewards, and the schedule of rounds, work duties and closing encounters is generated from a saved seed.
04 / System two
Evidence that stays consistent across views
A customer's identity and movement come from a seed. The same seeded pose plan is used for ordinary customers and anomalous ones, so posing alone does not give an anomaly away, and each of the 12 anomaly types is only drawn in the channel where it belongs. The license is a frozen still. The entry camera replays the customer's arrival from fixed camera geometry. After a wrong decision, the review screen reuses the same identity and the same clue channel to show the player exactly what they missed, including an ordinary reference when the customer was fine.
A deliberate tradeoff. The entry tape is a reconstruction, not recorded footage. Rebuilding the arrival from the seed keeps replays identical and per-player, and avoids storing frames. The cost is that the client receives enough information to draw the clue, which is a design choice I made knowingly.
05 / System three
Data-driven anomalies and a repeat-free schedule
The 12 anomaly types live in one data table, each with a visual style, the clue text shown in review, and the number of completed rounds needed to unlock it, so new tells are added by data rather than by rewriting the round logic. Contracts and ranks unlock the same way.
Work duties are scheduled from the seed like a shuffled deck: every option is used before any repeats, and the boundary between two shuffles is swapped when needed so the last night of one group never repeats as the first of the next.
-- Each saved schedule uses every option before reshuffling. Boundary swaps keep
-- the last night of one group from immediately repeating in the next group.
local function workDeck(count,block,seed,salt)
local function shuffled(index)
local rng=Random.new(((seed or 0)+index*104729+salt)%2147483647)
local deck={};for i=1,count do deck[i]=i end
for i=count,2,-1 do local j=rng:NextInteger(1,i);deck[i],deck[j]=deck[j],deck[i] end
return deck
end
local deck=shuffled(block)
if block>0 and deck[1]==shuffled(block-1)[count] then deck[1],deck[2]=deck[2],deck[1] end
return deck
end
Shared.luau: seeded shuffled-deck scheduling. The same seed always produces the same schedule.
06 / Supporting systems
Saving, audio and testing
Save data
Player data is defined by a normalized schema with defaults and validation, loaded through ProfileStore for session locking and autosave. If saved data fails validation, the load fails closed instead of accepting a corrupted profile, and captures are synced append-only. The locking engine is ProfileStore's work; the schema, validation and integration are mine.
Audio
A small audio module handles bounded one-shot sounds, bus mixing, preloading guarded by an epoch so stale loads are ignored, and cues driven by game state.
Testing
Game logic is covered by isolated Luau fixtures that run with simulated time and fake player data: server rule checks, a seeded 20-round campaign simulation, and the review screen exercised across all 12 anomaly types and an ordinary reference in four layouts. These tests cover game logic. They are not a substitute for live multiplayer or cloud-save testing.
07 / Work with me
Need Luau gameplay systems?
I am open to well-scoped Roblox scripting contracts: round and state logic, progression and economy, UI and camera systems, and save-data integration.