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" }