211 lines
8.4 KiB
C#
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('_', ' ')
|
|
}
|
|
};
|
|
}
|
|
}
|