worldbox-observer-mod/IdleSpectator/ActivityProse.cs

1129 lines
37 KiB
C#

using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>
/// Fact-preserving activity sentences with colored actor/target names.
/// </summary>
public static class ActivityProse
{
public const string NameColorHex = "#F0C14A";
/// <summary>
/// Long detailed templates. Tokens: {actor}{target}{species}{place}{place_at}{job}{job_as}{carrying}{with}.
/// Never invent outcomes - only flavor observed tasks.
/// Prefer <see cref="ActivityClauseAssembler"/> for full alive composition; Variants seed action phrases.
/// </summary>
private static readonly Dictionary<string, string[]> Variants = new Dictionary<string, string[]>
{
["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} moves about in quick, restless bursts{place_at}",
"{actor} darts back and forth for a while{place_at}",
"{actor} bounds around with eager energy{place_at}"
},
["just_played"] = new[]
{
"{actor} slows down after playing{place_at}",
"{actor} stops to catch a breath{place_at}",
"{actor} settles after a burst of activity{place_at}"
},
["child_play_at_one_spot"] = new[]
{
"{actor} hops and pivots without wandering far{place_at}",
"{actor} keeps busy in one small spot{place_at}",
"{actor} turns and bounces in place{place_at}"
},
["child_random_flips"] = new[]
{
"{actor} flips and tumbles about{place_at}",
"{actor} rolls forward and springs upright{place_at}",
"{actor} tries one quick flip after another{place_at}"
},
["child_random_jump"] = new[]
{
"{actor} jumps about in quick bursts{place_at}",
"{actor} leaps forward and doubles back{place_at}",
"{actor} bounces around without settling{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 and wheels around{place_at}",
"{actor} darts off and circles back{place_at}",
"{actor} skips along without hurry{place_at}"
},
["godfinger_random_fun_move"] = new[]
{
"{actor} moves in sudden, unpredictable turns{place_at}",
"{actor} darts one way, then abruptly another{place_at}",
"{actor} loops around in restless motion{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 "<color=" + NameColorHex + ">" + name + "</color>";
}
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);
}
}
/// <summary>Mark child life-stage from task key when context did not already capture it.</summary>
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;
}
}
/// <summary>Action-only phrase from pattern heuristics (no subject).</summary>
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);
}
/// <summary>Observed localized fact as an action-only phrase for unknown task ids.</summary>
public static string FactualActionPhrase(string rawFact, string key, bool hasTarget)
{
return ActivityClauseAssembler.StripSubject(LocalizedTemplate(rawFact, key, hasTarget));
}
/// <summary>Legacy helper used by older call sites / harness.</summary>
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(" against {target}", "")
.Replace(" before {target}", "")
.Replace(" beside {target}", "")
.Replace(" near {target}", "")
.Replace(" around {target}", "")
.Replace(" past {target}", "")
.Replace(" behind {target}", "")
.Replace(" under {target}", "")
.Replace(" over {target}", "")
.Replace(" across {target}", "")
.Replace(" from {target}", "")
.Replace(" into {target}", "")
.Replace(" upon {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;
}
/// <summary>
/// Turn game localized labels like "Laughing" / "Job Seeking" into real sentences.
/// </summary>
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<string, string[]> 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<string>(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;
}
}
}