Log inGet started
World

Fencing with Fences

Branchmain
Branchesmain
Commitc0197a6317

README.md

DuelFx

Everything between the rules firing and the player feeling it. The umpire decides what happened; this decides what it feels like, and where in the garden it sounded like it happened.

Attach it to DuelDirector. It reads DuelMatch and both Fencers and writes nothing back to either.

entity.find("DuelDirector").component.add("DuelFx", {})

Four commitments, four weights

A thrust arrives on a point and is over. A sweep is a whole fence panel and it turns the man it lands on. A lunge brings a body 1.30 m with it and tears grit off the piste the whole way. A riposte comes off a guard rather than out of a wind-up, and it costs two posts.

Which one landed is read out of DuelMatch.lastExchange, and every channel of the reaction is driven from one row of the PROFILE table:

recoilspinshovehitstopdustlanding sound
thrust0.880.08×1.0×0.9a narrow plume at chest heighttouch_thud +1.12, panel_clack
sweep1.001.00×2.1×1.6a low wide sheet across the pistetouch_thud −0.80, panel_crunch
lunge0.960.20×3.2×2.1a ground trail from the back foot to the contacttouch_thud −0.94, panel_clack
riposte0.920.30×1.4×1.3bright chips off the guardriposte_open, touch_thud

spin is the sweep's signature — only a broadside turns a fencer off the line, so a still frame of a sweep is nameable without sound. shove is the lunge's: it gives up three times the ground a thrust does.

The chalk, and the hold

At the measure a touch resolves at, two men holding whitewashed fence panels are one beige mass from side on. Two things put the read back into the frame, and both of them last as long as the umpire holds the bout rather than as long as an impact.

The chalk. Every panel in this garden is whitewashed timber, so a stroke that lands leaves its coat behind. The mark runs FROM the man who struck TO the man who took it, so which way the blow went is in the picture; its shape is which stroke it was:

what it writes
thrustone straight level line along the picket that led
lungea long low line off the back foot rising into the contact, with the point on the end of it
sweepfive strokes of a falling quarter circle, from above the shoulder it came off down onto the man
ripostethe guard it came off, crossed, and the answer out of it

It is drawn 0.62 m nearer the camera than the piste, so it is never swallowed by the two bodies it is describing, and it holds its length while it thins. chalk turns it off; chalkLife is how long it hangs.

The hold. A reaction used to be gone in three tenths of a second, which at this build's frame time is one frame — so the pose that names the commitment was never in the picture. contactHold pins each fencer's recoil, spin, lean, overreach and jam at the values the blow gave them, as a floor rather than an assignment, so a shove still running out or a second event inside the beat still reads on top of it.

PisteCamera swings off the piste axis for the same beat, which is the other half of the same fix — see its README.

The two faults, which are not touches

SHORT and CROWDED are recorded by the umpire and never reach the event bus, so fx derives them from the exchange record. Their feedback is deliberately built out of the opposite parts of the vocabulary from a touch, so the two can never be confused:

  • the reaction is on the attacker, not the defender
  • there is no hitstop, no contact flash and no rim
  • the shape left behind is a flat scuff disc lying on the piste — the only thing this component ever draws on the ground

SHORT pitches the attacker forward through a stroke that found nothing (whiff_air + gravel_scuff). CROWDED jams the panel and shoves both fencers apart (panel_scrape + a tight panel_clack).

The post is driven, not moved

The umpire awards a touch and the boundary is already somewhere else: the line is a number, and Garden.fence rebuilds the run at its new value inside the same frame. What the eye is given instead is the thing the rite is actually about.

Over driveTime, the surveyor's cord walks from where the line stood to where it now runs — driveWalk seconds of it, on a curve that leaves fast and settles — carrying a post held clear of the turf. Then driveBlows mallet blows put the post in, each one seating it further, throwing its own turf and its own post_drive at a lower pitch than the last, until it is flush with the boundary's own post.

driveFrom, driveTo, drivePct and driveState report it live, so a photograph taken across the pause can be matched to exactly how far in the post was. driveFrom is derived from the umpire's published lastDrive and lastTouch rather than remembered, so it is exact.

The rite

The umpire is on a beat at all times and DuelMatch.ceremonyPhase names it. Each beat has its own calls, drawn from a bag — a call is not heard twice until every other call on that beat has been heard once — and which calls are eligible depends on the state of the bout:

beatcallsnarrowed by
saluteumpire_salute, umpire_engarde, umpire_ready
enGardeumpire_engarde, umpire_ready, umpire_score, umpire_lastthe score only when it is level; the last post only when one post ends it
allezumpire_allez, umpire_play, umpire_come"come on" only on a ground that does not stand on form
haltumpire_halt, umpire_hold, umpire_arret"arrêt" only on a ground that does
postumpire_point, umpire_touche, umpire_award_for, umpire_award_againstthe award reads for or against by who scored
verdictumpire_verdict

Whether a ground stands on form is Duel.ceremonyScale(neighbourKey), so the umpire keeps a different rite in each of the six gardens without anybody writing six rites.

ceremonyLog is the record: one line per call, numbered, with the beat it belongs to and — where a frame was long enough to step over a 0.28 s beat entirely — the beat the umpire said he was on, as post@halt.

The bell counts. One strike opens an assize; three close it, rising, with the wall giving the last one back. Both men breathe in the pause, the one who took it harder than the other, and both of them harder once the bout is six touches deep.

Every neighbour has their own throat. NEIGHBOUR_VOICE pitches and weights the rival's tells, efforts and breath per neighbourKey — Mrs Pettifer light and high, Lady Ashcombe low and quiet, Young Tibb high and loud — so who you are fencing is audible before you look.

The soundstage

The lawn is thirty metres of piste between two hedges, and three of its places can be heard:

sourcewherewhat
amb_orchardwest end, x −14the player's own orchard: leaf wash, bees, a blackbird
amb_walleast end, x +14the neighbour's walled end: drier wind, a wood pigeon, a wren
sfx_bellthe garden wall, x 0 z −13the assize bell, and its return off the wall 0.40 s later
sfx_umpirewalks the piste beside the fightallez, halt, point
sfx_bedeverywherethe garden's own bed, non-spatial

The two hedge beds crossfade on where the fight has got to, so a bout driven into the player's garden sounds like the player's garden. ambience is the readback: focus=-10.00 near=orchard orchard=0.296 wall=0.171 bed=0.340.

Footfall knows what it is standing on. The piste is raked gravel end to end, but the ground within 1.05 m of a driven post is the spoil thrown out of its hole, so a fencer standing on the boundary steps in broken turf. stepSurface reports it.

Clip → event

Every clip in /source/audio/ and what fires it.

clipfired by
bell_assizesalute; over:* — three strikes, rising, at +0.55 / +1.05 / +1.55 s
bell_returnsalute (+0.40 s); over:* (+1.95 s) — the wall giving it back
umpire_allezthe allez beat
umpire_playthe allez beat
umpire_comethe allez beat, on a ground with no ceremony to observe
umpire_haltthe halt beat, and the one that ends the assize
umpire_holdthe halt beat
umpire_arretthe halt beat, on a ground that stands on form
umpire_pointthe post beat, 0.60 s after the stop
umpire_touchethe post beat, on a ground that stands on form
umpire_award_forthe post beat, when the touch was the player's
umpire_award_againstthe post beat, when it was against
umpire_salutethe salute beat that opens an assize
umpire_engardethe salute and enGarde beats
umpire_readythe salute and enGarde beats
umpire_scorethe enGarde beat, when the touches are level
umpire_lastthe enGarde beat, when one post ends the assize
umpire_verdictthe verdict beat, read over the last of the bells
breath_hardthe fencer who took the touch, in the pause after it
breath_softthe other one, and neither of them once the bout is deep
post_drivepost; allez (a light tap, pitched up)
touch_thudtouch:*, pitched by the commitment that landed
panel_clacktouch:* other than a sweep; parry:*; clash; crowded:*
panel_crunchtouch:* where the landing commitment was a sweep
panel_scrapecrowded:*; clash
riposte_openparry:*; riposte:*
whiff_airshort:*
gravel_scuffshort:* — the foot saving the over-reach
gravel_stepstep:* on the raked piste
turf_stepstep:* within 1.05 m of the boundary post
tell_thrust<role>:thrust, and any commitment with no voice of its own
tell_sweep<role>:sweep
tell_lunge<role>:lunge
tell_parry<role>:parry
effort_sweep<role>:sweep — the breath under the arc
effort_lunge<role>:lunge — the breath of going the whole way
garden_bedlooping, non-spatial
hedge_orchardlooping at the west end, gain by focus
hedge_walllooping at the east end, gain by focus

The flash

The rim is the outline channel on a struck fencer's models, and nothing else in the world writes it. It marks the body that took the touch, so it skips the two classes that are not one: what a fencer stands on, and what they are holding. Both are matched by name (panel, shoe, stocking, leg, foot, sole), because a name is the only property of a rig part that a pose does not change — position and scale are rewritten every frame by fencer_body.pose, so a geometric gate answers differently depending on which frame it is asked.

flashState reports the live rim level and how many models it is allowed on: player=0.64/11 rival=0.00/19. A rim that stops appearing because a body module renamed its parts is invisible in a screenshot and obvious there.

Timing, and the frame rate it was tuned at

Every window here was tuned against a measured 104 ms frame — about 9.6 fps (zero profiler retro --seconds 20), and every one is expressed in real seconds rather than frames, so it means the same thing when the frame rate comes up. The hitstops run 0.09 s to 0.26 s: one to three frames at 9.6 fps, five to sixteen at 60.

Instruments

fieldwhat it says
busProduced / busTaken / busMissedevents the umpire emitted, events drained off fxQueue, and the difference. busMissed is zero when nothing is lost.
recordsMissedexchange records overwritten inside one frame. The events still arrive; only the detail behind them is gone, and the reaction falls back to its default weight (lastReaction says unread).
eventCountevents dispatched, including the two derived faults.
fxPathqueue once the lossless read is live, derived before it.
soundCount / lastSound / soundLogevery clip that has played, stamped on the clock capture reports as sceneTime per cell.
lastReaction / reactionLogevery reaction, named — touch:player lunge, short:rival overreach, crowded:player jam.
ambience / stepSurface / flashState / motesAlivethe soundstage, the ground underfoot, the rim, the live mote count.
ceremonyLog / ceremonyCount / lastLineevery umpire line that has fired, in order, with the beat it fired on. What a rite that repeats looks like, as data.
driveFrom / driveTo / drivePct / driveStatewhere the line stood, where it now runs, and how far through putting the post in this frame is.

What it owns

A temporary duel_fx root holding the ears, seven emitters, the flash, the turf puff, the scuff disc, a sixteen-mote pool, a nine-segment chalk pool and the post, collar, mallet and cord of the drive; the rig child transform of each fencer; the outline channel on their body models; punch / kick / shake for PisteCamera; and engine.timeScale, for the length of one hitstop and no longer.

The drive's post goes in at z = -2.40, which is where garden.module stands the boundary's own post — the two are the same place on purpose, so what the drive puts in is the post that is already there.