feat(rust): bosses and passive NPCs on the profile form, and public boss triggers (runicnpc stage 6)
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / server-tests (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 46s

The profile form's boss box (D273-D277): the bar's distance, where its
lines are said, the spawn and death lines, and phases at health
percentages, each with optional damage, aim, speed, ranges, a kit swap,
adds of another profile and a line. The passive role (D278, D279,
D288): press E answers with a chat line or a window, it moves as its
profile says (now including `stand`), and it starts unhurtable.

The site checks both as RunicNPC does, in its words; a phase's kit is
checked on each server like the profile's own, and its adds must name a
site profile that is not a boss.

Public triggers rust.boss.spawned, rust.boss.phase and rust.boss.killed
(D277) from the bridge's new frames. In the feed, a boss appearing is
public; its death names the killer, so it sits behind the presence
setting; its phases are staff's, as the game tells them only to players
near it (D286). engagement-triggers.json regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-05 10:54:28 -05:00
parent 620ea63b54
commit f2ca314d10
11 changed files with 855 additions and 15 deletions

View File

@@ -163,6 +163,8 @@ async function describe() {
roles: shape.ROLES,
turrets: shape.TURRETS,
kitUses: shape.KIT_USES,
announces: shape.ANNOUNCES,
useModes: shape.USE_MODES,
}
}
@@ -212,8 +214,20 @@ async function validate(input, id = null) {
if (shared) throw new NpcError(`a profile called "${name}" is already on ${shared.name || shared.id}`, 409)
}
// Stage 6 (D275): a phase's adds are another of the site's profiles, and never a boss, as RunicNPC refuses.
const all = active(await db.listProfiles())
for (const [i, phase] of ((checked.value.boss && checked.value.boss.phases) || []).entries()) {
if (!phase.adds) continue
const named = all.filter((p) => p.name === phase.adds.profile && p.id !== id)
if (phase.adds.profile === name) throw new NpcError(`boss.phases[${i}].adds.profile: a boss does not summon itself`)
if (named.length === 0) throw new NpcError(`boss.phases[${i}].adds.profile: there is no profile '${phase.adds.profile}'`)
if (named.some((p) => p.body && p.body.boss)) {
throw new NpcError(`boss.phases[${i}].adds.profile: '${phase.adds.profile}' is a boss, and a boss does not summon bosses`)
}
}
const covered = allServers ? list : requested.map((s) => byId.get(s))
await checkKits(checked.value.kits, covered)
await checkKits(shape.kitsOf(checked.value), covered)
return { name, body: checked.value, allServers, servers: allServers ? [] : requested, killsScope }
}