using System; namespace IdleSpectator; /// Single deterministic action-selection pipeline. public static class ActivityActionComposer { public static string Compose( ActivityContext ctx, string key, string rawFact, bool hasTarget, long subjectId, int lineIndex) { ctx ??= ActivityContext.Empty; ProseVoice voice = EnsureVoice(ctx); string verb = ActivityVerbMap.Resolve(key); if (string.IsNullOrEmpty(verb)) { // Future unknown tasks still get the mob's authored work voice. // The exhaustive runtime audit blocks every current task from reaching this branch. verb = "work"; } string[] bag = GetBag(voice, verb, hasTarget, ctx.Terrain); string phrase = Pick(bag, subjectId, lineIndex); phrase = ActivityModifierVoiceOverlay.Apply(phrase, voice, verb, subjectId, lineIndex); return EnsureUnloadTokens(key, phrase); } private static string[] GetBag( ProseVoice voice, string verb, bool hasTarget, ActivityTerrain terrain) { voice ??= ProseVoice.Unknown(); string v = string.IsNullOrEmpty(verb) ? "move" : verb; if (voice.AssetKind == ActivityAssetKind.Vehicle) { return VehicleBag(v); } string[] authored = ActivitySpeciesVoiceCatalog.GetBag(voice.BaseSpeciesId, v, terrain); if (authored != null && authored.Length > 0) { return authored; } // Last resort for unknown future assets - not the normal prose path. return TinyFallback(v, hasTarget); } /// /// Haul-to-store keeps place and names cargo once (no trailing while-carrying needed). /// private static string EnsureUnloadTokens(string key, string phrase) { if (string.IsNullOrEmpty(phrase) || string.IsNullOrEmpty(key)) { return phrase ?? ""; } if (!key.Equals("BehUnloadResources", StringComparison.OrdinalIgnoreCase) && !key.Equals("BehThrowResources", StringComparison.OrdinalIgnoreCase)) { return phrase; } string result = phrase; if (result.IndexOf("{carrying}", StringComparison.Ordinal) < 0) { result = "unloads {carrying}, " + LowerFirst(result); } if (result.IndexOf("{place_at}", StringComparison.Ordinal) < 0 && result.IndexOf("{place}", StringComparison.Ordinal) < 0) { result += "{place_at}"; } return result; } private static string LowerFirst(string s) { if (string.IsNullOrEmpty(s)) { return ""; } if (s.Length == 1) { return s.ToLowerInvariant(); } return char.ToLowerInvariant(s[0]) + s.Substring(1); } private static ProseVoice EnsureVoice(ActivityContext ctx) { if (ctx.Voice != null && ctx.Voice.LiveSpeciesId.Equals(ctx.SpeciesId ?? "", StringComparison.Ordinal)) { return ctx.Voice; } ProseVoice voice = ActivityVoiceResolver.Resolve(ctx.SpeciesId); ctx.Voice = voice; ctx.BaseSpeciesId = voice.BaseSpeciesId; ctx.ModifierTag = ActivityVoiceResolver.ModifierName(voice.Modifier); ctx.Family = voice.BaseFamily; ctx.MannerTag = ActivityVoiceResolver.TagName(voice.EffectiveActionTag); ctx.IsCiv = voice.BaseIsCiv; ctx.IsHumanoid = voice.BaseIsHumanoid; ctx.IsAnimal = voice.BaseIsAnimal; return voice; } private static string Pick( string[] bag, long subjectId, int lineIndex) { if (bag == null || bag.Length == 0) { return "moves along"; } if (bag.Length == 1) { return bag[0]; } int actorVariation = (int)((subjectId ^ (lineIndex * 397L)) & 1L); int index = actorVariation % Math.Min(2, bag.Length); return bag[index]; } private static string[] VehicleBag(string verb) { return verb switch { "move" => new[] { "cuts steadily across the water", "sails onward with the current" }, "wait" => new[] { "rides quietly at rest on the water", "holds position on the gentle swell" }, "fish" => new[] { "works the water for fish", "casts for a catch from the deck" }, "trade" => new[] { "carries trade across the water{place_at}", "makes a measured trading run{place_at}" }, "haul" => new[] { "takes cargo aboard", "settles the load for passage" }, "settle" => new[] { "holds at the quay{place_at}", "ties up for harbor business{place_at}" }, "farm" => new[] { "works deck cargo from the fields{place_at}", "ferries produce to harbor{place_at}" }, _ => new[] { "continues working on the water", "keeps steadily to its course" } }; } private static string[] TinyFallback(string verb, bool hasTarget) { string place = (verb == "settle" || verb == "trade" || verb == "farm") ? "{place_at}" : ""; return verb switch { "farm" => new[] { "tends the fields with patient care" + place, "works the soil from row to row" + place }, "haul" => new[] { "unloads {carrying} after a long haul", "delivers {carrying} into storage" }, "pollinate" => new[] { "works carefully among the blossoms", "moves steadily from flower to flower" }, "fish" => new[] { "works the water for fish", "waits patiently for a bite" }, "heal" => hasTarget ? new[] { "tends carefully to {target}", "works to heal {target}" } : new[] { "tends wounds with careful hands", "works patiently to recover" }, "fire" => new[] { "fights the blaze with urgent care", "battles fire before it spreads" }, "flee" => new[] { "bolts away from danger", "retreats before danger closes in" }, "work" => new[] { "works steadily at the task", "labors through the task at hand" }, "trade" => new[] { "trades and bargains" + place, "haggles through a careful deal" + place }, "settle" => new[] { "handles civic business" + place, "sees to matters around the settlement" + place }, "warrior" => hasTarget ? new[] { "holds formation against {target}", "drills for war near {target}" } : new[] { "stands ready for battle", "keeps formation ready" }, "read" => new[] { "reads with quiet attention", "studies the page" }, "lover" => hasTarget ? new[] { "draws close to {target}", "shares a tender moment with {target}" } : new[] { "follows the pull of kinship", "seeks companionship" }, "gather_life" => new[] { "gathers with kin", "moves among the group" }, "move" => new[] { "wanders without hurry", "roams nearby, looking around" }, "wait" => new[] { "waits in watchful quiet", "stands still and watches" }, "play" => new[] { "paces, turns, and doubles back", "moves in quick, restless bursts" }, "eat" => new[] { "sits down to eat", "finishes a meal" }, "sleep" => new[] { "settles into deep sleep", "rests after the day's demands" }, "hunt" => hasTarget ? new[] { "pursues {target}", "closes in on {target}" } : new[] { "searches for prey", "moves quietly on the hunt" }, "fight" => hasTarget ? new[] { "fights {target} in a fierce clash", "trades blows with {target}" } : new[] { "fights with fierce resolve", "clashes without giving ground" }, "social" => hasTarget ? new[] { "spends time with {target}", "talks and lingers with {target}" } : new[] { "looks for company", "lingers near others" }, "laugh" => new[] { "laughs out loud", "breaks into bright laughter" }, _ => hasTarget ? new[] { "attends to " + verb.Replace('_', ' ') + " with {target}", "works on " + verb.Replace('_', ' ') + " near {target}" } : new[] { "attends to " + verb.Replace('_', ' '), "works through " + verb.Replace('_', ' ') } }; } }