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:
@@ -26,7 +26,7 @@ const inputStyle = {
|
||||
font: 'inherit',
|
||||
}
|
||||
|
||||
const blank = (profile = '') => ({ profile, count: '1', respawn: '300', respawnMode: 'each', move: '', radius: '' })
|
||||
const blank = (profile = '') => ({ profile, count: '1', respawn: '300', respawnMode: 'each', move: '', radius: '', tether: '' })
|
||||
|
||||
function formOf(p) {
|
||||
const v = p.placement || {}
|
||||
@@ -37,6 +37,7 @@ function formOf(p) {
|
||||
respawnMode: v.respawnMode || 'each',
|
||||
move: (v.movement && v.movement.mode) || '',
|
||||
radius: v.movement && v.movement.radius !== undefined ? String(v.movement.radius) : '',
|
||||
tether: v.tether || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +48,8 @@ function bodyOf(f) {
|
||||
respawn: Number(f.respawn),
|
||||
respawnMode: f.respawnMode,
|
||||
...(f.move ? { movement: { mode: f.move, radius: f.radius === '' ? undefined : Number(f.radius) } } : { movement: null }),
|
||||
// Stage 5 (D272): a ZoneManager zone its NPCs never leave; empty for none.
|
||||
tether: f.tether.trim() || null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +191,7 @@ function ServerPlacements({ server, profiles }) {
|
||||
<strong style={{ color: 'var(--head)' }}>{p.id}</strong>
|
||||
<span className="dim">
|
||||
{v.count} × {v.profile} · {p.alive} alive · respawn {v.respawn} s, {v.respawnMode} · {move}
|
||||
{v.tether ? ` · inside zone ${v.tether}` : ''}
|
||||
{v.position ? ` · ${Math.round(v.position.x)}, ${Math.round(v.position.z)}` : ''}
|
||||
</span>
|
||||
<span style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
|
||||
@@ -282,6 +286,10 @@ function PlacementFields({ form, setForm, profiles, routes }) {
|
||||
<input value={form.radius} onChange={set('radius')} inputMode="decimal" style={{ ...inputStyle, maxWidth: 80 }} />
|
||||
</label>
|
||||
)}
|
||||
<label style={label} title="A ZoneManager zone on this server its NPCs never leave. The server refuses a zone it does not have, or one that does not hold the spot.">
|
||||
<span style={{ color: 'var(--head)' }}>Keep inside zone</span>
|
||||
<input value={form.tether} onChange={set('tether')} placeholder="none" maxLength={64} style={{ ...inputStyle, maxWidth: 160 }} />
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user