From 0dfe3ab83231790d744717d3861287cc50e04991 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Tue, 14 Jul 2026 23:48:53 -0500 Subject: [PATCH] Add detailed activity prose for various actions in ActivityProse class Expanded the ActivityProse class with new templates for actions such as laughing, singing, and playing. Enhanced the formatting logic to ensure proper sentence structure for activities, including mood and task labels. Updated AgentHarness to utilize the new prose formatting for activity sentences and adjusted test scenarios in HarnessScenarios to validate these changes. Incremented version in mod.json to reflect the update. --- IdleSpectator/ActivityProse.cs | 447 +++++++++++++++++++++++++++--- IdleSpectator/AgentHarness.cs | 38 +++ IdleSpectator/HarnessScenarios.cs | 5 +- IdleSpectator/mod.json | 4 +- 4 files changed, 455 insertions(+), 39 deletions(-) diff --git a/IdleSpectator/ActivityProse.cs b/IdleSpectator/ActivityProse.cs index 2ae3005..a45f595 100644 --- a/IdleSpectator/ActivityProse.cs +++ b/IdleSpectator/ActivityProse.cs @@ -295,6 +295,96 @@ public static class ActivityProse "{actor} gathers loot", "{actor} claims spoils", "{actor} picks through loot" + }, + ["laughing"] = new[] + { + "{actor} laughs", + "{actor} bursts into laughter", + "{actor} is laughing" + }, + ["happy_laughing"] = new[] + { + "{actor} laughs with joy", + "{actor} is laughing happily", + "{actor} chuckles aloud" + }, + ["just_laughed"] = new[] + { + "{actor} just finished laughing", + "{actor} settles after a laugh", + "{actor} catches their breath from laughing" + }, + ["singing"] = new[] + { + "{actor} sings", + "{actor} lifts a song", + "{actor} is singing" + }, + ["task_unit_play"] = new[] + { + "{actor} plays", + "{actor} is at play", + "{actor} amuses themselves" + }, + ["just_played"] = new[] + { + "{actor} just finished playing", + "{actor} wraps up playtime", + "{actor} steps away from play" + }, + ["child_play_at_one_spot"] = new[] + { + "{actor} plays in place", + "{actor} keeps busy with play", + "{actor} amuses themselves on the spot" + }, + ["child_random_flips"] = new[] + { + "{actor} flips about", + "{actor} tumbles in play", + "{actor} shows off flips" + }, + ["child_random_jump"] = new[] + { + "{actor} jumps about", + "{actor} leaps in play", + "{actor} bounces around" + }, + ["child_follow_parent"] = new[] + { + "{actor} follows a parent", + "{actor} stays close to {target}", + "{actor} trails after kin" + }, + ["random_fun_move"] = new[] + { + "{actor} frolics about", + "{actor} moves for fun", + "{actor} skips along playfully" + }, + ["godfinger_random_fun_move"] = new[] + { + "{actor} frolics under divine whim", + "{actor} is nudged into playful motion", + "{actor} moves about for fun" + }, + ["try_to_read"] = new[] + { + "{actor} tries to read", + "{actor} opens a book", + "{actor} settles in to read" + }, + ["try_to_poop"] = new[] + { + "{actor} looks for a private moment", + "{actor} tends to nature's call", + "{actor} steps aside briefly" + }, + ["try_to_launch_fireworks"] = new[] + { + "{actor} tries to launch fireworks", + "{actor} readies a firework", + "{actor} prepares a celebration blast" } }; @@ -325,13 +415,25 @@ public static class ActivityProse rich = ""; string actor = string.IsNullOrEmpty(actorName) ? "Someone" : actorName; string targetPlain = ResolveTargetPlain(targetName, targetIsActor, placeOrFallback); - string template = PickTemplate(key, subjectId, lineIndex, !string.IsNullOrEmpty(targetPlain)); + bool hasTarget = !string.IsNullOrEmpty(targetPlain); + string template = PickTemplate(key, subjectId, lineIndex, hasTarget); + if (string.IsNullOrEmpty(template)) + { + template = PatternTemplate(key, hasTarget); + } if (string.IsNullOrEmpty(template)) { - plain = BuildFallbackPlain(actor, targetPlain, rawFact, key); - rich = BuildFallbackRich(actor, targetPlain, targetIsActor, rawFact, key); - return; + template = LocalizedTemplate(rawFact, key, hasTarget); + } + + if (string.IsNullOrEmpty(template)) + { + template = "{actor} " + LowerFirst(HumanizeKey(key)); + if (string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(rawFact)) + { + template = GerundSentence(rawFact); + } } plain = Apply(template, actor, targetPlain); @@ -404,43 +506,327 @@ public static class ActivityProse return t.Trim(); } - private static string BuildFallbackPlain(string actor, string target, string rawFact, string key) + private static string PatternTemplate(string key, bool hasTarget) { - string action = !string.IsNullOrEmpty(rawFact) ? rawFact : HumanizeKey(key); - if (string.IsNullOrEmpty(action)) + if (string.IsNullOrEmpty(key)) { - return actor; + return null; } - if (!string.IsNullOrEmpty(target) && action.IndexOf(target, System.StringComparison.OrdinalIgnoreCase) < 0) + string k = key.ToLowerInvariant(); + + if (k.StartsWith("try_to_")) { - return actor + " " + LowerFirst(action) + " → " + target; + return "{actor} tries to " + HumanizeKey(k.Substring("try_to_".Length)); } - return actor + " " + LowerFirst(action); + if (k.StartsWith("check_")) + { + return "{actor} checks " + HumanizeKey(k.Substring("check_".Length)); + } + + if (k.StartsWith("find_")) + { + return "{actor} looks for " + HumanizeKey(k.Substring("find_".Length)); + } + + if (k.StartsWith("decide_")) + { + return "{actor} decides " + HumanizeKey(k.Substring("decide_".Length)); + } + + if (k.StartsWith("build_")) + { + return "{actor} builds " + HumanizeKey(k.Substring("build_".Length)); + } + + if (k.StartsWith("boat_")) + { + return "{actor} " + BoatVerb(k.Substring("boat_".Length)); + } + + if (k.StartsWith("warrior_")) + { + return WarriorTemplate(k, hasTarget); + } + + if (k.StartsWith("socialize_")) + { + return hasTarget + ? "{actor} socializes with {target}" + : "{actor} socializes"; + } + + if (k.StartsWith("sexual_reproduction") || k.StartsWith("asexual_reproduction")) + { + return hasTarget + ? "{actor} tries to start a family with {target}" + : "{actor} tries to reproduce"; + } + + if (k.StartsWith("child_")) + { + return "{actor} " + LowerFirst(HumanizeKey(k.Substring("child_".Length))); + } + + if (k.StartsWith("ant_") || k.StartsWith("bee_") || k.StartsWith("ufo_") + || k.StartsWith("dragon_") || k.StartsWith("worm_")) + { + return "{actor} " + LowerFirst(HumanizeKey(StripCreaturePrefix(k))); + } + + if (k.StartsWith("task_unit_")) + { + return LocalizedStyleFromId(k.Substring("task_unit_".Length), hasTarget); + } + + if (k.Contains("laugh")) + { + return "{actor} laughs"; + } + + if (k.Contains("sing")) + { + return "{actor} sings"; + } + + if (k.Contains("play") && !k.Contains("display")) + { + return hasTarget ? "{actor} plays with {target}" : "{actor} plays"; + } + + if (k.Contains("wait")) + { + return "{actor} waits"; + } + + if (k.Contains("sleep")) + { + return "{actor} sleeps"; + } + + if (k.Contains("idle") || k.Contains("random_move") || k.EndsWith("_move") + || k.Contains("walking")) + { + return "{actor} wanders"; + } + + if (k.Contains("fight") || k.Contains("attack") || k.Contains("hunt")) + { + return hasTarget ? "{actor} strikes at {target}" : "{actor} fights"; + } + + if (k.Contains("heal") || k.Contains("cure")) + { + return hasTarget ? "{actor} tends to {target}" : "{actor} tends to wounds"; + } + + if (k.Contains("eat") || k.Contains("food")) + { + return "{actor} seeks a meal"; + } + + if (k.Contains("farm") || k.Contains("harvest") || k.Contains("forag") + || k.Contains("gather") || k.Contains("chop") || k.Contains("mine")) + { + return "{actor} " + LowerFirst(HumanizeKey(k)); + } + + return null; } - private static string BuildFallbackRich( - string actor, - string target, - bool targetIsActor, - string rawFact, - string key) + private static string WarriorTemplate(string k, bool hasTarget) { - string action = !string.IsNullOrEmpty(rawFact) ? rawFact : HumanizeKey(key); - string a = ColorName(actor); - if (string.IsNullOrEmpty(action)) + if (k.Contains("follow")) { - return a; + return hasTarget ? "{actor} follows {target}" : "{actor} follows the army"; } - if (!string.IsNullOrEmpty(target) && action.IndexOf(target, System.StringComparison.OrdinalIgnoreCase) < 0) + if (k.Contains("train")) { - string t = targetIsActor ? ColorName(target) : target; - return a + " " + LowerFirst(action) + " → " + t; + return "{actor} trains for war"; } - return a + " " + LowerFirst(action); + if (k.Contains("join")) + { + return "{actor} tries to join the army"; + } + + if (k.Contains("attack")) + { + return hasTarget ? "{actor} marches on {target}" : "{actor} marches to attack"; + } + + if (k.Contains("wait") || k.Contains("idle")) + { + return "{actor} holds formation"; + } + + return "{actor} " + LowerFirst(HumanizeKey(k.Replace("warrior_", ""))); + } + + private static string BoatVerb(string rest) + { + if (rest.Contains("fish")) + { + return "fishes from the boat"; + } + + if (rest.Contains("trade")) + { + return "trades by boat"; + } + + if (rest.Contains("dock") || rest.Contains("return")) + { + return "returns to dock"; + } + + if (rest.Contains("load")) + { + return "loads the boat"; + } + + if (rest.Contains("unload")) + { + return "unloads the boat"; + } + + if (rest.Contains("idle")) + { + return "idles aboard"; + } + + return LowerFirst(HumanizeKey(rest)) + " aboard"; + } + + private static string StripCreaturePrefix(string k) + { + int idx = k.IndexOf('_'); + return idx > 0 && idx < k.Length - 1 ? k.Substring(idx + 1) : k; + } + + private static string LocalizedStyleFromId(string rest, bool hasTarget) + { + if (rest == "play") + { + return "{actor} plays"; + } + + if (rest == "wait") + { + return "{actor} waits"; + } + + if (rest == "walk") + { + return "{actor} walks"; + } + + if (rest.Contains("social")) + { + return hasTarget ? "{actor} socializes with {target}" : "{actor} socializes"; + } + + return "{actor} " + LowerFirst(HumanizeKey(rest)); + } + + /// + /// Turn game localized labels like "Laughing" / "Job Seeking" into real sentences. + /// + private static string LocalizedTemplate(string rawFact, string key, bool hasTarget) + { + string loc = ExtractLocalizedBody(rawFact, key); + if (string.IsNullOrEmpty(loc)) + { + return null; + } + + return GerundSentence(loc); + } + + private static string ExtractLocalizedBody(string rawFact, string key) + { + if (string.IsNullOrEmpty(rawFact)) + { + return null; + } + + string body = rawFact; + int arrow = body.IndexOf('→'); + if (arrow < 0) + { + arrow = body.IndexOf("->", System.StringComparison.Ordinal); + } + + if (arrow >= 0) + { + body = body.Substring(0, arrow).Trim(); + } + + if (body.StartsWith("Task:", System.StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Ignore if it's just the raw key. + if (!string.IsNullOrEmpty(key) + && body.Equals(key, System.StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return body.Trim(); + } + + private static string GerundSentence(string loc) + { + if (string.IsNullOrEmpty(loc)) + { + return null; + } + + string body = loc.Trim(); + // "Laughing" / "Job Seeking" / "Holding Still" + if (IsGerundPhrase(body)) + { + return "{actor} is " + LowerFirst(body); + } + + // Already a short verb phrase + return "{actor} " + LowerFirst(body); + } + + private static bool IsGerundPhrase(string body) + { + if (string.IsNullOrEmpty(body)) + { + return false; + } + + string[] parts = body.Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0 || parts.Length > 4) + { + return false; + } + + // Last token ends with -ing (Laughing, Seeking, Holding...) + string last = parts[parts.Length - 1]; + return last.Length > 4 + && last.EndsWith("ing", System.StringComparison.OrdinalIgnoreCase); + } + + private static string HumanizeKey(string key) + { + if (string.IsNullOrEmpty(key)) + { + return ""; + } + + string s = key.Replace("Beh", "").Replace('_', ' ').Trim(); + // Collapse common noise words for readability. + s = s.Replace("task unit ", "").Replace("type ", ""); + return s; } private static string LowerFirst(string s) @@ -458,16 +844,6 @@ public static class ActivityProse return char.ToLowerInvariant(s[0]) + s.Substring(1); } - private static string HumanizeKey(string key) - { - if (string.IsNullOrEmpty(key)) - { - return ""; - } - - return key.Replace('_', ' ').Replace("Beh", "").Trim(); - } - private static string PickTemplate(string key, long subjectId, int lineIndex, bool hasTarget) { if (string.IsNullOrEmpty(key)) @@ -528,7 +904,6 @@ public static class ActivityProse if (suited.Count == 0) { - // Fall back to any template; Apply() will scrub missing targets. suited.AddRange(bag); } diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index d6c0132..fc54961 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -1918,6 +1918,44 @@ public static class AgentHarness detail = $"hit={hit} needle='{needle}' sample='{sample}' count={entries.Count}"; break; } + case "activity_prose_sentence": + { + // Ensure mood/task labels become sentences, not "{name} laughing". + long id = WatchCaption.CurrentUnitId; + string actorName = ""; + try + { + if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) + { + actorName = MoveCamera._focus_unit.getName() ?? ""; + } + } + catch + { + actorName = ""; + } + + ActivityProse.Format( + ActivityKind.TaskStart, + "laughing", + actorName, + "", + false, + "", + "Laughing", + id, + 99, + out string plain, + out _); + bool ok = !string.IsNullOrEmpty(plain) + && plain.IndexOf("laugh", System.StringComparison.OrdinalIgnoreCase) >= 0 + && !plain.Equals( + (string.IsNullOrEmpty(actorName) ? "Someone" : actorName) + " laughing", + System.StringComparison.Ordinal); + pass = ok; + detail = $"plain='{plain}' actor='{actorName}'"; + break; + } case "lore_detail_pane": { string want = (cmd.value ?? "life").Trim().ToLowerInvariant(); diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index de2d174..dd9c7b6 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -452,7 +452,10 @@ internal static class HarnessScenarios Step("act11", "wait", wait: 0.35f), Step("act12", "activity_probe"), Step("act13", "assert", expect: "activity_first_scoring"), - Step("act14", "activity_force", asset: "eating", label: "Taking a meal", count: 1), + Step("act13b", "assert", expect: "activity_prose_sentence"), + Step("act14", "activity_force", asset: "laughing", label: "Laughing", count: 1), + Step("act14b", "assert", expect: "activity_log_contains", value: "laugh"), + Step("act14c", "activity_force", asset: "eating", label: "Taking a meal", count: 1), Step("act15", "activity_force", asset: "farm", label: "Tends the fields", count: 1), Step("act16", "activity_force", asset: "hunt", label: "Hunts", count: 1, expect: "Barkley"), Step("act17", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 3, tier: "beat"), diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 3d8df80..bc244f8 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.12.29", - "description": "AFK Idle Spectator (I) + Lore (L). Detailed activity prose, Activity/Life tabs.", + "version": "0.12.30", + "description": "AFK Idle Spectator (I) + Lore (L). Full activity prose coverage for actor tasks.", "GUID": "com.dazed.idlespectator" }