feat(rust): the rewards — tally, kit reward, chat and the news leg (phase 13b, protocol 10)

Four event verbs and the announce leg, per PLAN.md §29:

- rust.participation.open / .collect: the plugin counts who takes part
  (seconds, kills or both, in a zone this run opened or the whole server)
  and collect files them as the run's participants, keyed by Steam id.
- rust.kit.entitle: the five recipient modes (D101), rows in the new
  rust_perm_run_grants (D84) unioned into the permission push, one extra
  use of the kit per reward as site-held credits on perm.sync (D103),
  and the rust.kit.entitled notice deferred from phase 10 (D64).
- rust.announce: one server or every server (D105).
- rust.chat announce leg, speaking only on servers whose new news switch
  is on (D104) - a card on Admin -> Rust visibility (D106).

Budgets rust.grants and rust.announcements; the kit source and four
fixed-choice sources (core has no enum param type). rust_perm_run_grants
carries core's idempotency key so a revert of a lost answer can find its
rows. Protocol 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 07:00:44 -05:00
parent 9753dda4af
commit cc185db26b
27 changed files with 2096 additions and 45 deletions

View File

@@ -218,6 +218,39 @@ const ACCOUNT = {
],
}
// ── A reward (phase 13b) ───────────────────────────────────────────────────
//
// Deferred from phase 10 (D64) to the phase that grants something. Emitted once
// per recipient USER when an event's `rust.kit.entitle` writes their rows, with
// that user as `ownerUserId` — so, like the link notice, `owner` is both the
// ceiling and the only audience there is: a reward is nobody else's news.
//
// The subject is the run and the step, so one award is one notification however
// often a retried step writes the same rows.
const REWARD = {
id: 'rust.kit.entitled',
label: 'An event rewarded you a kit',
description: 'An event on a Rust server rewarded you: a kit is waiting in the in-game Kits menu, with one extra use.',
kind: 'event',
subjectKey: 'rewardKey',
audience: 'owner',
ceiling: 'owner',
version: V1,
variables: [
...HEADLINE,
{ name: 'rewardKey', type: 'string', required: true, example: '41:7',
description: 'The run and step that awarded it. The cooldown subject; not meant for display.' },
{ name: 'kit', type: 'string', required: true, example: 'vip-starter',
description: 'The kit name, as the server Kits plugin has it.' },
...SERVER,
{ name: 'mode', type: 'string', required: false, example: 'top',
description: 'How the recipients were chosen: everyone, top, minScore, random or topPercent.' },
{ name: 'accountUrl', type: 'url', required: false, example: '/player/rust',
description: 'Site-relative path to your Rust account page.' },
],
}
// ── Clans ──────────────────────────────────────────────────────────────────
//
// `members` ceiling — clan membership is the clan's business (D49). Recipients
@@ -374,7 +407,7 @@ const MODERATION = [
},
]
const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, ...CLANS, ...MODERATION])
const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, REWARD, ...CLANS, ...MODERATION])
const TRIGGER_IDS = Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t.id, t.id])))