worldbox-observer-mod/IdleSpectator/ActivityProse.cs

557 lines
15 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>
/// Templates use {actor} and optional {target}. Never invent outcomes.
/// </summary>
private static readonly Dictionary<string, string[]> Variants = new Dictionary<string, string[]>
{
["attack"] = new[]
{
"{actor} strikes at {target}",
"{actor} presses the attack on {target}",
"{actor} lunges toward {target}"
},
["fight"] = new[]
{
"{actor} fights {target}",
"{actor} clashes with {target}",
"{actor} trades blows with {target}"
},
["fighting"] = new[]
{
"{actor} fights {target}",
"{actor} is locked in combat with {target}",
"{actor} clashes with {target}"
},
["hunt"] = new[]
{
"{actor} hunts {target}",
"{actor} stalks {target}",
"{actor} closes on {target}"
},
["BehAttackActorHuntingTarget"] = new[]
{
"{actor} hunts {target}",
"{actor} strikes at {target}",
"{actor} closes on {target}"
},
["heal"] = new[]
{
"{actor} tends to {target}",
"{actor} heals {target}",
"{actor} binds {target}'s wounds"
},
["check_heal"] = new[]
{
"{actor} checks on wounds",
"{actor} looks over injuries",
"{actor} takes stock of pain"
},
["cure"] = new[]
{
"{actor} seeks a cure",
"{actor} works to recover",
"{actor} fights sickness"
},
["eat"] = new[]
{
"{actor} takes a meal",
"{actor} eats",
"{actor} breaks hunger"
},
["eating"] = new[]
{
"{actor} takes a meal",
"{actor} eats",
"{actor} breaks hunger"
},
["sleep"] = new[]
{
"{actor} sleeps",
"{actor} rests",
"{actor} settles in to sleep"
},
["decide_where_to_sleep"] = new[]
{
"{actor} looks for a place to sleep",
"{actor} decides where to rest",
"{actor} seeks shelter for the night"
},
["random_move"] = new[]
{
"{actor} wanders",
"{actor} roams nearby",
"{actor} drifts along"
},
["city_idle_walking"] = new[]
{
"{actor} walks the streets",
"{actor} strolls through town",
"{actor} paces the settlement"
},
["move"] = new[]
{
"{actor} is on the move",
"{actor} travels onward",
"{actor} heads out"
},
["pollinate"] = new[]
{
"{actor} dusts the blossom",
"{actor} works the flower",
"{actor} leaves pollen behind"
},
["BehPollinate"] = new[]
{
"{actor} dusts the blossom",
"{actor} works the flower",
"{actor} leaves pollen behind"
},
["farm"] = new[]
{
"{actor} tends the fields",
"{actor} works the soil",
"{actor} farms the plot"
},
["harvest"] = new[]
{
"{actor} harvests the crop",
"{actor} gathers the yield",
"{actor} cuts the ripe fields"
},
["forag"] = new[]
{
"{actor} forages",
"{actor} gathers wild food",
"{actor} searches for forage"
},
["gather"] = new[]
{
"{actor} gathers stores",
"{actor} collects goods",
"{actor} forages supplies"
},
["mine"] = new[]
{
"{actor} delves the mine",
"{actor} chips at ore",
"{actor} works the dig"
},
["build"] = new[]
{
"{actor} builds",
"{actor} raises walls",
"{actor} works the construction"
},
["woodcut"] = new[]
{
"{actor} fells timber",
"{actor} cuts wood",
"{actor} takes an axe to the trees"
},
["fire"] = new[]
{
"{actor} fights the blaze",
"{actor} douses flames",
"{actor} races the fire"
},
["BehCityActorRemoveFire"] = new[]
{
"{actor} fights the blaze",
"{actor} douses flames",
"{actor} beats back the fire"
},
["social"] = new[]
{
"{actor} talks with {target}",
"{actor} socializes with {target}",
"{actor} converses with {target}"
},
["socialize"] = new[]
{
"{actor} talks with {target}",
"{actor} socializes with {target}",
"{actor} joins company near {target}"
},
["lover"] = new[]
{
"{actor} seeks out {target}",
"{actor} is drawn to {target}",
"{actor} pursues {target}'s affection"
},
["breed"] = new[]
{
"{actor} tries to start a family with {target}",
"{actor} courts {target}",
"{actor} seeks offspring with {target}"
},
["sexual_reproduction"] = new[]
{
"{actor} tries to start a family with {target}",
"{actor} courts {target}",
"{actor} seeks offspring with {target}"
},
["unload"] = new[]
{
"{actor} unloads goods in town",
"{actor} delivers the haul",
"{actor} empties the pack at home"
},
["BehUnloadResources"] = new[]
{
"{actor} unloads goods in town",
"{actor} delivers the haul",
"{actor} empties the pack at home"
},
["job"] = new[]
{
"{actor} seeks work",
"{actor} looks for a job",
"{actor} searches for employment"
},
["find_city_job"] = new[]
{
"{actor} seeks work in town",
"{actor} looks for a city job",
"{actor} searches for employment"
},
["find_house"] = new[]
{
"{actor} looks for a house",
"{actor} seeks a home",
"{actor} searches for shelter"
},
["claim"] = new[]
{
"{actor} claims land",
"{actor} stakes a claim",
"{actor} takes claim of ground"
},
["warrior"] = new[]
{
"{actor} follows the army",
"{actor} marches with the warband",
"{actor} keeps formation"
},
["army"] = new[]
{
"{actor} follows the army",
"{actor} marches with the warband",
"{actor} keeps formation"
},
["wait"] = new[]
{
"{actor} waits",
"{actor} holds still",
"{actor} bides time"
},
["make_decision"] = new[]
{
"{actor} considers next steps",
"{actor} weighs a choice",
"{actor} pauses in thought"
},
["reflection"] = new[]
{
"{actor} reflects",
"{actor} takes a quiet moment",
"{actor} thinks things over"
},
["run_away"] = new[]
{
"{actor} flees",
"{actor} runs for safety",
"{actor} retreats"
},
["trade"] = new[]
{
"{actor} trades",
"{actor} barters goods",
"{actor} is on a trade run"
},
["fish"] = new[]
{
"{actor} fishes",
"{actor} casts for fish",
"{actor} works the waters"
},
["family_group"] = new[]
{
"{actor} joins the herd",
"{actor} seeks the family group",
"{actor} gathers with kin"
},
["generate_loot"] = new[]
{
"{actor} gathers loot",
"{actor} claims spoils",
"{actor} picks through loot"
}
};
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)
{
plain = "";
rich = "";
string actor = string.IsNullOrEmpty(actorName) ? "Someone" : actorName;
string targetPlain = ResolveTargetPlain(targetName, targetIsActor, placeOrFallback);
string template = PickTemplate(key, subjectId, lineIndex, !string.IsNullOrEmpty(targetPlain));
if (string.IsNullOrEmpty(template))
{
plain = BuildFallbackPlain(actor, targetPlain, rawFact, key);
rich = BuildFallbackRich(actor, targetPlain, targetIsActor, rawFact, key);
return;
}
plain = Apply(template, actor, targetPlain);
rich = Apply(
template,
ColorName(actor),
targetIsActor && !string.IsNullOrEmpty(targetName)
? ColorName(targetName)
: targetPlain);
}
/// <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,
actorName: "",
targetName: target,
targetIsActor: !string.IsNullOrEmpty(target),
placeOrFallback: target,
rawFact: rawFact,
subjectId: subjectId,
lineIndex: lineIndex,
out string plain,
out _);
return plain;
}
private static string ResolveTargetPlain(string targetName, bool targetIsActor, string placeOrFallback)
{
if (!string.IsNullOrEmpty(targetName))
{
return targetName;
}
if (!string.IsNullOrEmpty(placeOrFallback))
{
return placeOrFallback;
}
return "";
}
private static string Apply(string template, string actor, string target)
{
string t = template.Replace("{actor}", actor ?? "");
if (string.IsNullOrEmpty(target))
{
// Drop dangling " with {target}" style if target missing - templates should
// prefer no-target variants via PickTemplate, but scrub leftovers.
t = t.Replace(" with {target}", "")
.Replace(" on {target}", "")
.Replace(" toward {target}", "")
.Replace(" at {target}", "")
.Replace(" {target}", "")
.Replace("{target}", "someone");
}
else
{
t = t.Replace("{target}", target);
}
return t.Trim();
}
private static string BuildFallbackPlain(string actor, string target, string rawFact, string key)
{
string action = !string.IsNullOrEmpty(rawFact) ? rawFact : HumanizeKey(key);
if (string.IsNullOrEmpty(action))
{
return actor;
}
if (!string.IsNullOrEmpty(target) && action.IndexOf(target, System.StringComparison.OrdinalIgnoreCase) < 0)
{
return actor + " " + LowerFirst(action) + " → " + target;
}
return actor + " " + LowerFirst(action);
}
private static string BuildFallbackRich(
string actor,
string target,
bool targetIsActor,
string rawFact,
string key)
{
string action = !string.IsNullOrEmpty(rawFact) ? rawFact : HumanizeKey(key);
string a = ColorName(actor);
if (string.IsNullOrEmpty(action))
{
return a;
}
if (!string.IsNullOrEmpty(target) && action.IndexOf(target, System.StringComparison.OrdinalIgnoreCase) < 0)
{
string t = targetIsActor ? ColorName(target) : target;
return a + " " + LowerFirst(action) + " → " + t;
}
return a + " " + LowerFirst(action);
}
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 HumanizeKey(string key)
{
if (string.IsNullOrEmpty(key))
{
return "";
}
return key.Replace('_', ' ').Replace("Beh", "").Trim();
}
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)
{
// Fall back to any template; Apply() will scrub missing targets.
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;
}
}
}