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.
This commit is contained in:
DazedAnon 2026-07-14 23:48:53 -05:00
parent 63bf44cae5
commit 0dfe3ab832
4 changed files with 455 additions and 39 deletions

View file

@ -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));
}
/// <summary>
/// Turn game localized labels like "Laughing" / "Job Seeking" into real sentences.
/// </summary>
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);
}

View file

@ -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();

View file

@ -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"),

View file

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