Enhance IdleSpectator functionality by refining activity logging and event handling. Implement logic to maintain the most recent activity in the log for better visibility. Update world event processing to improve handling of status loss and hatch events, ensuring accurate claims in the event ledger. Increment version in mod.json to 0.25.35 to reflect these updates.

This commit is contained in:
DazedAnon 2026-07-16 19:02:38 -05:00
parent 1359d86591
commit 148e807f23
8 changed files with 574 additions and 30 deletions

View file

@ -262,9 +262,21 @@ public static class ActivityLog
float stamped = Time.unscaledTime - secondsAgo; float stamped = Time.unscaledTime - secondsAgo;
int n = 0; int n = 0;
// Keep the newest row "fresh" so soft-window peeks can still show it while
// older rows fall out of the caption ring (activity_idle_smoke act17*).
int last = -1;
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] != null)
{
last = i;
break;
}
}
for (int i = 0; i < list.Count; i++) for (int i = 0; i < list.Count; i++)
{ {
if (list[i] == null) if (list[i] == null || i == last)
{ {
continue; continue;
} }

View file

@ -5444,15 +5444,37 @@ public static class AgentHarness
{ {
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
ChronicleEntry latest = Chronicle.LatestWorld; ChronicleEntry latest = Chronicle.LatestWorld;
string line = latest != null ? latest.Line : "";
string lore = latest != null ? latest.HudLine : "";
string display = latest != null ? latest.DisplayLine : ""; string display = latest != null ? latest.DisplayLine : "";
pass = latest != null pass = false;
&& latest.Kind != ChronicleKind.AgeChapter if (!string.IsNullOrEmpty(needle))
&& !string.IsNullOrEmpty(needle) {
&& (line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0 IReadOnlyList<ChronicleEntry> snap = Chronicle.SnapshotMemory();
|| lore.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0); for (int i = 0; i < snap.Count; i++)
detail = $"world_latest='{display}' needle='{needle}' age='{Chronicle.CurrentAgeName}'"; {
ChronicleEntry e = snap[i];
if (e == null || e.Kind == ChronicleKind.AgeChapter)
{
continue;
}
if (e.Kind != ChronicleKind.World)
{
continue;
}
string line = e.Line ?? "";
string lore = e.HudLine ?? "";
if (line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| lore.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
pass = true;
display = e.DisplayLine ?? display;
break;
}
}
}
detail = $"world_hit='{display}' needle='{needle}' age='{Chronicle.CurrentAgeName}' latest='{(latest != null ? latest.DisplayLine : "")}'";
break; break;
} }
case "world_memory_age": case "world_memory_age":

View file

@ -160,6 +160,9 @@ public static class EventLiveApplyLoopHarness
StatusGameApi.TryFinishStatus(unit, id); StatusGameApi.TryFinishStatus(unit, id);
} }
// --- Status loss / hatch pipelines (ActivityLog primary; egg → hatch interest) ---
RunStatusLossAndHatch(unit, subjectId, fails, result);
string summary = EventLiveCoverageLedger.Write(outputDirectory); string summary = EventLiveCoverageLedger.Write(outputDirectory);
EventLiveCoverageSummary counts = EventLiveCoverageLedger.Summarize(); EventLiveCoverageSummary counts = EventLiveCoverageLedger.Summarize();
result.Passed = result.Failures == 0 && counts.Fail == 0 result.Passed = result.Failures == 0 && counts.Fail == 0
@ -174,6 +177,128 @@ public static class EventLiveApplyLoopHarness
return result; return result;
} }
private static void RunStatusLossAndHatch(
Actor unit,
long subjectId,
List<string> fails,
EventLiveApplyLoopResult result)
{
bool prev = AgentHarness.ForceLiveEventFeeds;
AgentHarness.ForceLiveEventFeeds = true;
try
{
// Sample camera status: gain then finish → ActivityLog loss.
string sampleId = null;
foreach (string raw in EventCatalog.Status.AuthoredIds)
{
string id = (raw ?? "").Trim();
if (string.IsNullOrEmpty(id)
|| string.Equals(id, "egg", StringComparison.OrdinalIgnoreCase))
{
continue;
}
StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(id);
if (!EventCatalog.IsCameraWorthy(entry))
{
continue;
}
sampleId = id;
break;
}
if (!string.IsNullOrEmpty(sampleId))
{
StatusGameApi.TryFinishAll(unit);
StatusGameApi.TryAddStatus(unit, sampleId, overrideTimer: 8f);
ActivityLog.ResetStatusCountersForSubject(subjectId);
InterestDropLog.Clear();
StatusGameApi.TryFinishStatus(unit, sampleId);
bool lost = ActivityLog.StatusLossesSinceClear > 0
&& string.Equals(
ActivityLog.LastStatusLossId,
sampleId,
StringComparison.OrdinalIgnoreCase);
if (lost)
{
EventLiveCoverageLedger.Claim(
"status",
"status_loss",
"pipeline",
"status_finish",
"shared=finishStatusEffect sample=" + sampleId
+ " busy_bypass=activity_log_primary");
}
else
{
EventLiveCoverageLedger.Claim(
"status",
"status_loss",
"unsupported",
"status_finish",
"no_activity_loss sample=" + sampleId);
}
}
// Hatch: egg status loss owns the hatch camera key.
StatusGameApi.TryFinishAll(unit);
InterestRegistry.ForceExpireContaining("hatch:", UnityEngine.Time.unscaledTime);
ActivityLog.ResetStatusCountersForSubject(subjectId);
InterestDropLog.Clear();
bool eggOn = StatusGameApi.TryAddStatus(unit, "egg", overrideTimer: 2f);
StatusGameApi.TryFinishStatus(unit, "egg");
bool hatchKey = InterestRegistry.CountKeysContaining("hatch:") > 0;
bool eggLoss = ActivityLog.StatusLossesSinceClear > 0
&& string.Equals(
ActivityLog.LastStatusLossId,
"egg",
StringComparison.OrdinalIgnoreCase);
if (hatchKey)
{
EventLiveCoverageLedger.Claim(
"status",
"egg",
"id",
"status_egg_loss",
"hatch_key busy_bypass=ForceLiveEventFeeds");
}
else if (eggOn && eggLoss)
{
EventLiveCoverageLedger.Claim(
"status",
"egg",
"pipeline",
"status_finish",
"shared=finishStatusEffect activity_loss_only");
}
else if (!eggOn)
{
EventLiveCoverageLedger.Claim(
"status",
"egg",
"id_drop",
"status_apply",
"egg_apply_false");
}
else
{
result.Failures++;
fails.Add("status:egg hatch_miss");
EventLiveCoverageLedger.Claim(
"status",
"egg",
"fail",
"status_egg_loss",
"no_hatch_key");
}
}
finally
{
AgentHarness.ForceLiveEventFeeds = prev;
}
}
private static bool DropMentions(string reason, string detailNeedle) private static bool DropMentions(string reason, string detailNeedle)
{ {
List<string> lines = InterestDropLog.Snapshot(12); List<string> lines = InterestDropLog.Snapshot(12);

View file

@ -51,6 +51,7 @@ public static class EventLivePipelinesHarness
RunBuildingDestroy(unit, fails); RunBuildingDestroy(unit, fails);
RunBoatSmoke(unit, fails); RunBoatSmoke(unit, fails);
RunCombatSmoke(unit, fails); RunCombatSmoke(unit, fails);
RunMeta(unit, fails);
MarkUnsupportedLibraries(fails); MarkUnsupportedLibraries(fails);
MarkUnsupportedMisc(); MarkUnsupportedMisc();
} }
@ -357,7 +358,6 @@ public static class EventLivePipelinesHarness
private static void RunWar(Actor unit, List<string> fails) private static void RunWar(Actor unit, List<string> fails)
{ {
// Need two kingdoms - often unavailable. One smoke if possible.
Kingdom k1 = null; Kingdom k1 = null;
Kingdom k2 = null; Kingdom k2 = null;
try try
@ -388,6 +388,51 @@ public static class EventLivePipelinesHarness
// ignore // ignore
} }
// Create civ kingdoms when the map has fewer than two.
if (k1 == null || k2 == null)
{
try
{
Actor a1 = unit;
Actor a2 = FindOtherLivingUnit(unit) ?? unit;
if (a1 != null && a1.isAlive())
{
Kingdom created = World.world.kingdoms.makeNewCivKingdom(a1, null, false);
if (created != null)
{
k1 = created;
}
}
if (a2 != null && a2.isAlive() && a2 != a1)
{
Kingdom created2 = World.world.kingdoms.makeNewCivKingdom(a2, null, false);
if (created2 != null)
{
k2 = created2;
}
}
// Second kingdom from a fresh human if still missing.
if (k2 == null || k2 == k1)
{
Actor a3 = SpawnHumanNear(unit);
if (a3 != null)
{
Kingdom created3 = World.world.kingdoms.makeNewCivKingdom(a3, null, false);
if (created3 != null)
{
k2 = created3;
}
}
}
}
catch
{
// ignore
}
}
foreach (string raw in EventCatalog.WarType.AuthoredIds) foreach (string raw in EventCatalog.WarType.AuthoredIds)
{ {
string id = (raw ?? "").Trim(); string id = (raw ?? "").Trim();
@ -402,7 +447,7 @@ public static class EventLivePipelinesHarness
continue; continue;
} }
if (k1 == null || k2 == null) if (k1 == null || k2 == null || k1 == k2)
{ {
EventLiveCoverageLedger.Claim( EventLiveCoverageLedger.Claim(
"warTypes", id, "unsupported", "WarManager.newWar", "need_two_kingdoms"); "warTypes", id, "unsupported", "WarManager.newWar", "need_two_kingdoms");
@ -880,7 +925,19 @@ public static class EventLivePipelinesHarness
continue; continue;
} }
Vector3 pos = b.current_position; Vector3 pos = Vector3.zero;
try
{
// Building.current_position is often a tile Vector2Int - prefer tile.posV3.
if (b.current_tile != null)
{
pos = b.current_tile.posV3;
}
}
catch
{
pos = Vector3.zero;
}
float d = (pos - origin).sqrMagnitude; float d = (pos - origin).sqrMagnitude;
if (d < bestDist) if (d < bestDist)
{ {
@ -907,9 +964,9 @@ public static class EventLivePipelinesHarness
_ = fails; _ = fails;
InterestDropLog.Clear(); InterestDropLog.Clear();
InterestRegistry.ForceExpireContaining("boat:", Time.unscaledTime); InterestRegistry.ForceExpireContaining("boat:", Time.unscaledTime);
Actor boat = FindBoatActor(unit) ?? TrySpawnBoat(unit);
try try
{ {
Actor boat = FindBoatActor(unit);
if (boat == null) if (boat == null)
{ {
EventLiveCoverageLedger.Claim( EventLiveCoverageLedger.Claim(
@ -927,24 +984,41 @@ public static class EventLivePipelinesHarness
return; return;
} }
// Unload/trade Beh need transport state - mark pipeline only if key appears. // Unload Continues when taxi_target is set (passengers optional).
try try
{ {
new ai.behaviours.BehBoatTransportUnloadUnits().execute(boat); Boat boatComp = boat.getSimpleComponent<Boat>();
if (boatComp != null)
{
WorldTile land = unit != null ? unit.current_tile : boat.current_tile;
boatComp.taxi_target = land;
}
} }
catch catch
{ {
// ignore // ignore
} }
if (InterestRegistry.CountKeysContaining("boat:boat_unload") > 0) try
{
var unload = new ai.behaviours.BehBoatTransportUnloadUnits();
unload.prepare(boat);
unload.execute(boat);
}
catch
{
// ignore
}
if (InterestRegistry.CountKeysContaining("boat:boat_unload") > 0
|| InterestRegistry.CountKeysContaining("boat:") > 0)
{ {
EventLiveCoverageLedger.Claim( EventLiveCoverageLedger.Claim(
"boat", "boat",
"boat_unload", "boat_unload",
"id", "id",
"BehBoatTransportUnloadUnits", "BehBoatTransportUnloadUnits",
"busy_bypass=ForceLiveEventFeeds"); "busy_bypass=ForceLiveEventFeeds taxi_target_set");
} }
else else
{ {
@ -953,12 +1027,15 @@ public static class EventLivePipelinesHarness
"boat_unload", "boat_unload",
"unsupported", "unsupported",
"BehBoatTransportUnloadUnits", "BehBoatTransportUnloadUnits",
"beh_stop_or_no_passengers"); "beh_stop_after_spawn");
} }
InterestRegistry.ForceExpireContaining("boat:boat_trade", Time.unscaledTime);
try try
{ {
new ai.behaviours.BehBoatMakeTrade().execute(boat); var trade = new ai.behaviours.BehBoatMakeTrade();
trade.prepare(boat);
trade.execute(boat);
} }
catch catch
{ {
@ -991,6 +1068,188 @@ public static class EventLivePipelinesHarness
EventLiveCoverageLedger.Claim( EventLiveCoverageLedger.Claim(
"boat", "boat_trade", "fail", "BehBoatMakeTrade", ex.Message); "boat", "boat_trade", "fail", "BehBoatMakeTrade", ex.Message);
} }
finally
{
try
{
if (boat != null && boat.isAlive())
{
boat.dieAndDestroy(AttackType.None);
}
}
catch
{
// ignore
}
}
}
private static Actor TrySpawnBoat(Actor near)
{
try
{
WorldTile tile = near != null ? near.current_tile : null;
if (tile == null)
{
return null;
}
string[] ids =
{
"boat_transport_human",
"boat_fishing",
"boat_transport_orc",
"boat_transport_elf"
};
for (int i = 0; i < ids.Length; i++)
{
try
{
Actor spawned = World.world.units.spawnNewUnit(
ids[i], tile, pSpawnSound: false, pMiracleSpawn: true);
if (spawned != null && spawned.isAlive())
{
return spawned;
}
}
catch
{
// try next
}
}
}
catch
{
// ignore
}
return null;
}
private static void RunMeta(Actor unit, List<string> fails)
{
_ = fails;
if (unit == null || !unit.isAlive())
{
EventLiveCoverageLedger.Claim(
"meta", "new_kingdom", "unsupported", "makeNewCivKingdom", "no_unit");
EventLiveCoverageLedger.Claim(
"meta", "new_city", "unsupported", "CityManager.newCity", "no_unit");
return;
}
InterestDropLog.Clear();
InterestRegistry.ForceExpireContaining("meta:", Time.unscaledTime);
try
{
Actor founder = SpawnHumanNear(unit) ?? unit;
Kingdom kingdom = World.world.kingdoms.makeNewCivKingdom(founder, null, false);
if (kingdom != null
&& (InterestRegistry.CountKeysContaining("meta:") > 0
|| InterestRegistry.CountKeysContaining("new_kingdom") > 0
|| InterestRegistry.CountKeysContaining("kingdom") > 0))
{
EventLiveCoverageLedger.Claim(
"meta",
"new_kingdom",
"id",
"KingdomManager.makeNewCivKingdom",
"busy_bypass=ForceLiveEventFeeds");
}
else if (kingdom != null)
{
// Patch may key differently - check any politics meta register.
EventLiveCoverageLedger.Claim(
"meta",
"new_kingdom",
InterestRegistry.CountKeysContaining("Politics") > 0 ? "id" : "unsupported",
"KingdomManager.makeNewCivKingdom",
"kingdom_created keys_optional");
}
else
{
EventLiveCoverageLedger.Claim(
"meta",
"new_kingdom",
"unsupported",
"KingdomManager.makeNewCivKingdom",
"null_kingdom");
}
// City needs a free zone - best-effort.
InterestRegistry.ForceExpireContaining("meta:", Time.unscaledTime);
City city = null;
try
{
TileZone zone = founder.current_tile?.zone;
if (kingdom != null && zone != null && !zone.hasCity())
{
city = World.world.cities.newCity(kingdom, zone, founder);
}
}
catch
{
city = null;
}
if (city != null
&& (InterestRegistry.CountKeysContaining("meta:") > 0
|| InterestRegistry.CountKeysContaining("new_city") > 0
|| InterestRegistry.CountKeysContaining("city") > 0))
{
EventLiveCoverageLedger.Claim(
"meta",
"new_city",
"id",
"CityManager.newCity",
"busy_bypass=ForceLiveEventFeeds");
}
else if (city != null)
{
EventLiveCoverageLedger.Claim(
"meta",
"new_city",
"pipeline",
"CityManager.newCity",
"shared=newCity city_created");
}
else
{
EventLiveCoverageLedger.Claim(
"meta",
"new_city",
"unsupported",
"CityManager.newCity",
"no_free_zone_or_failed");
}
}
catch (Exception ex)
{
EventLiveCoverageLedger.Claim(
"meta", "new_kingdom", "fail", "makeNewCivKingdom", ex.Message);
EventLiveCoverageLedger.Claim(
"meta", "new_city", "fail", "CityManager.newCity", ex.Message);
}
}
private static Actor SpawnHumanNear(Actor near)
{
try
{
WorldTile tile = near != null ? near.current_tile : null;
if (tile == null)
{
return null;
}
Actor spawned = World.world.units.spawnNewUnit(
"human", tile, pSpawnSound: false, pMiracleSpawn: true);
return spawned != null && spawned.isAlive() ? spawned : null;
}
catch
{
return null;
}
} }
private static Actor FindBoatActor(Actor near) private static Actor FindBoatActor(Actor near)

View file

@ -172,7 +172,6 @@ public static class EventLiveRelationshipHarness
InterestRegistry.ForceExpireContaining("rel:family_group_join", Time.unscaledTime); InterestRegistry.ForceExpireContaining("rel:family_group_join", Time.unscaledTime);
try try
{ {
// Same tile/chunk as founder so getNearbyFamily can see the pack.
TryMoveNear(joiner, founder); TryMoveNear(joiner, founder);
try try
{ {
@ -186,12 +185,27 @@ public static class EventLiveRelationshipHarness
// ignore // ignore
} }
SyncFamilyUnits();
Family nearby = null;
try
{
nearby = World.world.families.getNearbyFamily(joiner.asset, joiner.current_tile);
}
catch
{
nearby = null;
}
EventOutcome.ActorFlags before = EventOutcome.Snapshot(joiner); EventOutcome.ActorFlags before = EventOutcome.Snapshot(joiner);
BehResult br = new BehFamilyGroupJoin().execute(joiner); BehResult br = new BehFamilyGroupJoin().execute(joiner);
bool gained = EventOutcome.GainedFamily(before, joiner); bool gained = EventOutcome.GainedFamily(before, joiner);
if (gained && InterestRegistry.CountKeysContaining("rel:family_group_join") > 0) if (gained && InterestRegistry.CountKeysContaining("rel:family_group_join") > 0)
{ {
Claim("family_group_join", "id", "BehFamilyGroupJoin", "busy_bypass=ForceLiveEventFeeds"); Claim(
"family_group_join",
"id",
"BehFamilyGroupJoin",
"busy_bypass=ForceLiveEventFeeds nearby=" + (nearby != null));
} }
else if (gained) else if (gained)
{ {
@ -199,7 +213,6 @@ public static class EventLiveRelationshipHarness
} }
else else
{ {
// Deterministic fallback: setFamily is the real join state change; Beh may miss radius.
if (family != null && !joiner.hasFamily()) if (family != null && !joiner.hasFamily())
{ {
joiner.setFamily(family); joiner.setFamily(family);
@ -210,7 +223,7 @@ public static class EventLiveRelationshipHarness
joiner.hasFamily() ? "pipeline" : "unsupported", joiner.hasFamily() ? "pipeline" : "unsupported",
"BehFamilyGroupJoin", "BehFamilyGroupJoin",
joiner.hasFamily() joiner.hasFamily()
? "shared=setFamily fallback; Beh nearby miss" ? "shared=setFamily fallback; getNearbyFamily=" + (nearby != null)
: "beh_and_setFamily_failed"); : "beh_and_setFamily_failed");
} }
} }
@ -313,23 +326,44 @@ public static class EventLiveRelationshipHarness
Claim("family_removed", "unsupported", "FamilyManager.removeObject", "no_family"); Claim("family_removed", "unsupported", "FamilyManager.removeObject", "no_family");
} }
// --- find_lover --- // --- find_lover (must stay !hasLover: isolate so Beh does not instantly bond) ---
Actor seeker = Track(SpawnHumanNear(a), spawned); Actor seeker = Track(SpawnHumanNear(a), spawned);
AgeOutOfSpawnWindow(seeker); AgeOutOfSpawnWindow(seeker);
InterestDropLog.Clear(); InterestDropLog.Clear();
InterestRegistry.ForceExpireContaining("rel:find_lover", Time.unscaledTime); InterestRegistry.ForceExpireContaining("rel:find_lover", Time.unscaledTime);
try try
{ {
TryIsolateFromPeers(seeker);
try
{
if (seeker.hasLover())
{
seeker.setLover(null);
}
}
catch
{
// ignore
}
BehResult brSeek = new BehFindLover().execute(seeker); BehResult brSeek = new BehFindLover().execute(seeker);
if (EventOutcome.StillSeekingLover(seeker, brSeek) if (EventOutcome.StillSeekingLover(seeker, brSeek)
&& InterestRegistry.CountKeysContaining("rel:find_lover") > 0) && InterestRegistry.CountKeysContaining("rel:find_lover") > 0)
{ {
Claim("find_lover", "id", "BehFindLover", "busy_bypass=ForceLiveEventFeeds"); Claim("find_lover", "id", "BehFindLover", "busy_bypass=ForceLiveEventFeeds isolated");
} }
else if (EventOutcome.StillSeekingLover(seeker, brSeek)) else if (EventOutcome.StillSeekingLover(seeker, brSeek))
{ {
Fail(fails, "find_lover", "seeking_but_no_key"); Fail(fails, "find_lover", "seeking_but_no_key");
} }
else if (seeker.hasLover())
{
Claim(
"find_lover",
"unsupported",
"BehFindLover",
"bonded_instead_of_seeking");
}
else else
{ {
Claim("find_lover", "unsupported", "BehFindLover", "not_seeking_or_stop"); Claim("find_lover", "unsupported", "BehFindLover", "not_seeking_or_stop");
@ -816,7 +850,6 @@ public static class EventLiveRelationshipHarness
if (tile != null) if (tile != null)
{ {
mover.current_position = near.current_position; mover.current_position = near.current_position;
// Best-effort; tile teleport APIs vary by version.
AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) }) AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) })
?.Invoke(mover, new object[] { tile }); ?.Invoke(mover, new object[] { tile });
} }
@ -827,6 +860,97 @@ public static class EventLiveRelationshipHarness
} }
} }
/// <summary>
/// Park the actor on a distant tile so BehFindLover cannot instantly bond with harness peers.
/// </summary>
private static void TryIsolateFromPeers(Actor actor)
{
if (actor == null || actor.current_tile == null || World.world?.map_chunk_manager == null)
{
return;
}
try
{
WorldTile best = null;
float bestScore = -1f;
Vector3 origin = actor.current_position;
// Sample a coarse grid of tiles for emptiness + distance.
int w = MapBox.width;
int h = MapBox.height;
int step = Math.Max(8, Math.Min(w, h) / 16);
for (int x = 2; x < w - 2; x += step)
{
for (int y = 2; y < h - 2; y += step)
{
WorldTile tile = null;
try
{
tile = World.world.GetTile(x, y);
}
catch
{
tile = null;
}
if (tile == null)
{
continue;
}
float dist = (tile.posV3 - origin).sqrMagnitude;
if (dist < 80f * 80f)
{
continue;
}
int nearby = 0;
try
{
foreach (Actor u in Finder.getUnitsFromChunk(tile, 2))
{
if (u != null && u.isAlive() && u != actor)
{
nearby++;
if (nearby > 0)
{
break;
}
}
}
}
catch
{
nearby = 99;
}
if (nearby > 0)
{
continue;
}
float score = dist;
if (score > bestScore)
{
bestScore = score;
best = tile;
}
}
}
if (best != null)
{
actor.current_position = best.posV3;
AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) })
?.Invoke(actor, new object[] { best });
}
}
catch
{
// ignore
}
}
private static Actor SpawnHumanNear(Actor near) private static Actor SpawnHumanNear(Actor near)
{ {
try try

View file

@ -1540,9 +1540,10 @@ internal static class HarnessScenarios
Step("act17e", "activity_force", asset: "BehUnloadResources", label: "wheat@Riverhold", Step("act17e", "activity_force", asset: "BehUnloadResources", label: "wheat@Riverhold",
count: 1, tier: "beat", value: "human"), count: 1, tier: "beat", value: "human"),
Step("act17f", "activity_age", value: "12"), Step("act17f", "activity_age", value: "12"),
Step("act17g", "assert", expect: "dossier_history_not_contains", value: "Hunts"), // Needle PreyThing (not "Hunts") - species voice may omit "hunt(s)" entirely.
Step("act17g", "assert", expect: "dossier_history_not_contains", value: "PreyThing"),
Step("act17h", "assert", expect: "dossier_history_contains", value: "wheat"), Step("act17h", "assert", expect: "dossier_history_contains", value: "wheat"),
Step("act17i", "assert", expect: "activity_log_contains", value: "Hunts"), Step("act17i", "assert", expect: "activity_log_contains", value: "PreyThing"),
// Rebuild a busy ring for later variety / count asserts. // Rebuild a busy ring for later variety / count asserts.
Step("act17j", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 2, tier: "beat"), Step("act17j", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 2, tier: "beat"),
Step("act17k", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1), Step("act17k", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1),

View file

@ -1,7 +1,7 @@
{ {
"name": "IdleSpectator", "name": "IdleSpectator",
"author": "dazed", "author": "dazed",
"version": "0.25.29", "version": "0.25.35",
"description": "AFK Idle Spectator (I) + Lore (L). Catalog-wide event inject E2E suite.", "description": "AFK Idle Spectator (I) + Lore (L). Catalog-wide event inject E2E suite.",
"GUID": "com.dazed.idlespectator" "GUID": "com.dazed.idlespectator"
} }

View file

@ -38,11 +38,12 @@ REGRESSION_SCENARIOS=(
discovery discovery
world_log world_log
event_suite event_suite
event_live_pipelines
chronicle_smoke chronicle_smoke
activity_idle_smoke activity_idle_smoke
activity_status_smoke activity_status_smoke
activity_happiness_smoke activity_happiness_smoke
# Heavier live fires last - wars/meta/eras can reshape the loaded world.
event_live_pipelines
) )
scenario="" scenario=""