1116 lines
37 KiB
C#
1116 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(" {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;
|
|
}
|
|
}
|
|
}
|