// ── module-uo's engagement triggers ──────────────────────────────────────── // // ENGAGEMENT.md §8.6 and Phase 11. The twin of `config/shardStreams.js`: that // file declares which shard events a player may get a content-free PUSH tickle // for, and this one declares the PAYLOAD CONTRACT behind an event — what a rule // may fire on, what a template may interpolate, and the widest audience an // operator may ever give it. // // **One namespace, two facets** (§7.2, the org lead's Phase 2 decision). A // trigger id and a stream id live in the same space and an id has exactly one // owner across both, so the seven grandfathered stream ids in `shardStreams.js` // (`idoc.warning`, `house.idoc`, …) are ALSO this module's for trigger purposes. // Nothing below reuses one: the trigger ids here are the `uo.*`-prefixed names // §8.6 specifies, and they are new. A trigger-only id gets email and in-app // preferences and no push toggle, which is correct — `allStreams()` serves the // stream facet only, so the shipped Android client's catalog is unchanged. // // **Every ✅ row of §8.6 is here except four, and each carve-out is recorded** // in ENGAGEMENT.md §8.6 with its reason rather than being silently absent: // // • `uo.market.item_listed` — a saved SEARCH, not a trigger. Its audience is // "users whose stored query matches this listing" and no per-user query store // exists anywhere in the tree. // • `uo.guild.joined` — core's `team.member.joined` already fires for it. A UO // guild IS a Team and this module is the Team provider, so `teamSync` emits // on every roster reconcile; a second trigger would be two mails for one join. // `uo.guild.left` and `uo.guild.disbanded` DO ship — core has neither. // • `uo.link.requested` — no addressable recipient by construction (the account // is not yet linked, which is the point of the event) and a ~5-minute TTL no // channel can beat. // • `uo.points.rank_changed`'s personal half — `points.board`'s `top[]` names a // mobile SERIAL and `shard_account_links` is keyed by ACCOUNT. The board-change // feed ships at `subscribers`; "you were pushed out" does not. // // **Three rules every declaration below obeys, all of them enforced at // registration** (`registries.js`), so a mistake here is a boot failure rather // than a defect discovered in someone's mailbox: // // 1. **`ceiling` is required and there is no default.** It is the widest // audience a rule may ever be given (G24), re-checked at save AND at send. // `uo.cheat.detected` is why the lattice exists: `owner` would mail the // cheat report to the player who was detected, and `staff` is the answer. // 2. **Every variable carries an `example`.** It is what the template editor // previews and test-sends with; without one, testing a template needs a live // game event, which is how template systems ship untested (§4.3 property 3). // 3. **A `url` variable is site-RELATIVE** and validated as such. A payload // value ends up in an href in an email, and `//evil.test/x` passes an "is it // rooted" check while being protocol-relative. // // **Nothing here emits.** `utils/shardEngagement.js` is the mapper that turns a // wire frame into a call; this file is only the contract. Keeping them apart is // what lets the declarations be read as a catalogue and diffed against §8.6. // Every trigger's `version`. Bumped per declaration when a variable's MEANING // changes, not when one is added — an added optional is what `required: false` // is for, and a stored rule keeps working across it. const V1 = 1 // ── Owned asset at risk — the flagship family ────────────────────────────── // // All three resolve through the frame's `ownerAcct` → `shard_account_links` → // a website user, which is what `ownerUserId` on the envelope carries. A house // or vendor whose owner never linked an account is nobody to notify, and the // mapper drops it rather than treating it as an error. const OWNED_ASSET = [ { id: 'uo.house.idoc_warning', label: 'Your house is decaying', description: 'One of your houses reached a late decay stage and will collapse if it is not refreshed.', kind: 'event', // The house, not the owner. A player with three decaying houses should hear // about all three; a cooldown keyed on them would report one and swallow the // rest. This is the case that makes `subjectKey` worth having at all. subjectKey: 'houseSerial', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'houseSerial', type: 'string', required: true, example: '0x400142F9', description: 'The house, as the shard names it. Also the cooldown subject.' }, { name: 'houseName', type: 'string', required: false, example: 'Millrace', description: 'The house sign\'s name, when it has one.' }, { name: 'stage', type: 'string', required: true, example: 'Greatly', description: 'The decay stage it just entered: Slightly, Somewhat, Fairly, Greatly or IDOC.' }, { name: 'previousStage', type: 'string', required: false, example: 'Fairly', description: 'The stage it was in before.' }, { name: 'region', type: 'string', required: false, example: 'Britain', description: 'The named region the house stands in.' }, { name: 'location', type: 'string', required: false, example: 'Felucca 1480, 1600', description: 'Facet and coordinates, already formatted for reading.' }, // **Protocol 5, and both are `required: false` on purpose.** A shard still // running a v4 overlay emits no `schedule` at all, and a dynamic-decay shard // omits `estimatedCollapse` at every stage before IDOC because ServUO draws // each stage's duration at random when the stage is entered. So the mail has // to read correctly without them — which is exactly what an optional // variable and a template that omits an absent one give you. { name: 'nextStage', type: 'datetime', required: false, example: '2026-09-01T20:33:15Z', description: 'When it leaves this stage. Absent under static decay, which keeps no stage clock.' }, { name: 'estimatedCollapse', type: 'datetime', required: false, example: '2026-09-06T20:33:15Z', description: 'When it collapses — present ONLY when the shard can state it exactly. Absent is "not knowable", never "not yet read".' }, { name: 'lastRefreshed', type: 'datetime', required: false, example: '2026-08-25T17:21:14Z', description: 'When the house was last refreshed.' }, { name: 'houseUrl', type: 'url', required: false, example: '/shard/houses', description: 'Site-relative path to the IDOC page.' }, ], }, { id: 'uo.house.collapsed', label: 'Your house collapsed', description: 'One of your houses fell — the bad news, so that it is not a surprise.', kind: 'event', subjectKey: 'houseSerial', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'houseSerial', type: 'string', required: true, example: '0x400142F9', description: 'The house, as the shard names it. Also the cooldown subject.' }, { name: 'houseName', type: 'string', required: false, example: 'Millrace', description: 'The house sign\'s name, when it had one.' }, { name: 'region', type: 'string', required: false, example: 'Britain', description: 'The named region it stood in.' }, { name: 'location', type: 'string', required: false, example: 'Felucca 1480, 1600', description: 'Facet and coordinates, already formatted for reading.' }, ], }, { id: 'uo.vendor.expiring', label: 'Your vendor is about to be dismissed', description: 'One of your player vendors is running out of gold for its fees and will be dismissed.', kind: 'event', subjectKey: 'vendorSerial', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'vendorSerial', type: 'string', required: true, example: '0x40001234', description: 'The vendor, as the shard names it. Also the cooldown subject.' }, { name: 'shopName', type: 'string', required: false, example: 'Darrow\'s Bargains', description: 'The shop\'s name.' }, { name: 'dismissalAt', type: 'datetime', required: true, example: '2026-09-08T21:01:21Z', description: 'When the vendor is destroyed if nothing is deposited. Exact — unlike a house\'s collapse, there is no randomness in it.' }, // **The int an operator narrows with**, because `conditions.js` compares a // declared variable against a LITERAL and has no relative-time operator: // "within 24 hours of dismissal" is not expressible as `dismissalAt < now + // 24h`. So the hours are computed at emit and the operator writes // `hoursRemaining is at most 24`. The mapper additionally fires only on a // threshold CROSSING, because `vendor.listing` is a sweep frame re-emitted // on any price change. { name: 'hoursRemaining', type: 'int', required: true, example: 22, description: 'Whole hours until dismissal at the moment this fired. The value to write a rule condition against.' }, { name: 'periodsRemaining', type: 'int', required: false, example: 1, description: 'Pay ticks the vendor survives. NOT days — under the old vendor system a period is one UO day (~2 real hours).' }, { name: 'funds', type: 'int', required: false, example: 8204, description: 'Gold available to pay the fees.' }, { name: 'chargePerPeriod', type: 'int', required: false, example: 10548, description: 'What each tick deducts.' }, { name: 'location', type: 'string', required: false, example: 'Trammel 1421, 1699 (Britain)', description: 'Where the shop stands, already formatted for reading.' }, { name: 'marketUrl', type: 'url', required: false, example: '/shard/market', description: 'Site-relative path to the market page.' }, ], }, ] // ── Passive income ───────────────────────────────────────────────────────── const PASSIVE_INCOME = [ { id: 'uo.vendor.sale', label: 'Your vendor sold something', // **The tier caveat belongs in the operator-facing text, not only in a // comment.** `vendor.sale` is emitted by a `PlayerVendorSale` EventSink that // lives in `servuo-plugins/patches/` — the opt-in patch tier — and is verified // only against ServUO 57.4. A shard that declined the tier emits this kind // never, so a rule on it is silently dormant rather than broken, and the only // way an operator finds out is if something says so where they are looking. description: 'One of your player vendors made a sale. Requires the optional ServUO patch tier — a shard that ' + 'declined it never emits this event, and a rule on it stays silent.', kind: 'event', subjectKey: 'vendorSerial', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'vendorSerial', type: 'string', required: true, example: '0x2E1', description: 'The vendor that made the sale. Also the cooldown subject.' }, { name: 'itemName', type: 'string', required: true, example: 'Longsword', description: 'What was sold.' }, { name: 'amount', type: 'int', required: false, example: 1, description: 'How many.' }, { name: 'price', type: 'int', required: true, example: 100, description: 'What it sold for, in gold.' }, { name: 'commission', type: 'int', required: false, example: 0, description: 'Commission taken, on a commission vendor.' }, ], }, ] // ── Personal security ────────────────────────────────────────────────────── const PERSONAL_SECURITY = [ { id: 'uo.account.login_failed', label: 'A failed login to your game account', description: 'Someone tried to log into your game account and was refused.', kind: 'event', // The account, so a burst of attempts against one account is one mail and // attempts against two accounts are two. subjectKey: 'account', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'account', type: 'string', required: true, example: 'seed_000', description: 'The game account that was tried. Also the cooldown subject.' }, { name: 'reason', type: 'string', required: false, example: 'BadPass', description: 'The shard\'s refusal reason: BadPass, Invalid, Blocked, InUse or BadComm.' }, { name: 'ip', type: 'string', required: false, example: '203.0.113.9', description: 'Where the attempt came from.' }, ], }, { id: 'uo.account.unlinked', label: 'Your game account was unlinked', description: 'Someone severed the tie between this game account and your website account, from in game.', kind: 'event', subjectKey: 'account', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'account', type: 'string', required: true, example: 'seed_000', description: 'The game account that was unlinked. Also the cooldown subject.' }, { name: 'characterName', type: 'string', required: false, example: 'Zara Crowe', description: 'The character who ran the command.' }, ], }, ] // ── Personal milestone ───────────────────────────────────────────────────── // // The two death triggers are a killfeed some players want and most do not. // Every rule ships disabled anyway (Q3), and 11b's seeded rules for these two // additionally default their channels `off` rather than relying on the rule // switch alone. const PERSONAL_MILESTONE = [ { id: 'uo.skill.capped', label: 'You capped a skill', description: 'One of your characters reached the cap in a skill.', kind: 'event', subjectKey: 'skill', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'characterName', type: 'string', required: true, example: 'Zara Crowe', description: 'The character who capped it.' }, { name: 'skill', type: 'string', required: true, example: 'Blacksmithy', description: 'The skill. Also the cooldown subject — capping two skills is two events.' }, { name: 'cap', type: 'float', required: true, example: 100, description: 'The cap that was reached.' }, ], }, { id: 'uo.quest.complete', label: 'You completed a quest', description: 'One of your characters finished a quest.', kind: 'event', subjectKey: 'quest', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'characterName', type: 'string', required: true, example: 'Zara Crowe', description: 'The character who finished it.' }, { name: 'quest', type: 'string', required: true, example: 'The Ancient Tome', description: 'The quest. Also the cooldown subject.' }, ], }, { id: 'uo.character.death', label: 'Your character died', description: 'One of your characters was killed. Opt-in — most players do not want this.', kind: 'event', subjectKey: 'characterName', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'characterName', type: 'string', required: true, example: 'Zara Crowe', description: 'Who died. Also the cooldown subject.' }, { name: 'killerName', type: 'string', required: false, example: 'an ogre lord', description: 'What killed them, when the shard names it.' }, ], }, { id: 'uo.character.murdered', label: 'Your character was murdered', description: 'One of your characters was killed by another player. Opt-in — most players do not want this.', kind: 'event', subjectKey: 'characterName', audience: 'owner', ceiling: 'owner', version: V1, variables: [ { name: 'characterName', type: 'string', required: true, example: 'Zara Crowe', description: 'Who was murdered. Also the cooldown subject.' }, { name: 'murdererName', type: 'string', required: false, example: 'Darrow', description: 'Who did it, when the shard names them.' }, ], }, ] // ── Social / civic ───────────────────────────────────────────────────────── // // The two guild triggers ceiling at `members` and resolve through the recipient // set the emit carries, not through a saved segment: "the members of THIS guild" // is a different answer for every firing, which a segment's constant params // cannot express. That is Phase 6's decision 2, and the Team fan-out is the // precedent it was built for. const SOCIAL_CIVIC = [ { id: 'uo.guild.left', label: 'A member left your guild', description: 'Someone left a guild you are in.', kind: 'event', subjectKey: 'guildName', audience: 'members', ceiling: 'members', version: V1, variables: [ { name: 'guildName', type: 'string', required: true, example: 'The Silver Hand', description: 'The guild. Also the cooldown subject.' }, // `guild.leave`'s `who` is a bare SERIAL string, not an actor object — the // mobile has already left, so the shard has nothing to attribute. The name // comes from this module's own roster mirror (`shard_guild_members`), and // is optional because a member the sweep never saw has no row there. { name: 'memberName', type: 'string', required: false, example: 'Bran', description: 'Who left, when the roster mirror still knows their name.' }, { name: 'guildUrl', type: 'url', required: false, example: '/shard/guilds', description: 'Site-relative path to the guilds page.' }, ], }, { id: 'uo.guild.disbanded', label: 'Your guild disbanded', description: 'A guild you are in was disbanded or removed.', kind: 'event', subjectKey: 'guildName', audience: 'members', ceiling: 'members', version: V1, variables: [ { name: 'guildName', type: 'string', required: true, example: 'The Silver Hand', description: 'The guild that is gone. Also the cooldown subject.' }, { name: 'abbreviation', type: 'string', required: false, example: 'TSH', description: 'Its abbreviation.' }, ], }, { id: 'uo.governor.elected', label: 'A town elected a governor', description: 'A city has a new governor.', kind: 'event', subjectKey: 'city', audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'city', type: 'string', required: true, example: 'Britain', description: 'The city. Also the cooldown subject.' }, { name: 'governorName', type: 'string', required: true, example: 'Darrow', description: 'The new governor.' }, { name: 'previousGovernorName', type: 'string', required: false, example: 'Mireille', description: 'Who held the seat before, when there was someone.' }, { name: 'governorsUrl', type: 'url', required: false, example: '/shard/governors', description: 'Site-relative path to the governors page.' }, ], }, { id: 'uo.election.opened', label: 'Voting opened in a town', // **The first trigger whose call to action genuinely expires**, which is why // `autoPickAt` is required rather than decorative: a mail saying "vote" with // no deadline is a mail nobody acts on, and one delivered after the deadline // is worse than none. 11b's template says the date, and the seeded rule uses // no delay for the same reason. description: 'A city\'s election entered its nomination or voting phase, with a deadline.', kind: 'event', subjectKey: 'city', audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'city', type: 'string', required: true, example: 'Britain', description: 'The city. Also the cooldown subject.' }, { name: 'phase', type: 'string', required: true, example: 'vote', description: 'Which phase opened: nominate or vote.' }, { name: 'autoPickAt', type: 'datetime', required: true, example: '2026-09-04T00:00:00Z', description: 'When the game decides for itself — the real deadline.' }, { name: 'candidates', type: 'int', required: false, example: 3, description: 'How many candidates stand.' }, { name: 'governorsUrl', type: 'url', required: false, example: '/shard/governors', description: 'Site-relative path to the governors page.' }, ], }, ] // ── Come online now ──────────────────────────────────────────────────────── const COME_ONLINE = [ { id: 'uo.champ.started', label: 'A champion spawn started', description: 'A champion spawn became active.', kind: 'event', subjectKey: 'spawnSerial', audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'spawnSerial', type: 'string', required: true, example: '0x40012345', description: 'The spawn controller. Also the cooldown subject.' }, { name: 'spawnName', type: 'string', required: true, example: 'Abyss', description: 'What is spawning.' }, { name: 'category', type: 'string', required: false, example: 'champion', description: 'champion, mini or sea.' }, { name: 'location', type: 'string', required: false, example: 'Felucca 5187, 570', description: 'Where, already formatted for reading.' }, { name: 'champsUrl', type: 'url', required: false, example: '/shard/champs', description: 'Site-relative path to the champions page.' }, ], }, { id: 'uo.champ.boss_up', label: 'A champion boss is up', description: 'A champion spawn reached its boss.', kind: 'event', subjectKey: 'spawnSerial', audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'spawnSerial', type: 'string', required: true, example: '0x40012345', description: 'The spawn controller. Also the cooldown subject.' }, { name: 'spawnName', type: 'string', required: true, example: 'Abyss', description: 'The spawn.' }, { name: 'bossName', type: 'string', required: false, example: 'Semidar', description: 'The boss, when the shard names it.' }, { name: 'location', type: 'string', required: false, example: 'Felucca 5187, 570', description: 'Where, already formatted for reading.' }, { name: 'champsUrl', type: 'url', required: false, example: '/shard/champs', description: 'Site-relative path to the champions page.' }, ], }, { id: 'uo.server.up', label: 'The shard came online', description: 'The game server started or came back after an outage.', kind: 'event', // **No `subjectKey`, and that is the whole point of this pair.** There is one // shard, so the subject a cooldown counts is the RECIPIENT — "do not tell me // the shard bounced more than once an hour". Keying it on a boot id would make // every restart a new subject and every cooldown a no-op, which is precisely // the mail loop §8.6 warns a flapping shard produces. 11b's seeded rules carry // a hard cooldown; this declaration is what makes that cooldown mean anything. audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'shardName', type: 'string', required: false, example: 'UOMysticmoon', description: 'What the shard calls itself.' }, { name: 'statusUrl', type: 'url', required: false, example: '/shard', description: 'Site-relative path to the shard status page.' }, ], }, { id: 'uo.server.down', label: 'The shard went offline', description: 'The game server shut down or crashed.', kind: 'event', audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'shardName', type: 'string', required: false, example: 'UOMysticmoon', description: 'What the shard calls itself.' }, { name: 'clean', type: 'boolean', required: false, example: true, description: 'Whether it was a clean shutdown rather than a crash.' }, { name: 'statusUrl', type: 'url', required: false, example: '/shard', description: 'Site-relative path to the shard status page.' }, ], }, ] // ── Leaderboard ──────────────────────────────────────────────────────────── const LEADERBOARD = [ { id: 'uo.points.rank_changed', label: 'A leaderboard top spot changed', // §8.6 originally described this firing both ways — "you entered a top N" and // "you were pushed out". The personal half is carved out: `points.board`'s // `top[]` entries are `{rank, serial, name, points}` and `shard_account_links` // is keyed by game ACCOUNT, so a serial resolves to a person only for someone // currently online (`shard_online`) or in a guild (`shard_guild_members`). A // leaderboard mail that reaches half the board reads as favouritism, so the // board feed ships and the personal one waits for a serial→account map. description: 'The top of a leaderboard changed hands.', kind: 'event', subjectKey: 'system', audience: 'subscribers', ceiling: 'authenticated', version: V1, variables: [ { name: 'system', type: 'string', required: true, example: 'QueensLoyalty', description: 'The points system. Also the cooldown subject.' }, { name: 'systemName', type: 'string', required: false, example: 'Queen\'s Loyalty', description: 'Its display name, when the shard gives one.' }, { name: 'leaderName', type: 'string', required: true, example: 'Darrow', description: 'Who is first now.' }, { name: 'previousLeaderName', type: 'string', required: false, example: 'Mireille', description: 'Who was first before.' }, { name: 'points', type: 'int', required: false, example: 29500, description: 'The new leader\'s points.' }, ], }, ] // ── Staff-facing ─────────────────────────────────────────────────────────── // // These are why the ceiling exists. Phase 3 already filters a role-ceilinged // trigger out of a player's preferences catalogue AND gates it on write, so this // family is the production proof of that work rather than new mechanism. const STAFF_FACING = [ { id: 'uo.page.new', label: 'A player opened a help page', description: 'A player raised a support ticket in game.', kind: 'event', subjectKey: 'pageType', audience: 'staff', ceiling: 'staff', version: V1, variables: [ { name: 'pageType', type: 'string', required: true, example: 'Stuck', description: 'Bug, Stuck, Account, Question, Suggestion, Other, VerbalHarassment or PhysicalHarassment. Also the cooldown subject.' }, { name: 'senderName', type: 'string', required: false, example: 'Zara Crowe', description: 'Who raised it.' }, { name: 'message', type: 'string', required: false, example: 'I am stuck under the Britain bank.', description: 'What they wrote.' }, { name: 'location', type: 'string', required: false, example: 'Trammel 1421, 1699', description: 'Where they are, already formatted for reading.' }, { name: 'pagesUrl', type: 'url', required: false, example: '/admin/shard', description: 'Site-relative path to the help-page queue.' }, ], }, { id: 'uo.cheat.detected', label: 'The cheat detector fired', description: 'The shard\'s own speed-hack detector flagged a player.', kind: 'event', // **`staff`, and never `owner`.** This is the declaration the whole lattice // was written for: under a flat "fewer people is narrower" ordering a // `staff` ceiling would also permit `owner`, and the rule an operator would // then be able to save mails the cheat report to the player who was detected. subjectKey: 'characterName', audience: 'staff', ceiling: 'staff', version: V1, variables: [ { name: 'characterName', type: 'string', required: true, example: 'Zara Crowe', description: 'Who was flagged. Also the cooldown subject.' }, { name: 'account', type: 'string', required: false, example: 'seed_000', description: 'Their game account.' }, { name: 'ip', type: 'string', required: false, example: '203.0.113.9', description: 'Where they were connected from.' }, { name: 'detector', type: 'string', required: false, example: 'fastwalk', description: 'Which detector fired.' }, ], }, ] // ── Operator-facing ──────────────────────────────────────────────────────── // // `admin`, the ceiling Phase 11 added to the lattice (decision 1). The narrowest // value before it was `staff` — admin, editor AND moderator — so ceilinging a // digest of what moderators did at `staff` would have sent it to the moderators. // All three are digest-shaped by nature; none should ever be instant, which is a // property of 11b's seeded rules rather than of these declarations. const OPERATOR_FACING = [ { id: 'uo.audit.staff_action', label: 'A staff member acted in game', description: 'A staff command, a property change, or a moderation action.', kind: 'event', subjectKey: 'staffName', audience: 'admin', ceiling: 'admin', version: V1, variables: [ { name: 'staffName', type: 'string', required: false, example: 'Mireille', description: 'Who acted. Absent when the shard cannot attribute it. Also the cooldown subject.' }, { name: 'action', type: 'string', required: true, example: 'set', description: 'What kind of action: set, command, ban, kick, mute…' }, { name: 'detail', type: 'string', required: false, example: 'Str 100 → 125 on Zara Crowe', description: 'The action in one line, already formatted for reading.' }, { name: 'target', type: 'string', required: false, example: 'Zara Crowe', description: 'Who or what it was applied to.' }, { name: 'origin', type: 'string', required: false, example: 'in-game', description: 'web or in-game — where the action was issued from.' }, ], }, { id: 'uo.economy.milestone', label: 'The economy crossed a threshold', description: 'The shard\'s total gold supply or account count crossed one of the module\'s reporting thresholds.', kind: 'event', subjectKey: 'metric', audience: 'admin', ceiling: 'admin', version: V1, variables: [ { name: 'metric', type: 'string', required: true, example: 'gold', description: 'gold or accounts. Also the cooldown subject.' }, { name: 'value', type: 'int', required: true, example: 1000000000, description: 'The value that crossed.' }, { name: 'threshold', type: 'int', required: true, example: 1000000000, description: 'The threshold it crossed.' }, { name: 'direction', type: 'string', required: true, example: 'up', description: 'up or down.' }, { name: 'economyUrl', type: 'url', required: false, example: '/shard', description: 'Site-relative path to the shard status page.' }, ], }, { id: 'uo.world.saved', label: 'The world saved', description: 'A world save completed, with the item and mobile counts it wrote.', kind: 'event', audience: 'admin', ceiling: 'admin', version: V1, variables: [ { name: 'items', type: 'int', required: false, example: 1482301, description: 'Items written.' }, { name: 'mobiles', type: 'int', required: false, example: 41022, description: 'Mobiles written.' }, ], }, ] const TRIGGERS = [ ...OWNED_ASSET, ...PASSIVE_INCOME, ...PERSONAL_SECURITY, ...PERSONAL_MILESTONE, ...SOCIAL_CIVIC, ...COME_ONLINE, ...LEADERBOARD, ...STAFF_FACING, ...OPERATOR_FACING, ] // The ids, as a Set, for the mapper's own guard: `shardEngagement.js` refuses to // emit an id this file does not declare, so a typo there is a boot-time-visible // mistake rather than a dropped event nobody notices. const TRIGGER_IDS = new Set(TRIGGERS.map((t) => t.id)) module.exports = { TRIGGERS, TRIGGER_IDS, OWNED_ASSET, PASSIVE_INCOME, PERSONAL_SECURITY, PERSONAL_MILESTONE, SOCIAL_CIVIC, COME_ONLINE, LEADERBOARD, STAFF_FACING, OPERATOR_FACING, }