diff --git a/runicnpc/PLAN.md b/runicnpc/PLAN.md index 4f920d9..de52fdf 100644 --- a/runicnpc/PLAN.md +++ b/runicnpc/PLAN.md @@ -6,8 +6,9 @@ decided with it. **Stage 1 measured 2026-09-30** (§9): six answers on both rigs **Stage 2's design answered 2026-09-30:** D232–D238 (§0), which reshape stage 2 (§9). **Stage 2 built and tested 2026-09-30** on both rigs (§9); its API is [API.md](API.md). **Stage 3's design answered 2026-09-30:** D239–D242 (§0). **Stage 3 built and tested 2026-09-30** on both rigs (§9); its in-game walk waits for a player. **Stage 4's -design answered 2026-09-30** (D243–D252, §0), **built and walked 2026-09-30** on both rigs (§9). **Stage 4's -design answered 2026-09-30:** D243–D252 (§0). +design answered 2026-09-30** (D243–D252, §0), **built and walked 2026-09-30** on both rigs (§9). **Stage 5's +design answered 2026-09-30 and 2026-10-01** (D253–D266, §0), **its spike measured 2026-10-01** on both rigs (§9), +and **its build's own questions answered 2026-10-01** (D267–D272). RunicNPC is Runic Gateway's own NPC plugin for Rust servers, in its own repository, [`RunicGateway/runicnpc-rust`](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust). It runs on Oxide and @@ -83,6 +84,12 @@ architectural or design decision is implemented. | **D264** | **`turrets: always` means the safe-zone sentries too**: Outpost's and Bandit Camp's turrets shoot that profile's NPCs as they shoot a hostile player. `default` is what Rust does (player turrets shoot it, safe-zone sentries do not), and `ignore` means no turret shoots it (stage 5 spike, refines D259). | Dropping `always`, since `default` already means player turrets. | | **D265** | **The main weapon's ammunition is endless, as in Rust.** An NPC never runs dry, and its loot keeps the kit's ammunition. Only D260's extras (syringes, grenades, rockets and the like) are used up (stage 5 spike, refines D260). | Ammunition used up shot by shot; a per-profile setting. | | **D266** | **Fire burns as it does in Rust.** A flamethrower NPC's flames burn every NPC in them, its own allies included, because Rust filters only bullets between NPCs. Admins place flamethrower NPCs with that in mind (stage 5 spike). | Our NPCs taking no fire damage from their allies. | +| **D267** | **`turrets: always` reaches the safe-zone sentries through a Harmony patch.** Rust's sentry (`NPCAutoTurret`) refuses every scientist in `Ignore` and `IsEntityHostile`, before any Oxide or Carbon hook runs. RunicNPC patches those two methods so they answer "hostile" for its own NPCs whose profile says `always`, and as Rust does for everything else, as the zone and dome helpers patch their plugins (D181). It unpatches on unload, and a failed patch is reported at boot. **The org lead's condition: if the walk shows a sentry targeting Rust's own scientists or bandit guards, the sentry half of `always` is dropped** (stage 5 build, refines D264). | RunicNPC handing nearby sentries the target again every 0.25 s; dropping the sentries from `always`. | +| **D268** | **A faction pair is one row, both ways.** "bandits ↔ guards: hostile" makes each hunt the other; the table is a grid with one cell per pair. **Any of our NPCs that is shot fights back at whoever shot it**, whatever the table says, as it does a turret (D259) (stage 5 build). | Each direction set separately. | +| **D269** | **Escort is an order, not a role.** Any roamer or guard profile can be told to escort, by an event step, the API or `/rnpc follow` (D258). Its NPCs keep within a few metres of the target and fight whoever attacks it. **When the target dies or leaves the server, they walk back to where they were placed** and carry on as their profile does there (stage 5 build). | Holding the spot where the target was lost; an `escort` role whose NPCs stand idle until given someone. | +| **D270** | **An event's Place NPCs step names the escort by a player's Steam id** (typed, or a `{placeholder}` filled from the run's start params), **and the ally by a clan from the site's clan list, or as "the team of player <Steam id>"**. Both are optional (stage 5 build). | One Steam id for both; the ally alone, with escort left to the API. | +| **D271** | **A profile's PVE opt-out is two checkboxes: "players can hurt it" and "it can hurt players"**, both on by default. Turning one off is enforced by RunicNPC itself on every server, with a PVE plugin or without one, and TruePVE and NextGenPVE are told the same. Turrets, fire and other NPCs are not affected (stage 5 build, refines D261). | One "follow the PVE rules" switch, with unkillable meaning `damageTaken` 0. | +| **D272** | **A zone tether is set on a placement (`tether=`) and on an event step ("inside the zone this event made"), not on a profile**, because a ZoneManager zone belongs to one server and a profile may be pushed to several (stage 5 build, refines §6). | A profile field; a profile default overridden per placement and step. | **Borrowing, not copying.** NpcSpawn states no licence at all, so its source grants us nothing and is read only as a description of *what* can be done in Rust. HumanNPC is MIT on uMod, which is GPL-compatible, but §1.2 rules out its @@ -369,7 +376,9 @@ own (D257). Allies within a profile's alert radius join a fight one of them is i **Zone tethering** keeps an NPC inside a ZoneManager zone. At the edge it turns back, and if it ends up outside (it was pushed, or the zone moved) it is returned home. ZoneManager is already required by `module-rust`; to RunicNPC it -is optional, and a profile that asks for a tether on a server without it is refused at save. +is optional. **The tether is a placement's (`tether=`) or an event step's, not a profile's (D272)**, because a +zone belongs to one server; a placement that names a tether on a server without ZoneManager, or a zone it does not +have, is refused when it is set. --- @@ -969,10 +978,25 @@ What this adds to the build (D263–D266): - **Turrets (D264):** `default` needs no code. `ignore` answers `CanBeTargeted` with "no" for the auto turret, flame turret and trap. `always` makes Outpost's and Bandit Camp's sentries (`NPCAutoTurret`) target it, which Rust's own `Ignore` and `IsEntityHostile` refuse for every `ScientistNPC`. Stage 5's build finds the hook that reaches those - two, or answers for the sentry from RunicNPC's side, and says which in this plan. + two, or answers for the sentry from RunicNPC's side, and says which in this plan. **No hook reaches them: Rust's + `AutoTurret.TargetScan` calls `Ignore` before anything Oxide or Carbon injects, so it is a Harmony patch (D267).** - **Ammunition (D265) and fire (D266):** nothing to build. The NPC's reload stays Rust's own, and fire damage is not touched. +**The build's own questions, answered by the org lead on 2026-10-01 (D267–D272):** + +1. **How a sentry is made to shoot an `always` NPC. → D267:** a Harmony patch on `NPCAutoTurret.Ignore` and + `IsEntityHostile`, for our `always` NPCs only. If the walk shows a sentry targeting Rust's own scientists or + bandit guards, the sentry half of `always` is dropped. +2. **Whether a faction pair runs both ways. → D268:** one row, both ways; and any of ours shot fights back. +3. **Escort as a role or an order, and what happens when the target is gone. → D269:** an order any roamer or guard + takes; they walk back to their spot when the target dies or leaves. +4. **How an event step names the escort and the ally. → D270:** a Steam id (or `{placeholder}`) for the escort; a clan + from the site's list, or the team of a player, for the ally. +5. **The PVE opt-out's shape. → D271:** two checkboxes, "players can hurt it" and "it can hurt players", enforced by + RunicNPC itself. +6. **Where a tether is set. → D272:** on a placement and on an event step, not on a profile. + ### Stage 6 — Roles Bosses (a health bar drawn with CUI, phases at thresholds that swap profile values, announcements through the bridge