worldbox-observer-mod/IdleSpectator/ActivityActionComposer.cs

178 lines
5.7 KiB
C#

using System;
using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Single deterministic action-selection pipeline.</summary>
public static class ActivityActionComposer
{
private static readonly Dictionary<string, string[]> BeatTemplates =
new Dictionary<string, string[]>(StringComparer.Ordinal)
{
["BehUnloadResources"] = new[]
{
"unloads {carrying} at {place}",
"delivers {carrying} into store{place_at}"
},
["BehThrowResources"] = new[]
{
"throws {carrying} toward storage",
"tosses {carrying} from the pack"
},
["BehSocializeTalk"] = new[]
{
"talks{with}{place_at}",
"shares words{with}{place_at}"
},
["BehTryToSocialize"] = new[]
{
"tries to socialize{with}{place_at}",
"looks for a conversation{with}{place_at}"
},
["BehPlantCrops"] = new[]
{
"plants crops{place_at}{job_as}",
"sets seed in the soil{place_at}"
},
["BehCityActorFertilizeCrop"] = new[]
{
"fertilizes the crops{place_at}{job_as}",
"feeds the soil{place_at}"
},
["BehBuildTarget"] = new[]
{
"works the build{place_at}{job_as}",
"raises construction{place_at}"
},
["BehPoopOutside"] = new[]
{
"steps aside for a private moment outdoors",
"tends to nature's call outside"
},
["BehPoopInside"] = new[]
{
"steps aside for a private moment indoors",
"tends to nature's call inside"
},
["BehBoatFishing"] = new[]
{
"fishes from the boat{place_at}",
"works the waters from aboard{place_at}"
},
["BehBoatCollectFish"] = new[]
{
"collects the catch aboard{place_at}",
"hauls in the boat's catch{place_at}"
},
["BehClaimZoneForCityActorBorder"] = new[]
{
"claims border land{place_at}",
"stakes new ground for the settlement{place_at}"
}
};
public static string Compose(
ActivityContext ctx,
string key,
string rawFact,
bool hasTarget,
long subjectId,
int lineIndex)
{
ctx ??= ActivityContext.Empty;
ProseVoice voice = EnsureVoice(ctx);
if (!string.IsNullOrEmpty(key)
&& BeatTemplates.TryGetValue(key, out string[] beatBag)
&& voice.AssetKind != ActivityAssetKind.Vehicle)
{
return Pick(beatBag, voice, key, subjectId, lineIndex);
}
string verb = ActivityVerbMap.Resolve(key);
if (string.IsNullOrEmpty(verb))
{
string factual = ActivityProse.FactualActionPhrase(rawFact, key, hasTarget);
factual = ActivityClauseAssembler.StripSpeciesTokensPublic(factual, ctx);
if (!string.IsNullOrEmpty(factual))
{
return factual;
}
string patterned = ActivityProse.PatternActionPhrase(key, hasTarget);
if (!string.IsNullOrEmpty(patterned))
{
return patterned;
}
string humanized = ActivityProse.HumanizeKeyPublic(key);
verb = string.IsNullOrEmpty(humanized) ? "move" : humanized.ToLowerInvariant();
}
string[] bag = ActivityActionLexicon.GetBag(voice, verb, hasTarget);
return Pick(bag, voice, verb, subjectId, lineIndex);
}
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,
ProseVoice voice,
string verb,
long subjectId,
int lineIndex)
{
if (bag == null || bag.Length == 0)
{
return "moves through the living moment{place_at}{job_as}";
}
if (bag.Length == 1)
{
return bag[0];
}
int slot = VerbSlot(verb);
int bit = voice.AssetKind == ActivityAssetKind.Vehicle
? 0
: ((voice.FlavorOrdinal >> slot) & 1);
int actorVariation = (int)((subjectId ^ (lineIndex * 397L)) & 1L);
int index = (bit ^ actorVariation) % Math.Min(2, bag.Length);
return bag[index];
}
private static int VerbSlot(string verb)
{
switch ((verb ?? "").ToLowerInvariant())
{
case "move": return 0;
case "wait": return 1;
case "play": return 2;
case "eat": return 3;
case "sleep": return 4;
case "hunt": return 5;
case "fight": return 6;
case "social": return 7;
case "laugh": return 8;
default: return 9;
}
}
}