Log inGet started
World

Fencing with Fences

Branchmain
Branchesmain
Commitc0197a6317

README.md

DuelHud

The shell around the fight, and the shape of a session. Sits beside DuelMatch on the DuelDirector entity.

The shell is furniture at the edges

The boundary itself is the real health bar — it stands in the world where both fencers can see it — so the shell reads the ceremony rather than covering it. Four screens:

screenlayerwhat it is
hedgerow_assizes10the ribbon along the top edge: the two scores, the boundary strip with the leading post standing in it, and under it the umpire's call on a caption band
assize_read10the read strip along the bottom edge: a chip per fencer naming the commitment as its wind-up starts, the measure gauge between them, and a footer carrying the docket line and the live key hints
assize_riposte11the riposte banner, which exists only while a riposte is open, tucked under the caption where the sky is
assize_card12the writ that opens a docket and the verdict that closes an assize — anchored over the sky, never over the lawn
assize_scrim8a top-down ramp that settles the sky behind the writ. The verdict gets none: the board at the end of a bout is the picture

Measured at a 1280×720 canvas: the ribbon group is 38–66 pt tall (the caption band is only as tall as the words in it and collapses to nothing when the court is quiet) and the read strip 68 pt, so the shell occupies 5.3–9.2 % of frame height at the top, 9.4 % at the bottom, and 7.9–8.2 % of frame area, with 79–83 % of the frame height carrying nothing at all.

The canvas is the same 1280×720 whatever the framebuffer is — ui.screenSize() is pinned there while the window is 1920×1080 — so the shell is laid out on a 720p canvas and upscaled uniformly, and every fraction above holds at both sizes. pinCanvas(960, 600) moves the ribbon to x 210 and the read strip to y 522, which is the proof that the layout is canvas-relative rather than measured against numbers somebody typed.

The docket

An assize is one bout. A docket is the court's whole sitting: every neighbour from the one you name down to the last on the roster, heard in order, with the ground won and lost carried forward.

  • docketFrom — the neighbour the sitting opened at (1–rosterSize)
  • docketHeard / docketWon — assizes concluded, and how many you took
  • docketGround — metres of boundary held on the day, summed across assizes
  • docketRecord — the same thing as a readable string, pettifer:lost:-10:0-9:29s | wickham:won:+10:10-6:74s
  • public:docket() returns all of it as structured data, one row per assize

The docket advances on the transition out of state == "over", whichever path takes it there — this card's button, the restart key, or an agent calling DuelMatch:restart() — so no path has to know the docket exists. The next neighbour is armed the moment an assize ends, by writing DuelMatch.skill, .neighbour and .neighbourKey before any restart can read them.

Identity, and what this component owns

DuelMatch owns identity (neighbourKey) and time (matchClock); this component owns the prose — names, seats, writs, notes and the umpire's whole vocabulary. Both optional fields are detected by enumerating the component rather than by touching a name, because reading an absent field logs an engine error on every frame. When they are absent the HUD falls back to its own roster index and its own bout clock, so neither component has to land first.

Commitments and key hints are generated from Duel.ACTIONS and the live input profile (Zin.actions.get), so a stroke added to duel.module or a rebound key appears on the writ, in the measure gauge and in the hint line with no edit here.

The front door

A player arriving gets the writ: what the fence means, what a touch does to the boundary, the four commitments with their live keys and the triangle between them, and the whole roster as tiles. Naming a tile opens the sitting at that neighbour and hears every one below them in turn, so all of the roster is reachable from a cold arrival.

The opening is gated on this component's own boot flag — a module local, fresh exactly when the component is — and on nothing that persists. Every counter that persists has already proved it can reach a value the door never opens at again: assizes was a replicated field gating the writ on == 0, it reached 1 the first time anybody opened a docket in this world, and after that a new player arrived inside a stranger's losing bout with nothing on screen. The sitting is now held in a module-local table and the fields are written from it every tick, so the layer's overlay restoring a docket counter five seconds after a load is undone inside a frame and the fields stay what they are for — a readable mirror for everyone else.

The board itself is only put back when it is not being fenced. Arriving in the middle of somebody's live bout raises the writ and leaves their fight alone.

The way out

N rises the court from anywhere — mid-bout, on a verdict, on the court-rises card. It clears the docket, puts the boundary back to nought, names the first neighbour again and shows the writ. It is what makes the opening impossible to lock out, so it does not depend on any other component: the scheme is asked for an action named rise first, and the raw KeyN code is used when the scheme has none. A rise action added to fencing_controls is picked up with no edit here — including its touch button, which the raw code cannot give.

The card's own RISE — A FRESH SITTING button does the same thing with a pointer.

Clicks are recorded, not obeyed on the spot

Every action the cards offer ends in DuelMatch:restart(), which rebuilds the garden, the fence and the rival — over a second of work on a loaded machine. Run straight from the UI callback it was cut off by the script watchdog partway through, leaving the board at nought-all with state still over while gui.clickElement reported ok. onCallback records what was asked and the component's own update serves it. A board that reads over with nobody named is not a verdict — it is a restart that did not finish — so it carries no card, goes on nobody's docket, and is asked for again.

Methods

  • public:beginAssize(index) — open a fresh sitting at that neighbour
  • public:callNext() — call the next assize on the open docket
  • public:riseCourt() — rise the court and return to the writ
  • public:chosenNeighbour() — { index, key, name, skill } of who stands opposite
  • public:docket() — the sitting as data, one row per assize concluded
  • public:pinCanvas(w, h) — lay the shell out against a pretend canvas

Fields

screen / readScreen / cardScreen / scrimScreen (registered screen names), neighbour, card ("" | "writ" | "verdict" | "docket"), cardAge, assizes, refresh, boutClock, and the six docket fields above. None of them replicate: the sitting belongs to whoever is sitting in it, and every one of these is a mirror of the module-local copy that actually holds it.

Runs in both edit and play so the shell is visible while authoring.