DangerousRoads
[ALPHA] Wandering encounters for Outward: out in an overworld region, 1-3 hostiles native to that region turn up out of sight, 40-200m away, and walk in. Vanilla loot, never in a town or dungeon. Toast + compass blips. Co-op is not yet verified.DangerousRoads changelog
0.1.13 — 2026-09-05
- Release prep 2026-09-05: body-ladder order gate, bundle-origin yaw, six-stage release protocol + whole-release r2modman check
- Static-analysis wave 5: release prep for Beastwhispering 0.2.18 + DangerousRoads 0.1.13
- Merge branch 'worktree-agent-adc24fd1bf3071fa0'
- StoryKit 0.1.12: complete the partial bump (manifest/thunderstore/csproj + consumer pins)
- Merge branch 'worktree-agent-a70757d0f1c5d341c'
- Merge branch 'worktree-agent-a3de361a4d0f73193'
- DangerousRoads: cards request species be warmed — the wish seam replaces the patrol special case
- DangerousRoads: warm variety is a target, not a floor — a patrol fields min(warm, slots) distinct species
- DangerousRoads: friendly dots rebase — enrolment is ground truth, probe demoted to diagnostic
- Merge branch 'worktree-agent-afec089b8479d9db0'
- DangerousRoads: warm species and faction coherence agree, so patrols field a varied cast
- DangerousRoads: green compass dots for every event friendly (guiding stars)
- DangerousRoads: ShieldWard review fixes — per-shield scoping, kill switch, [DRWARD] tag
- DangerousRoads: ShieldWard — the for-all guard shield no longer disarms our own hostiles
- DangerousRoads: TrogCast counts locally-resident templates as warm
- DangerousRoads: C2 prewarm the patrol cast so the trog stall is paid during travel
- DangerousRoads: patrol guards defend the player from nearby hostiles
- ForgeKit 0.4.13 / DonorKit 0.1.9 / SpawnKit 0.6.1: session-3 fixes
- Fix release-blocking version skew: NetKit 0.2.10, Beastwhispering 0.2.17
- DangerousRoads: roof placement veto — never spawn inside a structure
- … and 51 more (see git history)
0.1.13 — 2026-09-04 (static-analysis wave 5; BUILT, not live-verified — retest rows SA-0904-DR* in docs/static-analysis-fixes-testplan.md)
Findings from docs/static-analysis-2026-09-04.md (lanes 5 and 6). Every item below is a code change
that passed the unit suite; none has been through a live session yet.
- Re-enabling the mod mid-load no longer arms against the scene being left. The
Disabled → armededge requires a sane player position (same readiness rule as the region guard); retestSA-0904-DR11. - The stranger card can no longer wedge for the rest of a region (L5-16, P1).
StrangerFight.TickRemovalreads.C.transform.positionbefore establishing the body still existed — the exact shape the B2 guard fixed inGuardParty.TickLeavingon 2026-08-30, never ported. A stranger body destroyed out from under the row threw on every tick,Finish()was never reached,_fightstayed live and every later stranger draw refused with "strangers are already on the road". The row is classified before any transform read (Core.StrangerRemoval, unit-tested), a gone body is pruned withbody was destroyed out from under the encounter, and the two fight-phase transform reads carry the same guard. - A region change whose player-ready wait times out no longer leaves the director armed on the
previous region's roster (L5-01, P2).
OnRegionReadywas the only rebuild path and its 30 s wait ended in one warning line; the 0.5 s poll only disarmed when the new area was not a zone at all. Now the timeout callsOnRegionAbandoned(stand down to Idle) and the poll runsCore.RegionArmGateon the area-changed edge and on every poll while Idle: roster built for another overworld scene + a READY player (sane position, the same test the wait uses) → re-arm, superseding the pending scene-load wait so a load never arms twice; no ready player → stand down; city/dungeon → keep (no roster needed).SpeciesRoster.BuiltForScenefinally gates something. (Same-tier review tightened the readiness test and the Idle parking — a plainDisarmin an active zone is undone by the re-enable block next frame.) - Far cache: the cap never evicts a restore in flight (L5-03).
Addevicted the longest-parked entry, which is precisely the one most likely mid-restore; the ledger's count went wrong while the creature still landed. Eviction skipsInFlight; with every entry in flight the park is admitted over the cap (bounded by one restore tick) rather than refused. - Far cache: one room-warm decision per species per tick (L5-02, perf). A room-warm HOLD spends no
budget, so the restore loop asked
RoomWarmDecisiononce per parked ENTRY at 2 Hz — N template component walks for a cache full of one cold species.Core.WarmGate.Memocollapses it per tick. - The quiet-roads ledger now reports "the room warmed to X — no longer refused here" (L5-07). Flush ran only inside a refusal, so once the peers warmed a species nothing refused and the line could never print; the director now flushes at 1 Hz while anything is pending.
- Merchant raid: a WIN ends the raid (L5-17, live FAIL RM24).
RaidRunnernever leftRaidingwhen every raider died;roadsmerchantprintedraid=RAIDING … 0/N alivefor the rest of his walk.Core.RaidPlan.IsWon+EndRaid. Safe only because raiders now carryNeverFarPark(L5-18) like every other event-owned body — a far-parked raider used to read as a dead one — released when the raid ends or the runner is destroyed. - Deck diagnostics (L5-04/L5-05/L5-06):
EventDirector.Bypassedno longer overwrites the last-draw record as a side effect of a read; the roll is pulled lazily insideEventDeck.Draw(Func<double>)so a one-drawable non-ambush deck consumes noRandom.value(the header's parity claim now holds for every one-card deck); the[ROSTER] warm requestsline saysno runner in this buildinstead ofdisabledfor a weighted card with no runner. - DoT cleanse only strips maluses (L5-20): a
poison/bleed/burnsubstring in a BUFF's identifier (a resistance, a modded status) is no longer cleansed off a surviving guard;IsMalusEffectgates it. - Fourteen bug-hiding
catch { }blocks now log (L6-11, control flow unchanged): the room-warm ask and active-scene reads inRoomWarmGate, the AI/frozen-cull/health reads inFarCacheDriver, the shield-drop / targetable-pair / hit-tap teardown paths inGuardParty,StrangerFightandShieldWard;WarmCast.Plan.ResidentReadFailurescounts a throwing residency probe andDescribesays so. - DR declares the CompanionKit consumer contract (L6-05) — the SHAPE handshake BW and Hireling had
and DR did not, with the same NoInlining isolation. A
[CONTRACT] 'DangerousRoads' built against CompanionKit consumer contract …boot line;Kits.Integration.Testsnow gates every CompanionKit dependant for it. - Refactors, no behaviour change: the director's own-cap reads use the non-allocating
Spawner.Active(tag, list)(L5-08);WaveRunner's deadh == nullbranch is gone (L5-09);Core.RosterFilter.Keysreplaces two hand-copied roster projections (L5-10);Placement/SpotPicker.PickOnereplaces the duplicatedat=here/ verified-spot preamble in the patrol and stranger cards (L5-11);SpeciesRosterreadsDonorKit.DonorHarvest.DonorScenesinstead of the deprecated CompanionKit forwarder shim (L6-06). - Docs: the ten
[Patrol]keys with no wiki mention (L6-10) are now ondocs/wiki/mods/dangerous-roads.md.
0.1.12 — 2026-09-04 (live-tested on legion; see per-item status)
The 2026-09-04 live session. Requires SpawnKit >= 0.6.2.
- The trog spawn stall is paid during travel, not at the encounter (C2). Measured for the first
time in four sessions:
[ALIVE] worstMs=3585.5— a single 3.59-second frame, isolated to the additive donor scene load (donor scene loaded in 3.87s). Harvests serialize, so a two-cold-species encounter paid ~9.7s back to back, which is very likely the "9" in the tester's "2-9 seconds". Frame-slicing cannot fix that; the cost is inside Unity's scene load, not our code. SoPatrolPlan.TrogSpeciesnow reaches the host's LOCAL prewarm queue — it previously only ever reached the peer-facing room-warm ask — and is harvested during the ~60s of quiet travel the prewarmer already has. Seeded at depth 2 viaPrewarmSlate, round-robin with the region roster so seven trog names cannot starve the ambush card. Live: 3585ms -> 756ms. TrogCastcounts locally-resident templates as warm. The prewarm above warmed a species the card could not see:TrogCastbuilt its warm set only fromRoster.SpawnableNow(), so a resident template that is not in the region roster read as cold. Live proof: the patrol loggedno troglodyte species is warm herewhile[TEMPLATE] cache hit for 'Troglodyte'fired three times in the same session. Now unions roster ∪ resident (viaSpawner.CanMintNow), withWarmCastholding aMinVariety = 2floor — Cobalt's live note was that a uniform cast "is a setback", so warmth alone must not collapse the party to one species; at most one cold species is admitted to meet the floor. The peer-facingRoomWarmGatefilter is unchanged and still applied after.ShieldWard: the for-all guard shield no longer disarms our own hostiles.[Patrol] GuardProtectedwrites a for-allIsTargetable=falseon each guard, and AggroKit's three prefixes then veto every vanilla acquisition route against it — includingCharHurt, so a DR mob beaten by guards could never even retaliate. Observed live: a Marsh Archer Captain atstate=AISSuspicious target=- hp=38/300, damaged and permanently passive. DR-spawned hostiles now get pair re-allows onto shielded guards, refreshed at 1 Hz. Anti-cheese is preserved and was re-verified in review: candidates come only fromSpawner.Active(Ident.OWNER_TAG)(DR-minted bodies, never a player, pet, anchor or partner) andWardPlan.Classifyrefuses player / Player-faction / non-AI as its first three rules. Scoped per-shield: only the patrol wards its spawns — the caravan merchant and the stranger keep their deliberate per-attacker pairing (RaidRunnerhand-picks its raiders), because an early version let an unrelated ambush kill the merchant.- New:
[Patrol] WardOwnSpawns(default true, live viareloadcfg) — stops the pairing without lowering the shield.[Species] HarvestLoadingPriority(default off) — an A/B knob forApplication.backgroundLoadingPriorityaround a harvest; unset in the repo until tonight, and a 3585ms frame is ~900x its 4ms budget, so the case for it is an argument rather than a measurement. - Log tag
[WARD]->[DRWARD]; the bare tag already belonged to Beastwhispering's Mana Ward. - Retests:
road-patrol-testplan.md,dr-farcache-testplan.md,road-merchant-testplan.md(WD1-WD7 plus the merchant-bystander row).
0.1.11 — 2026-08-30 (built, not live-verified)
The bug swarm off the 2026-08-30 live session. Requires StoryKit >= 0.1.11.
- Patrol trog casts are faction-coherent (BUG-TROGVSTROG; Cobalt watched two trogs fight each
other live). The roster spans two hostility buckets (Bandits vs Mercs), each trog rolled its own
species, so mixed parties brawled with themselves. The cast now picks one faction bucket per party
(size-weighted, so Bandits stays the common outcome), via the live FactionBook so a mis-shipped row
self-heals. A trog↔trog truce via AggroKit's deny overrides was mechanically possible but rejected
on cost/coupling grounds — and a Grenadier volley would land through a truce anyway
(RaycastProjectile checks faction only). Data correction:
SpeciesFactions.txtTroglodyte Knight=Mercsper the live re-read. Retest:road-patrol-testplan.mdBUG-TROGVSTROG. - A distance-culled body no longer reads as fighting forever (FC13 FAIL → fixed). Vanilla's
CharAIDisable culler freezes the AI state machine mid-state, so a body frozen in combat pinned the
far cache at its cap.
ReadsAsFightinggains a frozen-cull discriminator (CharacterAI disabled OR AIStatesRoot inactive, gameplay-live only); a confirmed-frozen body can no longer hide behind the unreadable-body fail-safe. Live fights are untouched (FC12's exemption stands). Retest: FC13; unblocks FC15 leg 2. - The stall/stuck leg of the shop-despawn test was unreachable by design — a greet-paused
merchant holds the progress clock, so "stuck with a shop open" cannot occur; the SK-D8 row is
re-staged via the arrival leg.
roadsmerchant despawnno longer prints a spuriousnone on the road.after its own HELD line, androadsmerchant statusalways reportsdespawnHeld=.
0.1.10 — 2026-08-30 (built, not live-verified)
The pre-session follow-up wave, so the three deliberate behavior shifts in 0.1.9 don't read as regressions in the field. Requires SpawnKit >= 0.6.0.
[FarCache] ParkMidFightBeyond(default 0 = never park a fighting creature, i.e. the B1 fix untouched): a hard distance past which even an active fight is parked — the bounded relief valve for players who find roads too persistent after B1. Values belowDespawnRadiusare raised to it (a bind-time warning names the effective figure);roadscacheand the config summary print the effective radius, and a mid-fight park line names the knob so it can never be misread as B1 regressing. Retest:dr-farcache-testplan.mdFC14.- Patrol trogs and stranger mobs are never far-parked while their encounter is unresolved
(
SpawnOptions.NeverFarPark; released in Teardown so survivors go back to being ordinary road creatures). Retest: FC15. - Quiet roads now say why. Every room-warm refusal is recorded in a pruning ledger
(
QuietRoads); the host logs ONE dense, rate-limited line naming the refused species, the runner, the reason and the cold peer actor(s) — with "THIS IS NOT A REGRESSION" in as many words — and[Coop] AnnounceQuietRoads(default false) adds one toast per region for testers. Species that warm are pruned (with their own "no longer refused" line), so the report never goes stale. Retest:road-patrol-testplan.mdPT-PR2c/PT-PR2d. - Warm asks are batched (one list per refusal/tick instead of a per-key loop that multiplied
RoomWarmRequestCap), and the draw-timeroomwarm:condition is mode-faithful: under the defaultRoomDegradeda card sits out only when its species are TERMINAL dead ends on a peer — mirroring the B12 spawn gate — so no more drawn-then-refused blank moments; underRoomStrictnothing changed. Retests: PR2-BATCH1,dangerous-roads-testplan.mdPR3-5.
0.1.9 — 2026-08-30 (built, not live-verified)
- The far cache's "never park a creature that is fighting" rule now actually works (B1). It was
asking
Character.InCombat, which ism_engagedCharacters.Count > 0— and the only writer of that list opens withif (!IsAI && …), so it is permanently false for every AI body. The exemption the config has promised since the feature shipped had therefore never once fired, in solo either: any creature fighting more than[FarCache] DespawnRadius(120 m) from every player was parked mid-swing. Downstream, a town patrol whose troglodytes were parked reported itself resolved in about a second withalive=0 dead=0, because a park isDespawn(kill: false)and fires noOnDied. The read is nowCharacterAI.IsFightingOR a non-nullTargetingSystem.LockedCharacter(the transition frames), with the deadInCombatkept only as belt-and-braces; a throw reads as FIGHTING, because the fail-safe direction is keeping a creature we could not interrogate, never parking one. New pureCore.ParkRule. Note the balance change, which applies in solo too: a far-away fight now continues instead of quietly ending, so it keeps holding its[Wave] MaxOwnActiveslot until it disengages. That is intended. The[FarCache] DespawnRadiusdescription, which said something false for the whole life of the feature, is rewritten to name the signal it actually uses. RowsFC12/FC13indocs/dr-farcache-testplan.md,PT-B1indocs/road-patrol-testplan.md. - A guard destroyed while the party was walking away no longer throws every tick (B2). 3,779
NullReferenceExceptions out ofGuardParty.TickLeavingin one session — one per 0.5 s tick, forever, because the loop readg.C.transform.positionbefore anything established the body still existed and the throw happened BEFORE the prune, so the dead row could never be removed.TickLeavingnow establishesbodyGonefirst (Unity== nullon both the Character and its transform, in a try/catch), asks for distance only when there is something to measure, and routes a gone body through the ordinaryRemovepath so the walker, the shield, the targetable pairs and the registry entry all come off with it. New pureCore.LeaveRule. RowPT-B2. - Co-op: five more spawn paths consult the room-warm intersection before spawning (PR2). The wave,
the merchant raid, the far-cache restore, the stranger mobs and the town-patrol troglodytes now check
what the ambush card already checked, and refuse before spawning rather than showing a guest a ghost.
The gate is deliberately PEER-only:
ColdLocallyis not a refusal, because paying a template mint is what these runners have always done, so in solo the predicate is constant-false and every gate is the identity function (asserted inWarmGateTests.SoloIsANoOp). The wave refuses all-or-nothing rather than dropping a member, since composition was chosen against the spot count. A far-parked creature is no longer dropped when a peer is cold — under the old unconditional drop, a guest joining mid-session silently deleted every parked creature in one 0.5 s tick, with no death, no corpse and no line naming a cause. A room-warm refusal is now a WAIT: the entry stays parked, the party is asked to warm the species, and[FarCache] MaxAgeMinutesis what eventually forgets it. NewCore.WarmGate(predicate- injected, so DR.Core gains no SpawnKit reference) plus the one adapterRoomWarmGate, which fails OPEN on a throw. The patrol's full-TrogSpeciesfallback is peer-gated rather than deleted: in solo the whole list survives untouched, and in a room only peer-showable species come through — if that leaves nothing, the card refuses with a string, which is an outcome the old code had no way to express (it just spawned cold species anyway, which is what killed the tester's guard encounter). In co-op that IS an encounter-density change: expect fewer patrols in a room where no peer can show a troglodyte. RowsDR-PR2W/DR-PR2F,PT-PR2a/PT-PR2b,ST-PR2,RM-PR2. - A card that cannot run in the current co-op room sits out the draw instead of winning it and burning
its cooldown (PR3). New
roomwarm:NAME[,NAME…]condition leaf, and the room-warm push now asks the party for the patrol's troglodytes too, not only the ambush roster —WarmSlate.Mergeinterleaves rather than concatenates, becauseRequestRoomWarmsends an ORDERED list that SpawnKit caps per peer, so appending the trogs would have put them permanently past the cap. The no-stamp is scoped to the co-op-cold reason BY CONSTRUCTION rather than by a conditional: an ineligible card never entersEligibleDeck.Cards, so it cannot be drawn andMarkFiredis never reached for it. Solo is unchanged. ⚠ The leaf is INERT at the shipped default. Its predicate is SpawnKit'sCanMintRoomWide, and[Coop] RoomWarmMode=RoomDegraded— the default — rewrites every verdict to Spawnable on the way out, so at defaults the patrol is dealt exactly as before. That is kept deliberately: RoomDegraded means "the host spawns anyway, the guest pays and warms", a product choice already made, and DangerousRoads keeps ONE opinion about the room — SpawnKit's. RowsPR3-1andPR3-2therefore carryneeds [Coop] RoomWarmMode=RoomStrictas a precondition and FAIL as written on a default install;PR3-3(the push) andPR3-4(solo) are mode-independent. - The road merchant is never despawned out from under an open shop (SK-D8).
RoadWalkernow asksEndOrDefer(reason)between the walk andEnd: thearrivedandstuckbranches defer while the shop is busy (dead/loststill end directly), the held end is re-asked at the top of every 0.25 s tick, and nothing else about the walk runs while it is held — the agent is parked and the progress clock stops.Describe()reportsdespawnHeld=. The hold has a ceiling: new[Merchant] DespawnDeferSeconds(180 s; 0 = no ceiling), and an Overdue verdict is carried intoMerchantCard.OnEndedas a forcing despawn, so Overdue means what it says. New pureCore.ShopDefer, the same shape as the existingCombatDefer. This protects a host-side shopper only until StoryKit's SK-D12 is live —IsInDialoguewas blind to guests, so a guest's open shop read as "not busy" on the host; SK-D12 ships in StoryKit 0.1.9 alongside this. RowSK-D8indocs/road-merchant-testplan.md. - Every other despawn call site was audited against StoryKit's new in-registry busy guard (SK-D13
follow-up):
MerchantCard.ClearLivedecides busy FIRST and keeps every handle on a refusal (loggingdespawn … HELD),GuardParty.RemoveandStrangerFight.Removeskip a busy NPC and let the tick loop retry, and the teardown paths — region change,SlumbushCard.Clear— force, because the body dies with the scene either way. - Needs StoryKit 0.1.9 and SpawnKit 0.5.5; pins raised across the sweep floors.
0.1.8 — 2026-08-30 (built, not live-verified)
- Backup save folders are no longer offered as familiar-stranger candidates (ST12). A renamed
Save_<uid>.bak-<date>-<label>folder scanned as a whole extra "character" — an empty-handed near-duplicate of a live candidate.roadsstranger savesnow reports it underexcluded, naming the original it copies, rather than filtering it silently. The predicate is shared, pure and tested (core/StoryKit.Core/Saves/SaveArtifact.cs); Echoes carried the same defect and is fixed with it. - Decks per kind of place.
ZoneKind {Overworld, City, Dungeon}foldsRegionGate's verdict;WorldSnapshot.Zone+ azone:condition term. The director now arms and draws in cities and dungeons too (RegionWatch.IsActiveZone); the roster/prewarm/room-warm androadswavestay overworld-only. The four road cards carryzone:overworldas their own rule. - The blank card (
blank): does nothing, counts as fired so the clock re-arms. Weight per zone:[Events] BlankWeightOverworld=0 / BlankWeightCity=0.97 / BlankWeightDungeon=1.0— the overworld deck is byte-identical to before, cities are mostly blanks, dungeons all blanks. - New card: Slumbush (
slumbush, ~3% of Levant city draws; own rulezone:city & region:Levant). 1–3 tattered thugs with iron are ring-spawned ~7 m behind the player (StoryKitRingSpawner), the player is halted (ForgeKitPlayerHalt), the lead's dialogue opens on them: pay 200 silver (Inventory.RemoveMoney) or fight (faction Bandits + AggroKit pairwise locks). One thug's pouch holds 60 silver for the loot path.[Slumbush]section +[Events] SlumbushWeight/Cooldown/When. Log tag[SLUM], AggroKit ownerdangerousroads.slumbush. DeckEnabled=falsebypasses to the ambush ONLY in the overworld (elsewhere the draw is null,LastNullReasonsays "deck disabled") — the legacy switch never dealt anything in a city.- Fable review (2026-08-30) folded in: the card closes its own dialogue tree after the answer
(root replies loop back to the greeting, so the rows stayed clickable — double-pay), pay/fight
callbacks are idempotent, the graph is probed one frame after
RequestStartDialogue(a silent non-start no longer holds the player forDecisionSeconds), the card is Soft while the player is in a menu/dialogue (it would have yanked a trade), corpses no longer block the next draw, a region change aborts the wait loops. Manifest floors: ForgeKit 0.4.12, StoryKit 0.1.8. - Needs ForgeKit 0.4.12 (
PlayerHalt) and StoryKit 0.1.8 (RingSpawner).
0.1.7 — 2026-08-30 (built, not live-verified)
- Cards decide whether they join the deck before anything is drawn. Each due tick the director
takes ONE
WorldSnapshot(WorldProbe: game hour,IsNight, rain, region, story faction via theTagSourceManager.*Factionquest signatures, lazy quest-flag lookup) andEligibleDeck.Buildasks every card'sICardCondition; cards that refuse sit out — no roll share, no cooldown stamp — and the draw runs over what joined. A card sitting out that leaves the ambush alone takes the legacy no-roll bypass, so parity holds per world. [Events] AmbushWhen / MerchantWhen / PatrolWhen / StrangerWhen(empty = always): a one-line rule grammar —always | night | day | hour=A-B | rain>X | rain<X | flag:UID | faction:NAME | region:NAME, joined by&/|,!negates. Unparseable text is logged once and treated as always.IEventCard.Conditionis the code-level rule a card can add; the two are ANDed.roadsdeckprints the world line, per-cardwhen=andSITS OUT — needs …; a forced card that would have sat out still runs and says so.LastNullReasonnames the exclusions when nothing was eligible.- Core:
WorldSnapshot,CardCondition(leaves, composites, parser,And),EligibleDeck;EventCardgained an optionalCondition. Tests:CardConditionTests,EligibleDeckTests.
0.1.6 — 2026-08-29 (built, not live-verified)
- Both spawn soft-blocks read the kit gate.
AmbushDirector.SoftBlockchecked onlyIsOverallLoadingDone(it missed the loader's paused window after it); the stranger card carried its own two-flag check. Both areForgeKit.Lifecycle.IsGameplayLive()now — reason linesgameplay not live (level loading, or still paused by the loader)/gameplay still paused by the loader(ST11's key, unchanged). Behind them, StoryKit'sSpawnAtAndGetand SpawnKit'sPreflightrefuse on the same predicate, so no card can put a T-posed body on the road. - The stranger card's wander speed is
NpcSpec.WalkSpeedby name (was the 0.3f literal).
0.1.5 — 2026-08-29
- New event card: the familiar stranger (
stranger, deck card #3 — built, not live-verified;docs/stranger-testplan.mdST0–ST10). One or two of YOUR OTHER saved characters are found on the road fighting 2–4 of the region's mobs; they die honestly. Save them and the survivor says the line and offers "Look in the backpack": that character's real bag + pouch in the vanilla container panel, one take, and the item is moved into your inventory AND written out of the saved character's save as a new snapshot (StoryKitSaveScan/SavedCharacterSpec/SaveHandoff, lifted from Echoes). Master-only; the offer refuses on a co-op guest. - Config:
[Events] StrangerWeight 0.25 / StrangerCooldownSeconds 900; new[Stranger]section (MobsMin 2 / MobsMax 4 / MaxStrangers 2 / StalemateSeconds 90 / RewardEnabled true). - Verbs:
roadsstranger [status|saves|spawn [at=here count= mobs=]|resolve|help|despawn],roadscard stranger [at=here count= mobs=]. Log tag[STRANGER]; AggroKit owner tagdangerousroads.stranger. - With no other saved character on the machine the card is a soft block, warned once at boot.
0.1.4 — 2026-08-28
-
Fix stale dependency pins across the fleet; DonorKit 0.1.7
-
DangerousRoads: town-guard patrol card (deck card #2)
-
DangerousRoads: stop the merchant thrashing on a stale combat lock
-
road merchant: halve the health pool (900 -> 450) and trim dialogue to the shop
-
RM18b PASS: torso look-at guard verified live by A/B
-
RM18b: root cause CONFIRMED at the clamp rail — restore the guard, add a pose sampler
-
RM18b: root-cause the merchant's backward spine arch + guard behind a live toggle
-
SpawnKit + DR: guest voluntary warm (GM22–GM28, built, not verified)
-
DangerousRoads: stop the road merchant's greet-pose lean (RM18)
-
Merchant raid: hold the timed spring while the merchant is far-parked
-
Merchant walk: lift the vanilla distance cull off the road merchant
-
Merge branch 'main' into fix/sa-0823-f2
-
AF2-12: the shrink ledger records a degraded veto too, keyed on the reason
-
AF2-8: say "sent this sweep" — the count is the whole want book, not my species
-
Merge pull request #28 from cobalt-craft/fix/sa-0823-o1
-
AF2-4: a degraded room verdict is not "already warm" — hand it to the want book
-
AO1-13: refuse an unknown card id before recording or stamping it
-
AF2-2: read "dead end on a peer" off the Reason, not the downgraded Verdict
-
AO1-12: cache the parsed blip colours
-
AO1-11: refuse every forced path on a co-op guest
-
… and 19 more (see git history)
-
New event card: the town-guard patrol (
patrol, 2026-08-26 — built, not live-verified;docs/road-patrol-testplan.md). Two or three guards in the region's town colours fight four to six troglodytes on the road, and they are in real danger without you. Help — any damage to any of the trogs, yours or your pet's or an arrow's — and the guard left standing pays the town's bounty once: five or six silver per troglodyte that fell. Watch instead and you get thanks. Afterwards the survivors walk off toward the far end of the road and are removed once they are out of everyone's render range, which is what gives their spawn slots back. You cannot hurt the guards ([Patrol] GuardProtected); the trogs can. The Troglodyte Queen is explicitly refused. New config[Events] PatrolWeight/PatrolCooldownSecondsand a[Patrol]section; new verbroadspatrolandroadscard patrol [key=value …]; log tag[PATROL]. -
Road merchant tuning (Cobalt 2026-08-26):
[Merchant] Healthdefault 900 → 450 — still enough to outlast a raid until you arrive, half the tank he was. Note a changed default does not migrate into an existingBepInEx/config/cobalt.dangerousroads.cfg; hand-edit the deployed cfg on each host. His dialogue is the shop only — the "Where are you headed?" reply is retired (MerchantText.RoadFlavourand its tests stay, unwired, if it comes back). -
Road merchant, RM18b (the backward spine arch, live 2026-08-24):
roadsmerchant posedumps the three candidate causes in one read — vanilla's torso look-at pitch (locked/weapon/torsoLookAt), a latched additive KnockHurt layer (layer6), and the player-only slope+IK paths an SL body runs becauseCharacter.IsAIis false (isAI/ik/slopeSmooth).roadsmerchant rebindcallsAnimator.Rebind()as the frozen-vs-live discriminator, androadsmerchant torsofix on|offtoggles StoryKit's guard so the whole A/B runs in one session without a rebuild. The old RM18 covered two unrelated defects and is now split: RM18a (themoveForward/moveSidelocomotion residual,HoldAgentStill) and RM18b (the spine). -
Road-merchant live-bug pass (2026-08-22, legion — he fought the pet, jogged off, spawned naked, nobody raided him): Protected —
AggroTools.Protectat spawn /Unprotectat despawn, the player side (weapons, lock-on, pet + anchor, Player-faction AI) cannot touch him, raiders re-allowed pairwise ([Merchant] Protected, card keyprotected=). Stops to greet — withinGreetRadius(4 m) or in dialogue the carrot parks at his feet, the stall clock holds, he faces the nearest player; resumesGreetResumeSeconds(3 s) after the last player leaves (CoreGreetPause, tested;[MERCHANT] paused to greet/resumed the walk).WalkSpeeddefault 1.1 → 0.5 (a SpeedModif multiplier; 1.1 is a jog). The raid —Merchant/RaidRunner: rolled at spawn (RaidChance0.75,RaidMin/RaidMax2–3), springs withinRaidTriggerMeters(90 m) of a player or afterRaidDelaySeconds(120 s); roster species preferring Bandits (CoreRaidPlan, tested), 15–30 m ring on walkable navmesh,Spawner.Spawnunder our OwnerTag,ForceTargetonce alive; toast "Bandits are closing on the merchant!"; card keysraid=0|1 raiders= raidspecies=;roadsmerchant raidforces it,statusshows it. Dressed —NpcSpec.RandomVisuals+OutfitPoolfromMerchant/MerchantOutfits(seven display-name combos), card keyoutfit=. Testplan RM14–RM17; RM1 FAIL / RM5 partial evidence recorded. Review pass (same day): the raid is clamped to the slots left under[Wave] MaxOwnActive(CoreRaidPlan.CapToRoom, tested; full cap = no raid, logged);ClearLivesweeps everydangerousroads.caravanoverride by owner so a region change mid-raid cannot leak the shield or the pairs; the raid toast has its own throttle (a merchant spawned insideRaidTriggerMeterssprings 0.5 s after the merchant toast); a spawn callback landing after the runner was destroyed no longer tries to start a coroutine on it. -
roadscard <id> [key=value …]: force any card with optional overrides (CoreCardArgs, tested;EventContext.Args). Merchant keysexit= dest= health= protection= damage= speed= defends= at=here name=; ambush keyscount= species=. Absent keys = the card's defaults, so a bareroadscard merchantis a deck draw; unknown keys warn and run.roadsdeck <id> …androadsmerchant spawn …take the same tail; the forge catalog completes the card id. -
Green merchant compass dot:
CompassBlips.Tick(hostile, friendly)— one pool, two colours; the merchant's dot is[Compass] MerchantBlipColor(default#34C759), 1.4× size, no range cap, shown from the toast until despawn;[Compass] ShowMerchantBlipturns it off independently ofShowBlips.MerchantCard.LivePositionfeeds it;roadsblips/roadsstatuscount both kinds. -
Walk stall fix (live 2026-08-22: "no progress for 90s toward waypoint 1/6 (16m away)" → despawn at 134 s): roadside seeds are path-checked from the spawn at route build and unreachable ones dropped; the carrot is sampled with the walkable mask (was
AllAreas); a stall toward an intermediate waypoint SKIPS it (CoreRouteSkip, max 3 per walk, tested), a stall toward the exit gets one last straight try, only thenstuck; every stall warning carries the agent'spathStatus/hasPath/remainingDistance/isOnNavMesh/velocity, the AI state and the carrot. -
Fallback stock names fixed against the item registry (
Bandages,Makeshift Torch;Bandage/Torchmatched nothing) and verified once per session at the first spawn ([MERCHANT]warning names any that fail). StoryKit'snpcmerchanttestcarried the same names — fixed too. -
Wandering merchant (card #1):
Events/MerchantCardbuilds a per-spawn-unique StoryKitNpcSpec(Merchants faction, Mefino's trade backpack, caravan-trader shop, greeting/Shop/road-flavour dialogue), spawns it on a verified out-of-sight spot andMerchant/RoadWalkerwalks it to a zone exit via a carrot GO onAISWander.FollowTransform; despawns on arrival, stall (StuckSeconds), death or region change. Rescue edge (hit + combat ended) swaps the greeting to the thanks line.[Merchant]config section; verbsroadsmerchant,roadsexits,roadsstock;[MERCHANT]log tag;IEventCard.OnRegionChangedseam; CoreWalkProgress/RescueEdge+ tests. -
Event deck: each due tick draws a card (
[Events]DeckEnabled / AmbushWeight / MerchantWeight / MerchantCooldownSeconds). Ambush = card #0 viaEvents/AmbushCardover the unchangedWaveRunner.Run; merchant card registers later throughEventDirector.Register. Deck off or ambush-only = byte-identical to the old timer path (no roll pulled).roadsdeck [draw|<id>]verb;roadsstatusshows the last draw. Core:EventDeck,RoadRoute,MerchantText+ tests. -
Merchant review fixes: the SideLoader AI is built by a spawn COROUTINE frames after
Spawn()returns, soRoadWalkernow bindsAISWanderlazily on its tick (was: looked once at attach, found nothing, and the merchant never walked); the stuck clock is held during combat instead of merely skipped (a long fight no longer ends in an instant "stuck" despawn); every finished or refused merchant spec isNpcRegistry.Unregistered (no per-event registry/template growth). -
Deck review fixes: a card that throws (or ends without reporting) is routed to a soft outcome via
Core/EventGuardinstead of leavingRunningstuck; every weight at 0 now fires nothing (was: silently ran the ambush anyway); a lone card honours its own cooldown (ambush's is 0, so parity holds);RoadRoute.ThreadSeedstie order is stable (List.Sortis not, above 16 items).
0.1.3 — 2026-08-19
- Static-analysis wave F3c: leaf-kit fixes (A7-1/2/4/8/9/13/14/15/16/20/23/24/25/27)
- Coercion wave: every ForgeKit integration completes cleanly through the TUI seam
- Forge shell: catalog dump + response protocol + set/cfgdump in ForgeKit; forge CLI/REPL + completion packs; wiki-enriched name db
- DangerousRoads: re-enabling the kill-switch mid-region arms without a scene change
0.1.2 — 2026-08-11
- Fable-review fixes: viewID watermark reads outstanding ids (latched), corpse-release fallback never parks a neutralized view, FarCache InFlight can't leak, visual-pass retries count as busy, Notify header placement
- DangerousRoads far-cache: distance park + in-place restore with HP, hysteresis, roadscache verb + testplans for all three waves
- SpawnKit perf wave: hot-loop allocs removed, adaptive replica enforce, prune throttle, viewID watermark + opt-in corpse release, AI sleep radius, caps 8->12
- Merge branch 'main' into fix/sa-0808-kits
- Static-analysis 2026-08-08 wave D-kits: P2-13/P2-15 + the kit P3 group
- W4: fixes for everything D1 found, plus the join-race P1
- DangerousRoads 'stops at 6': MaxOwnActive default 6->8 + owner-visible caps
- DangerousRoads: fake-null cleanup in the squad scans and the UI dump
- A1: the fake-null accessors, and every Plugin.cs cleared
0.1.1 — 2026-08-02
- DangerousRoads 0.1.1 + SpawnKit 0.4.2: re-pin off CompanionKit 0.4.2
- Logging: answer "why did nothing happen?" without needing LogVerbose on in advance
- Cleanup: one anchor-collect loop, one ownership side-table
- DangerousRoads: exclude the whole Wolfgang family, and stop a fresh boot crashing roadsstatus
- Docs sweep: archive, condense, and validate the whole documentation tree
0.1.0 — 2026-08-02
- DangerousRoads: say what the creatures actually do, and cut the README to size
- DangerousRoads: Thunderstore release prep + icon
- Live-session findings + a DangerousRoads ambush-interval tweak
- DangerousRoads: cluster waves, compass blips, and stop combat starving the timer
- DangerousRoads: wandering encounters on SpawnKit
0.1.0 — initial release
- Wandering encounters in the six overworld regions. While you are out in Chersonese, Enmerkar Forest, Hallowed Marsh, Abrassar, Antique Plateau or Caldera, 1–3 hostile creatures turn up every 20–60 seconds, out of sight, 40–200 m away, and walk in. They fight, they die, and they drop their normal vanilla loot, because they are the ordinary wild version of that creature.
- Region-native species only, composed where possible from a real vanilla squad's roster, so a wave looks like something that already belonged there. Every member of a wave shares one species and therefore one faction — the thing that stops an "ambush" from fighting itself.
- Never in a town, never in a dungeon, never indoors. Cities, villages, dungeons, interiors and New Sirocco are all excluded, and an unrecognised area fails closed (no spawns) rather than open.
- Placement is the whole feature. Candidates come from a configurable chain of anchor sources — live creatures, gathering spots, vanilla's own hand-placed squad markers, world interactables, and a procedural fallback — and every one is put through the same verification: real navmesh ground, a flat plateau check that rejects tent roofs and boulder tops, a walking-vs-straight-line path ratio that rejects the far side of a gorge, and a line-of-sight test against every player.
- A throttled toast names the species, the count and the direction, and live spawns are marked on the game's HUD compass while they are within range.
- Never stalls the game. Only species whose body is already loaded are used; the rest are warmed quietly in the background between waves.
- Cooldown floor after each wave, a cap on how many creatures the mod may have alive at once, and a bounded hold-off while you are already in combat.
- Master kill-switch
[General] Enabled, and every knob is live-reloadable withreloadcfg— no relaunch needed to retune. - Dev/diagnostic verbs on
BepInEx/config/DangerousRoads_cmd.txt:roadsstatus,roadsroster,roadsledger,roadsanchors,roadsprobe,roadsmark,roadsnow,roadssweepand more. - Alpha. First live session passed 22 checks across three regions; the remaining three regions, co-op, and a couple of known issues are still open — see the README's Known issues.
