feat: API 4, factions, escort, allies, tether, turrets, kit extras and PVE (stage 5) #8

Merged
whitlocktech merged 3 commits from feat/stage-5 into edge 2026-10-05 08:38:34 +00:00
4 changed files with 1895 additions and 39 deletions
Showing only changes of commit 21d3c477dc - Show all commits

View File

@@ -24,9 +24,11 @@
#
# 1 was stage 0's: `RunicNpc_ApiVersion()` and nothing else.
# 2 was stage 2's whole API, PLAN.md §4.
# Current: 3 — stage 4 (D249): RunicNpc_AddPlacement, _RenamePlacement, _RespawnPlacement and the
# OnRunicNpcPlacementChanged hook, documented in docs/runicnpc/API.md.
api = 3
# 3 was stage 4 (D249): RunicNpc_AddPlacement, _RenamePlacement, _RespawnPlacement and the
# OnRunicNpcPlacementChanged hook.
# Current: 4 — stage 5: RunicNpc_Factions, _SetFactions, _Escort, _Ally, _Tether, the profile's stage 5
# fields, a placement's tether, and the OnRunicNpcEscortEnded hook, documented in docs/runicnpc/API.md.
api = 4
# ── Framework floors ─────────────────────────────────────────────────────────
#
@@ -45,6 +47,6 @@ min_carbon_version = "2.0.259"
# `doctor`, which reports a missing one rather than installing it. The PR check
# holds this list and the plugin's `// Requires:` lines equal.
#
# ZoneManager is NOT listed: a zone tether is optional (PLAN.md §6), and a
# profile that asks for one on a server without it is refused when it is saved.
# ZoneManager is NOT listed: a zone tether is optional (PLAN.md §6). A placement
# or an event step that asks for one on a server without it is refused (D272).
requires_plugins = ["Kits"]

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,17 @@ const PLUGIN_TOML = path.join(ROOT, 'plugin.toml')
* That half cannot be read statically; it is the reviewer's to check, which is
* why the reason goes here where the reviewer will see it.
*/
const ANSWERS_DELIBERATELY = Object.create(null)
const ANSWERS_DELIBERATELY = Object.assign(Object.create(null), {
// Stage 5 (D259, D264): turrets: ignore. Answers false for RunicNPC's own NPCs whose profile says
// ignore, so no auto turret, flame turret or shotgun trap targets them; null for everything else.
CanBeTargeted: 'turrets: ignore (D259, D264), our own NPCs only',
// Stage 5 (D261, D271): TruePVE's and NextGenPVE's question. Answers only for one of ours facing a
// real player, with the profile's two PVE checkboxes; null for everything else.
CanEntityTakeDamage: 'PVE allowed both ways, or the profile opt-out (D261, D271), our own NPCs only',
// Stage 5 (D271): cancels damage only when one of ours whose profile says it cannot hurt players
// hits a real player. Its other job, defending an ally or escort (D257, D269), never answers.
OnEntityTakeDamage: 'a harmless profile never hurts a player (D271), our own NPCs only',
})
/** Plugins RunicNPC must require, whatever else it requires (D217). */
const REQUIRED_PLUGINS = ['Kits']

View File

@@ -100,13 +100,13 @@ test('returning null is not good enough — the signature is the rule', () => {
// `return null` today is one edit away from `return true` tomorrow, and the
// edit that breaks it looks harmless in a diff. A void method cannot be turned
// into a veto without changing its signature, which is visible.
const methods = ` private bool CanBeTargeted(BaseCombatEntity entity, AutoTurret turret)
const methods = ` private bool CanLootEntity(BasePlayer player, StorageContainer container)
{
return true;
}`
const problems = check(source({ expected: ['CanBeTargeted'], methods }), toml())
assert.match(problems[0], /CanBeTargeted returns bool, not void/)
const problems = check(source({ expected: ['CanLootEntity'], methods }), toml())
assert.match(problems[0], /CanLootEntity returns bool, not void/)
})
test('a method that is not shaped like a hook is left alone', () => {