feat(rust): the kit weekend, linked rewards, reward messages and the kit warning (protocol 14)
PLAN_REDESIGNS §2, the Rust half (D164, D192, D332, D333):
- rust.template.kit_weekend (core registerEventTemplates, MODULE_API 1.13.0):
inputs kit/minutes/message/delivery; one phase advance after the weekend,
core.lease rust.group.permission default/<kit permission> + rust.kit.entitle
linked. Steps only where the kit can use them; validated by the real core.
- rust.kit.entitle: recipients 'linked' (every linked player who has played
on the server, no tally, refused past the bound), optional message ({kit})
and delivery, stored per row (rust_perm_run_grants.notice*).
- perm.sync notices: the untold set to the picked account, keyed
run:step:user, in the digest; the report's noticesTold marks them told.
- rust.options.grouppermissions notes a kit's permission rows via kits.uses
(cached a minute per server; a failure is no note).
- duration: 'minutes' on rust.zone.open and rust.participation.open; the
group-permission lease's value example is true.
- protocol 13 -> 14, coreApi ^1.13.0; ci/core-ref.json pinned to website#212's
head while it is open; engagement-triggers.json regenerated (coreApi line).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
154
server/eventTemplates.js
Normal file
154
server/eventTemplates.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// ── Event templates (PLAN_REDESIGNS.md §2.5, D164, D192) ────────────────────
|
||||
//
|
||||
// A named starting point for a new event, through core's
|
||||
// `api.registerEventTemplates` (MODULE_API 1.13.0). The author fills in a few
|
||||
// inputs; `build()` turns them into an ordinary draft — phases, steps, params —
|
||||
// which the editor opens unsaved. Core validates the plan as a spec and keeps
|
||||
// nothing about the template, so this file can change without touching a
|
||||
// single event it was ever used for.
|
||||
//
|
||||
// ── The kit weekend ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// "Everybody gets kit vip this weekend." One phase that lasts the weekend, and
|
||||
// two steps in it:
|
||||
//
|
||||
// 1. core.lease → rust.group.permission: the `default` group holds the kit's
|
||||
// permission for the weekend, so every player can see and redeem it. The
|
||||
// game holds the deadline and gives it back on time whether or not the site
|
||||
// is up.
|
||||
// 2. rust.kit.entitle → that kit, every linked player of the server (`linked`),
|
||||
// one more use each — the use a player who already spent theirs needs to
|
||||
// take the kit again. Withdrawn at teardown if not yet redeemed (D103).
|
||||
//
|
||||
// A kit with no permission is open to everyone already, so its weekend is step
|
||||
// 2 alone; a kit with no use limit has no "one more use", so its weekend is step
|
||||
// 1 alone. A kit with neither is refused by `readKit`, because a weekend of it
|
||||
// gives nobody anything.
|
||||
|
||||
const { serverFor } = require('./eventLeases')
|
||||
const { readKit, splitKit, rewardNotice, MAX_CHAT } = require('./eventRewards')
|
||||
|
||||
/** The lease's own ceiling (D77): a weekend longer than seven days is refused by the game. */
|
||||
const MAX_WEEKEND_MINUTES = 7 * 24 * 60
|
||||
|
||||
/** "48 hours", "90 minutes". */
|
||||
function minutesWords(n) {
|
||||
if (n >= 120 && n % 60 === 0) return `${n / 60} hours`
|
||||
return `${n} minute${n === 1 ? '' : 's'}`
|
||||
}
|
||||
|
||||
const kitWeekend = {
|
||||
id: 'rust.template.kit_weekend',
|
||||
label: 'Kit weekend',
|
||||
description:
|
||||
'A kit for everybody, for a while: the default group holds the kit\'s permission for the length of the weekend, and every linked player of the server gets one more use of it. You can change every part of the plan before saving it.',
|
||||
inputs: [
|
||||
{
|
||||
name: 'kit',
|
||||
type: 'string',
|
||||
required: true,
|
||||
example: 'main/vip',
|
||||
source: 'rust.options.kits',
|
||||
description: 'The kit, as server/kit. The weekend happens on that server only.',
|
||||
},
|
||||
{
|
||||
name: 'minutes',
|
||||
type: 'int',
|
||||
required: true,
|
||||
example: 2880,
|
||||
duration: 'minutes',
|
||||
description: `How long the weekend lasts, up to ${MAX_WEEKEND_MINUTES} (seven days). 2880 is two days.`,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
required: false,
|
||||
example: 'Kit weekend! One more use of {kit} — type /kit {kit}',
|
||||
description: `Told once to each linked player in game, at once or when they next wake. {kit} is the kit's name; at most ${MAX_CHAT} characters. Leave it empty to say nothing.`,
|
||||
},
|
||||
{
|
||||
name: 'delivery',
|
||||
type: 'string',
|
||||
required: false,
|
||||
example: 'popup',
|
||||
source: 'rust.options.delivery',
|
||||
description: 'Chat, or a popup where the server has PopupNotifications.',
|
||||
},
|
||||
],
|
||||
|
||||
async build({ inputs }) {
|
||||
const parsed = splitKit(inputs.kit)
|
||||
if (!parsed) return { ok: false, error: `"${inputs.kit}" is not a kit — pick one from the list, as server/kit` }
|
||||
|
||||
const minutes = Number(inputs.minutes)
|
||||
if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_WEEKEND_MINUTES) {
|
||||
return { ok: false, error: `a kit weekend lasts 1 to ${MAX_WEEKEND_MINUTES} minutes, not ${inputs.minutes}` }
|
||||
}
|
||||
|
||||
const notice = rewardNotice({ message: inputs.message, delivery: inputs.delivery }, parsed.kit)
|
||||
if (!notice.ok) return { ok: false, error: notice.error }
|
||||
|
||||
const found = await serverFor(parsed.serverId)
|
||||
if (!found.ok) return { ok: false, error: found.error }
|
||||
const server = found.server
|
||||
const serverName = server.name || server.id
|
||||
|
||||
// The kit as its server describes it, now. A server that is down cannot say
|
||||
// which permission gates the kit, and a plan guessed at would be wrong.
|
||||
const kit = await readKit(server, parsed.kit)
|
||||
if (!kit.ok) return { ok: false, error: kit.error }
|
||||
|
||||
const steps = []
|
||||
if (kit.permission) {
|
||||
steps.push({
|
||||
actionId: 'core.lease',
|
||||
label: `Open ${kit.kit} to everyone`,
|
||||
params: {
|
||||
lease: 'rust.group.permission',
|
||||
target: `${server.id}/default/${kit.permission}`,
|
||||
value: 'true',
|
||||
minutes,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (kit.max > 0) {
|
||||
steps.push({
|
||||
actionId: 'rust.kit.entitle',
|
||||
label: 'One more use for every linked player',
|
||||
params: {
|
||||
kit: `${server.id}/${kit.kit}`,
|
||||
recipients: 'linked',
|
||||
...(notice.text ? { message: notice.text, delivery: notice.delivery } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const parts = [
|
||||
kit.permission ? `everybody on ${serverName} may redeem ${kit.kit}` : null,
|
||||
kit.max > 0 ? 'every linked player gets one more use of it' : null,
|
||||
].filter(Boolean)
|
||||
|
||||
return {
|
||||
title: `Kit weekend: ${kit.kit} on ${serverName}`,
|
||||
summary: `For ${minutesWords(minutes)}, ${parts.join(', and ')}.`,
|
||||
spec: {
|
||||
schedule: { kind: 'manual' },
|
||||
phases: [
|
||||
{
|
||||
key: 'weekend',
|
||||
label: 'The weekend',
|
||||
// The phase IS the weekend: it holds the run open for as long as the
|
||||
// lease lasts, so teardown gives the permission back on time and not
|
||||
// a second after the steps finish (§2.2, U-6).
|
||||
advance: { after: `${minutes}m` },
|
||||
steps,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const TEMPLATES = [kitWeekend]
|
||||
|
||||
module.exports = { TEMPLATES, MAX_WEEKEND_MINUTES, minutesWords }
|
||||
Reference in New Issue
Block a user