Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Holdfast: the room, the look and the HUD

This page describes how Holdfast runs in its room and how every page draws it.

The scene module re-exports the plugins list src/game.ts exports, weapons() and the siege’s plugin, and the page passes Game the same list. A page in a room steps only the player’s own character, so the rules run in the room alone and every page draws what the room streams. The game’s own traits carry the siege, each warden’s health and cards, the monsters, the coins and the short effects. Each shot’s line comes from the room’s shot result, which says where every pellet ended.

What changes over time runs on the engine’s state machines, so spawnite play dump shows where each stands and how long its wait has left:

  • phase, on the siege’s entity: waiting, breather, fight, over and dawn, with the gathering’s countdown (counting, counted), the beat before the deal (dealing, dealt), the rest (resting), the wave’s opening (opening, fighting) and the last fall (falling, fallen) as states that wait. The room’s guards read its tags, such as phase.fight, through isPhase, which takes the phase machine’s tags alone. A page reads the phase through usePhase, which is the engine’s useMachineState on the record the stream writes, and the page bot and the timeline through readPhase.
  • life, on each warden: standing; sheltered, catchingUp for her 20 seconds and spent once they run out; or down, reviving while someone gets her up and lying while nobody does. revived holds the seconds of getting up she has.
  • readiness, on each warden beside it: unready, or ready by her key (ready.key) or by the ring (ready.ring). A sheltered latecomer readies like anyone else, so it is a machine of its own.
  • slamming, on each colossus: ready, winding for its 1.3-second wind-up, landing, then cooling. spitting, on each spitter: cooling, then ready. Both machines name the game’s FrozenTrait in heldBy, so a freeze holds their waits where they stand.

While no player is connected, a system before the machines count puts the game’s UnattendedTrait on the siege and every warden, and takes it off once a player is back. phase and life name it in heldBy, so the run’s countdowns and every shelter hold where they stand.

The scene’s timeline records what spawnite play room reads a wave at a time: the wave and the phase, kills, downs, coins, the monsters standing, each reaction and those a warden sets off between her own two elements, the points taken on the element lines, and the lines at their capstone. It also records what a balance pass reads and the game keeps nowhere else, on a room-only tally on the siege:

  • monsterSeconds and colossusSeconds: the seconds each monster stood, summed, and the colossi’s apart. Over a colossus wave, the rise of colossusSeconds is how long the colossus took to kill.
  • kills<Kind> and damage<Kind> for the husk, skitter, brute, spitter and colossus: kills, and the greatest health of those killed.
  • hurt, and hurt<Kind> by the kind that struck: the health the wardens’ blows took, or would have taken from an invulnerable warden, so an --invulnerable run still says how hard a wave pressed.
  • revives and selfRevives: each time a teammate got a warden up, and each time a warden alone got herself up.

The scene’s room bot plays as a decent player would, so a pass of bots measures the night against a person rather than a perfect aim. Its skill, decentWarden, waits 0.35 seconds before its first shot at each new target, trails a moving target by 0.1 seconds, strays each shot by a normal draw, and backs away from a monster within 4 m. It runs out of a slam’s ring once it has seen the wind-up, sidesteps a bolt whose path passes it within a step, walks to a downed teammate during a wave to get her up, and spends ten seconds of each breather before it says it is ready. Between waves it buys its colour’s gun and the gun’s tiers at the rack, takes the rarest element card of each offer, buys one more card a breather and two from wave 5 once its coins run past what it saves, and feeds the fire the rest. Play the room with bots says what each number of a skill does.

The blaster is the engine’s instant weapon with the warden’s gun stats laid on its own numbers, so the room judges every pellet with its rewind, and the kill goes to the warden whose damage the monster took last.

Each gun names its dose in its weapon’s hit data, so every hit carries it. At the start of each step, a room system reads each monster’s DamagedEvent hits, which the room lands between steps as it judges each shot, and lays each dose of the shooter’s elements. Another system ticks the burns, fades the burn and chill, thaws the frozen and counts the marks and rests down. A monster’s elements are the room’s alone; each page draws the look the room sets from them, only where it changed. Each element’s moment, an arc, a reaction or a capstone, is an entry in an event on the siege, which the room sends once and each page draws from its pools; no entity is spawned for it. A steam cloud, which stands 5 seconds, is an entity. Taking a card, getting ready, cancelling it, starting without the rest, a reroll, buying a gun, an upgrade and a feed are each a game message with no payload, which the room checks against its rate and the game’s system reads as that warden’s word: a purchase names what it buys, and the room reads the price, the warden’s place and their wallet from the world. A monster’s coins are an event on the siege, as the element’s moments are, which the room sends once and each page flies from its own pool; no entity is spawned for a coin. The awards are a trait on the siege, which the room sets once as the run ends. Ready counts while the wardens gather, at dawn and in a breather. The scene places a spawn point at each colour’s place by the fire, so a warden who joins appears at her own, and gives the engine’s chase a filter, so a monster walks at the nearest warden still standing who is neither sheltered nor dropped. A sheltered warden wears the engine’s DisarmedTrait, and a dropped one’s player reads ConnectionState.Held in her ConnectionTrait, which isControllerHeld reads off the warden.

Every page draws each shot, its own at once and the others’ as the room streams them:

  • A line of the shooter’s colour from her gun’s muzzle to where the crosshair’s ray lands: a bolt from the blaster, a spray of short, thin pellets from the scattergun, and a thick white line from the rail that hangs a moment and runs on through what it hits.
  • A flash at the muzzle and a kick of the gun, which lifts her arms with it, a heavy gun’s most. The scattergun throws a spray of sparks along its cone and racks its pump between shots; the rail throws a ring of heat, its muzzle rings drain at each shot and fill again as it charges, and they flash as it is ready.
  • Each upgrade tier burns the line a little wider and throws more sparks where it lands, and the third tier’s line burns gold, the colour of its band.
  • A spark where the bolt hits a monster, and damage numbers that float up from it.
  • A push back from the hit, as far as the gun is heavy: a blaster bolt 0.12 m, a scattergun blast 0.28 m and a rail 0.4 m, each easing home over a quarter to a third of a second. A colossus stands its ground and takes a bolt’s push whatever hits it.
  • For the shooter, a knock under the hit’s sound when her shot kills, and a mark round the crosshair when a shot lands: white ticks for a hit, larger red ones for a kill, and gold ticks punched in with a ring of struck metal for a weak spot, round a kill’s red ring for a kill there. A shot the room refuses takes its mark back, and the room’s word on the weak spot settles whether the mark stays gold.
  • A weak spot’s damage number floats up gold, larger and slammed in with a shake.
  • The shooter’s element where it lands: two tiny zigzags of lightning and a pinpoint flash for Storm, a white-hot flash with a lick of flame and a few sparks for Ember, and glints of ice with a puff of cold mist for Frost.
  • An upgraded scattergun’s bounce, a short streak of the shooter’s colour from monster to monster with a ping of metal.

A monster that dies plays its death clip where it fell, then sinks into the ground. A death is drawn by what fell, so the moments that matter stand out of a crowd: a husk, a skitter or a spitter leaves a puff of dust and a few motes of its color, a brute throws dark debris as well, and only a colossus bursts with a flash, a shockwave and a light. A monster the room recalls, one that strayed off the field or stood stuck, sinks into a rift where it stood, in the pose it stood in, and rises again with the wave’s next batch. The rift names the monster by its id in the stream, so a monster that dies beside it in the same moment falls as it died.

A burning monster licks with tongues of flame, white-yellow at their heart and red at their tips, and throws embers that rise and wink out, more the more it burns; where a burn ends, or a burning monster falls, a scorch lies on the ground for 8 seconds and fades over the last three; a chilled one frosts over: its outline turns ice blue, frost glints on its body and frost spreads on the ground at its feet, all more with each stack; a frozen one stands still in a cut shell of ice, clear in the middle so its own colors show and bright at its edges, over frost on the ground and a curl of cold mist, which snaps on with a white-blue flash and breaks off as it thaws in a spray of ice chips that land, glint and melt. A frozen monster’s ice glows for it and a blazing one’s three full tongues of flame, taller and brighter than a full burn’s, each with a ring turning at its feet, and wears its mark’s sign over its head: the element’s flame or snowflake in a dark disc, the same size on screen however far away it stands, and drawn over anything in front of it, so a mark reads from across the circle. Storm’s arc is a thin, jagged bolt with a white core in a pale yellow glow and a short fork or two, drawn over the bodies it leaps between, with a flash where it lands on each; it flashes for about a tenth of a second and lingers a moment as a dim afterglow. A reaction winds up for a tenth of a second before it bursts: the spent mark’s color snaps in to a white point on each monster it spends.

The camera is the engine’s shooter preset, over her right shoulder, with no first person. At level aim the eye stands over her head and the crosshair sits above her right shoulder, and each bolt leaves her muzzle for the point the crosshair shows.

The big moments shake the camera of each warden near them, small and brief, as Halo and Deep Rock Galactic keep theirs. A reaction or a capstone fades to none 20 m away: Blast hardest, then Meltdown, Chain Shock, Shatter and Steam Cloud, and Thunderhead, which falls every few seconds, barely. A teammate’s reaction or capstone shakes her half as hard as her own. A colossus’s slam shakes a warden within 20 m of the blow, and a colossus’s rising and its fall the whole circle, 45 m round. Her own heavy guns kick her camera a little at each shot: the rail the most, the scattergun less, and the blaster, at six shots a second, not at all. A rail shot that kills through a weak spot kicks it harder. However many moments land at once, the shake stacks to 0.6 of the engine’s whole shake at most, so the view rolls no more than 0.86°, and her own shots stack to 0.35 at most, so a held trigger shakes no more than one heavy shot. The player’s Screen shake setting turns it down or off, and Reduced motion turns it off.

The HUD draws every readout in one dusk slate style: the wave banner, the gathering banner, the colossus’s health, health, coins, the gun and its tier, the scoreboard, the cards and the chip they fold to, the Use prompt at the rack and the fire, and the downed, end and dawn screens with their awards. It grows with the screen’s height, as Unity’s Canvas Scaler matches a reference resolution: every size is in rem, and the page’s rem runs from 14 px at 540 rows through 16 px at 720 to 20 px at 1080, and no further than 24 px, so the browser lays the HUD out again at each size and its type stays sharp. Each warden’s elements show as badges in their colours beside their name, on the scoreboard and the end screen, and as her element lines on the health panel. An element’s card shows its job as a picture and a word, in its colour’s frame.

  • The wave banner shows a row of the night’s fifteen waves, the colossi’s larger, with those held lit and the next one breathing, then the wave’s number large beside “Wave 7 of 15”, or “Endless · wave 17”, the wave’s name in its colour, and a red bar of the wave left with its count, which bumps as each monster falls. In a breather the countdown reads words first, “Wave 2 in 17”, its number turning red over its last five seconds, and while her cards stand open the shop’s bar under them holds the countdown and Ready, and the banner names the wave alone; the first breather reads “The Hollow wakes in 8” over “The night’s monsters. Hold them off until dawn” and asks nothing. The banner names the next wave when it has a name or a colossus, with a line on how to fight it, and holds the one thing to do: the Ready button with its R key, and how else to be ready or to take it back. The scoreboard says who else is ready. The colossus’s health stands under it while one stands, named as the banner and the call name its wave, “The last colossus” at the fifteenth; the banner leaves the name to it meanwhile.
  • The calls stand under the banner and the colossus’s health, clear of the aim: a wave rising, named in its colour where it has a name, over “Wave 7”, with a lower horn under a clash, the last colossus called as such; a wave held, by its title alone; the fire growing a level; a teammate going down and getting up; her own warden back on her feet; her own purchase at the rack; and, once on a page, after her first free card, “Spend coins at the rack”, which says where coins go. Each slams in, and lifts away after two seconds; a newer call steps the one before it down to a line. Her cards’ deal takes the screen: a call up as the cards come leaves, and one made while they turn waits for them to settle into her hand. While her hand is open a call is one line between the banner and the hand, and the rack’s tags step back under any call.
  • The scoreboard shows each warden’s name, cut short with an ellipsis only where the row runs out of room so the tags beside it stay whole, with her career level beside it, “you” beside her own and a microphone while a warden speaks, her elements and her state: Down, Ready, Joining or Away, and Choosing beside Ready for a warden whose free card still waits. Each teammate’s coins and every warden’s kills show between waves and while Tab is held; during a wave the board keeps to names and states. Her own coins are her health panel’s, and the shop bar’s while her cards are open, and her row leaves that column empty. A row flashes red as its warden goes down and green as she gets up, and each count bumps as it changes. On a touch screen the list stands under the banner instead, as one compact row as wide as the screen: each warden’s name, level, elements and state, with no counts and no “you”, her own chip lit, wrapping onto a second line where the screen runs out. A phone held upright starts the top of the screen under the corner’s Menu and microphone buttons, so the banner never covers them. While her element pick stands open on a touch screen, or any hand of hers on a phone, the wardens row steps aside for it, as does the gathering banner for the pick and the wave banner for a hand on a phone held upright; they come back as she picks, hides the hand or readies.
  • The rack’s tags stand over their stands, in a breather and while the wardens gather, never during a wave, nor while her element pick is open, nor while her hand stands at the top of a phone held upright. A tag shows its gun’s name and price for a stand within 10 m of her feet, within 30° of the camera’s forward and in sight: a stone, a teammate or a monster between the camera and the tag hides it, as Roblox’s prompts need a line of sight. Within 4 m it adds what the gun does, and within 2.2 m the Use prompt speaks instead. A tag fades in over a quarter second and out over 0.4 seconds, and keeps a metre past each distance and 6° past the facing, so a warden standing on an edge never sees it flicker. Where the stands line up on the screen, the nearest keeps its place and each farther tag lifts above the ones it would cover, and above her open cards or their chip.
  • The controls stand along the bottom from her first moment on the page, WASD to move, the mouse to aim and its left button held to fire, above her element’s chip, as Vampire Survivors shows its controls once, small. Move checks off as she walks, Aim and fire as she fires, and the strip goes for the rest of the page three seconds after she has walked 2 m, fired or not, so it never outstays her first steps. A touch screen shows none, since the game has no touch controls yet.
  • A rift out of her view gets a chevron at the edge of the screen toward it, in the eye colour of what climbs out, as Left 4 Dead points at a special it announces. It turns as she does and fades within about two and a half seconds, faster once the rift is in view. Rifts within 30° share one. It draws larger in waves 1 to 3 and for a colossus.
  • The Use prompt stands under the crosshair, or over it while her hand is open, while a warden stands at a stand of the rack or beside the fire: what E buys there, its price, how many coins more it needs where the purse is short, and what the gun or the tier does. On a touch screen the prompt itself is the button.
  • The downed panel sits under the crosshair, which she still aims with, with a beating heart, and says what gets her up and that she fires at half her rate. Its bar lights amber, “A warden is getting you up”, while a teammate stands over her, and dims to “Waiting for a warden” once none does. A warden alone sees “Getting yourself up”, and that a second fall before the next colossus falls ends the run.
  • The health panel stands in the bottom left. On a computer it draws at four fifths of the size a touch screen gets, every row with it, so it keeps to its corner as the own-status corners of Overwatch and Fortnite do. A touch screen, whose pointer is coarse, keeps the larger size. The panel of a warden alone, on her feet during a run, says whether her self-revive is in hand, or that she has none until the next colossus falls.
  • The dawn screen stands in gold over a warm veil, with a rising chord.
  • The end and dawn screens keep their two buttons in a row under the pane, as wide as it; on a short screen the pane scrolls and the buttons stay in view.

The sounds are recorded CC0 files from Kenney’s packs, each played a little higher or lower every time. The scattergun crunches and the rail cracks over a low sweep, both rolling off the treeline in a dark echo. Each upgrade tier plays a gun’s shot a little higher over a layer of its own, as Pack-a-Punch does: a rising zap from the first tier, a low thump from the second and a high shimmer at the third. A teammate’s shot plays quieter the farther she stands, at half the level of the player’s own and among the crowd’s sounds, so the player’s own gun stays on top. A gun bought at the rack latches over a coin’s chime, an upgrade chimes rising, a feed rushes like flame, a reroll sweeps the old hand away, and the fire rising a level roars under a rising fifth; a purchase the purse cannot meet buzzes. Ice cracks as a monster freezes, flame rushes as one catches full burn, and each reaction and capstone has its own: an electric crack for Chain Shock, a low explosion for Blast, a hiss for Steam Cloud, thunder for Thunderhead, a roar of fire for Meltdown and glass for Shatter. Storm’s crackling arc is synthesized on the page, as are a card’s flick as it turns, an epic’s climb before it bursts and the chime of a taken card landing. A moment farther from a warden plays quieter, and a teammate’s arc quieter still. A monster’s fall is a soft thud, the quietest sound of a fight; a brute’s is lower, and a colossus’s a deep boom. Each sound plays at most a few at once and no sooner than a moment after the last, so a wave falling is a patter rather than a wall, and a reaction, a capstone or a colossus falling ducks the crowd’s sounds, the falls, hits, arcs and feet, for a moment under it. A warden’s own coins that land in quick succession chime a step higher each, up a major scale to an octave, and the coin count on the HUD swells as each lands. A looping night bed of crickets, the fire’s crackle and a low drone plays under them; while a wave stands, the crickets hush and the drone swells, and both come back as the breather opens. Until a cue’s file loads, or where it fails to, that cue is synthesized on the page instead; an ambience layer that fails stays silent while the other plays.

The three guns and the monsters are in packages/assets/models/holdfast/, and the sounds in packages/assets/sounds/holdfast/. packages/assets/catalog.json records where each file comes from and its licence. The wardens’ avatars are the platform’s own, in packages/assets/avatars/.