using System.Collections.Generic; namespace IdleSpectator; /// /// Fact-preserving activity sentences with colored actor/target names. /// public static class ActivityProse { public const string NameColorHex = "#F0C14A"; /// /// Long detailed templates. Tokens: {actor}{target}{species}{place}{place_at}{job}{job_as}{carrying}{with}. /// Never invent outcomes - only flavor observed tasks. /// Prefer for full alive composition; Variants seed action phrases. /// private static readonly Dictionary Variants = new Dictionary { ["attack"] = new[] { "{actor} strikes hard at {target}{place_at}", "{actor} presses the attack on {target}{place_at}", "{actor} lunges toward {target} with clear intent{place_at}" }, ["fight"] = new[] { "{actor} fights {target} in a fierce clash{place_at}", "{actor} clashes with {target}{place_at}{job_as}", "{actor} trades blows with {target}{place_at}" }, ["fighting"] = new[] { "{actor} is locked in combat with {target}{place_at}", "{actor} fights {target} without giving ground{place_at}", "{actor} clashes with {target}{place_at}" }, ["hunt"] = new[] { "{actor} pursues {target}{place_at}", "{actor} stalks {target} from cover{place_at}", "{actor} closes in on {target}{place_at}" }, ["BehAttackActorHuntingTarget"] = new[] { "{actor} hunts {target} and strikes when the moment opens{place_at}", "{actor} closes on {target} in the chase{place_at}", "{actor} strikes at {target} while hunting{place_at}" }, ["heal"] = new[] { "{actor} tends carefully to {target}{place_at}", "{actor} works to heal {target}{place_at}", "{actor} binds {target}'s wounds with steady hands{place_at}" }, ["check_heal"] = new[] { "{actor} checks over wounds with worried care{place_at}", "{actor} looks closely at injuries{place_at}", "{actor} takes stock of pain before moving on{place_at}" }, ["cure"] = new[] { "{actor} seeks a cure for what ails them{place_at}", "{actor} works to recover from sickness{place_at}", "{actor} fights sickness with stubborn will{place_at}" }, ["eat"] = new[] { "{actor} sits down to eat{place_at}{job_as}", "{actor} finds something to eat{place_at}", "{actor} finishes a meal{place_at}" }, ["eating"] = new[] { "{actor} sits down to eat{place_at}{job_as}", "{actor} finds something to eat{place_at}", "{actor} finishes a meal{place_at}" }, ["sleep"] = new[] { "{actor} settles into sleep and lets the world go quiet{place_at}", "{actor} rests deeply after the day's demands{place_at}{job_as}", "{actor} curls into sleep{place_at}" }, ["decide_where_to_sleep"] = new[] { "{actor} searches for a safe place to sleep{place_at}", "{actor} weighs where to rest for the night{place_at}", "{actor} seeks shelter before settling down{place_at}" }, ["random_move"] = new[] { "{actor} wanders without hurry{place_at}", "{actor} roams nearby, looking around{place_at}", "{actor} drifts along with nowhere urgent to be{place_at}" }, ["city_idle_walking"] = new[] { "{actor} walks the streets at an easy pace{place_at}{job_as}", "{actor} strolls through the settlement, watching life pass{place_at}", "{actor} paces familiar paths between errands{place_at}" }, ["move"] = new[] { "{actor} is on the move with purpose{place_at}{job_as}", "{actor} travels onward through the world{place_at}", "{actor} heads out toward the next mark{place_at}" }, ["pollinate"] = new[] { "{actor} dusts the blossom with careful work{place_at}", "{actor} works the flower and leaves the air sweet{place_at}", "{actor} leaves pollen behind on the bloom{place_at}" }, ["BehPollinate"] = new[] { "{actor} dusts the blossom with careful work{place_at}", "{actor} works the flower and leaves the air sweet{place_at}", "{actor} leaves pollen behind on the bloom{place_at}" }, ["farm"] = new[] { "{actor} tends the fields with patient labor{place_at}{job_as}", "{actor} works the soil until it answers{place_at}", "{actor} farms the plot through the day{place_at}" }, ["harvest"] = new[] { "{actor} harvests the ripe crop with practiced hands{place_at}{job_as}", "{actor} gathers the yield before it spoils{place_at}", "{actor} cuts the ripe fields clean{place_at}" }, ["forag"] = new[] { "{actor} forages through wild growth for food{place_at}", "{actor} gathers what the land offers freely{place_at}", "{actor} searches the brush for forage{place_at}" }, ["gather"] = new[] { "{actor} gathers stores for leaner days{place_at}{job_as}", "{actor} collects goods with steady purpose{place_at}", "{actor} forages supplies into the pack{place_at}" }, ["mine"] = new[] { "{actor} delves the mine for ore and stone{place_at}{job_as}", "{actor} chips at the rock face with hard labor{place_at}", "{actor} works the dig until the haul is ready{place_at}" }, ["build"] = new[] { "{actor} builds with focused craft{place_at}{job_as}", "{actor} raises walls and timber into shape{place_at}", "{actor} labors on construction until it stands{place_at}" }, ["woodcut"] = new[] { "{actor} fells timber for the settlement's need{place_at}{job_as}", "{actor} takes an axe to the trees with measured swings{place_at}", "{actor} cuts wood and stacks the haul{place_at}" }, ["fire"] = new[] { "{actor} races the blaze and fights to keep it down{place_at}", "{actor} douses flames before they claim more ground{place_at}", "{actor} battles the fire with urgent work{place_at}" }, ["BehCityActorRemoveFire"] = new[] { "{actor} races the blaze and fights to keep it down{place_at}", "{actor} douses flames before they claim more ground{place_at}", "{actor} beats back the fire with urgent work{place_at}" }, ["social"] = new[] { "{actor} talks at length{with}{place_at}{job_as}", "{actor} socializes and shares the moment{with}{place_at}", "{actor} converses warmly{with}{place_at}" }, ["socialize"] = new[] { "{actor} talks at length{with}{place_at}{job_as}", "{actor} socializes and shares the moment{with}{place_at}", "{actor} joins company nearby{with}{place_at}" }, ["lover"] = new[] { "{actor} seeks out {target} with unmistakable longing{place_at}", "{actor} is drawn toward {target} through the day{place_at}", "{actor} pursues {target}'s affection with quiet resolve{place_at}" }, ["breed"] = new[] { "{actor} tries to start a family with {target}{place_at}", "{actor} courts {target} with clear intent{place_at}", "{actor} seeks offspring with {target}{place_at}" }, ["sexual_reproduction"] = new[] { "{actor} tries to start a family with {target}{place_at}", "{actor} courts {target} with clear intent{place_at}", "{actor} seeks offspring with {target}{place_at}" }, ["unload"] = new[] { "{actor} unloads {carrying} into store{place_at}", "{actor} delivers {carrying} after a long haul{place_at}", "{actor} empties the pack of {carrying}{place_at}" }, ["BehUnloadResources"] = new[] { "{actor} unloads {carrying} at {place}", "{actor} delivers {carrying} into store{place_at}", "{actor} empties the pack of {carrying}{place_at}" }, ["job"] = new[] { "{actor} looks for honest work{place_at}", "{actor} looks for a job that will keep them fed{place_at}", "{actor} searches for employment through the settlement{place_at}" }, ["find_city_job"] = new[] { "{actor} seeks work in town{place_at}", "{actor} checks the available city jobs{place_at}", "{actor} searches the settlement for employment{place_at}" }, ["find_house"] = new[] { "{actor} looks for a house to call home{place_at}", "{actor} seeks shelter and a lasting roof{place_at}", "{actor} searches for a place to settle{place_at}" }, ["claim"] = new[] { "{actor} claims new ground for the settlement{place_at}", "{actor} stakes a claim on open land{place_at}", "{actor} takes claim of border ground{place_at}" }, ["warrior"] = new[] { "{actor} follows the army with disciplined steps{place_at}", "{actor} marches with the warband{place_at}{job_as}", "{actor} keeps formation and waits for orders{place_at}" }, ["army"] = new[] { "{actor} follows the army with disciplined steps{place_at}", "{actor} marches with the warband{place_at}", "{actor} keeps formation among the ranks{place_at}" }, ["wait"] = new[] { "{actor} waits in watchful quiet{place_at}{job_as}", "{actor} holds still and lets the moment pass{place_at}", "{actor} bides time without wasting motion{place_at}" }, ["make_decision"] = new[] { "{actor} considers next steps with care{place_at}", "{actor} weighs a choice before moving{place_at}{job_as}", "{actor} pauses in thought while the world waits{place_at}" }, ["reflection"] = new[] { "{actor} reflects on what the day has asked{place_at}", "{actor} takes a quiet moment to think things over{place_at}", "{actor} settles into reflection away from noise{place_at}" }, ["run_away"] = new[] { "{actor} flees hard for safer ground{place_at}", "{actor} runs for safety without looking back{place_at}", "{actor} retreats in a burst of fear{place_at}" }, ["trade"] = new[] { "{actor} keeps a careful eye on traded goods{place_at}{job_as}", "{actor} barters over prices{place_at}", "{actor} makes a trading run{place_at}" }, ["fish"] = new[] { "{actor} waits patiently for a bite{place_at}{job_as}", "{actor} casts for fish where the current runs{place_at}", "{actor} works the waters for a catch{place_at}" }, ["family_group"] = new[] { "{actor} joins the herd and stays close to kin{place_at}", "{actor} seeks the family group across open ground{place_at}", "{actor} gathers with kin for safety and company{place_at}" }, ["generate_loot"] = new[] { "{actor} gathers loot from what the fight left behind{place_at}", "{actor} claims spoils with practical hands{place_at}", "{actor} picks through loot before moving on{place_at}" }, ["laughing"] = new[] { "{actor} laughs aloud and lets the mood carry{place_at}", "{actor} bursts into laughter without restraint{place_at}", "{actor} keeps laughing until short of breath{place_at}" }, ["happy_laughing"] = new[] { "{actor} laughs with bright, unguarded joy{place_at}", "{actor} laughs happily among nearby company{place_at}", "{actor} chuckles aloud until the mood softens{place_at}" }, ["just_laughed"] = new[] { "{actor} just finished laughing and catches a breath{place_at}", "{actor} settles after a laugh, still smiling{place_at}", "{actor} catches their breath from laughing{place_at}" }, ["singing"] = new[] { "{actor} sings out for whoever will hear{place_at}{job_as}", "{actor} lifts a song into the open air{place_at}", "{actor} is singing through the quiet stretch{place_at}" }, ["task_unit_play"] = new[] { "{actor} finds a way to amuse itself{place_at}", "{actor} turns idle time into play{place_at}", "{actor} plays with restless enthusiasm{place_at}" }, ["just_played"] = new[] { "{actor} just finished playing and settles again{place_at}", "{actor} wraps up playtime and looks outward{place_at}", "{actor} steps away from play with leftover cheer{place_at}" }, ["child_play_at_one_spot"] = new[] { "{actor} plays in place with busy hands and feet{place_at}", "{actor} keeps busy with play without wandering far{place_at}", "{actor} amuses themselves right where they stand{place_at}" }, ["child_random_flips"] = new[] { "{actor} flips about in a burst of play{place_at}", "{actor} tumbles through playful motion{place_at}", "{actor} shows off flips for whoever watches{place_at}" }, ["child_random_jump"] = new[] { "{actor} jumps about with bright energy{place_at}", "{actor} leaps in play across the open ground{place_at}", "{actor} bounces around as if the day were light{place_at}" }, ["child_follow_parent"] = new[] { "{actor} follows a parent with careful steps{place_at}", "{actor} stays close to {target} through the day{place_at}", "{actor} trails after kin and will not stray far{place_at}" }, ["random_fun_move"] = new[] { "{actor} frolics about with no errand but joy{place_at}", "{actor} moves for fun through the open moment{place_at}", "{actor} skips along playfully without hurry{place_at}" }, ["godfinger_random_fun_move"] = new[] { "{actor} frolics under a strange divine whim{place_at}", "{actor} is nudged into playful motion by unseen will{place_at}", "{actor} moves about for fun as if guided lightly{place_at}" }, ["try_to_read"] = new[] { "{actor} tries to read and settle into the page{place_at}", "{actor} opens a book and gives it quiet attention{place_at}", "{actor} settles in to read while the world softens{place_at}" }, ["try_to_poop"] = new[] { "{actor} looks for a private moment away from eyes{place_at}", "{actor} tends to nature's call with hurried dignity{place_at}", "{actor} steps aside briefly for a private need{place_at}" }, ["try_to_launch_fireworks"] = new[] { "{actor} tries to launch fireworks into celebration{place_at}", "{actor} readies a firework with careful hands{place_at}", "{actor} prepares a celebration blast for the sky{place_at}" } }; public static string ColorName(string name) { if (string.IsNullOrEmpty(name)) { return ""; } return "" + name + ""; } public static void Format( ActivityKind kind, string key, string actorName, string targetName, bool targetIsActor, string placeOrFallback, string rawFact, long subjectId, int lineIndex, out string plain, out string rich) { ActivityContext ctx = ActivityContext.ForHarness( actorName, targetIsActor ? targetName : "", place: placeOrFallback); if (!targetIsActor && !string.IsNullOrEmpty(targetName) && string.IsNullOrEmpty(ctx.PlaceLabel)) { ctx.PlaceLabel = targetName; } if (targetIsActor) { ctx.TargetName = targetName ?? ""; ctx.TargetIsActor = !string.IsNullOrEmpty(targetName); } Format(kind, key, ctx, rawFact, subjectId, lineIndex, out plain, out rich); } public static void Format( ActivityKind kind, string key, ActivityContext ctx, string rawFact, long subjectId, int lineIndex, out string plain, out string rich) { plain = ""; rich = ""; if (ctx == null) { ctx = ActivityContext.Empty; } EnsureDerivedFields(ctx); EnsureChildFromKey(ctx, key); bool hasTarget = ctx.TargetIsActor && !string.IsNullOrEmpty(ctx.TargetName); // Trait salts variety only (dossier shows trait chips) - never names the trait in prose. long varietyId = subjectId ^ ActivityClauseAssembler.TraitVarietySaltPublic(ctx); string template = ActivityClauseAssembler.Assemble( ctx, key, rawFact, hasTarget, varietyId, lineIndex); if (string.IsNullOrEmpty(template)) { template = "{actor} attends to " + LowerFirst(HumanizeKey(key)) + "{place_at}{job_as}"; if (string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(rawFact)) { template = GerundSentence(rawFact); } } plain = Apply(template, ctx, colored: false); rich = Apply(template, ctx, colored: true); _ = kind; } private static void EnsureDerivedFields(ActivityContext ctx) { if (ctx.Voice == null || !ctx.Voice.LiveSpeciesId.Equals(ctx.SpeciesId ?? "", System.StringComparison.Ordinal)) { ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(ctx.SpeciesId); if (asset != null) { ActivityVoiceResolver.ApplyToContext(ctx, asset); } else { ctx.Voice = ActivityVoiceResolver.Resolve(ctx.SpeciesId); ctx.BaseSpeciesId = ctx.Voice.BaseSpeciesId; ctx.Family = ctx.Voice.BaseFamily; ctx.MannerTag = ActivityVoiceResolver.TagName(ctx.Voice.EffectiveActionTag); ctx.ModifierTag = ActivityVoiceResolver.ModifierName(ctx.Voice.Modifier); } } if (string.IsNullOrEmpty(ctx.RoleLabel)) { ctx.RoleLabel = ActivityInterestTable.DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior); } if (string.IsNullOrEmpty(ctx.TopTraitLabel) && !string.IsNullOrEmpty(ctx.TopTraitId)) { ctx.TopTraitLabel = ActivityInterestTable.TraitLabelFromId(ctx.TopTraitId); } } /// Mark child life-stage from task key when context did not already capture it. private static void EnsureChildFromKey(ActivityContext ctx, string key) { if (ctx == null || ctx.IsChild || string.IsNullOrEmpty(key)) { return; } if (key.StartsWith("child_", System.StringComparison.OrdinalIgnoreCase)) { ctx.IsChild = true; } } /// Action-only phrase from pattern heuristics (no subject). public static string PatternActionPhrase(string key, bool hasTarget) { string full = PatternTemplate(key, hasTarget); if (string.IsNullOrEmpty(full)) { return ""; } return ActivityClauseAssembler.StripSubject(full); } public static string HumanizeKeyPublic(string key) { return HumanizeKey(key); } public static string HumanizeJobPublic(string jobId) { return HumanizeJob(jobId); } /// Observed localized fact as an action-only phrase for unknown task ids. public static string FactualActionPhrase(string rawFact, string key, bool hasTarget) { return ActivityClauseAssembler.StripSubject(LocalizedTemplate(rawFact, key, hasTarget)); } /// Legacy helper used by older call sites / harness. public static string Format( ActivityKind kind, string key, string rawFact, string target, long subjectId, int lineIndex) { Format( kind, key, ActivityContext.ForHarness("", target, place: target), rawFact, subjectId, lineIndex, out string plain, out _); return plain; } private static string Apply(string template, ActivityContext ctx, bool colored) { if (string.IsNullOrEmpty(template)) { return ""; } string actorRaw = string.IsNullOrEmpty(ctx.ActorName) ? "Someone" : ctx.ActorName; string actor = colored ? ColorName(actorRaw) : actorRaw; string targetRaw = ctx.TargetName ?? ""; string target = ""; if (!string.IsNullOrEmpty(targetRaw)) { target = colored && ctx.TargetIsActor ? ColorName(targetRaw) : targetRaw; } string species = !string.IsNullOrEmpty(ctx.SpeciesLabel) ? ctx.SpeciesLabel : (string.IsNullOrEmpty(ctx.SpeciesId) ? "creature" : ctx.SpeciesId.Replace('_', ' ')); string place = ctx.PlaceLabel ?? ""; if (ActivityInterestTable.LooksLikeSpeciesPublic(place, ctx.SpeciesId)) { place = ""; } string job = HumanizeJob(ctx.JobId); string carrying = string.IsNullOrEmpty(ctx.CarryingLabel) ? "goods" : ctx.CarryingLabel; string withClause = string.IsNullOrEmpty(target) ? "" : (" with " + target); string placeAt = string.IsNullOrEmpty(place) ? "" : (" in " + place); string jobAs = string.IsNullOrEmpty(job) ? "" : (" as " + job); string t = template; t = t.Replace("{actor}", actor); t = t.Replace("{species}", species); t = t.Replace("{with}", withClause); t = t.Replace("{place_at}", placeAt); t = t.Replace("{job_as}", jobAs); t = t.Replace("{carrying}", carrying); t = t.Replace("{place}", string.IsNullOrEmpty(place) ? "town" : place); t = t.Replace("{job}", string.IsNullOrEmpty(job) ? "their work" : job); if (string.IsNullOrEmpty(target)) { t = t.Replace(" with {target}", "") .Replace(" on {target}", "") .Replace(" toward {target}", "") .Replace(" after {target}", "") .Replace(" at {target}", "") .Replace(" {target}", "") .Replace("{target}", "someone"); } else { t = t.Replace("{target}", target); } // Collapse leftover double spaces from empty optional clauses. while (t.IndexOf(" ", System.StringComparison.Ordinal) >= 0) { t = t.Replace(" ", " "); } return t.Trim(); } private static string HumanizeJob(string jobId) { if (string.IsNullOrEmpty(jobId)) { return ""; } string s = jobId.Replace('_', ' ').Trim(); if (s.StartsWith("job ", System.StringComparison.OrdinalIgnoreCase)) { s = s.Substring(4).Trim(); } return LowerFirst(s); } private static string PatternTemplate(string key, bool hasTarget) { if (string.IsNullOrEmpty(key)) { return null; } string k = key.ToLowerInvariant(); const string clauses = "{place_at}{job_as}"; if (k.StartsWith("try_to_")) { return "{actor} tries to " + HumanizeKey(k.Substring("try_to_".Length)) + " with careful effort" + clauses; } if (k.StartsWith("check_")) { return "{actor} checks " + HumanizeKey(k.Substring("check_".Length)) + " before deciding what comes next" + clauses; } if (k.StartsWith("find_")) { return "{actor} looks for " + HumanizeKey(k.Substring("find_".Length)) + clauses; } if (k.StartsWith("decide_")) { return "{actor} decides " + HumanizeKey(k.Substring("decide_".Length)) + " after a moment of thought" + clauses; } if (k.StartsWith("build_")) { return "{actor} builds " + HumanizeKey(k.Substring("build_".Length)) + " with steady hands" + clauses; } if (k.StartsWith("boat_")) { return "{actor} " + BoatVerb(k.Substring("boat_".Length)) + clauses; } if (k.StartsWith("warrior_")) { return WarriorTemplate(k, hasTarget) + clauses; } if (k.StartsWith("socialize_")) { return hasTarget ? "{actor} spends time with {target}" + clauses : "{actor} looks for company" + clauses; } if (k.StartsWith("sexual_reproduction") || k.StartsWith("asexual_reproduction")) { return hasTarget ? "{actor} tries to start a family with {target}" + clauses : "{actor} tries to reproduce and carry life forward" + clauses; } if (k.StartsWith("child_")) { return "{actor} " + LowerFirst(HumanizeKey(k.Substring("child_".Length))) + " eagerly" + clauses; } if (k.StartsWith("ant_") || k.StartsWith("bee_") || k.StartsWith("ufo_") || k.StartsWith("dragon_") || k.StartsWith("worm_")) { return "{actor} " + LowerFirst(HumanizeKey(StripCreaturePrefix(k))) + clauses; } if (k.StartsWith("task_unit_")) { return LocalizedStyleFromId(k.Substring("task_unit_".Length), hasTarget); } if (k.Contains("laugh")) { return "{actor} laughs aloud and lets the mood carry" + clauses; } if (k.Contains("sing")) { return "{actor} sings out for whoever will hear" + clauses; } if (k.Contains("play") && !k.Contains("display")) { return hasTarget ? "{actor} plays with {target}" + clauses : "{actor} finds a way to amuse itself" + clauses; } if (k.Contains("wait")) { return "{actor} waits in watchful quiet" + clauses; } if (k.Contains("sleep")) { return "{actor} settles into sleep and lets the world go quiet" + clauses; } if (k.Contains("idle") || k.Contains("random_move") || k.EndsWith("_move") || k.Contains("walking")) { return "{actor} wanders without hurry" + clauses; } if (k.Contains("fight") || k.Contains("attack") || k.Contains("hunt")) { return hasTarget ? "{actor} strikes at {target} with clear intent" + clauses : "{actor} braces for a fight" + clauses; } if (k.Contains("heal") || k.Contains("cure")) { return hasTarget ? "{actor} tends carefully to {target}" + clauses : "{actor} tends to wounds with worried care" + clauses; } if (k.Contains("eat") || k.Contains("food")) { return "{actor} sits down to eat" + clauses; } if (k.Contains("farm") || k.Contains("harvest") || k.Contains("forag") || k.Contains("gather") || k.Contains("chop") || k.Contains("mine") || k.Contains("build") || k.Contains("woodcut")) { return "{actor} " + LowerFirst(HumanizeKey(k)) + " with steady labor" + clauses; } if (k.Contains("trade") || k.Contains("fish") || k.Contains("unload") || k.Contains("claim") || k.Contains("fire")) { return "{actor} " + LowerFirst(HumanizeKey(k)) + clauses; } return null; } private static string WarriorTemplate(string k, bool hasTarget) { if (k.Contains("follow")) { return hasTarget ? "{actor} follows {target} with disciplined steps" : "{actor} follows the army with disciplined steps"; } if (k.Contains("train")) { return "{actor} trains for war with hard repetition"; } if (k.Contains("join")) { return "{actor} tries to join the army and take a place in the ranks"; } if (k.Contains("attack")) { return hasTarget ? "{actor} marches on {target} with the warband" : "{actor} marches to attack with the warband"; } if (k.Contains("wait") || k.Contains("idle")) { return "{actor} holds formation and waits for orders"; } return "{actor} " + LowerFirst(HumanizeKey(k.Replace("warrior_", ""))) + " among the ranks"; } private static string BoatVerb(string rest) { if (rest.Contains("fish")) { return "fishes from the boat and waits for a bite"; } if (rest.Contains("trade")) { return "carries trade goods by boat"; } if (rest.Contains("dock") || rest.Contains("return")) { return "returns to dock after time on the water"; } if (rest.Contains("load")) { return "loads the boat for the next run"; } if (rest.Contains("unload")) { return "unloads the boat and clears the hold"; } if (rest.Contains("idle")) { return "waits aboard between trips"; } 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) { const string clauses = "{place_at}{job_as}"; if (rest == "play") { return "{actor} finds a way to amuse itself" + clauses; } if (rest == "wait") { return "{actor} waits in watchful quiet" + clauses; } if (rest == "walk") { return "{actor} walks at an easy pace" + clauses; } if (rest.Contains("social")) { return hasTarget ? "{actor} spends time with {target}" + clauses : "{actor} looks for company" + clauses; } return "{actor} " + LowerFirst(HumanizeKey(rest)) + clauses; } /// /// 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) + "{place_at}{job_as}"; } // Already a short verb phrase - still give it place/job room. return "{actor} " + LowerFirst(body) + "{place_at}{job_as}"; } 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) { if (string.IsNullOrEmpty(s)) { return s; } if (s.Length == 1) { return s.ToLowerInvariant(); } return char.ToLowerInvariant(s[0]) + s.Substring(1); } private static string PickTemplate(string key, long subjectId, int lineIndex, bool hasTarget) { if (string.IsNullOrEmpty(key)) { return null; } string[] bag = null; if (Variants.TryGetValue(key, out bag) && bag != null && bag.Length > 0) { return PreferTargetAware(bag, hasTarget, subjectId, key, lineIndex); } string lower = key.ToLowerInvariant(); string bestKey = null; string[] bestBag = null; int bestLen = -1; foreach (KeyValuePair kv in Variants) { if (kv.Value == null || kv.Value.Length == 0) { continue; } if (lower.IndexOf(kv.Key, System.StringComparison.Ordinal) >= 0 && kv.Key.Length > bestLen) { bestLen = kv.Key.Length; bestKey = kv.Key; bestBag = kv.Value; } } if (bestBag == null) { return null; } return PreferTargetAware(bestBag, hasTarget, subjectId, bestKey, lineIndex); } private static string PreferTargetAware( string[] bag, bool hasTarget, long subjectId, string key, int lineIndex) { var suited = new List(bag.Length); for (int i = 0; i < bag.Length; i++) { string line = bag[i]; bool needsTarget = line.IndexOf("{target}", System.StringComparison.Ordinal) >= 0; if (hasTarget || !needsTarget) { suited.Add(line); } } if (suited.Count == 0) { suited.AddRange(bag); } return suited[PickIndex(subjectId, key, lineIndex, suited.Count)]; } private static int PickIndex(long subjectId, string key, int lineIndex, int count) { if (count <= 1) { return 0; } unchecked { int h = (int)(subjectId * 397) ^ (key?.GetHashCode() ?? 0) ^ (lineIndex * 31); h ^= (int)(UnityEngine.Time.unscaledTime * 10f); if (h < 0) { h = -h; } return h % count; } } }