Skip to content

jengine-stealth

Guards who see in cones and hear in circles, alert states that decay, and a level whose line of sight is exact because the grid is the map.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-stealth
Version0.1.0
Entry pointjengine_stealth::StealthPackage
Script namespaceStealth
Listed byprojects/arcade-classics, projects/shooter-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-stealth", "source": { "type": "path", "path": "../../packages/jengine-stealth" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Stealth == nil then return end.

Scene keyRust type
qh_missionMissionpackage only: needs a v2 scene
hl_floorHlFloorpackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Stealth.hl_start(e, floor, mask)qh_hl_start
Stealth.hl_input(e, mx, my, ax, ay, buttons)qh_hl_input
Stealth.hl_auto(e, mode)qh_hl_auto
Stealth.start(e, which)qh_start
Stealth.input(e, dx, dy, gait)qh_input
Stealth.noise(e, x, y, carried)qh_noise
  • projects/arcade-classics/assets/scenes/halls.scene
  • projects/shooter-classics/assets/scenes/hotline.scene

From packages/jengine-stealth/README.md.

Guards who see in cones and hear in circles, alert states that decay, and line of sight walked over the grid the player is reading.

Used by projects/arcade-classics (Quiet Halls) and projects/shooter-classics (HOTLINE, through the hotline module - see “Hotline” below).

ModuleWhat it is
levelThe map as an ASCII drawing, and every way one can be unplayable while looking fine.
visionWhat a guard can see and hear. Pure, and the most important thing in the game to be right.
guardThe alert machine — patrol, suspicious, hunting, searching.
runThe hero, the noise, and the tick that joins them.
contentLevels and guard types as data tables.
hotlineThe same eyes with guns behind them: a top-down floor where one hit kills, guards who shoot and charge, doors that swing, and an autopilot.

This is the game the arcade’s roadmap picked to force Physics.raycast, and Physics.raycast does now work in all four runtimes because of it — it was the last script function that did not (BUGS #146, test-lab scene 92). It is deliberately not what decides whether a guard can see you.

A stealth game is a game about predicting what a guard can see. The player looks at a corner and decides whether stepping out is safe, and then has to be right. That makes determinism worth more here than physical accuracy, and rapier loses on three counts:

  • it answers against merged tilemap colliders, whose seams do not line up with the tiles the player is reading;
  • it answers in floating point against a body that has drifted a fraction of a unit;
  • and it can answer differently on two machines.

“I thought I was hidden” is the worst sentence a stealth game can produce, and each of those can produce it. So sight is a walk over the grid, and the answer is the same every time, on every runtime, for everybody. What Physics.raycast is for here is the thing the grid does not know: a dynamic obstacle the physics world owns.

Two rules that came out of that, both chosen rather than fallen into

Section titled “Two rules that came out of that, both chosen rather than fallen into”

A corner hides you completely. The walk is supercover, not Bresenham, because Bresenham steps diagonally between two walls that meet at a corner — so a guard sees through a crack the player can see is shut. Supercover visits both tiles at a corner, which means either being solid blocks the look, not only both. The permissive version is closer to real line of sight and is the wrong call: “a guard cannot see past a corner” is learnable in one level, and “unless the corner is a full right angle” is a rule nobody works out. It also errs the safe way — a guard seeing less than expected is a pleasant surprise.

Standing in cover hides you, and a guard in cover can still see out. Sight tests the target’s tile and skips the eye’s. Getting inside a crate is the fundamental verb of the genre, and a version that only blocked sight through cover would make every cover tile scenery, leaving nowhere to wait in a level built around waiting. Blinding a guard beside a crate would make cover a place guards go to stop working.

The cost is that sight is one-way: a guard in a bush can see you while you cannot see it. That is true of bushes, and the game only ever asks guard-to-player. What is symmetric is the part in between — the walls — checked over every pair of a hundred tiles, because a wall that blocked one way and not the other would be a real bug with nothing on screen to explain it.

Noticing takes time, and that is the whole feel of the game

Section titled “Noticing takes time, and that is the whole feel of the game”

The obvious guard sees you and instantly knows. That guard is unplayable twice over: every mistake is already fatal by the time it is visible, and the edge of a cone becomes a cliff, so the game is about pixel positions instead of about reading a room.

So a guard fills a suspicion meter, faster the more exposed you are, and it drains when nothing is seen. Everything the player experiences as tension is that number moving: the glance that nearly caught you, the corner you reached in time, the guard who walks off still half-sure. At full exposure the player gets about a second and a half; edging along a far wall buys three times that.

exposure weights range above angle, deliberately — so the answer to being seen is to retreat, not to sidestep.

Four states, because the player has to predict them

Section titled “Four states, because the player has to predict them”
StateDoesLeaves when
Patrolwalks its route, glancing about at each waypointsuspicion rises
Suspiciousturns toward it and stands stillsuspicion fills, or drains away
Huntingwalks to where it last had youit arrives and finds nothing
Searchinglooks around near therea timer runs out

Each transition is the way round it is for a reason, and the reasons are in the tests:

  • Hunting does not end when you break line of sight. It ends by arriving. A guard that gave up the moment it lost sight of you would make every chase a matter of stepping behind one crate.
  • Searching returns to Patrol, not to Suspicious. A guard that could never fully calm down turns one mistake into a lost run, and restarting becomes the only recovery mechanic — which is the same as not having one.
  • Suspicious stands still. That stillness is the player’s window to move. A guard that advanced on every flicker would never give one.
  • A noise can never make a guard certain, only suspicious. A sound says something is there, not what or exactly where — and if noise could fill the meter, a thrown distraction would be a way to get caught from another room.
  • A patrolling guard glances, it does not spin. Turning full circle at every waypoint sounds thorough and makes a room unplayable: seven tiles of sight sweeping a room eight across means nowhere in it is safe for more than a few seconds, so there is nothing to plan around and the only tactic left is luck.

Being seen once should be survivable and being seen repeatedly should not. A life count makes the first sighting free and the last one arbitrary; a meter that rises while guards hunt and falls while they do not makes recovery an activity — break line of sight, wait, and watch it come down. That waiting is the most stealth-like thing in the game and it only exists because the number is continuous.

A level is a table row, and its failures are about routes

Section titled “A level is a table row, and its failures are about routes”

Almost everything that can be wrong with a stealth level is a route rather than a tile, and none of it crashes. Each is a player walking the level again convinced they have missed something. So Level::parse refuses:

  • an exit that cannot be reached at all, even with every door open;
  • an exit behind a door whose only keycard is behind the same door;
  • a patrol waypoint inside a wall;
  • a patrol that cannot walk its own route — both waypoints on floor, the level looks right, and the guard stops at a wall partway round, so the patrol the level appears to describe never happens;
  • a guard whose route starts within four tiles of @. A guard’s near radius ignores facing, so one placed beside the start notices the player before they have touched anything. Three levels were drawn with this mistake before it became a rule.

content::check adds the pairing rule across the two tables — every route must name a guard that exists — plus the guard numbers that are playability rules rather than difficulty knobs: one that notices in under a second, one that never calms down, one that chases slower than it patrols.

And whether a level can actually be snuck through

Section titled “And whether a level can actually be snuck through”

None of the above can say that. A level whose only route is watched the whole way is not a stealth level, it is a chase, and it does not look any different on the page. So tests/halls.rs plays each of the arcade’s levels with a crude player: it creeps, and when noticed it heads for the nearest cover that is not toward whatever noticed it.

Building that player found more than the levels did — the doorway nudge, the glancing patrol, cover that did not hide you, and the start-clearance rule above. The lesson is in the shape of the test rather than in any one bug: the questions “is this traversable” and “is this playable” are different questions, and only one of them can be answered by looking.

o — cover — is the only tile that answers “can I walk here” and “can I see through here” differently, and it is where most of the interest in a layout comes from. A corridor of cover is a corridor you can cross while a guard is looking straight down it.

~ — hard ground — is the other half: a wall made of noise. It is how a level says “not through here in a hurry” without a wall.

"qh_mission": {
"tables": { "levels": "data/qh_levels", "guards": "data/qh_guards" },
"which": 0, "sheet": "atlases/halls.png", "tile": 40.0
}
CallDoes
Stealth.start(e, which)build a level and begin
Stealth.input(e, dx, dy, gait)−1..1 each; gait 0 creep, 1 walk, 2 run
Stealth.noise(e, x, y, carried)a noise the script made — a thrown bottle

QhState carries the phase, the level’s name and index, time and best time, the alarm as a fraction, sightings, keycards, score, the hero’s position in both world and tile space, how much noise they are making, and a guards array with each guard’s position, angle, state, suspicion, range and half-angle.

That array is the one thing that crosses the bridge every frame, and it has to: the cones are the interface. A stealth game where you cannot see what a guard can see is a guessing game, so the game draws a wedge per guard from those numbers. The package deliberately has no opinion about how — a scaled sprite, an arc of lines, a shader — because that is a rendering decision.

QhEvents carries start, spotted, lost, noise, card, door, caught and escaped. noise fires every tick the hero moves, which is sixty a second; the game rate-limits the footstep sound itself, because that is a presentation decision too.

The hotline module is the second game on these eyes (Shooter Classics’ HOTLINE, a Hotline Miami). Quiet Halls asks can a guard see me; Hotline asks the next question - and then what. It is a separate component with its own script commands, so Quiet Halls plays exactly as before.

ModuleWhat it is
hotline::floorA floor as an ASCII drawing (walls, three floors, doors, windows, furniture, guards by weapon, weapons on the floor, lamps, the way out), refusing one that cannot be played: an open edge, a door not on a line, a guard within six tiles of the start, anything unreachable.
hotline::simThe floor simulated at a fixed 60 Hz step, with no clock and no randomness it was not handed, so a restart resets the floor exactly.
hotline::scoreCombo, boldness, variety and speed, and a grade measured against the floor.
hotline::pilotAn autopilot that plays through the player’s own controls.
hotline::pathA* over the floor’s cells, and whether a straight walk is clear.

They see with this package’s vision - the cone, and the supercover walk over the grid, so a corner still hides you completely - and they hear in a circle walls do not stop. Seeing you, a guard shoots if it holds a gun (after the floor’s reaction, which is most of the difficulty curve) and charges if it holds anything else, winding up before the blow so a player who stands their ground strikes first. Losing you, it walks to where it last saw you and looks about; hearing a gunshot, it comes to look. A guard knocked down drops its weapon and, getting up without one, goes for the nearest weapon on the floor. Dogs are fast, bite, and die to anything.

Anything that reaches the hero kills it (the BEAR mask takes one hit). Guards die to a blade, a bat, a pipe or a bullet; a punch, a thrown weapon or a door swung into them only knocks them down for three seconds, and a guard on the floor is finished by standing over it.

A door is a leaf on a hinge in the middle of the wall line. Walking into it swings it out of the way, as fast as you walked, and a leaf swinging faster than DOOR_BASH knocks down whoever is on the other side. It swings until it lies against the wall’s face. Sight and bullets meet the leaf where it hangs, not the door’s cell - the first version tested the cell, and a hero standing in a doorway shot every bullet into a door it could see past. So what can be seen can be shot, and a door half open hides exactly the wedge behind it.

Mask by index: none, Tiger (punches kill), Owl (your guns are silent), Hare (faster), Wolf (start with a knife), Bear (take one hit), Fox (doors kill), Moth (thrown weapons kill).

hotline::pilot answers with letters - W A S D move, L lock on, F attack, E pick up or throw, C finish - which is exactly what a keyboard can say. The attract demo feeds them back in; a hub test holds the keys they name; and tests/hotline.rs plays every floor that way, with the frame of latency real input adds and taps every other frame, restarting on a death. A retry plays a little differently (it waits a moment at the start, and may take another guard first), because the floor itself never changes. Building it found the door leaf above, a pilot that dithered between two guards about as far (it now keeps its target), and one that emptied a shotgun at a gunman fourteen tiles away.

"hl_floor": { "table": "data/hotline_floors", "sheet": "sprites/hotline/actors.png", "tile": 48.0 }

The sheet is a packed .atlas (listed in the scene’s atlas_refs) with regions hero, thug, gunman, dog, their _down / _dead poses, w_<weapon>, door and bullet; the package draws every actor, weapon, door and bullet from it as BulkEntity sprites. unlit: true draws them Unlit. The floor itself is the game’s to draw (HlMap below).

CallDoes
Stealth.hl_start(e, floor, mask)build floor floor (from 0, in table order) with a mask and begin
Stealth.hl_input(e, mx, my, ax, ay, buttons)move and aim in world axes (y up); buttons 1 attack, 2 pick up / throw, 4 lock on, 8 finish
Stealth.hl_auto(e, mode)0 off, 1 the autopilot plays, 2 it only publishes its advice

The commands share the qh_ prefix with Quiet Halls’ (qh_hl_start), so the namespace stays Stealth and Quiet Halls keeps Stealth.start.

HlState: the phase (play, dead, clear, done), the floor, the hero (position, facing, weapon, ammo, armour, what killed it), the score (points, combo and how long its window has left, best combo, bold kills, ways of killing, the end card’s total, bonuses and grade), the lock-on, the exits, a compact actors list, and the autopilot’s advice. HlEvents: the step’s shot, swing, kill, down, spark, pickup, throw, bash, creak, alert, armour, dead, clear and done, in world coordinates. HlMap, rewritten on every start: the floor’s look rows, its size, its top-left corner, its lamps and its exit cells.