worldbox-observer-mod/IdleSpectator/ActivityActionComposer.cs

211 lines
8.4 KiB
C#

using System;
namespace IdleSpectator;
/// <summary>Single deterministic action-selection pipeline.</summary>
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);
}
/// <summary>
/// Haul-to-store keeps place and names cargo once (no trailing while-carrying needed).
/// </summary>
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('_', ' ')
}
};
}
}