feat(events): send the idempotency key, and declare champ.boss.killed (Phase 11a)
The website's half of protocol 6. Every event-driven write now carries the step's idempotency key, and `uo.broadcast` stops being un-retryable. Phase 9 shipped it answering `retry: false` to everything including a 503 from a shard that was merely restarting, with a comment naming the line that would change when the wire could refuse a repeat. This is that line: it defers to `sidecarFailure`, the same helper its two siblings already used, so the hand-rolled variant that forced every outcome terminal is gone rather than re-tuned. One verb was less idempotent than its own id made it look. Both keyed verbs post under a run-scoped id and a repeat replaces — but `news.add` with `announce: true` makes the criers proclaim the title on every post, so a retry replaced the article silently and proclaimed it again. The key stops the second proclamation. `champ.boss.killed` is mapped to the `champs` feature (rule 2 would otherwise fail it closed to admin), with `damagers` a nested `staff` field rule: the kill is public because a champion falling is what the board is for, the ranked roll of who was strong enough to fell it is not. `uo.champ.boss_killed` is declared as a trigger — which is what makes it usable as an event PHASE CONDITION, since a condition is written over a trigger firing — and it carries `damagerCount`, never a damager name, because a trigger variable reaches mail an operator may address to every subscriber. Its seeded rule is its own group, `champ-boss-killed-v1`: `triggers-v1` is stamped once under a settings guard, so appending a 27th entry would have reached fresh installs and nothing else. It also ships email+inapp and NOT push, and the comment says why — no trigger in this module is also a registered stream, so no engagement rule here can push. That is pre-existing in twenty rules and flagged rather than fixed; this one declines to be the twenty-first. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -662,6 +662,53 @@ const MAPPERS = {
|
||||
}
|
||||
},
|
||||
|
||||
// Protocol 6. A boss defeat, which until now could only be GUESSED at from
|
||||
// `champ.update` losing its `bossUp` — a signal that also fires when a spawn is
|
||||
// reset by a GM, when a boss despawns, and when the sweep simply reconnects.
|
||||
// This one fires on the death itself.
|
||||
//
|
||||
// **The subject is the SPAWN, so it matches `uo.champ.boss_up`'s.** A rule with
|
||||
// a cooldown on one altar therefore counts a boss going up and that same boss
|
||||
// coming down as the same subject, which is what an operator writing "not more
|
||||
// than once an hour about Destard" means. A kill the shard could not attribute
|
||||
// to an altar carries no spawn, so the boss's own serial stands in — it is a
|
||||
// subject that exists exactly once, which is all a cooldown needs of it.
|
||||
//
|
||||
// **Damagers are not surfaced as variables.** The table is on the frame and it
|
||||
// is `staff` in the visibility config; putting names into a trigger's data
|
||||
// would route them into mail an operator can address to `subscribers`, which is
|
||||
// the field rule undone one layer up. `damagerCount` is a number and says the
|
||||
// thing worth saying: how many took part.
|
||||
'champ.boss.killed': (ev, tracker, out) => {
|
||||
const spawnSerial = ev.serial == null ? null : String(ev.serial)
|
||||
const bossSerial = ev.bossSerial == null ? null : String(ev.bossSerial)
|
||||
const subject = spawnSerial || bossSerial
|
||||
if (!subject) return
|
||||
|
||||
// The board no longer has a boss on this altar. Kept in step with the sweep's
|
||||
// own view so the next `champ.update` carrying `bossUp: true` is read as a
|
||||
// transition rather than as more of the same.
|
||||
if (spawnSerial) tracker.champBossUp.set(spawnSerial, false)
|
||||
|
||||
const damagers = Array.isArray(ev.damagers) ? ev.damagers : []
|
||||
|
||||
out.push({
|
||||
triggerId: 'uo.champ.boss_killed',
|
||||
data: defined({
|
||||
spawnSerial: subject,
|
||||
champsUrl: PATHS.champs,
|
||||
bossName: ev.boss || ev.bossType || 'the champion',
|
||||
category: ev.category || undefined,
|
||||
location: place(ev),
|
||||
atPlace: trailing(place(ev), (p) => ` at ${p}`),
|
||||
killerName: actorName(ev.killer),
|
||||
damagerCount: damagers.length || undefined,
|
||||
damagerNote: trailing(damagers.length || null, (n) =>
|
||||
n === 1 ? ' One player fought it.' : ` ${n} players fought it.`),
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
'champ.remove': (ev, tracker) => {
|
||||
if (ev.serial == null) return
|
||||
tracker.champActive.delete(String(ev.serial))
|
||||
|
||||
Reference in New Issue
Block a user