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.
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:
| recoil | spin | shove | hitstop | dust | landing sound | |
|---|---|---|---|---|---|---|
| thrust | 0.88 | 0.08 | ×1.0 | ×0.9 | a narrow plume at chest height | touch_thud +1.12, panel_clack |
| sweep | 1.00 | 1.00 | ×2.1 | ×1.6 | a low wide sheet across the piste | touch_thud −0.80, panel_crunch |
| lunge | 0.96 | 0.20 | ×3.2 | ×2.1 | a ground trail from the back foot to the contact | touch_thud −0.94, panel_clack |
| riposte | 0.92 | 0.30 | ×1.4 | ×1.3 | bright chips off the guard | riposte_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 | |
|---|---|
| thrust | one straight level line along the picket that led |
| lunge | a long low line off the back foot rising into the contact, with the point on the end of it |
| sweep | five strokes of a falling quarter circle, from above the shoulder it came off down onto the man |
| riposte | the 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:
| beat | calls | narrowed by |
|---|---|---|
salute | umpire_salute, umpire_engarde, umpire_ready | |
enGarde | umpire_engarde, umpire_ready, umpire_score, umpire_last | the score only when it is level; the last post only when one post ends it |
allez | umpire_allez, umpire_play, umpire_come | "come on" only on a ground that does not stand on form |
halt | umpire_halt, umpire_hold, umpire_arret | "arrêt" only on a ground that does |
post | umpire_point, umpire_touche, umpire_award_for, umpire_award_against | the award reads for or against by who scored |
verdict | umpire_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:
| source | where | what |
|---|---|---|
amb_orchard | west end, x −14 | the player's own orchard: leaf wash, bees, a blackbird |
amb_wall | east end, x +14 | the neighbour's walled end: drier wind, a wood pigeon, a wren |
sfx_bell | the garden wall, x 0 z −13 | the assize bell, and its return off the wall 0.40 s later |
sfx_umpire | walks the piste beside the fight | allez, halt, point |
sfx_bed | everywhere | the 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.
| clip | fired by |
|---|---|
bell_assize | salute; over:* — three strikes, rising, at +0.55 / +1.05 / +1.55 s |
bell_return | salute (+0.40 s); over:* (+1.95 s) — the wall giving it back |
umpire_allez | the allez beat |
umpire_play | the allez beat |
umpire_come | the allez beat, on a ground with no ceremony to observe |
umpire_halt | the halt beat, and the one that ends the assize |
umpire_hold | the halt beat |
umpire_arret | the halt beat, on a ground that stands on form |
umpire_point | the post beat, 0.60 s after the stop |
umpire_touche | the post beat, on a ground that stands on form |
umpire_award_for | the post beat, when the touch was the player's |
umpire_award_against | the post beat, when it was against |
umpire_salute | the salute beat that opens an assize |
umpire_engarde | the salute and enGarde beats |
umpire_ready | the salute and enGarde beats |
umpire_score | the enGarde beat, when the touches are level |
umpire_last | the enGarde beat, when one post ends the assize |
umpire_verdict | the verdict beat, read over the last of the bells |
breath_hard | the fencer who took the touch, in the pause after it |
breath_soft | the other one, and neither of them once the bout is deep |
post_drive | post; allez (a light tap, pitched up) |
touch_thud | touch:*, pitched by the commitment that landed |
panel_clack | touch:* other than a sweep; parry:*; clash; crowded:* |
panel_crunch | touch:* where the landing commitment was a sweep |
panel_scrape | crowded:*; clash |
riposte_open | parry:*; riposte:* |
whiff_air | short:* |
gravel_scuff | short:* — the foot saving the over-reach |
gravel_step | step:* on the raked piste |
turf_step | step:* 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_bed | looping, non-spatial |
hedge_orchard | looping at the west end, gain by focus |
hedge_wall | looping 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
| field | what it says |
|---|---|
busProduced / busTaken / busMissed | events the umpire emitted, events drained off fxQueue, and the difference. busMissed is zero when nothing is lost. |
recordsMissed | exchange 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). |
eventCount | events dispatched, including the two derived faults. |
fxPath | queue once the lossless read is live, derived before it. |
soundCount / lastSound / soundLog | every clip that has played, stamped on the clock capture reports as sceneTime per cell. |
lastReaction / reactionLog | every reaction, named — touch:player lunge, short:rival overreach, crowded:player jam. |
ambience / stepSurface / flashState / motesAlive | the soundstage, the ground underfoot, the rim, the live mote count. |
ceremonyLog / ceremonyCount / lastLine | every 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 / driveState | where 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.
Scoped to this part · feeds back into the world's score.