557 lines
15 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|