feat(rust): NPC sides, the faction table, an event's escort, ally and tether (runicnpc stage 5)

RunicNPC stage 5 on the site (docs runicnpc/PLAN.md, D253-D272):

- profiles gain the guard role, faction, relations (its own exceptions),
  alertRadius, turrets, hurtByPlayers, hurtsPlayers and kitUse, checked in
  RunicNPC's order and words, and defaulting to "players only" (D255);
- the faction table: one site-wide table, one row per pair and both ways
  (D254, D268), stored in rust_npc_factions, edited on the NPC profiles page
  (PUT /admin/rust/npcs/factions), pushed to every server with its
  profiles and hashed with them, and a standalone server's own pairs
  adopted at its first push with the site's winning (D244, D251);
- the Place NPCs step takes escort (a Steam id or a {placeholder}), an ally
  (a clan from the new rust.options.clans source, or the team of a player)
  and tether (the zone this event made), for a RunicNPC profile only
  (D269, D270, D272);
- a placement may be held inside a zone (tether, D272);
- the bridge's RunicNPC API floor is 4.

Tests: 488 server, 66 client. routes.manifest.json regenerated against the
pinned core (one route added).

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-01 07:58:13 -05:00
parent b83fa8174c
commit 11619a4dc6
18 changed files with 1171 additions and 42 deletions

View File

@@ -1121,7 +1121,7 @@ module.exports = {
names: { type: 'array', items: { type: 'string' }, example: ['Warden', 'Old Warden'] },
kits: { type: 'array', items: { type: 'string' }, example: ['warden_rifle'] },
prefab: { type: 'string', example: 'scientistnpc_roam' },
role: { type: 'string', enum: ['roamer', 'sentry'], example: 'roamer' },
role: { type: 'string', enum: ['roamer', 'sentry', 'guard'], example: 'roamer' },
movement: { type: 'object', example: { mode: 'wander', radius: 20 } },
health: { type: 'number', example: 250 },
damageDealt: { type: 'number', example: 1 },
@@ -1131,6 +1131,23 @@ module.exports = {
visionCone: { type: 'number', example: -0.8 },
sleepDistance: { type: 'number', example: 160 },
healthThresholds: { type: 'array', items: { type: 'number' }, example: [0.5] },
faction: { type: 'string', nullable: true, description: 'Its faction (stage 5, D254); null: its allies are its own profile’s NPCs (D256).', example: 'bandits' },
relations: { type: 'object', additionalProperties: { type: 'string', enum: ['hostile', 'neutral', 'allied'] }, description: 'Its own exceptions, which win over the faction table: a faction, scientists, animals or profile:<name>.', example: { scientists: 'hostile' } },
alertRadius: { type: 'number', description: 'Metres within which its allies join a fight one of them is in (D256); 0 is off.', example: 40 },
turrets: { type: 'string', enum: ['default', 'ignore', 'always'], description: 'default: as Rust’s scientists; ignore: no turret targets it; always: the safe-zone sentries too (D259, D264, D267).', example: 'default' },
hurtByPlayers: { type: 'boolean', description: 'Off: unkillable by players, on every server (D271).', example: true },
hurtsPlayers: { type: 'boolean', description: 'Off: harmless to players; it still fights NPCs and turrets (D271).', example: true },
kitUse: { type: 'object', description: 'The kit’s extras it uses, each used up (D260, D265).', example: { heal: false, grenades: false, melee: false, rockets: false, flamethrower: false } },
},
},
RustNpcFactionPair: {
type: 'object',
description: 'One row of the faction table, both ways (D268). scientists and animals are Rust’s own; a pair of those two is refused.',
required: ['a', 'b', 'relation'],
properties: {
a: { type: 'string', example: 'bandits' },
b: { type: 'string', example: 'guards' },
relation: { type: 'string', enum: ['hostile', 'neutral', 'allied'], example: 'hostile' },
},
},
RustNpcProfile: {
@@ -1174,7 +1191,7 @@ module.exports = {
name: { type: 'string', example: 'Main · Vanilla' },
enabled: { type: 'boolean', example: true },
online: { type: 'boolean', nullable: true, example: true },
runicNpc: { type: 'object', nullable: true, example: { loaded: true, version: '0.2.0', api: 3 } },
runicNpc: { type: 'object', nullable: true, example: { loaded: true, version: '0.3.0', api: 4 } },
ready: { type: 'boolean', example: true },
absence: { type: 'string', nullable: true, example: null },
sync: { type: 'object', nullable: true, example: { state: 'ok', adoptedAt: '2026-09-30T10:00:00.000Z', syncedAt: '2026-09-30T10:00:01.000Z', refused: {}, error: null } },
@@ -1185,6 +1202,12 @@ module.exports = {
prefabs: { type: 'array', items: { type: 'string' }, example: ['scientistnpc_roam'] },
killsScopes: { type: 'array', items: { type: 'string' }, example: ['server', 'name', 'profile'] },
defaults: { $ref: '#/components/schemas/RustNpcProfileBody' },
factions: { type: 'array', items: { $ref: '#/components/schemas/RustNpcFactionPair' } },
factionNames: { type: 'array', items: { type: 'string' }, description: 'The factions the profiles name.', example: ['bandits', 'guards'] },
builtInFactions: { type: 'array', items: { type: 'string' }, example: ['scientists', 'animals'] },
roles: { type: 'array', items: { type: 'string' }, example: ['roamer', 'sentry', 'guard'] },
turrets: { type: 'array', items: { type: 'string' }, example: ['default', 'ignore', 'always'] },
kitUses: { type: 'array', items: { type: 'string' }, example: ['heal', 'grenades', 'melee', 'rockets', 'flamethrower'] },
},
},
RustNpcPlacements: {
@@ -1220,6 +1243,7 @@ module.exports = {
respawn: { type: 'number', example: 300 },
respawnMode: { type: 'string', enum: ['each', 'group'], example: 'each' },
movement: { type: 'object', nullable: true, example: { mode: 'route:gate', radius: 0 } },
tether: { type: 'string', nullable: true, description: 'A ZoneManager zone its NPCs never leave (stage 5, D272). The server refuses one it does not have, or that does not hold the spot.', example: 'arena' },
},
},
RustNpcBoardProfiles: {