Enhance ActivityInterestTable and ActivityLog with comprehensive context building for actors. Introduced the BuildContext method to encapsulate actor details, improving activity tracking and logging. Updated ActivityLog to utilize the new context for better clarity in logged actions. Adjusted test scenarios in HarnessScenarios to validate these enhancements. Incremented version in mod.json to reflect the update.
This commit is contained in:
parent
0dfe3ab832
commit
88a0c38c00
19 changed files with 4534 additions and 303 deletions
178
IdleSpectator/ActivityActionComposer.cs
Normal file
178
IdleSpectator/ActivityActionComposer.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
370
IdleSpectator/ActivityActionLexicon.cs
Normal file
370
IdleSpectator/ActivityActionLexicon.cs
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
using System;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Phrase fragments keyed by the already-resolved voice. No ActorAsset lookup.</summary>
|
||||
public static class ActivityActionLexicon
|
||||
{
|
||||
public static string[] GetBag(ProseVoice voice, string verb, bool hasTarget)
|
||||
{
|
||||
voice ??= ProseVoice.Unknown();
|
||||
string v = string.IsNullOrEmpty(verb) ? "move" : verb;
|
||||
|
||||
if (voice.AssetKind == ActivityAssetKind.Vehicle)
|
||||
{
|
||||
return VehicleBag(v);
|
||||
}
|
||||
|
||||
if (voice.Modifier != ActivityModifier.None)
|
||||
{
|
||||
string[] modifier = ModifierBag(voice, v, hasTarget);
|
||||
if (modifier != null) return modifier;
|
||||
}
|
||||
|
||||
string[] profile = ProfileBag(voice.ProfileTag, v, hasTarget);
|
||||
if (profile != null) return profile;
|
||||
|
||||
string[] body = BodyBag(voice.BaseBodyTag, v, hasTarget);
|
||||
return body ?? GenericBag(v, hasTarget);
|
||||
}
|
||||
|
||||
private static string[] ModifierBag(ProseVoice voice, string verb, bool hasTarget)
|
||||
{
|
||||
switch (voice.Modifier)
|
||||
{
|
||||
case ActivityModifier.Undead:
|
||||
return UndeadBag(voice.BaseBodyTag, verb, hasTarget);
|
||||
case ActivityModifier.Elemental:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "surges onward in a living blaze{place_at}", "flows ahead with elemental force{place_at}" },
|
||||
"wait" => new[] { "holds in a wavering elemental pause{place_at}", "burns steadily while waiting{place_at}" },
|
||||
"play" => new[] { "flares through a wild game{place_at}", "dances in playful elemental arcs{place_at}" },
|
||||
"eat" => new[] { "feeds the inner flame{place_at}", "draws in fuel with bright appetite{place_at}" },
|
||||
"sleep" => new[] { "dims into a banked rest{place_at}", "settles into a low elemental glow{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "surges after {target}{place_at}", "hunts {target} in a trail of heat{place_at}", "searches for prey in flickering sweeps{place_at}", "hunts with elemental focus{place_at}"),
|
||||
"fight" => Targeted(hasTarget, "crashes into {target} with elemental force{place_at}", "flares against {target}{place_at}", "fights in a storm of living heat{place_at}", "clashes with elemental fury{place_at}"),
|
||||
"laugh" => new[] { "crackles with bright laughter{place_at}", "flares in a laughing burst{place_at}" },
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Mush:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "shuffles onward in soft fungal steps{place_at}", "spreads along with mycelial patience{place_at}" },
|
||||
"wait" => new[] { "rests in spore-soft quiet{place_at}", "waits while faint spores drift{place_at}" },
|
||||
"play" => new[] { "puffs through an odd little game{place_at}", "spores about in playful motion{place_at}" },
|
||||
"eat" => new[] { "feeds through the damp earth{place_at}", "draws a slow meal from decay{place_at}" },
|
||||
"sleep" => new[] { "settles into a damp fungal rest{place_at}", "folds into mycelial quiet{place_at}" },
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Tumor:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "pulses onward in malignant motion{place_at}", "lurches ahead with invasive purpose{place_at}" },
|
||||
"wait" => new[] { "throbs in unsettling stillness{place_at}", "holds a malignant pause{place_at}" },
|
||||
"play" => new[] { "writhes through a grotesque game{place_at}", "pulses with uncanny playfulness{place_at}" },
|
||||
"eat" => new[] { "feeds with consuming hunger{place_at}", "absorbs a meal into the growing mass{place_at}" },
|
||||
"fight" => Targeted(hasTarget, "lashes into {target} with malignant force{place_at}", "surges against {target}{place_at}", "fights with invasive fury{place_at}", "clashes in a malignant rush{place_at}"),
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Alien:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "glides onward with unfamiliar ease{place_at}", "tracks ahead in precise alien motion{place_at}" },
|
||||
"wait" => new[] { "observes in unreadable stillness{place_at}", "holds an unfamiliar watch{place_at}" },
|
||||
"play" => new[] { "probes the moment in curious play{place_at}", "experiments with a strange little game{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "tracks {target} with alien focus{place_at}", "closes on {target} in eerie silence{place_at}", "scans for prey{place_at}", "hunts by unfamiliar instinct{place_at}"),
|
||||
"fight" => Targeted(hasTarget, "strikes at {target} with alien precision{place_at}", "clashes with {target} in strange motion{place_at}", "fights with unreadable purpose{place_at}", "attacks in a precise rush{place_at}"),
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Construct:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "advances in measured constructed steps{place_at}", "shifts onward with deliberate precision{place_at}" },
|
||||
"wait" => new[] { "idles in rigid watchfulness{place_at}", "holds a perfectly measured pause{place_at}" },
|
||||
"play" => new[] { "tests a playful sequence of motions{place_at}", "turns precise movement into a game{place_at}" },
|
||||
"eat" => new[] { "takes in fuel with mechanical care{place_at}", "replenishes what keeps it moving{place_at}" },
|
||||
"sleep" => new[] { "powers down into quiet rest{place_at}", "settles into a dormant stillness{place_at}" },
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] UndeadBag(ActivityBodyTag body, string verb, bool hasTarget)
|
||||
{
|
||||
if (verb == "move")
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => new[] { "pads onward with lifeless purpose{place_at}", "sniffs and shambles through the living world{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "slinks onward in deathly quiet{place_at}", "pads ahead with hollow patience{place_at}" },
|
||||
ActivityBodyTag.Insect => new[] { "skitters onward in deathly little steps{place_at}", "crawls ahead with hollow purpose{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "creeps onward in withered motion{place_at}", "drags dead growth through the living world{place_at}" },
|
||||
ActivityBodyTag.Dragon => new[] { "lumbers onward with ancient deathly weight{place_at}", "advances like ruin given motion{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "flutters onward on lifeless wings{place_at}", "hops ahead in ragged deathly motion{place_at}" },
|
||||
_ => new[] { "shambles onward through the living world{place_at}", "drifts along in restless undeath{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
return verb switch
|
||||
{
|
||||
"wait" => new[] { "waits in hollow stillness{place_at}", "holds a restless undeath pause{place_at}" },
|
||||
"play" => new[] { "lurches through a strange game{place_at}", "toys with the moment in hollow cheer{place_at}" },
|
||||
"eat" => new[] { "feeds with deathless hunger{place_at}", "tears into a meal with hollow appetite{place_at}" },
|
||||
"sleep" => new[] { "slumps into a hollow rest{place_at}", "goes still in restless undeath{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "shambles after {target}{place_at}", "hunts {target} with hollow hunger{place_at}", "hunts with deathless hunger{place_at}", "searches for prey without rest{place_at}"),
|
||||
"fight" => Targeted(hasTarget, "presses against {target} without fear{place_at}", "clashes with {target} in deathly fury{place_at}", "fights with hollow force{place_at}", "presses on without giving ground{place_at}"),
|
||||
"laugh" => new[] { "groans in a broken laugh{place_at}", "makes a hollow sound of laughter{place_at}" },
|
||||
"social" => Targeted(hasTarget, "lingers near {target} in uneasy silence{place_at}", "shares a hollow moment with {target}{place_at}", "seeks company in deathly quiet{place_at}", "lingers among the living{place_at}"),
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] ProfileBag(string profile, string verb, bool hasTarget)
|
||||
{
|
||||
if (string.IsNullOrEmpty(profile)) return null;
|
||||
if (profile == "dragon")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "advances with ancient patience{place_at}", "moves as if the world should make way{place_at}" },
|
||||
"sleep" => new[] { "coils into a guarded sleep{place_at}", "rests with one eye on the world{place_at}" },
|
||||
"eat" => new[] { "feeds with terrifying appetite{place_at}", "takes a meal with ancient hunger{place_at}" },
|
||||
"laugh" => new[] { "rumbles with deep laughter{place_at}", "laughs in a dangerous rolling growl{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "descends on {target}{place_at}", "hunts {target} with ancient focus{place_at}", "circles in search of prey{place_at}", "hunts with ancient appetite{place_at}"),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "fairy")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "flits onward in bright little arcs{place_at}", "drifts ahead on glittering wings{place_at}" },
|
||||
"play" => new[] { "dances through a sparkling game{place_at}", "flutters about with mischievous delight{place_at}" },
|
||||
"laugh" => new[] { "chimes with tiny laughter{place_at}", "laughs in a bright little trill{place_at}" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "lemon_snail")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "glides onward with patient citrus calm{place_at}", "eases ahead in a slow leafy trail{place_at}" },
|
||||
"play" => new[] { "loops through a patient little game{place_at}", "sways into slow, cheerful play{place_at}" },
|
||||
"eat" => new[] { "feeds with quiet citrus appetite{place_at}", "takes a slow, careful meal{place_at}" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "cat")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "pads softly through the living world{place_at}", "slinks along with private purpose{place_at}" },
|
||||
"play" => new[] { "bats and twists through a private game{place_at}", "springs into sudden playful leaps{place_at}" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "buffalo")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "trudges onward with grounded strength{place_at}", "ambles ahead in heavy steady steps{place_at}" },
|
||||
"play" => new[] { "frolics with heavy earnest energy{place_at}", "stomps through a clumsy game{place_at}" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "crabzilla")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "strides sideways with colossal purpose{place_at}", "scuttles onward like a moving fortress{place_at}" },
|
||||
"fight" => Targeted(hasTarget, "brings a colossal claw against {target}{place_at}", "crashes into {target} like a living siege{place_at}", "fights with city-shaking force{place_at}", "clashes like a living fortress{place_at}"),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string[] BodyBag(ActivityBodyTag body, string verb, bool hasTarget)
|
||||
{
|
||||
if (verb == "move")
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Humanoid => new[] { "walks onward with quiet purpose{place_at}", "wanders ahead at an easy pace{place_at}" },
|
||||
ActivityBodyTag.Canine => new[] { "trots along without hurry{place_at}", "roams ahead with curious paws{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "pads softly through the living world{place_at}", "slinks along at an easy pace{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "flutters onward through the air{place_at}", "hops and wings along nearby{place_at}" },
|
||||
ActivityBodyTag.Insect => new[] { "scuttles along without hurry{place_at}", "skitters across the ground{place_at}" },
|
||||
ActivityBodyTag.Arachnid => new[] { "skitters onward on many legs{place_at}", "creeps ahead with careful steps{place_at}" },
|
||||
ActivityBodyTag.Amphibian => new[] { "hops onward through the living world{place_at}", "bounces along in damp springy steps{place_at}" },
|
||||
ActivityBodyTag.Aquatic => new[] { "glides through the water{place_at}", "swims onward with fluid ease{place_at}" },
|
||||
ActivityBodyTag.Reptile => new[] { "slides along without hurry{place_at}", "creeps forward with cool patience{place_at}" },
|
||||
ActivityBodyTag.Livestock => new[] { "ambles along without hurry{place_at}", "wanders with heavy steady steps{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "creeps along with patient plant motion{place_at}", "eases forward in slow leafy steps{place_at}" },
|
||||
ActivityBodyTag.Fungi => new[] { "drifts along in soft fungal steps{place_at}", "spreads onward with patient motion{place_at}" },
|
||||
ActivityBodyTag.Protist => new[] { "oozes onward without hurry{place_at}", "pulses along through the living world{place_at}" },
|
||||
ActivityBodyTag.Machina => new[] { "advances in measured machine steps{place_at}", "tracks onward with precise motion{place_at}" },
|
||||
ActivityBodyTag.Mythic => new[] { "drifts along with mythic calm{place_at}", "moves with otherworldly purpose{place_at}" },
|
||||
ActivityBodyTag.Gregalia => new[] { "gregs onward through the living world{place_at}", "wanders in restless greg motion{place_at}" },
|
||||
ActivityBodyTag.Primate => new[] { "lopes onward with nimble curiosity{place_at}", "moves ahead in quick clever steps{place_at}" },
|
||||
ActivityBodyTag.Rodent => new[] { "scurries onward without hurry{place_at}", "pads ahead in quick little steps{place_at}" },
|
||||
ActivityBodyTag.Ursid => new[] { "lumbers onward with steady weight{place_at}", "pads ahead in heavy unhurried steps{place_at}" },
|
||||
ActivityBodyTag.Seal => new[] { "slides onward with sleek ease{place_at}", "glides ahead through the wet paths{place_at}" },
|
||||
ActivityBodyTag.Procyonid => new[] { "pads onward with busy curiosity{place_at}", "scampers ahead with clever purpose{place_at}" },
|
||||
ActivityBodyTag.Hyaenid => new[] { "lopes onward with rough confidence{place_at}", "trots ahead in rangy strides{place_at}" },
|
||||
ActivityBodyTag.Armadillo => new[] { "trundles onward close to the ground{place_at}", "scurries ahead in armored little steps{place_at}" },
|
||||
ActivityBodyTag.Crawler => new[] { "writhes onward through the living world{place_at}", "crawls ahead without hurry{place_at}" },
|
||||
ActivityBodyTag.Cold => new[] { "moves onward in a breath of cold{place_at}", "drifts ahead with wintry calm{place_at}" },
|
||||
ActivityBodyTag.Crystal => new[] { "glides onward with crystalline precision{place_at}", "moves ahead in bright rigid steps{place_at}" },
|
||||
ActivityBodyTag.Crab => new[] { "scuttles sideways through the living world{place_at}", "sidesteps onward with armored purpose{place_at}" },
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
}
|
||||
|
||||
if (verb == "play")
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => new[] { "tumbles about in lively play{place_at}", "bounds and wrestles through play{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "bats and twists through a private game{place_at}", "plays with sudden leaps{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "flutters through playful arcs{place_at}", "hops and wings about in play{place_at}" },
|
||||
ActivityBodyTag.Insect => new[] { "scuttles about in tiny play{place_at}", "darts through a quick little game{place_at}" },
|
||||
ActivityBodyTag.Arachnid => new[] { "skitters through a restless game{place_at}", "toys with the moment on many legs{place_at}" },
|
||||
ActivityBodyTag.Amphibian => new[] { "hops about in damp play{place_at}", "bounces through a lively game{place_at}" },
|
||||
ActivityBodyTag.Aquatic => new[] { "splashes through play in the water{place_at}", "weaves playful arcs through the wet{place_at}" },
|
||||
ActivityBodyTag.Reptile => new[] { "slides through a low game{place_at}", "plays with slow coiled energy{place_at}" },
|
||||
ActivityBodyTag.Livestock => new[] { "stomps about in clumsy play{place_at}", "frolics with heavy earnest energy{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "sways through a slow game{place_at}", "turns the moment into leafy play{place_at}" },
|
||||
ActivityBodyTag.Primate => new[] { "clambers through a clever game{place_at}", "plays with nimble mischief{place_at}" },
|
||||
ActivityBodyTag.Rodent => new[] { "scampers through a tiny game{place_at}", "darts about in quick play{place_at}" },
|
||||
ActivityBodyTag.Ursid => new[] { "rolls through a heavy game{place_at}", "plays with lumbering cheer{place_at}" },
|
||||
ActivityBodyTag.Crawler => new[] { "loops through a low little game{place_at}", "writhes about in restless play{place_at}" },
|
||||
_ => new[] { "makes a game of the moment{place_at}", "plays with easy living energy{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
if (verb == "wait")
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Insect => new[] { "holds still in a tiny pause{place_at}", "waits with antennae alert{place_at}" },
|
||||
ActivityBodyTag.Amphibian => new[] { "crouches in a damp pause{place_at}", "waits in springy stillness{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "settles into a watchful crouch{place_at}", "waits with quiet patience{place_at}" },
|
||||
ActivityBodyTag.Canine => new[] { "waits with alert ears{place_at}", "holds a restless pause{place_at}" },
|
||||
ActivityBodyTag.Aquatic => new[] { "hovers in the water and waits{place_at}", "holds still in the wet quiet{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "perches and waits{place_at}", "holds a light watchful pause{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "holds a rooted pause{place_at}", "waits in leafy quiet{place_at}" },
|
||||
ActivityBodyTag.Machina => new[] { "idles in a watchful hold{place_at}", "waits through a measured pause{place_at}" },
|
||||
_ => new[] { "waits in watchful quiet{place_at}", "holds still through a living pause{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
if (verb == "hunt")
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => Targeted(hasTarget, "hunts {target} with keen nose{place_at}", "closes on {target} through the living world{place_at}", "hunts with keen nose{place_at}", "stalks prey through the living world{place_at}"),
|
||||
ActivityBodyTag.Feline => Targeted(hasTarget, "hunts {target} with silent focus{place_at}", "stalks {target} with patient care{place_at}", "hunts with silent focus{place_at}", "stalks prey with quiet patience{place_at}"),
|
||||
ActivityBodyTag.Bird => Targeted(hasTarget, "hunts {target} from above{place_at}", "circles after {target}{place_at}", "hunts from above{place_at}", "circles after prey{place_at}"),
|
||||
ActivityBodyTag.Insect => Targeted(hasTarget, "scuttles after {target}{place_at}", "hunts {target} with tiny focus{place_at}", "hunts with tiny predatory focus{place_at}", "scuttles after prey{place_at}"),
|
||||
ActivityBodyTag.Reptile => Targeted(hasTarget, "slides after {target}{place_at}", "hunts {target} with cold patience{place_at}", "hunts with cold patience{place_at}", "stalks prey in a low slide{place_at}"),
|
||||
_ => Targeted(hasTarget, "hunts {target} through the living world{place_at}", "closes on {target} with careful focus{place_at}", "hunts through the living world{place_at}", "stalks prey with careful steps{place_at}")
|
||||
};
|
||||
}
|
||||
|
||||
return verb switch
|
||||
{
|
||||
"fight" => Targeted(hasTarget, "fights {target} in a fierce clash{place_at}", "trades blows with {target}{place_at}", "fights with fierce resolve{place_at}", "clashes without giving ground{place_at}"),
|
||||
"eat" => body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => new[] { "eats with eager appetite{place_at}", "tears into a meal{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "eats with neat focus{place_at}", "takes a careful meal{place_at}" },
|
||||
ActivityBodyTag.Insect => new[] { "feeds with tiny industry{place_at}", "eats in quick little bites{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "drinks in a slow meal{place_at}", "feeds with quiet plant appetite{place_at}" },
|
||||
ActivityBodyTag.Livestock => new[] { "grazes with steady appetite{place_at}", "settles in to feed{place_at}" },
|
||||
_ => new[] { "settles in to take a proper meal{place_at}", "eats with focused appetite{place_at}" }
|
||||
},
|
||||
"sleep" => body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => new[] { "curls into sleep{place_at}", "settles into deep rest{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "curls into a neat sleep{place_at}", "settles into quiet rest{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "tucks into sleep{place_at}", "roosts into rest{place_at}" },
|
||||
ActivityBodyTag.Aquatic => new[] { "drifts into rest in the water{place_at}", "settles into wet quiet{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "settles into leafy rest{place_at}", "goes still in plant quiet{place_at}" },
|
||||
_ => new[] { "settles into deep sleep{place_at}", "rests after the day's demands{place_at}" }
|
||||
},
|
||||
"social" => Targeted(hasTarget, "shares a living moment with {target}{place_at}", "talks and lingers with {target}{place_at}", "seeks company nearby{place_at}", "socializes among whoever is near{place_at}"),
|
||||
"laugh" => new[] { "laughs aloud and lets the mood carry{place_at}", "breaks into open laughter{place_at}" },
|
||||
"sing" => body == ActivityBodyTag.Bird
|
||||
? new[] { "sings out on light wings{place_at}", "fills the air with bright song{place_at}" }
|
||||
: new[] { "sings out for whoever will hear{place_at}", "lifts a living song{place_at}" },
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] VehicleBag(string verb)
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "cuts steadily across the water{place_at}", "sails onward with the current{place_at}" },
|
||||
"wait" => new[] { "rides quietly at rest on the water{place_at}", "holds position on the gentle swell{place_at}" },
|
||||
"fish" => new[] { "works the water for fish{place_at}", "casts for a catch from the deck{place_at}" },
|
||||
"trade" => new[] { "carries trade across the water{place_at}", "makes a measured trading run{place_at}" },
|
||||
"haul" => new[] { "takes cargo aboard{place_at}", "settles the load for passage{place_at}" },
|
||||
_ => new[] { "works steadily upon the water{place_at}", "continues its voyage with quiet purpose{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] GenericBag(string verb, bool hasTarget)
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"farm" => new[] { "tends the fields with patient care{place_at}{job_as}", "works the soil through the living day{place_at}{job_as}" },
|
||||
"haul" => new[] { "unloads {carrying} after a long haul{place_at}{job_as}", "delivers {carrying} into storage{place_at}" },
|
||||
"pollinate" => new[] { "works the blossom with careful industry{place_at}", "pollinates the flower in a living rhythm{place_at}" },
|
||||
"fish" => new[] { "works the water for fish{place_at}{job_as}", "fishes with patient focus{place_at}" },
|
||||
"heal" => Targeted(hasTarget, "tends carefully to {target}{place_at}", "works to heal {target}{place_at}", "tends wounds with careful hands{place_at}", "works patiently to recover{place_at}"),
|
||||
"fire" => new[] { "fights the blaze with urgent care{place_at}", "battles fire before it spreads{place_at}" },
|
||||
"flee" => new[] { "flees from danger in a living rush{place_at}", "retreats before danger closes in{place_at}" },
|
||||
"work" => new[] { "works with focused craft{place_at}{job_as}", "labors through the task at hand{place_at}{job_as}" },
|
||||
"trade" => new[] { "trades and bargains{place_at}{job_as}", "haggles through a careful deal{place_at}" },
|
||||
"settle" => new[] { "settles civic business{place_at}{job_as}", "attends to belonging and place{place_at}" },
|
||||
"warrior" => Targeted(hasTarget, "holds formation against {target}{place_at}", "drills for war near {target}{place_at}", "holds warrior discipline{place_at}", "keeps formation ready{place_at}"),
|
||||
"read" => new[] { "reads with quiet attention{place_at}", "studies the page{place_at}{job_as}" },
|
||||
"lover" => Targeted(hasTarget, "draws close to {target}{place_at}", "shares a tender moment with {target}{place_at}", "follows the pull of kinship{place_at}", "seeks a living bond{place_at}"),
|
||||
"gather_life" => new[] { "gathers with kin{place_at}", "moves among the group{place_at}" },
|
||||
"move" => new[] { "wanders without hurry through the living world{place_at}", "roams nearby with quiet purpose{place_at}" },
|
||||
"wait" => new[] { "waits in watchful quiet{place_at}", "holds still through a living pause{place_at}" },
|
||||
"play" => new[] { "plays with restless living energy{place_at}", "turns the moment into a game{place_at}" },
|
||||
"eat" => new[] { "settles in to take a proper meal{place_at}", "eats with focused appetite{place_at}" },
|
||||
"sleep" => new[] { "settles into deep sleep{place_at}", "rests after the day's demands{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "hunts {target} through the living world{place_at}", "closes on {target} with careful focus{place_at}", "hunts through the living world{place_at}", "stalks prey with careful steps{place_at}"),
|
||||
"fight" => Targeted(hasTarget, "fights {target} in a fierce clash{place_at}", "trades blows with {target}{place_at}", "fights with fierce resolve{place_at}", "clashes without giving ground{place_at}"),
|
||||
"social" => Targeted(hasTarget, "shares a living moment with {target}{place_at}", "talks and lingers with {target}{place_at}", "seeks company nearby{place_at}", "socializes among whoever is near{place_at}"),
|
||||
_ => hasTarget
|
||||
? new[] { "sets about " + verb.Replace('_', ' ') + " with {target}{place_at}", "turns to " + verb.Replace('_', ' ') + " near {target}{place_at}" }
|
||||
: new[] { "sets about " + verb.Replace('_', ' ') + " through the living moment{place_at}", "turns to " + verb.Replace('_', ' ') + " with quiet purpose{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] Targeted(
|
||||
bool hasTarget,
|
||||
string withTargetA,
|
||||
string withTargetB,
|
||||
string noTargetA,
|
||||
string noTargetB)
|
||||
{
|
||||
return hasTarget
|
||||
? new[] { withTargetA, withTargetB }
|
||||
: new[] { noTargetA, noTargetB };
|
||||
}
|
||||
}
|
||||
212
IdleSpectator/ActivityAssetCatalog.cs
Normal file
212
IdleSpectator/ActivityAssetCatalog.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Live asset/task lookup and species-token hygiene. No voice classification.</summary>
|
||||
public static class ActivityAssetCatalog
|
||||
{
|
||||
private static HashSet<string> _liveSpeciesPlaceTokens;
|
||||
private static int _liveSpeciesPlaceTokensCount = -1;
|
||||
|
||||
public static ActorAsset TryGetActorAsset(string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return AssetManager.actor_library?.get(speciesId.Trim());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsLiveActorAssetId(string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return AssetManager.actor_library != null
|
||||
&& AssetManager.actor_library.has(speciesId.Trim());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string SpeciesDisplayLabel(ActorAsset asset)
|
||||
{
|
||||
if (asset == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string translated = asset.getTranslatedName();
|
||||
if (!string.IsNullOrEmpty(translated))
|
||||
{
|
||||
string localeId = asset.getLocaleID() ?? "";
|
||||
if (!translated.Equals(localeId, StringComparison.Ordinal)
|
||||
&& translated.IndexOf('_') < 0)
|
||||
{
|
||||
return translated.Trim().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Localization may not be ready during early harness setup.
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(asset.name_locale))
|
||||
{
|
||||
return asset.name_locale.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(asset.id) ? "" : asset.id.Replace('_', ' ').Trim();
|
||||
}
|
||||
|
||||
public static string SpeciesDisplayLabel(string speciesId)
|
||||
{
|
||||
ActorAsset asset = TryGetActorAsset(speciesId);
|
||||
return asset != null
|
||||
? SpeciesDisplayLabel(asset)
|
||||
: (string.IsNullOrEmpty(speciesId) ? "" : speciesId.Replace('_', ' ').Trim());
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveActorAssetIds()
|
||||
{
|
||||
var ids = new List<string>();
|
||||
try
|
||||
{
|
||||
ActorAssetLibrary lib = AssetManager.actor_library;
|
||||
if (lib?.list == null)
|
||||
{
|
||||
return ids;
|
||||
}
|
||||
|
||||
for (int i = 0; i < lib.list.Count; i++)
|
||||
{
|
||||
ActorAsset asset = lib.list[i];
|
||||
if (asset != null && !string.IsNullOrEmpty(asset.id))
|
||||
{
|
||||
ids.Add(asset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Library can be unavailable during early boot.
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveTaskIds()
|
||||
{
|
||||
var ids = new List<string>();
|
||||
try
|
||||
{
|
||||
object lib = typeof(AssetManager).GetField("tasks_actor")?.GetValue(null);
|
||||
object listObj = lib?.GetType().GetField("list")?.GetValue(lib)
|
||||
?? lib?.GetType().GetProperty("list")?.GetValue(lib, null);
|
||||
System.Collections.IList list = listObj as System.Collections.IList;
|
||||
if (list == null)
|
||||
{
|
||||
return ids;
|
||||
}
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
object task = list[i];
|
||||
object idObj = task?.GetType().GetField("id")?.GetValue(task)
|
||||
?? task?.GetType().GetProperty("id")?.GetValue(task, null);
|
||||
string id = idObj as string;
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Library can be unavailable during early boot.
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public static bool IsLiveSpeciesPlaceToken(string label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureLiveSpeciesPlaceTokens();
|
||||
return _liveSpeciesPlaceTokens != null
|
||||
&& _liveSpeciesPlaceTokens.Contains(label.Trim());
|
||||
}
|
||||
|
||||
private static void EnsureLiveSpeciesPlaceTokens()
|
||||
{
|
||||
try
|
||||
{
|
||||
ActorAssetLibrary lib = AssetManager.actor_library;
|
||||
if (lib?.list == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int count = lib.list.Count;
|
||||
if (_liveSpeciesPlaceTokens != null && _liveSpeciesPlaceTokensCount == count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tokens = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ActorAsset asset = lib.list[i];
|
||||
if (asset == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Add(tokens, asset.id);
|
||||
Add(tokens, asset.kingdom_id_wild);
|
||||
Add(tokens, asset.name_locale);
|
||||
if (!string.IsNullOrEmpty(asset.id))
|
||||
{
|
||||
Add(tokens, asset.id.Replace('_', ' '));
|
||||
}
|
||||
}
|
||||
|
||||
_liveSpeciesPlaceTokens = tokens;
|
||||
_liveSpeciesPlaceTokensCount = count;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep prior cache if any.
|
||||
}
|
||||
}
|
||||
|
||||
private static void Add(HashSet<string> tokens, string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
tokens.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
387
IdleSpectator/ActivityClauseAssembler.cs
Normal file
387
IdleSpectator/ActivityClauseAssembler.cs
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
using System;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Assembles one alive activity sentence from observed <see cref="ActivityContext"/> clauses.
|
||||
/// Packs observed slots (life, identity, role/job, act, target, carrying, place, combat).
|
||||
/// Traits salt wording variety only - never named in the line (dossier already shows them).
|
||||
/// </summary>
|
||||
public static class ActivityClauseAssembler
|
||||
{
|
||||
/// <summary>
|
||||
/// Build a template still containing tokens for <see cref="ActivityProse.Apply"/>.
|
||||
/// Optional <paramref name="polishTemplate"/> is celebrity/family/beat flavor - still enriched.
|
||||
/// </summary>
|
||||
public static string Assemble(
|
||||
ActivityContext ctx,
|
||||
string key,
|
||||
string rawFact,
|
||||
bool hasTarget,
|
||||
long subjectId,
|
||||
int lineIndex)
|
||||
{
|
||||
if (ctx == null)
|
||||
{
|
||||
ctx = ActivityContext.Empty;
|
||||
}
|
||||
|
||||
string action = ActivityActionComposer.Compose(
|
||||
ctx,
|
||||
key,
|
||||
rawFact,
|
||||
hasTarget,
|
||||
subjectId,
|
||||
lineIndex);
|
||||
|
||||
bool placeInAppositive = false;
|
||||
string subject = BuildSubject(ctx, action, out placeInAppositive);
|
||||
|
||||
if (placeInAppositive)
|
||||
{
|
||||
action = StripToken(action, "{place_at}");
|
||||
action = StripToken(action, "{job_as}");
|
||||
action = action.Replace(" at {place}", "");
|
||||
action = action.Replace(" into store{place}", " into store");
|
||||
action = StripToken(action, "{place}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Job already in appositive → drop trailing job_as; keep place_at on the verb.
|
||||
if (HasJobOrRole(ctx))
|
||||
{
|
||||
action = StripToken(action, "{job_as}");
|
||||
}
|
||||
}
|
||||
|
||||
// Traits stay on the dossier chips - they only salt variety below, never named here.
|
||||
|
||||
string pressure = BuildPressureClause(ctx, action);
|
||||
string line = subject + " " + action + pressure;
|
||||
return CollapseSpaces(line).Trim();
|
||||
}
|
||||
|
||||
private static string BuildSubject(ActivityContext ctx, string action, out bool placeInAppositive)
|
||||
{
|
||||
placeInAppositive = false;
|
||||
string life = ctx.IsChild ? "young " : "";
|
||||
|
||||
// Species stays on the dossier nametag (Name (species)) - never "the angle" / "the dog" here.
|
||||
// Taxonomy still drives MannerTag variety in the action phrase.
|
||||
string identity = "{actor}";
|
||||
|
||||
string appositive = BuildAppositive(ctx, action, out placeInAppositive);
|
||||
if (string.IsNullOrEmpty(appositive))
|
||||
{
|
||||
return life + identity;
|
||||
}
|
||||
|
||||
return life + identity + ", " + appositive + ",";
|
||||
}
|
||||
|
||||
private static string BuildAppositive(ActivityContext ctx, string action, out bool placeInAppositive)
|
||||
{
|
||||
placeInAppositive = false;
|
||||
string roleOrJob = RoleOrJobNoun(ctx);
|
||||
string place = SafePlace(ctx);
|
||||
|
||||
// Traits are dossier chrome - never spell them in the activity line appositive.
|
||||
if (string.IsNullOrEmpty(roleOrJob))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Already named in the action (rare polish) - skip duplicate appositive.
|
||||
if (action.IndexOf(roleOrJob, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string head = "a " + roleOrJob;
|
||||
bool actionHasPlace = action.IndexOf("{place", StringComparison.Ordinal) >= 0
|
||||
|| (!string.IsNullOrEmpty(place)
|
||||
&& action.IndexOf(place, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
if (!string.IsNullOrEmpty(place) && !actionHasPlace)
|
||||
{
|
||||
placeInAppositive = true;
|
||||
return head + " of " + place;
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
private static string BuildPressureClause(ActivityContext ctx, string action)
|
||||
{
|
||||
if (!ctx.InCombat && !ctx.HasAttackTarget)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (action.IndexOf("fight", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("clash", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("attack", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("blow", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (ctx.TargetIsActor && !string.IsNullOrEmpty(ctx.TargetName)
|
||||
&& action.IndexOf("{target}", StringComparison.Ordinal) < 0)
|
||||
{
|
||||
return " while locked on {target}";
|
||||
}
|
||||
|
||||
return " under combat pressure";
|
||||
}
|
||||
|
||||
private static bool HasJobOrRole(ActivityContext ctx)
|
||||
{
|
||||
return !string.IsNullOrEmpty(RoleOrJobNoun(ctx));
|
||||
}
|
||||
|
||||
private static string StripUnusedTargetClauses(string action)
|
||||
{
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string t = action;
|
||||
t = t.Replace(" with {target}", "");
|
||||
t = t.Replace(" on {target}", "");
|
||||
t = t.Replace(" toward {target}", "");
|
||||
t = t.Replace(" after {target}", "");
|
||||
t = t.Replace(" at {target}", "");
|
||||
t = t.Replace(" {target}", "");
|
||||
t = t.Replace("{target}", "");
|
||||
return t.Trim();
|
||||
}
|
||||
|
||||
/// <summary>Drop species tokens / "the dog" leftovers - dossier already shows species.</summary>
|
||||
private static string StripSpeciesTokens(string action, ActivityContext ctx)
|
||||
{
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string t = action;
|
||||
t = t.Replace(" the {species}", "");
|
||||
t = t.Replace("{species}", "");
|
||||
|
||||
if (ctx != null)
|
||||
{
|
||||
string word = !string.IsNullOrEmpty(ctx.SpeciesLabel)
|
||||
? ctx.SpeciesLabel.Trim()
|
||||
: (ctx.SpeciesId ?? "").Replace('_', ' ').Trim();
|
||||
if (!string.IsNullOrEmpty(word) && word.Length > 1)
|
||||
{
|
||||
t = ReplaceIgnoreCase(t, " the " + word, "");
|
||||
}
|
||||
}
|
||||
|
||||
return t.Trim();
|
||||
}
|
||||
|
||||
public static string StripSpeciesTokensPublic(string action, ActivityContext ctx)
|
||||
{
|
||||
return StripSpeciesTokens(action, ctx);
|
||||
}
|
||||
|
||||
private static string ReplaceIgnoreCase(string hay, string needle, string replacement)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hay) || string.IsNullOrEmpty(needle))
|
||||
{
|
||||
return hay;
|
||||
}
|
||||
|
||||
int idx = hay.IndexOf(needle, StringComparison.OrdinalIgnoreCase);
|
||||
if (idx < 0)
|
||||
{
|
||||
return hay;
|
||||
}
|
||||
|
||||
return hay.Substring(0, idx) + replacement + hay.Substring(idx + needle.Length);
|
||||
}
|
||||
|
||||
private static string RoleOrJobNoun(ActivityContext ctx)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ctx.RoleLabel))
|
||||
{
|
||||
return ctx.RoleLabel.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
return ActivityProse.HumanizeJobPublic(ctx.JobId);
|
||||
}
|
||||
|
||||
/// <summary>Public salt so Format can mix trait into polish pick without naming the trait.</summary>
|
||||
public static long TraitVarietySaltPublic(ActivityContext ctx)
|
||||
{
|
||||
return TraitVarietySalt(ctx);
|
||||
}
|
||||
|
||||
private static long TraitVarietySalt(ActivityContext ctx)
|
||||
{
|
||||
if (ctx == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
string id = ctx.TopTraitId;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
id = ctx.TopTraitLabel;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return id.GetHashCode();
|
||||
}
|
||||
|
||||
private static string SafePlace(ActivityContext ctx)
|
||||
{
|
||||
string place = ctx.PlaceLabel ?? "";
|
||||
if (string.IsNullOrEmpty(place))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (ActivityInterestTable.LooksLikeSpeciesPublic(place, ctx.SpeciesId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return place.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Peel subject prefixes from celebrity/family templates so the assembler owns identity clauses.
|
||||
/// </summary>
|
||||
public static string StripSubject(string template)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string t = template.Trim();
|
||||
if (t.StartsWith("young ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
t = t.Substring("young ".Length).TrimStart();
|
||||
}
|
||||
|
||||
if (t.StartsWith("{actor} the {species}", StringComparison.Ordinal))
|
||||
{
|
||||
t = t.Substring("{actor} the {species}".Length).TrimStart();
|
||||
}
|
||||
else if (t.StartsWith("{actor}", StringComparison.Ordinal))
|
||||
{
|
||||
t = t.Substring("{actor}".Length).TrimStart();
|
||||
// "{actor} the dog tumbles..." → "the dog tumbles..." → "tumbles..."
|
||||
if (t.StartsWith("the ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int sp = t.IndexOf(' ', 4);
|
||||
if (sp > 4)
|
||||
{
|
||||
string after = t.Substring(sp).TrimStart();
|
||||
if (!string.IsNullOrEmpty(after) && char.IsLower(after[0]))
|
||||
{
|
||||
t = after;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t.Trim();
|
||||
}
|
||||
|
||||
private static bool LooksLikeActionPhrase(string phrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(phrase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if it still looks like a full subject line.
|
||||
if (phrase.StartsWith("{actor}", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Measure substance without optional clause tokens - "{actor} plays{place_at}"
|
||||
// must not count as a living action once place is empty.
|
||||
string substance = phrase
|
||||
.Replace("{place_at}", "")
|
||||
.Replace("{job_as}", "")
|
||||
.Replace("{with}", "")
|
||||
.Replace("{place}", "")
|
||||
.Replace("{job}", "")
|
||||
.Replace("{target}", "")
|
||||
.Replace("{carrying}", "")
|
||||
.Replace("{species}", "")
|
||||
.Trim();
|
||||
while (substance.IndexOf(" ", StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
substance = substance.Replace(" ", " ");
|
||||
}
|
||||
|
||||
if (substance.Length < 12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Single-token leftovers like "plays" / "waits" are not alive enough.
|
||||
if (substance.IndexOf(' ') < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string StripToken(string text, string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(token))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Replace(token, "");
|
||||
}
|
||||
|
||||
private static string CollapseSpaces(string t)
|
||||
{
|
||||
if (string.IsNullOrEmpty(t))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
while (t.IndexOf(" ", StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
t = t.Replace(" ", " ");
|
||||
}
|
||||
|
||||
t = t.Replace(" ,", ",");
|
||||
t = t.Replace(",,", ",");
|
||||
return t;
|
||||
}
|
||||
|
||||
private static string LowerFirst(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (s.Length == 1)
|
||||
{
|
||||
return s.ToLowerInvariant();
|
||||
}
|
||||
|
||||
return char.ToLowerInvariant(s[0]) + s.Substring(1);
|
||||
}
|
||||
}
|
||||
128
IdleSpectator/ActivityContext.cs
Normal file
128
IdleSpectator/ActivityContext.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of live actor facts for activity prose. Only observed fields - never invent events.
|
||||
/// Species / taxonomy / flags come from <see cref="Actor.asset"/> for every unit in the game.
|
||||
/// </summary>
|
||||
public sealed class ActivityContext
|
||||
{
|
||||
public string ActorName = "";
|
||||
/// <summary>Raw actor asset id (frog, human, beetle, ...).</summary>
|
||||
public string SpeciesId = "";
|
||||
/// <summary>Recovered living/body source for meaningful transformed variants.</summary>
|
||||
public string BaseSpeciesId = "";
|
||||
/// <summary>Readable species word for prose (locale / asset id).</summary>
|
||||
public string SpeciesLabel = "";
|
||||
public string Family = ActivityVoiceFamilies.Default;
|
||||
public string ModifierTag = "";
|
||||
public ProseVoice Voice;
|
||||
public string TaxonomicKingdom = "";
|
||||
public string TaxonomicPhylum = "";
|
||||
public string TaxonomicClass = "";
|
||||
public string TaxonomicOrder = "";
|
||||
public string TaxonomicFamily = "";
|
||||
public string TaxonomicGenus = "";
|
||||
public string TaxonomicSpecies = "";
|
||||
public bool IsHumanoid;
|
||||
public bool IsCiv;
|
||||
/// <summary>From <see cref="ActorAsset.default_animal"/> - animal tone when not civ humanoid.</summary>
|
||||
public bool IsAnimal;
|
||||
public int Age;
|
||||
public bool IsChild;
|
||||
public string JobId = "";
|
||||
public string CityName = "";
|
||||
public string KingdomName = "";
|
||||
public bool IsKing;
|
||||
public bool IsLeader;
|
||||
public bool IsWarrior;
|
||||
/// <summary>Observed role noun: king / leader / warrior when flags are true.</summary>
|
||||
public string RoleLabel = "";
|
||||
public string TargetName = "";
|
||||
public bool TargetIsActor;
|
||||
/// <summary>Building, city, kingdom, or soft place label for clauses.</summary>
|
||||
public string PlaceLabel = "";
|
||||
public string CarryingLabel = "";
|
||||
public bool InCombat;
|
||||
public bool HasAttackTarget;
|
||||
/// <summary>Observed trait id - salts prose variety only; never spelled in the activity line.</summary>
|
||||
public string TopTraitId = "";
|
||||
/// <summary>Humanized trait label for salt/debug - dossier chips show the trait to the player.</summary>
|
||||
public string TopTraitLabel = "";
|
||||
/// <summary>Locomotion / body manner from live taxonomy/flags (insect, amphibian, flying, ...).</summary>
|
||||
public string MannerTag = "default";
|
||||
public string TaskKey = "";
|
||||
|
||||
public static ActivityContext Empty { get; } = new ActivityContext();
|
||||
|
||||
/// <summary>Harness / ForceNote: minimal context; species resolved from live library when present.</summary>
|
||||
public static ActivityContext ForHarness(
|
||||
string actorName,
|
||||
string targetName,
|
||||
string speciesId = "",
|
||||
string place = "",
|
||||
string carrying = "",
|
||||
string jobId = "",
|
||||
string taskKey = "",
|
||||
string traitId = "",
|
||||
bool isChild = false,
|
||||
bool inCombat = false,
|
||||
bool isKing = false,
|
||||
bool isLeader = false,
|
||||
bool isWarrior = false)
|
||||
{
|
||||
ActivityContext ctx = new ActivityContext
|
||||
{
|
||||
ActorName = actorName ?? "",
|
||||
TargetName = targetName ?? "",
|
||||
TargetIsActor = !string.IsNullOrEmpty(targetName),
|
||||
SpeciesId = speciesId ?? "",
|
||||
PlaceLabel = place ?? "",
|
||||
CarryingLabel = carrying ?? "",
|
||||
JobId = jobId ?? "",
|
||||
TaskKey = taskKey ?? "",
|
||||
TopTraitId = traitId ?? "",
|
||||
IsChild = isChild,
|
||||
InCombat = inCombat,
|
||||
HasAttackTarget = inCombat && !string.IsNullOrEmpty(targetName),
|
||||
IsKing = isKing,
|
||||
IsLeader = isLeader,
|
||||
IsWarrior = isWarrior
|
||||
};
|
||||
|
||||
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(ctx.SpeciesId);
|
||||
if (asset != null)
|
||||
{
|
||||
ActivityVoiceResolver.ApplyToContext(ctx, asset);
|
||||
ctx.SpeciesLabel = ActivityAssetCatalog.SpeciesDisplayLabel(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.SpeciesLabel = ActivityAssetCatalog.SpeciesDisplayLabel(ctx.SpeciesId);
|
||||
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);
|
||||
ctx.IsCiv = ctx.Voice.BaseIsCiv;
|
||||
ctx.IsHumanoid = ctx.Voice.BaseIsHumanoid;
|
||||
ctx.IsAnimal = ctx.Voice.BaseIsAnimal;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(taskKey)
|
||||
&& taskKey.StartsWith("child_", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ctx.IsChild = true;
|
||||
}
|
||||
|
||||
// Strip species-like place labels the same way live BuildContext does.
|
||||
if (!string.IsNullOrEmpty(ctx.PlaceLabel)
|
||||
&& ActivityInterestTable.LooksLikeSpeciesPublic(ctx.PlaceLabel, ctx.SpeciesId))
|
||||
{
|
||||
ctx.PlaceLabel = "";
|
||||
}
|
||||
|
||||
ctx.RoleLabel = ActivityInterestTable.DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior);
|
||||
ctx.TopTraitLabel = ActivityInterestTable.TraitLabelFromId(ctx.TopTraitId);
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
|
@ -294,6 +295,26 @@ public static class ActivityInterestTable
|
|||
{
|
||||
placeLabel = "the wilds";
|
||||
}
|
||||
|
||||
// Prefer settlement names when no building/tile label won.
|
||||
if (string.IsNullOrEmpty(placeLabel) && string.IsNullOrEmpty(targetName))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (actor.city != null && !string.IsNullOrEmpty(actor.city.name))
|
||||
{
|
||||
placeLabel = actor.city.name;
|
||||
}
|
||||
else if (actor.kingdom != null && !string.IsNullOrEmpty(actor.kingdom.name))
|
||||
{
|
||||
placeLabel = actor.kingdom.name;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -319,6 +340,493 @@ public static class ActivityInterestTable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Build a prose context snapshot from a live actor.</summary>
|
||||
public static ActivityContext BuildContext(Actor actor, string taskKeyHint = null)
|
||||
{
|
||||
ActivityContext ctx = new ActivityContext();
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return ctx;
|
||||
}
|
||||
|
||||
ctx.ActorName = ActorName(actor);
|
||||
try
|
||||
{
|
||||
if (actor.asset != null)
|
||||
{
|
||||
ActorAsset asset = actor.asset;
|
||||
ActivityVoiceResolver.ApplyToContext(ctx, asset);
|
||||
ctx.SpeciesLabel = ActivityAssetCatalog.SpeciesDisplayLabel(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.SpeciesId = "";
|
||||
ctx.SpeciesLabel = "";
|
||||
ctx.Family = ActivityVoiceFamilies.Default;
|
||||
ctx.Voice = ProseVoice.Unknown();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx.SpeciesId = "";
|
||||
ctx.SpeciesLabel = "";
|
||||
ctx.Family = ActivityVoiceFamilies.Default;
|
||||
ctx.Voice = ProseVoice.Unknown();
|
||||
}
|
||||
|
||||
if (ctx.Voice == null)
|
||||
{
|
||||
ctx.Voice = ActivityVoiceResolver.Resolve(ctx.SpeciesId);
|
||||
ctx.Family = ctx.Voice.BaseFamily;
|
||||
}
|
||||
|
||||
ctx.TaskKey = taskKeyHint ?? SafeTaskId(actor);
|
||||
ctx.JobId = SafeJobId(actor);
|
||||
|
||||
try
|
||||
{
|
||||
ctx.Age = actor.getAge();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx.Age = 0;
|
||||
}
|
||||
|
||||
ctx.IsChild = false;
|
||||
if (!string.IsNullOrEmpty(ctx.TaskKey)
|
||||
&& ctx.TaskKey.StartsWith("child_", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ctx.IsChild = true;
|
||||
}
|
||||
else if (ctx.Age > 0 && ctx.Age < 12
|
||||
&& (ctx.IsHumanoid || ctx.Family == ActivityVoiceFamilies.Humanoid))
|
||||
{
|
||||
// Age bands are reliable for civ humanoids; animals often stay "young" by year count.
|
||||
ctx.IsChild = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ctx.CityName = actor.city != null ? (actor.city.name ?? "") : "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx.CityName = "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ctx.KingdomName = actor.kingdom != null ? (actor.kingdom.name ?? "") : "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx.KingdomName = "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ctx.IsKing = actor.isKing();
|
||||
ctx.IsLeader = actor.isCityLeader();
|
||||
ctx.IsWarrior = actor.is_profession_warrior;
|
||||
ctx.HasAttackTarget = actor.has_attack_target;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (actor.hasTask() && actor.ai?.task != null)
|
||||
{
|
||||
ctx.InCombat = actor.ai.task.in_combat || actor.has_attack_target;
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.InCombat = actor.has_attack_target;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx.InCombat = ctx.HasAttackTarget;
|
||||
}
|
||||
|
||||
ResolveTarget(actor, out string targetName, out bool targetIsActor, out string place);
|
||||
ctx.TargetName = targetName;
|
||||
ctx.TargetIsActor = targetIsActor;
|
||||
ctx.PlaceLabel = PickPlaceLabel(place, ctx.CityName, ctx.KingdomName, ctx.SpeciesId);
|
||||
|
||||
ctx.CarryingLabel = TryGetCarryingLabel(actor);
|
||||
ctx.TopTraitId = TryTopInterestingTraitId(actor);
|
||||
ctx.TopTraitLabel = TraitLabelFromId(ctx.TopTraitId);
|
||||
ctx.RoleLabel = DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static string DeriveRoleLabel(bool isKing, bool isLeader, bool isWarrior)
|
||||
{
|
||||
if (isKing)
|
||||
{
|
||||
return "king";
|
||||
}
|
||||
|
||||
if (isLeader)
|
||||
{
|
||||
return "leader";
|
||||
}
|
||||
|
||||
if (isWarrior)
|
||||
{
|
||||
return "warrior";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>Humanize a live trait id the same way the dossier does - manner only.</summary>
|
||||
public static string TraitLabelFromId(string traitId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(traitId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ActorTrait trait = AssetManager.traits?.get(traitId.Trim());
|
||||
if (trait != null)
|
||||
{
|
||||
string locale = trait.getLocaleID();
|
||||
if (!string.IsNullOrEmpty(locale))
|
||||
{
|
||||
string localized = LocalizedTextManager.getText(locale);
|
||||
if (!string.IsNullOrEmpty(localized) && localized != locale)
|
||||
{
|
||||
return localized.Trim().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to id humanize
|
||||
}
|
||||
|
||||
string s = traitId.Replace('_', ' ').Trim().ToLowerInvariant();
|
||||
if (s.StartsWith("trait ", StringComparison.Ordinal))
|
||||
{
|
||||
s = s.Substring(6).Trim();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Real places only. Reject species ids, family/taxonomy buckets, and soft markers.
|
||||
/// Wild kingdoms are often named after a species or type (buffalo, insect) - those are not places.
|
||||
/// </summary>
|
||||
public static string PickPlaceLabel(
|
||||
string resolvedPlace,
|
||||
string cityName,
|
||||
string kingdomName,
|
||||
string speciesId)
|
||||
{
|
||||
if (IsUsablePlace(cityName, speciesId))
|
||||
{
|
||||
return cityName.Trim();
|
||||
}
|
||||
|
||||
if (IsConcreteResolvedPlace(resolvedPlace, speciesId))
|
||||
{
|
||||
return resolvedPlace.Trim();
|
||||
}
|
||||
|
||||
// Kingdom only when it reads like a settlement/realm name, not a species bucket.
|
||||
if (IsUsablePlace(kingdomName, speciesId) && LooksLikeRealmName(kingdomName))
|
||||
{
|
||||
return kingdomName.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(resolvedPlace)
|
||||
&& resolvedPlace.Equals("the wilds", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "the wilds";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static bool IsUsablePlace(string name, string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !LooksLikeSpeciesLabel(name, speciesId);
|
||||
}
|
||||
|
||||
private static bool IsConcreteResolvedPlace(string place, string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(place))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (place.Equals("their mark", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| place.Equals("the wilds", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !LooksLikeSpeciesLabel(place, speciesId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Civ kingdom/city names tend to be multi-word or proper-cased inventions.
|
||||
/// Single taxonomic tokens like "insect" fail this check.
|
||||
/// </summary>
|
||||
private static bool LooksLikeRealmName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string n = name.Trim();
|
||||
if (n.IndexOf(' ') >= 0 || n.IndexOf('-') >= 0 || n.IndexOf('\'') >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Single token: allow only if it does not look like a plain species/type word.
|
||||
// Realms like "Rome" pass; "insect" / "buffalo" fail via LooksLikeSpeciesLabel already,
|
||||
// but also reject short all-lowercase taxonomy leftovers here.
|
||||
if (n.Length <= 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasUpper = false;
|
||||
for (int i = 0; i < n.Length; i++)
|
||||
{
|
||||
if (char.IsUpper(n[i]))
|
||||
{
|
||||
hasUpper = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpper;
|
||||
}
|
||||
|
||||
public static bool LooksLikeSpeciesPublic(string label, string speciesId)
|
||||
{
|
||||
return LooksLikeSpeciesLabel(label, speciesId);
|
||||
}
|
||||
|
||||
private static readonly string[] TaxonomyBuckets =
|
||||
{
|
||||
"insect", "insects", "animal", "animals", "beast", "beasts", "creature", "creatures",
|
||||
"monster", "monsters", "undead", "nature", "wildlife", "civilian", "civilians",
|
||||
"unit", "units", "boat", "boats", "humanoid", "livestock", "bird", "birds",
|
||||
"canine", "feline", "aquatic", "default", "wild"
|
||||
};
|
||||
|
||||
private static bool LooksLikeSpeciesLabel(string label, string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string a = label.Trim();
|
||||
string aLower = a.ToLowerInvariant();
|
||||
|
||||
for (int i = 0; i < TaxonomyBuckets.Length; i++)
|
||||
{
|
||||
if (aLower.Equals(TaxonomyBuckets[i], System.StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(speciesId))
|
||||
{
|
||||
string b = speciesId.Trim();
|
||||
if (a.Equals(b, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string bHuman = b.Replace('_', ' ');
|
||||
if (a.Equals(bHuman, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string family = ActivityVoiceResolver.Resolve(speciesId).BaseFamily;
|
||||
if (!string.IsNullOrEmpty(family)
|
||||
&& a.Equals(family, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Live actor library: any asset id / wild kingdom / locale name is not a place.
|
||||
if (ActivityAssetCatalog.IsLiveSpeciesPlaceToken(a)
|
||||
|| ActivityAssetCatalog.IsLiveActorAssetId(aLower))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string TryGetCarryingLabel(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!actor.isCarryingResources())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ActorBag inv = actor.inventory;
|
||||
string id = "";
|
||||
try
|
||||
{
|
||||
id = inv.getItemIDToRender() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
id = "";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
try
|
||||
{
|
||||
id = inv.getRandomResourceID() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
id = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
return SanitizeCarrying(HumanizeResourceId(id));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return "what they carry";
|
||||
}
|
||||
|
||||
private static string HumanizeResourceId(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return id.Replace('_', ' ').Trim();
|
||||
}
|
||||
|
||||
private static string SanitizeCarrying(string tip)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tip))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string s = tip.Replace("\n", " ").Replace("\r", " ").Trim();
|
||||
// Strip simple rich-text tags if present.
|
||||
while (true)
|
||||
{
|
||||
int open = s.IndexOf('<');
|
||||
if (open < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int close = s.IndexOf('>', open);
|
||||
if (close < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
s = s.Remove(open, close - open + 1);
|
||||
}
|
||||
|
||||
s = s.Trim();
|
||||
if (s.Length > 48)
|
||||
{
|
||||
s = s.Substring(0, 45).Trim() + "...";
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private static string TryTopInterestingTraitId(Actor actor)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (actor == null || !actor.hasTraits())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
foreach (ActorTrait trait in actor.getTraits())
|
||||
{
|
||||
if (trait == null || string.IsNullOrEmpty(trait.id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip default species traits when detectable.
|
||||
try
|
||||
{
|
||||
if (actor.asset != null
|
||||
&& trait.default_for_actor_assets != null
|
||||
&& trait.default_for_actor_assets.Contains(actor.asset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return trait.id;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string hay, params string[] needles)
|
||||
{
|
||||
for (int i = 0; i < needles.Length; i++)
|
||||
|
|
|
|||
|
|
@ -167,10 +167,12 @@ public static class ActivityLog
|
|||
}
|
||||
|
||||
LastTaskId[id] = taskId;
|
||||
string jobId = ActivityInterestTable.SafeJobId(actor);
|
||||
ActivityInterestTable.ResolveTarget(
|
||||
actor, out string targetName, out bool targetIsActor, out string place);
|
||||
string actorName = ActivityInterestTable.ActorName(actor);
|
||||
ActivityContext ctx = ActivityInterestTable.BuildContext(actor, taskId);
|
||||
string jobId = ctx.JobId;
|
||||
string actorName = ctx.ActorName;
|
||||
string targetName = ctx.TargetName;
|
||||
bool targetIsActor = ctx.TargetIsActor;
|
||||
string place = ctx.PlaceLabel;
|
||||
string loc = "";
|
||||
try
|
||||
{
|
||||
|
|
@ -195,10 +197,7 @@ public static class ActivityLog
|
|||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
taskId,
|
||||
actorName,
|
||||
targetName,
|
||||
targetIsActor,
|
||||
place,
|
||||
ctx,
|
||||
raw,
|
||||
id,
|
||||
idx,
|
||||
|
|
@ -267,15 +266,31 @@ public static class ActivityLog
|
|||
}
|
||||
|
||||
LastBeatAt[id] = now;
|
||||
ActivityContext ctx = ActivityInterestTable.BuildContext(actor, actionKey);
|
||||
string taskId = ActivityInterestTable.SafeTaskId(actor);
|
||||
string jobId = ActivityInterestTable.SafeJobId(actor);
|
||||
string actorName = ActivityInterestTable.ActorName(actor);
|
||||
ActivityInterestTable.ResolveTarget(
|
||||
actor, out string resolvedName, out bool targetIsActor, out string place);
|
||||
if (string.IsNullOrEmpty(resolvedName) && !string.IsNullOrEmpty(target))
|
||||
string jobId = ctx.JobId;
|
||||
if (string.IsNullOrEmpty(ctx.TargetName) && !string.IsNullOrEmpty(target))
|
||||
{
|
||||
resolvedName = target;
|
||||
targetIsActor = true;
|
||||
ctx.TargetName = target;
|
||||
ctx.TargetIsActor = true;
|
||||
}
|
||||
|
||||
if (actionKey == "BehUnloadResources" || actionKey == "BehThrowResources")
|
||||
{
|
||||
if (string.IsNullOrEmpty(ctx.CarryingLabel))
|
||||
{
|
||||
ctx.CarryingLabel = ActivityInterestTable.TryGetCarryingLabel(actor);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ctx.CarryingLabel))
|
||||
{
|
||||
ctx.CarryingLabel = "goods";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ctx.PlaceLabel))
|
||||
{
|
||||
ctx.PlaceLabel = !string.IsNullOrEmpty(ctx.CityName) ? ctx.CityName : "town";
|
||||
}
|
||||
}
|
||||
|
||||
string fact = string.IsNullOrEmpty(rawFact) ? actionKey : rawFact;
|
||||
|
|
@ -283,10 +298,7 @@ public static class ActivityLog
|
|||
ActivityProse.Format(
|
||||
ActivityKind.ActionBeat,
|
||||
actionKey,
|
||||
actorName,
|
||||
resolvedName,
|
||||
targetIsActor,
|
||||
place,
|
||||
ctx,
|
||||
fact,
|
||||
id,
|
||||
idx,
|
||||
|
|
@ -300,7 +312,9 @@ public static class ActivityLog
|
|||
Kind = ActivityKind.ActionBeat,
|
||||
TaskId = string.IsNullOrEmpty(taskId) ? actionKey : taskId,
|
||||
JobId = jobId,
|
||||
TargetLabel = !string.IsNullOrEmpty(resolvedName) ? resolvedName : (place ?? ""),
|
||||
TargetLabel = !string.IsNullOrEmpty(ctx.TargetName)
|
||||
? ctx.TargetName
|
||||
: (ctx.PlaceLabel ?? ""),
|
||||
Line = fact,
|
||||
DisplayLine = display,
|
||||
DisplayLineRich = displayRich
|
||||
|
|
@ -314,7 +328,17 @@ public static class ActivityLog
|
|||
string rawLine,
|
||||
bool asBeat = false,
|
||||
string actorName = "",
|
||||
string targetName = "")
|
||||
string targetName = "",
|
||||
string speciesId = "",
|
||||
string place = "",
|
||||
string carrying = "",
|
||||
string jobId = "",
|
||||
string traitId = "",
|
||||
bool isChild = false,
|
||||
bool inCombat = false,
|
||||
bool isKing = false,
|
||||
bool isLeader = false,
|
||||
bool isWarrior = false)
|
||||
{
|
||||
if (subjectId == 0 || string.IsNullOrEmpty(taskOrAction))
|
||||
{
|
||||
|
|
@ -324,13 +348,122 @@ public static class ActivityLog
|
|||
int idx = NextIndex(subjectId);
|
||||
string fact = string.IsNullOrEmpty(rawLine) ? taskOrAction : rawLine;
|
||||
ActivityKind kind = asBeat ? ActivityKind.ActionBeat : ActivityKind.TaskStart;
|
||||
|
||||
// Prefer live focus context when available, then apply harness overrides.
|
||||
ActivityContext ctx = ActivityContext.Empty;
|
||||
try
|
||||
{
|
||||
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null
|
||||
&& MoveCamera._focus_unit.getID() == subjectId)
|
||||
{
|
||||
ctx = ActivityInterestTable.BuildContext(MoveCamera._focus_unit, taskOrAction);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx = ActivityContext.Empty;
|
||||
}
|
||||
|
||||
if (ctx == null || string.IsNullOrEmpty(ctx.ActorName))
|
||||
{
|
||||
ctx = ActivityContext.ForHarness(
|
||||
actorName,
|
||||
targetName,
|
||||
speciesId,
|
||||
place,
|
||||
carrying,
|
||||
jobId,
|
||||
taskOrAction,
|
||||
traitId,
|
||||
isChild,
|
||||
inCombat,
|
||||
isKing,
|
||||
isLeader,
|
||||
isWarrior);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(actorName))
|
||||
{
|
||||
ctx.ActorName = actorName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(targetName))
|
||||
{
|
||||
ctx.TargetName = targetName;
|
||||
ctx.TargetIsActor = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(speciesId))
|
||||
{
|
||||
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(speciesId);
|
||||
if (asset != null)
|
||||
{
|
||||
ActivityVoiceResolver.ApplyToContext(ctx, asset);
|
||||
ctx.SpeciesLabel = ActivityAssetCatalog.SpeciesDisplayLabel(asset);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.SpeciesId = speciesId;
|
||||
ctx.SpeciesLabel = ActivityAssetCatalog.SpeciesDisplayLabel(speciesId);
|
||||
ctx.Voice = ActivityVoiceResolver.Resolve(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);
|
||||
ctx.IsCiv = ctx.Voice.BaseIsCiv;
|
||||
ctx.IsHumanoid = ctx.Voice.BaseIsHumanoid;
|
||||
ctx.IsAnimal = ctx.Voice.BaseIsAnimal;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(place))
|
||||
{
|
||||
ctx.PlaceLabel = place;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(carrying))
|
||||
{
|
||||
ctx.CarryingLabel = carrying;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(jobId))
|
||||
{
|
||||
ctx.JobId = jobId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(traitId))
|
||||
{
|
||||
ctx.TopTraitId = traitId;
|
||||
ctx.TopTraitLabel = ActivityInterestTable.TraitLabelFromId(traitId);
|
||||
}
|
||||
|
||||
if (isChild)
|
||||
{
|
||||
ctx.IsChild = true;
|
||||
}
|
||||
|
||||
if (inCombat)
|
||||
{
|
||||
ctx.InCombat = true;
|
||||
ctx.HasAttackTarget = ctx.HasAttackTarget || ctx.TargetIsActor;
|
||||
}
|
||||
|
||||
if (isKing || isLeader || isWarrior)
|
||||
{
|
||||
ctx.IsKing = isKing || ctx.IsKing;
|
||||
ctx.IsLeader = isLeader || ctx.IsLeader;
|
||||
ctx.IsWarrior = isWarrior || ctx.IsWarrior;
|
||||
ctx.RoleLabel = ActivityInterestTable.DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior);
|
||||
}
|
||||
|
||||
ctx.TaskKey = taskOrAction;
|
||||
}
|
||||
|
||||
ActivityProse.Format(
|
||||
kind,
|
||||
taskOrAction,
|
||||
actorName,
|
||||
targetName,
|
||||
!string.IsNullOrEmpty(targetName),
|
||||
"",
|
||||
ctx,
|
||||
fact,
|
||||
subjectId,
|
||||
idx,
|
||||
|
|
@ -342,6 +475,7 @@ public static class ActivityLog
|
|||
SubjectId = subjectId,
|
||||
Kind = kind,
|
||||
TaskId = taskOrAction,
|
||||
JobId = ctx.JobId ?? "",
|
||||
TargetLabel = targetName ?? "",
|
||||
Line = fact,
|
||||
DisplayLine = display,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
75
IdleSpectator/ActivityVerbMap.cs
Normal file
75
IdleSpectator/ActivityVerbMap.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Maps every live task shape to an action concept; no creature voice logic.</summary>
|
||||
public static class ActivityVerbMap
|
||||
{
|
||||
public static string Resolve(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return "";
|
||||
string k = key.ToLowerInvariant();
|
||||
if (k.Contains("laugh")) return "laugh";
|
||||
if (k.Contains("sing") || k.Contains("just_sang")) return "sing";
|
||||
if ((k.Contains("play") && !k.Contains("display"))
|
||||
|| k.Contains("fun_move") || k.Contains("frolic") || k.StartsWith("child_")) return "play";
|
||||
if (k.Contains("hunt") || k == "behattackactorhuntingtarget") return "hunt";
|
||||
if (k.Contains("fight") || k.Contains("attack") || k.Contains("tantrum")) return "fight";
|
||||
if (k.Contains("social") || k.Contains("talk") || k.Contains("trysocialize")
|
||||
|| k.Contains("crying") || k.Contains("just_cried") || k.Contains("swore")
|
||||
|| k.Contains("madness")) return "social";
|
||||
if (k.Contains("lover") || k.Contains("breed") || k.Contains("reproduction")
|
||||
|| k.Contains("desire")) return "lover";
|
||||
if (k.Contains("farm") || k.Contains("plant") || k.Contains("fertiliz")
|
||||
|| k.Contains("harvest") || k.Contains("hoe")) return "farm";
|
||||
if (k == "work" || k.Contains("mine") || k.Contains("woodcut") || k.Contains("chop")
|
||||
|| k.Contains("gather") || k.Contains("forag") || k.Contains("build")
|
||||
|| k.Contains("construct") || k.Contains("collect_") || k.Contains("cleaning")
|
||||
|| k.Contains("make_items") || k.Contains("make_skeleton") || k.Contains("manure")
|
||||
|| k.Contains("burn_tumor") || k == "axe" || k == "hammer" || k == "basket"
|
||||
|| k == "bucket" || k == "flag" || k == "book" || k == "coffee_cup") return "work";
|
||||
if (k.Contains("pollinat") || k.Contains("bee_") || k == "behpollinate") return "pollinate";
|
||||
if (k.Contains("trade") || k.Contains("barter") || k.Contains("tax")
|
||||
|| k.Contains("boat_trading")) return "trade";
|
||||
if (k.Contains("fish")) return "fish";
|
||||
if (k.Contains("unload") || k.Contains("throw_resource") || k.Contains("carrying")) return "haul";
|
||||
if (k.Contains("heal") || k.Contains("cure")) return "heal";
|
||||
if (k.Contains("fire") || k.Contains("fireman") || k.Contains("blaze")) return "fire";
|
||||
if (k.Contains("sleep") || k.Contains("wakeup") || k.Contains("burrow")) return "sleep";
|
||||
if (k.Contains("eat") || k.Contains("food") || k.Contains("meal") || k.StartsWith("diet_")) return "eat";
|
||||
if (k.Contains("run_away") || k.Contains("flee") || k.Contains("retreat")
|
||||
|| k.Contains("danger_check") || k.Contains("banish") || k.Contains("kill_unruly")) return "flee";
|
||||
if (k.Contains("job") || k.Contains("employ") || k.Contains("find_house")
|
||||
|| k.Contains("claim") || k.Contains("king_") || k.Contains("leader_")
|
||||
|| k.Contains("citizen") || k.Contains("join_city") || k.Contains("check_city")
|
||||
|| k.Contains("check_plot") || k.Contains("family_check") || k.Contains("embark")
|
||||
|| k.Contains("boat_return") || k.Contains("force_into_a_boat")
|
||||
|| k.Contains("boat_transport") || k.Contains("boat_check") || k.Contains("investigate"))
|
||||
return "settle";
|
||||
if (k.Contains("warrior") || k.Contains("army") || k.Contains("formation")
|
||||
|| k.Contains("invincible")) return "warrior";
|
||||
if (k.Contains("wait") || k == "reflection" || k.Contains("hold_still")
|
||||
|| k.Contains("make_decision") || k.Contains("think") || k.Contains("stuck")) return "wait";
|
||||
if (k.Contains("idle") || k.Contains("random_move") || k.Contains("walking")
|
||||
|| k.EndsWith("_move") || k.Contains("wander") || k == "move"
|
||||
|| k.Contains("dragon_") || k.Contains("godfinger") || k.Contains("follow_")
|
||||
|| k.StartsWith("ant_") || k.StartsWith("ufo_") || k.StartsWith("worm_")
|
||||
|| k.Contains("slide") || k.Contains("land") || k.Contains("fly")) return "move";
|
||||
if (k.Contains("read")) return "read";
|
||||
if (k.Contains("loot") || k.Contains("family_group")) return "gather_life";
|
||||
return "";
|
||||
}
|
||||
|
||||
public static bool HasActionCore(string key)
|
||||
{
|
||||
return !string.IsNullOrEmpty(Resolve(key))
|
||||
|| !string.IsNullOrEmpty(ActivityProse.PatternActionPhrase(key, false))
|
||||
|| !string.IsNullOrEmpty(key);
|
||||
}
|
||||
|
||||
public static List<string> EnumerateLiveTaskIds()
|
||||
{
|
||||
return ActivityAssetCatalog.EnumerateLiveTaskIds();
|
||||
}
|
||||
}
|
||||
176
IdleSpectator/ActivityVoice.cs
Normal file
176
IdleSpectator/ActivityVoice.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
namespace IdleSpectator;
|
||||
|
||||
public enum ActivityAssetKind
|
||||
{
|
||||
Creature,
|
||||
Vehicle,
|
||||
Special
|
||||
}
|
||||
|
||||
public enum ActivityModifier
|
||||
{
|
||||
None,
|
||||
Undead,
|
||||
Elemental,
|
||||
Mush,
|
||||
Tumor,
|
||||
Alien,
|
||||
Construct
|
||||
}
|
||||
|
||||
public enum ActivityBodyTag
|
||||
{
|
||||
Default,
|
||||
Humanoid,
|
||||
Canine,
|
||||
Feline,
|
||||
Bird,
|
||||
Insect,
|
||||
Arachnid,
|
||||
Amphibian,
|
||||
Aquatic,
|
||||
Reptile,
|
||||
Livestock,
|
||||
Plant,
|
||||
Fungi,
|
||||
Protist,
|
||||
Machina,
|
||||
Mythic,
|
||||
Gregalia,
|
||||
Dragon,
|
||||
Primate,
|
||||
Rodent,
|
||||
Ursid,
|
||||
Seal,
|
||||
Procyonid,
|
||||
Hyaenid,
|
||||
Armadillo,
|
||||
Crawler,
|
||||
Crystal,
|
||||
Cold,
|
||||
Crab,
|
||||
Vehicle,
|
||||
Undead,
|
||||
Elemental,
|
||||
Mush,
|
||||
Tumor,
|
||||
Alien,
|
||||
Construct
|
||||
}
|
||||
|
||||
public enum ActivityEcologyTag
|
||||
{
|
||||
Default,
|
||||
Civilized,
|
||||
Land,
|
||||
Air,
|
||||
Water,
|
||||
Plant,
|
||||
Fungal,
|
||||
Mythic,
|
||||
Machine,
|
||||
Cold,
|
||||
Alien,
|
||||
Undead
|
||||
}
|
||||
|
||||
/// <summary>One immutable interpretation of a live ActorAsset for activity prose.</summary>
|
||||
public sealed class ProseVoice
|
||||
{
|
||||
public string LiveSpeciesId { get; }
|
||||
public string BaseSpeciesId { get; }
|
||||
public string BaseFamily { get; }
|
||||
public ActivityBodyTag BaseBodyTag { get; }
|
||||
public ActivityEcologyTag EcologyTag { get; }
|
||||
public ActivityModifier Modifier { get; }
|
||||
public ActivityBodyTag EffectiveActionTag { get; }
|
||||
public ActivityAssetKind AssetKind { get; }
|
||||
public bool BaseIsCiv { get; }
|
||||
public bool BaseIsHumanoid { get; }
|
||||
public bool BaseIsAnimal { get; }
|
||||
public string FlavorKey { get; }
|
||||
public int FlavorOrdinal { get; }
|
||||
public string ProfileTag { get; }
|
||||
public bool BaseRecoveryExpected { get; }
|
||||
public bool BaseRecoverySucceeded { get; }
|
||||
|
||||
public ProseVoice(
|
||||
string liveSpeciesId,
|
||||
string baseSpeciesId,
|
||||
string baseFamily,
|
||||
ActivityBodyTag baseBodyTag,
|
||||
ActivityEcologyTag ecologyTag,
|
||||
ActivityModifier modifier,
|
||||
ActivityBodyTag effectiveActionTag,
|
||||
ActivityAssetKind assetKind,
|
||||
bool baseIsCiv,
|
||||
bool baseIsHumanoid,
|
||||
bool baseIsAnimal,
|
||||
string flavorKey,
|
||||
int flavorOrdinal,
|
||||
string profileTag,
|
||||
bool baseRecoveryExpected,
|
||||
bool baseRecoverySucceeded)
|
||||
{
|
||||
LiveSpeciesId = liveSpeciesId ?? "";
|
||||
BaseSpeciesId = baseSpeciesId ?? LiveSpeciesId;
|
||||
BaseFamily = string.IsNullOrEmpty(baseFamily) ? ActivityVoiceFamilies.Default : baseFamily;
|
||||
BaseBodyTag = baseBodyTag;
|
||||
EcologyTag = ecologyTag;
|
||||
Modifier = modifier;
|
||||
EffectiveActionTag = effectiveActionTag;
|
||||
AssetKind = assetKind;
|
||||
BaseIsCiv = baseIsCiv;
|
||||
BaseIsHumanoid = baseIsHumanoid;
|
||||
BaseIsAnimal = baseIsAnimal;
|
||||
FlavorKey = string.IsNullOrEmpty(flavorKey) ? LiveSpeciesId : flavorKey;
|
||||
FlavorOrdinal = flavorOrdinal;
|
||||
ProfileTag = profileTag ?? "";
|
||||
BaseRecoveryExpected = baseRecoveryExpected;
|
||||
BaseRecoverySucceeded = baseRecoverySucceeded;
|
||||
}
|
||||
|
||||
public static ProseVoice Unknown(string speciesId = "")
|
||||
{
|
||||
return new ProseVoice(
|
||||
speciesId,
|
||||
speciesId,
|
||||
ActivityVoiceFamilies.Default,
|
||||
ActivityBodyTag.Default,
|
||||
ActivityEcologyTag.Default,
|
||||
ActivityModifier.None,
|
||||
ActivityBodyTag.Default,
|
||||
ActivityAssetKind.Creature,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
speciesId,
|
||||
0,
|
||||
"",
|
||||
false,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ActivityVoiceFamilies
|
||||
{
|
||||
public const string Default = "default";
|
||||
public const string Humanoid = "humanoidCiv";
|
||||
public const string Canine = "canine";
|
||||
public const string Feline = "feline";
|
||||
public const string Bird = "bird";
|
||||
public const string Insect = "insect";
|
||||
public const string Arachnid = "arachnid";
|
||||
public const string Amphibian = "amphibian";
|
||||
public const string Aquatic = "aquatic";
|
||||
public const string Reptile = "reptile";
|
||||
public const string Livestock = "livestock";
|
||||
public const string Plant = "plant";
|
||||
public const string Fungi = "fungi";
|
||||
public const string Protist = "protist";
|
||||
public const string Machina = "machina";
|
||||
public const string Mythic = "mythic";
|
||||
public const string Gregalia = "gregalia";
|
||||
public const string Mammal = "mammal";
|
||||
public const string Special = "special";
|
||||
}
|
||||
384
IdleSpectator/ActivityVoiceHarness.cs
Normal file
384
IdleSpectator/ActivityVoiceHarness.cs
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
public sealed class ActivityVoiceAuditResult
|
||||
{
|
||||
public bool Passed;
|
||||
public string Detail = "";
|
||||
}
|
||||
|
||||
/// <summary>Exhaustive live-library gates for voice classification and prose fingerprints.</summary>
|
||||
public static class ActivityVoiceHarness
|
||||
{
|
||||
private static readonly string[] FingerprintKeys =
|
||||
{
|
||||
"move", "wait", "task_unit_play", "task_eat", "task_sleep",
|
||||
"task_hunt", "task_attack", "social", "laugh", "work"
|
||||
};
|
||||
|
||||
public static ActivityVoiceAuditResult RunAudit(string outputDirectory)
|
||||
{
|
||||
List<string> ids = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
|
||||
var rows = new StringBuilder(32768);
|
||||
rows.AppendLine("id\tbase\tkind\tmodifier\tfamily\tbody\teffective\tecology\tordinal\trecovery\tfingerprint");
|
||||
|
||||
int missing = 0;
|
||||
int defaults = 0;
|
||||
int emptyProse = 0;
|
||||
int leaks = 0;
|
||||
int recoveryFail = 0;
|
||||
int modifierFail = 0;
|
||||
int aquaticFalsePositive = 0;
|
||||
int collisions = 0;
|
||||
int vehicleMismatch = 0;
|
||||
string sample = "";
|
||||
string vehicleFingerprint = null;
|
||||
var fingerprints = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
{
|
||||
string id = ids[i];
|
||||
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(id);
|
||||
if (asset == null)
|
||||
{
|
||||
missing++;
|
||||
AddSample(ref sample, id + ":missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
ProseVoice voice = ActivityVoiceResolver.Resolve(asset);
|
||||
if (voice.EffectiveActionTag == ActivityBodyTag.Default
|
||||
|| string.IsNullOrEmpty(voice.BaseFamily)
|
||||
|| voice.BaseFamily == ActivityVoiceFamilies.Default)
|
||||
{
|
||||
defaults++;
|
||||
AddSample(ref sample, id + ":default");
|
||||
}
|
||||
|
||||
if (voice.BaseRecoveryExpected && !voice.BaseRecoverySucceeded)
|
||||
{
|
||||
recoveryFail++;
|
||||
AddSample(ref sample, id + ":base?");
|
||||
}
|
||||
|
||||
if (!ExpectedModifierMatches(asset, voice))
|
||||
{
|
||||
modifierFail++;
|
||||
AddSample(ref sample, id + ":modifier=" + voice.Modifier);
|
||||
}
|
||||
|
||||
if (MustNotBeAquatic(id)
|
||||
&& (voice.BaseBodyTag == ActivityBodyTag.Aquatic
|
||||
|| voice.EcologyTag == ActivityEcologyTag.Water
|
||||
&& voice.AssetKind != ActivityAssetKind.Vehicle))
|
||||
{
|
||||
aquaticFalsePositive++;
|
||||
AddSample(ref sample, id + ":aquatic");
|
||||
}
|
||||
|
||||
int localEmpty;
|
||||
int localLeaks;
|
||||
string fingerprint = Fingerprint(id, out localEmpty, out localLeaks);
|
||||
emptyProse += localEmpty;
|
||||
leaks += localLeaks;
|
||||
|
||||
if (voice.AssetKind == ActivityAssetKind.Vehicle)
|
||||
{
|
||||
if (vehicleFingerprint == null)
|
||||
{
|
||||
vehicleFingerprint = fingerprint;
|
||||
}
|
||||
else if (!vehicleFingerprint.Equals(fingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
vehicleMismatch++;
|
||||
AddSample(ref sample, id + ":vehicle-fingerprint");
|
||||
}
|
||||
}
|
||||
else if (fingerprints.TryGetValue(fingerprint, out string prior))
|
||||
{
|
||||
collisions++;
|
||||
AddSample(ref sample, prior + "=" + id);
|
||||
}
|
||||
else
|
||||
{
|
||||
fingerprints[fingerprint] = id;
|
||||
}
|
||||
|
||||
rows.Append(id).Append('\t')
|
||||
.Append(voice.BaseSpeciesId).Append('\t')
|
||||
.Append(voice.AssetKind).Append('\t')
|
||||
.Append(voice.Modifier).Append('\t')
|
||||
.Append(voice.BaseFamily).Append('\t')
|
||||
.Append(voice.BaseBodyTag).Append('\t')
|
||||
.Append(voice.EffectiveActionTag).Append('\t')
|
||||
.Append(voice.EcologyTag).Append('\t')
|
||||
.Append(voice.FlavorOrdinal).Append('\t')
|
||||
.Append(voice.BaseRecoveryExpected ? voice.BaseRecoverySucceeded.ToString() : "n/a").Append('\t')
|
||||
.Append(fingerprint.Replace('\t', ' ')).Append('\n');
|
||||
}
|
||||
|
||||
int pairFail = PairwiseFailures(ref sample);
|
||||
bool liveContextOk = VerifyLiveContext(ref sample);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
File.WriteAllText(Path.Combine(outputDirectory, "activity-voice-audit.tsv"), rows.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddSample(ref sample, "write:" + ex.Message);
|
||||
}
|
||||
|
||||
bool passed = ids.Count > 50
|
||||
&& missing == 0
|
||||
&& defaults == 0
|
||||
&& emptyProse == 0
|
||||
&& leaks == 0
|
||||
&& recoveryFail == 0
|
||||
&& modifierFail == 0
|
||||
&& aquaticFalsePositive == 0
|
||||
&& collisions == 0
|
||||
&& vehicleMismatch == 0
|
||||
&& pairFail == 0
|
||||
&& liveContextOk;
|
||||
return new ActivityVoiceAuditResult
|
||||
{
|
||||
Passed = passed,
|
||||
Detail = "total=" + ids.Count
|
||||
+ " missing=" + missing
|
||||
+ " defaults=" + defaults
|
||||
+ " empty=" + emptyProse
|
||||
+ " leaks=" + leaks
|
||||
+ " recoveryFail=" + recoveryFail
|
||||
+ " modifierFail=" + modifierFail
|
||||
+ " aquaticFalsePositive=" + aquaticFalsePositive
|
||||
+ " collisions=" + collisions
|
||||
+ " vehicleMismatch=" + vehicleMismatch
|
||||
+ " pairFail=" + pairFail
|
||||
+ " liveContextOk=" + liveContextOk
|
||||
+ " sample='" + sample + "'"
|
||||
};
|
||||
}
|
||||
|
||||
private static bool VerifyLiveContext(ref string sample)
|
||||
{
|
||||
try
|
||||
{
|
||||
Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
if (actor == null || actor.asset == null)
|
||||
{
|
||||
AddSample(ref sample, "live-context:no-focus");
|
||||
return false;
|
||||
}
|
||||
|
||||
ActivityContext ctx = ActivityInterestTable.BuildContext(actor, "move");
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"move",
|
||||
ctx,
|
||||
"Moving",
|
||||
actor.getID(),
|
||||
1,
|
||||
out string line,
|
||||
out _);
|
||||
bool ok = ctx.Voice != null
|
||||
&& ctx.Voice.LiveSpeciesId == (actor.asset.id ?? "")
|
||||
&& ctx.Voice.EffectiveActionTag != ActivityBodyTag.Default
|
||||
&& !string.IsNullOrEmpty(line);
|
||||
if (!ok)
|
||||
{
|
||||
AddSample(ref sample, "live-context:" + (actor.asset.id ?? "") + ":" + line);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddSample(ref sample, "live-context:" + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fingerprint(string speciesId, out int empty, out int leaks)
|
||||
{
|
||||
empty = 0;
|
||||
leaks = 0;
|
||||
var corpus = new StringBuilder();
|
||||
ActivityContext probe = ActivityContext.ForHarness("VoiceActor", "VoiceTarget", speciesId);
|
||||
string speciesLabel = probe.SpeciesLabel ?? "";
|
||||
|
||||
for (int i = 0; i < FingerprintKeys.Length; i++)
|
||||
{
|
||||
string key = FingerprintKeys[i];
|
||||
bool target = key == "task_hunt" || key == "task_attack" || key == "social";
|
||||
ActivityContext ctx = ActivityContext.ForHarness(
|
||||
"VoiceActor",
|
||||
target ? "VoiceTarget" : "",
|
||||
speciesId,
|
||||
place: "VoicePlace",
|
||||
carrying: "VoiceCargo",
|
||||
jobId: "VoiceJob",
|
||||
taskKey: key,
|
||||
traitId: "VoiceTrait");
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
key,
|
||||
ctx,
|
||||
key,
|
||||
4242,
|
||||
i + 1,
|
||||
out string line,
|
||||
out _);
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
empty++;
|
||||
}
|
||||
|
||||
if (ContainsSpeciesOrTrait(line, speciesId, speciesLabel, "VoiceTrait"))
|
||||
{
|
||||
leaks++;
|
||||
}
|
||||
|
||||
corpus.Append(Normalize(line, speciesId, speciesLabel)).Append('|');
|
||||
}
|
||||
|
||||
return corpus.ToString();
|
||||
}
|
||||
|
||||
private static int PairwiseFailures(ref string sample)
|
||||
{
|
||||
string[,] pairs =
|
||||
{
|
||||
{ "human", "zombie_human" },
|
||||
{ "dog", "zombie_animal_dog" },
|
||||
{ "lemon_snail", "zombie_animal_lemon_snail" },
|
||||
{ "dragon", "zombie_dragon" },
|
||||
{ "fairy", "zombie_animal_fairy" },
|
||||
{ "fire_elemental_horse", "fire_elemental_snake" }
|
||||
};
|
||||
|
||||
int failures = 0;
|
||||
for (int i = 0; i < pairs.GetLength(0); i++)
|
||||
{
|
||||
string a = pairs[i, 0];
|
||||
string b = pairs[i, 1];
|
||||
if (!ActivityAssetCatalog.IsLiveActorAssetId(a)
|
||||
|| !ActivityAssetCatalog.IsLiveActorAssetId(b))
|
||||
{
|
||||
failures++;
|
||||
AddSample(ref sample, a + "/" + b + ":missing-pair");
|
||||
continue;
|
||||
}
|
||||
|
||||
string fa = Fingerprint(a, out _, out _);
|
||||
string fb = Fingerprint(b, out _, out _);
|
||||
if (fa.Equals(fb, StringComparison.Ordinal))
|
||||
{
|
||||
failures++;
|
||||
AddSample(ref sample, a + "=" + b);
|
||||
}
|
||||
}
|
||||
|
||||
List<string> ids = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
|
||||
string boat = null;
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
{
|
||||
if (!ids[i].StartsWith("boat_", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
string fp = Fingerprint(ids[i], out _, out _);
|
||||
if (boat == null) boat = fp;
|
||||
else if (!boat.Equals(fp, StringComparison.Ordinal))
|
||||
{
|
||||
failures++;
|
||||
AddSample(ref sample, ids[i] + ":boat-pair");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
private static string Normalize(string line, string speciesId, string speciesLabel)
|
||||
{
|
||||
string text = Regex.Replace(line ?? "", "<[^>]+>", "");
|
||||
text = ReplaceIgnoreCase(text, "VoiceActor", "");
|
||||
text = ReplaceIgnoreCase(text, "VoiceTarget", "");
|
||||
text = ReplaceIgnoreCase(text, "VoicePlace", "");
|
||||
text = ReplaceIgnoreCase(text, "VoiceCargo", "");
|
||||
text = ReplaceIgnoreCase(text, "VoiceJob", "");
|
||||
text = ReplaceIgnoreCase(text, "VoiceTrait", "");
|
||||
text = ReplaceIgnoreCase(text, speciesLabel, "");
|
||||
text = ReplaceIgnoreCase(text, (speciesId ?? "").Replace('_', ' '), "");
|
||||
text = Regex.Replace(text.ToLowerInvariant(), "[^a-z0-9]+", " ").Trim();
|
||||
return text;
|
||||
}
|
||||
|
||||
private static bool ContainsSpeciesOrTrait(
|
||||
string line,
|
||||
string speciesId,
|
||||
string speciesLabel,
|
||||
string trait)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line)) return false;
|
||||
if (!string.IsNullOrEmpty(trait)
|
||||
&& line.IndexOf(trait, StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
if (!string.IsNullOrEmpty(speciesLabel)
|
||||
&& line.IndexOf(" the " + speciesLabel, StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
||||
string idWords = (speciesId ?? "").Replace('_', ' ');
|
||||
return !string.IsNullOrEmpty(idWords)
|
||||
&& line.IndexOf(" the " + idWords, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static bool ExpectedModifierMatches(ActorAsset asset, ProseVoice voice)
|
||||
{
|
||||
string id = asset.id ?? "";
|
||||
string wild = asset.kingdom_id_wild ?? "";
|
||||
if (id.StartsWith("boat_", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.AssetKind == ActivityAssetKind.Vehicle;
|
||||
if (wild.Equals("undead", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Undead;
|
||||
if (wild.Equals("fire_elemental", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.StartsWith("fire_elemental", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Elemental;
|
||||
if (id.StartsWith("mush_", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Mush;
|
||||
if (id.StartsWith("tumor_", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Tumor;
|
||||
if (wild.Equals("aliens", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Alien;
|
||||
if (wild.Equals("living_houses", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Construct;
|
||||
if (id.Equals("assimilator", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.Equals("printer", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.Equals("god_finger", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.Equals("crystal_sword", StringComparison.OrdinalIgnoreCase))
|
||||
return voice.Modifier == ActivityModifier.Construct;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool MustNotBeAquatic(string id)
|
||||
{
|
||||
return id.StartsWith("ant_", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.IndexOf("spider", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| id.Equals("worm", StringComparison.OrdinalIgnoreCase)
|
||||
|| id.Equals("printer", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ReplaceIgnoreCase(string text, string value, string replacement)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(value)) return text;
|
||||
return Regex.Replace(text, Regex.Escape(value), replacement ?? "", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private static void AddSample(ref string sample, string value)
|
||||
{
|
||||
if (sample.Length >= 500) return;
|
||||
if (sample.Length > 0) sample += ";";
|
||||
sample += value;
|
||||
}
|
||||
}
|
||||
40
IdleSpectator/ActivityVoiceProfiles.cs
Normal file
40
IdleSpectator/ActivityVoiceProfiles.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Thin flavor identities for creatures whose voice should be unmistakable.</summary>
|
||||
public static class ActivityVoiceProfiles
|
||||
{
|
||||
private static readonly Dictionary<string, string> Profiles =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["dragon"] = "dragon",
|
||||
["fairy"] = "fairy",
|
||||
["cat"] = "cat",
|
||||
["dog"] = "dog",
|
||||
["buffalo"] = "buffalo",
|
||||
["lemon_snail"] = "lemon_snail",
|
||||
["human"] = "human",
|
||||
["elf"] = "elf",
|
||||
["dwarf"] = "dwarf",
|
||||
["orc"] = "orc",
|
||||
["crabzilla"] = "crabzilla",
|
||||
["god_finger"] = "god_finger",
|
||||
["greg"] = "greg",
|
||||
["angle"] = "angle",
|
||||
["cold_one"] = "cold",
|
||||
["snowman"] = "cold",
|
||||
["crystal_sword"] = "crystal"
|
||||
};
|
||||
|
||||
public static string Resolve(string baseSpeciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(baseSpeciesId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return Profiles.TryGetValue(baseSpeciesId, out string profile) ? profile : "";
|
||||
}
|
||||
}
|
||||
468
IdleSpectator/ActivityVoiceResolver.cs
Normal file
468
IdleSpectator/ActivityVoiceResolver.cs
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>The only ActorAsset → prose voice classifier.</summary>
|
||||
public static class ActivityVoiceResolver
|
||||
{
|
||||
private static readonly Dictionary<string, int> FlavorOrdinals =
|
||||
new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private static int _ordinalLibraryCount = -1;
|
||||
|
||||
public static ProseVoice Resolve(string speciesId)
|
||||
{
|
||||
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(speciesId);
|
||||
return asset != null ? Resolve(asset) : ProseVoice.Unknown(speciesId);
|
||||
}
|
||||
|
||||
public static ProseVoice Resolve(ActorAsset liveAsset)
|
||||
{
|
||||
if (liveAsset == null)
|
||||
{
|
||||
return ProseVoice.Unknown();
|
||||
}
|
||||
|
||||
string liveId = liveAsset.id ?? "";
|
||||
int flavorOrdinal = FlavorOrdinal(liveId);
|
||||
|
||||
if (IsVehicle(liveId))
|
||||
{
|
||||
return Build(
|
||||
liveAsset,
|
||||
liveAsset,
|
||||
ActivityAssetKind.Vehicle,
|
||||
ActivityModifier.None,
|
||||
ActivityBodyTag.Vehicle,
|
||||
ActivityEcologyTag.Water,
|
||||
ActivityVoiceFamilies.Special,
|
||||
flavorOrdinal,
|
||||
false,
|
||||
true);
|
||||
}
|
||||
|
||||
ActivityModifier modifier = ResolveModifier(liveAsset);
|
||||
bool recoveryExpected = modifier == ActivityModifier.Undead
|
||||
&& liveId.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase);
|
||||
ActorAsset baseAsset = ResolveBaseAsset(liveAsset, modifier, out bool recoverySucceeded);
|
||||
if (baseAsset == null)
|
||||
{
|
||||
baseAsset = liveAsset;
|
||||
}
|
||||
|
||||
ActivityBodyTag body = ResolveBody(baseAsset, liveAsset, modifier);
|
||||
ActivityEcologyTag ecology = ResolveEcology(baseAsset, body, modifier);
|
||||
string family = ResolveFamily(baseAsset, body);
|
||||
ActivityBodyTag effective = EffectiveTag(modifier, body);
|
||||
ActivityAssetKind kind = IsExplicitSpecial(liveId)
|
||||
? ActivityAssetKind.Special
|
||||
: ActivityAssetKind.Creature;
|
||||
|
||||
return Build(
|
||||
liveAsset,
|
||||
baseAsset,
|
||||
kind,
|
||||
modifier,
|
||||
effective,
|
||||
ecology,
|
||||
family,
|
||||
flavorOrdinal,
|
||||
recoveryExpected,
|
||||
recoverySucceeded);
|
||||
}
|
||||
|
||||
public static void ApplyToContext(ActivityContext ctx, ActorAsset liveAsset)
|
||||
{
|
||||
if (ctx == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProseVoice voice = Resolve(liveAsset);
|
||||
ctx.Voice = voice;
|
||||
ctx.SpeciesId = voice.LiveSpeciesId;
|
||||
ctx.BaseSpeciesId = voice.BaseSpeciesId;
|
||||
ctx.Family = voice.BaseFamily;
|
||||
ctx.MannerTag = TagName(voice.EffectiveActionTag);
|
||||
ctx.ModifierTag = ModifierName(voice.Modifier);
|
||||
ctx.IsCiv = voice.BaseIsCiv;
|
||||
ctx.IsHumanoid = voice.BaseIsHumanoid;
|
||||
ctx.IsAnimal = voice.BaseIsAnimal;
|
||||
|
||||
if (liveAsset != null)
|
||||
{
|
||||
ctx.TaxonomicKingdom = liveAsset.name_taxonomic_kingdom ?? "";
|
||||
ctx.TaxonomicPhylum = liveAsset.name_taxonomic_phylum ?? "";
|
||||
ctx.TaxonomicClass = liveAsset.name_taxonomic_class ?? "";
|
||||
ctx.TaxonomicOrder = liveAsset.name_taxonomic_order ?? "";
|
||||
ctx.TaxonomicFamily = liveAsset.name_taxonomic_family ?? "";
|
||||
ctx.TaxonomicGenus = liveAsset.name_taxonomic_genus ?? "";
|
||||
ctx.TaxonomicSpecies = liveAsset.name_taxonomic_species ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
public static string TagName(ActivityBodyTag tag)
|
||||
{
|
||||
return tag.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static string ModifierName(ActivityModifier modifier)
|
||||
{
|
||||
return modifier == ActivityModifier.None ? "" : modifier.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static ProseVoice Build(
|
||||
ActorAsset liveAsset,
|
||||
ActorAsset baseAsset,
|
||||
ActivityAssetKind kind,
|
||||
ActivityModifier modifier,
|
||||
ActivityBodyTag effective,
|
||||
ActivityEcologyTag ecology,
|
||||
string family,
|
||||
int flavorOrdinal,
|
||||
bool recoveryExpected,
|
||||
bool recoverySucceeded)
|
||||
{
|
||||
string liveId = liveAsset?.id ?? "";
|
||||
string baseId = baseAsset?.id ?? liveId;
|
||||
ActivityBodyTag baseBody = kind == ActivityAssetKind.Vehicle
|
||||
? ActivityBodyTag.Vehicle
|
||||
: ResolveBody(baseAsset, liveAsset, modifier);
|
||||
return new ProseVoice(
|
||||
liveId,
|
||||
baseId,
|
||||
family,
|
||||
baseBody,
|
||||
ecology,
|
||||
modifier,
|
||||
effective,
|
||||
kind,
|
||||
baseAsset != null && baseAsset.civ,
|
||||
baseAsset != null && (baseAsset.is_humanoid || baseAsset.civ),
|
||||
baseAsset != null && baseAsset.default_animal,
|
||||
liveId,
|
||||
flavorOrdinal,
|
||||
ActivityVoiceProfiles.Resolve(baseId),
|
||||
recoveryExpected,
|
||||
recoverySucceeded);
|
||||
}
|
||||
|
||||
private static ActivityModifier ResolveModifier(ActorAsset asset)
|
||||
{
|
||||
string id = asset?.id ?? "";
|
||||
string wild = asset?.kingdom_id_wild ?? "";
|
||||
|
||||
if (EqualsIgnore(wild, "undead")
|
||||
|| id.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase)
|
||||
|| EqualsIgnore(id, "zombie")
|
||||
|| EqualsIgnore(id, "skeleton")
|
||||
|| EqualsIgnore(id, "ghost"))
|
||||
{
|
||||
return ActivityModifier.Undead;
|
||||
}
|
||||
|
||||
if (EqualsIgnore(wild, "fire_elemental")
|
||||
|| id.StartsWith("fire_elemental", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ActivityModifier.Elemental;
|
||||
}
|
||||
|
||||
if (EqualsIgnore(wild, "mush") || id.StartsWith("mush_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ActivityModifier.Mush;
|
||||
}
|
||||
|
||||
if (EqualsIgnore(wild, "tumor") || id.StartsWith("tumor_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ActivityModifier.Tumor;
|
||||
}
|
||||
|
||||
if (EqualsIgnore(wild, "aliens") || EqualsIgnore(id, "alien") || EqualsIgnore(id, "ufo"))
|
||||
{
|
||||
return ActivityModifier.Alien;
|
||||
}
|
||||
|
||||
if (EqualsIgnore(wild, "living_houses")
|
||||
|| EqualsIgnore(id, "living_house")
|
||||
|| EqualsIgnore(id, "assimilator")
|
||||
|| EqualsIgnore(id, "printer")
|
||||
|| EqualsIgnore(id, "god_finger")
|
||||
|| EqualsIgnore(id, "crystal_sword"))
|
||||
{
|
||||
return ActivityModifier.Construct;
|
||||
}
|
||||
|
||||
return ActivityModifier.None;
|
||||
}
|
||||
|
||||
private static ActorAsset ResolveBaseAsset(
|
||||
ActorAsset liveAsset,
|
||||
ActivityModifier modifier,
|
||||
out bool recoverySucceeded)
|
||||
{
|
||||
recoverySucceeded = true;
|
||||
if (liveAsset == null || modifier != ActivityModifier.Undead)
|
||||
{
|
||||
return liveAsset;
|
||||
}
|
||||
|
||||
string id = liveAsset.id ?? "";
|
||||
string candidate = "";
|
||||
if (id.StartsWith("zombie_civ_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
candidate = id.Substring("zombie_".Length);
|
||||
}
|
||||
else if (id.StartsWith("zombie_animal_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
candidate = id.Substring("zombie_animal_".Length);
|
||||
}
|
||||
else if (id.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
candidate = id.Substring("zombie_".Length);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(candidate))
|
||||
{
|
||||
return liveAsset;
|
||||
}
|
||||
|
||||
ActorAsset baseAsset = ActivityAssetCatalog.TryGetActorAsset(candidate);
|
||||
recoverySucceeded = baseAsset != null;
|
||||
return baseAsset ?? liveAsset;
|
||||
}
|
||||
|
||||
private static ActivityBodyTag ResolveBody(
|
||||
ActorAsset baseAsset,
|
||||
ActorAsset liveAsset,
|
||||
ActivityModifier modifier)
|
||||
{
|
||||
string id = baseAsset?.id ?? liveAsset?.id ?? "";
|
||||
string liveId = liveAsset?.id ?? id;
|
||||
string taxClass = baseAsset?.name_taxonomic_class ?? "";
|
||||
string taxOrder = baseAsset?.name_taxonomic_order ?? "";
|
||||
string taxFamily = baseAsset?.name_taxonomic_family ?? "";
|
||||
|
||||
if (IsVehicle(liveId)) return ActivityBodyTag.Vehicle;
|
||||
if (EqualsIgnore(id, "dragon")) return ActivityBodyTag.Dragon;
|
||||
if (EqualsIgnore(id, "crabzilla")) return ActivityBodyTag.Crab;
|
||||
if (EqualsIgnore(id, "crystal_sword")) return ActivityBodyTag.Crystal;
|
||||
if (EqualsIgnore(id, "snowman") || EqualsIgnore(id, "cold_one")) return ActivityBodyTag.Cold;
|
||||
if (EqualsIgnore(id, "living_plants")) return ActivityBodyTag.Plant;
|
||||
if (EqualsIgnore(id, "living_house") || EqualsIgnore(id, "god_finger")) return ActivityBodyTag.Construct;
|
||||
if (EqualsIgnore(id, "assimilator") || EqualsIgnore(id, "printer")) return ActivityBodyTag.Machina;
|
||||
if (EqualsIgnore(id, "sand_spider")) return ActivityBodyTag.Arachnid;
|
||||
if (EqualsIgnore(id, "worm")) return ActivityBodyTag.Crawler;
|
||||
if (id.StartsWith("ant_", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Insect;
|
||||
|
||||
if (modifier == ActivityModifier.Elemental)
|
||||
{
|
||||
if (liveId.EndsWith("_horse", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Livestock;
|
||||
if (liveId.EndsWith("_snake", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Reptile;
|
||||
if (liveId.EndsWith("_slug", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Crawler;
|
||||
if (liveId.EndsWith("_blob", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Protist;
|
||||
return ActivityBodyTag.Mythic;
|
||||
}
|
||||
|
||||
if (modifier == ActivityModifier.Mush) return ActivityBodyTag.Fungi;
|
||||
if (modifier == ActivityModifier.Tumor) return ActivityBodyTag.Tumor;
|
||||
if (modifier == ActivityModifier.Alien) return ActivityBodyTag.Alien;
|
||||
|
||||
if (baseAsset != null && baseAsset.civ) return ActivityBodyTag.Humanoid;
|
||||
if (EqualsIgnore(taxClass, "aves")) return ActivityBodyTag.Bird;
|
||||
if (EqualsIgnore(taxClass, "insecta")
|
||||
|| EqualsIgnore(taxOrder, "hymenoptera")
|
||||
|| EqualsIgnore(taxOrder, "coleoptera")
|
||||
|| EqualsIgnore(taxOrder, "lepidoptera")
|
||||
|| EqualsIgnore(taxOrder, "diptera")
|
||||
|| EqualsIgnore(taxOrder, "orthoptera")) return ActivityBodyTag.Insect;
|
||||
if (EqualsIgnore(taxClass, "arachnida")
|
||||
|| EqualsIgnore(taxOrder, "scorpiones")
|
||||
|| EqualsIgnore(taxOrder, "araneae")) return ActivityBodyTag.Arachnid;
|
||||
if (EqualsIgnore(taxClass, "amphibia")) return ActivityBodyTag.Amphibian;
|
||||
if (EqualsIgnore(taxClass, "malacostraca")
|
||||
|| EqualsIgnore(taxClass, "actinopterygii")
|
||||
|| EqualsIgnore(taxClass, "chondrichthyes")
|
||||
|| EqualsIgnore(taxClass, "cephalopoda")
|
||||
|| EqualsIgnore(taxOrder, "testudines")) return ActivityBodyTag.Aquatic;
|
||||
if (EqualsIgnore(taxClass, "reptilia")
|
||||
|| EqualsIgnore(taxOrder, "squamata")
|
||||
|| EqualsIgnore(taxOrder, "crocodilia")) return ActivityBodyTag.Reptile;
|
||||
if (EqualsIgnore(taxFamily, "canidae")) return ActivityBodyTag.Canine;
|
||||
if (EqualsIgnore(taxFamily, "felidae")) return ActivityBodyTag.Feline;
|
||||
if (EqualsIgnore(taxFamily, "ursidae")) return ActivityBodyTag.Ursid;
|
||||
if (EqualsIgnore(taxFamily, "phocidae")) return ActivityBodyTag.Seal;
|
||||
if (EqualsIgnore(taxFamily, "procyonidae")) return ActivityBodyTag.Procyonid;
|
||||
if (EqualsIgnore(taxFamily, "hyaenidae")) return ActivityBodyTag.Hyaenid;
|
||||
if (EqualsIgnore(taxOrder, "primates")) return ActivityBodyTag.Primate;
|
||||
if (EqualsIgnore(taxOrder, "rodentia")) return ActivityBodyTag.Rodent;
|
||||
if (EqualsIgnore(taxOrder, "cingulata") || EqualsIgnore(taxFamily, "dasypodidae"))
|
||||
return ActivityBodyTag.Armadillo;
|
||||
if (EqualsIgnore(taxOrder, "artiodactyla")
|
||||
|| EqualsIgnore(taxOrder, "perissodactyla")
|
||||
|| EqualsIgnore(taxFamily, "bovidae")
|
||||
|| EqualsIgnore(taxFamily, "suidae")
|
||||
|| EqualsIgnore(taxFamily, "leporidae")
|
||||
|| EqualsIgnore(taxFamily, "elephantidae")
|
||||
|| EqualsIgnore(taxFamily, "rhinocerotidae")
|
||||
|| EqualsIgnore(taxFamily, "camelidae")
|
||||
|| EqualsIgnore(taxFamily, "equidae")) return ActivityBodyTag.Livestock;
|
||||
|
||||
string kingdom = baseAsset?.name_taxonomic_kingdom ?? "";
|
||||
if (EqualsIgnore(kingdom, "plantae")) return ActivityBodyTag.Plant;
|
||||
if (EqualsIgnore(kingdom, "fungi")) return ActivityBodyTag.Fungi;
|
||||
if (EqualsIgnore(kingdom, "protista")) return ActivityBodyTag.Protist;
|
||||
if (EqualsIgnore(kingdom, "machina")) return ActivityBodyTag.Machina;
|
||||
if (EqualsIgnore(kingdom, "mythoria")) return ActivityBodyTag.Mythic;
|
||||
if (EqualsIgnore(kingdom, "gregalia")) return ActivityBodyTag.Gregalia;
|
||||
|
||||
// All remaining current animalia assets are terrestrial mammals or explicit specials.
|
||||
if (EqualsIgnore(taxClass, "mammalia")) return ActivityBodyTag.Livestock;
|
||||
if (!string.IsNullOrEmpty(id)) return ActivityBodyTag.Mythic;
|
||||
return ActivityBodyTag.Default;
|
||||
}
|
||||
|
||||
private static string ResolveFamily(ActorAsset asset, ActivityBodyTag body)
|
||||
{
|
||||
if (asset != null && asset.civ) return ActivityVoiceFamilies.Humanoid;
|
||||
string kingdom = asset?.name_taxonomic_kingdom ?? "";
|
||||
if (EqualsIgnore(kingdom, "plantae")) return ActivityVoiceFamilies.Plant;
|
||||
if (EqualsIgnore(kingdom, "fungi")) return ActivityVoiceFamilies.Fungi;
|
||||
if (EqualsIgnore(kingdom, "protista")) return ActivityVoiceFamilies.Protist;
|
||||
if (EqualsIgnore(kingdom, "machina")) return ActivityVoiceFamilies.Machina;
|
||||
if (EqualsIgnore(kingdom, "mythoria")) return ActivityVoiceFamilies.Mythic;
|
||||
if (EqualsIgnore(kingdom, "gregalia")) return ActivityVoiceFamilies.Gregalia;
|
||||
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => ActivityVoiceFamilies.Canine,
|
||||
ActivityBodyTag.Feline => ActivityVoiceFamilies.Feline,
|
||||
ActivityBodyTag.Bird => ActivityVoiceFamilies.Bird,
|
||||
ActivityBodyTag.Insect => ActivityVoiceFamilies.Insect,
|
||||
ActivityBodyTag.Arachnid => ActivityVoiceFamilies.Arachnid,
|
||||
ActivityBodyTag.Amphibian => ActivityVoiceFamilies.Amphibian,
|
||||
ActivityBodyTag.Aquatic => ActivityVoiceFamilies.Aquatic,
|
||||
ActivityBodyTag.Seal => ActivityVoiceFamilies.Aquatic,
|
||||
ActivityBodyTag.Reptile => ActivityVoiceFamilies.Reptile,
|
||||
ActivityBodyTag.Livestock => ActivityVoiceFamilies.Livestock,
|
||||
ActivityBodyTag.Plant => ActivityVoiceFamilies.Plant,
|
||||
ActivityBodyTag.Fungi => ActivityVoiceFamilies.Fungi,
|
||||
ActivityBodyTag.Protist => ActivityVoiceFamilies.Protist,
|
||||
ActivityBodyTag.Machina => ActivityVoiceFamilies.Machina,
|
||||
ActivityBodyTag.Mythic => ActivityVoiceFamilies.Mythic,
|
||||
ActivityBodyTag.Dragon => ActivityVoiceFamilies.Mythic,
|
||||
ActivityBodyTag.Cold => ActivityVoiceFamilies.Mythic,
|
||||
ActivityBodyTag.Crystal => ActivityVoiceFamilies.Special,
|
||||
ActivityBodyTag.Crab => ActivityVoiceFamilies.Special,
|
||||
ActivityBodyTag.Alien => ActivityVoiceFamilies.Special,
|
||||
ActivityBodyTag.Construct => ActivityVoiceFamilies.Special,
|
||||
ActivityBodyTag.Tumor => ActivityVoiceFamilies.Special,
|
||||
ActivityBodyTag.Gregalia => ActivityVoiceFamilies.Gregalia,
|
||||
ActivityBodyTag.Humanoid => ActivityVoiceFamilies.Humanoid,
|
||||
ActivityBodyTag.Vehicle => ActivityVoiceFamilies.Special,
|
||||
ActivityBodyTag.Default => ActivityVoiceFamilies.Default,
|
||||
_ => ActivityVoiceFamilies.Mammal
|
||||
};
|
||||
}
|
||||
|
||||
private static ActivityEcologyTag ResolveEcology(
|
||||
ActorAsset asset,
|
||||
ActivityBodyTag body,
|
||||
ActivityModifier modifier)
|
||||
{
|
||||
if (modifier == ActivityModifier.Undead) return ActivityEcologyTag.Undead;
|
||||
if (modifier == ActivityModifier.Alien) return ActivityEcologyTag.Alien;
|
||||
if (modifier == ActivityModifier.Construct) return ActivityEcologyTag.Machine;
|
||||
if (body == ActivityBodyTag.Vehicle || body == ActivityBodyTag.Aquatic || body == ActivityBodyTag.Seal)
|
||||
return ActivityEcologyTag.Water;
|
||||
if (body == ActivityBodyTag.Bird || body == ActivityBodyTag.Insect) return ActivityEcologyTag.Air;
|
||||
if (body == ActivityBodyTag.Plant) return ActivityEcologyTag.Plant;
|
||||
if (body == ActivityBodyTag.Fungi || modifier == ActivityModifier.Mush) return ActivityEcologyTag.Fungal;
|
||||
if (body == ActivityBodyTag.Machina) return ActivityEcologyTag.Machine;
|
||||
if (body == ActivityBodyTag.Cold) return ActivityEcologyTag.Cold;
|
||||
if (asset != null && asset.civ) return ActivityEcologyTag.Civilized;
|
||||
if (body == ActivityBodyTag.Mythic || body == ActivityBodyTag.Dragon
|
||||
|| modifier == ActivityModifier.Elemental) return ActivityEcologyTag.Mythic;
|
||||
return ActivityEcologyTag.Land;
|
||||
}
|
||||
|
||||
private static ActivityBodyTag EffectiveTag(ActivityModifier modifier, ActivityBodyTag body)
|
||||
{
|
||||
return modifier switch
|
||||
{
|
||||
ActivityModifier.Undead => ActivityBodyTag.Undead,
|
||||
ActivityModifier.Elemental => ActivityBodyTag.Elemental,
|
||||
ActivityModifier.Mush => ActivityBodyTag.Mush,
|
||||
ActivityModifier.Tumor => ActivityBodyTag.Tumor,
|
||||
ActivityModifier.Alien => ActivityBodyTag.Alien,
|
||||
ActivityModifier.Construct => ActivityBodyTag.Construct,
|
||||
_ => body
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsVehicle(string id)
|
||||
{
|
||||
return !string.IsNullOrEmpty(id)
|
||||
&& id.StartsWith("boat_", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsExplicitSpecial(string id)
|
||||
{
|
||||
return EqualsIgnore(id, "crabzilla")
|
||||
|| EqualsIgnore(id, "god_finger")
|
||||
|| EqualsIgnore(id, "living_plants")
|
||||
|| EqualsIgnore(id, "living_house")
|
||||
|| EqualsIgnore(id, "printer")
|
||||
|| EqualsIgnore(id, "assimilator")
|
||||
|| EqualsIgnore(id, "crystal_sword");
|
||||
}
|
||||
|
||||
private static int FlavorOrdinal(string id)
|
||||
{
|
||||
EnsureFlavorOrdinals();
|
||||
if (!string.IsNullOrEmpty(id) && FlavorOrdinals.TryGetValue(id, out int ordinal))
|
||||
{
|
||||
return ordinal;
|
||||
}
|
||||
|
||||
return Math.Abs(StableHash(id)) % 1024;
|
||||
}
|
||||
|
||||
private static void EnsureFlavorOrdinals()
|
||||
{
|
||||
List<string> ids = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
|
||||
if (ids.Count == _ordinalLibraryCount && FlavorOrdinals.Count == ids.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ids.Sort(StringComparer.Ordinal);
|
||||
FlavorOrdinals.Clear();
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
{
|
||||
FlavorOrdinals[ids[i]] = i;
|
||||
}
|
||||
|
||||
_ordinalLibraryCount = ids.Count;
|
||||
}
|
||||
|
||||
private static int StableHash(string value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
uint h = 2166136261;
|
||||
string s = value ?? "";
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
h ^= s[i];
|
||||
h *= 16777619;
|
||||
}
|
||||
|
||||
return (int)(h & 0x7fffffff);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EqualsIgnore(string a, string b)
|
||||
{
|
||||
return !string.IsNullOrEmpty(a) && a.Equals(b, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ namespace IdleSpectator;
|
|||
|
||||
/// <summary>
|
||||
/// Feeds <see cref="ActivityLog"/> from task changes and curated behaviour beats.
|
||||
/// Only patch Beh types that declare <c>execute</c> - Harmony fails with "method null"
|
||||
/// when the method is inherited-only (e.g. BehSocializeTalk).
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
public static class ActorActivityPatches
|
||||
|
|
@ -63,7 +65,11 @@ public static class ActorActivityPatches
|
|||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehUnloadResources", "Unloads resources", "town");
|
||||
string carrying = ActivityInterestTable.TryGetCarryingLabel(pActor);
|
||||
string fact = string.IsNullOrEmpty(carrying)
|
||||
? "Unloads resources"
|
||||
: "Unloads " + carrying;
|
||||
ActivityLog.NoteActionBeat(pActor, "BehUnloadResources", fact, "town");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehAttackActorHuntingTarget), nameof(BehAttackActorHuntingTarget.execute))]
|
||||
|
|
@ -94,4 +100,170 @@ public static class ActorActivityPatches
|
|||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehCityActorRemoveFire", "Fights fire", "blaze");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehTryToSocialize), nameof(BehTryToSocialize.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixTrySocialize(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string target = ActivityInterestTable.TargetLabel(pActor);
|
||||
ActivityLog.NoteActionBeat(
|
||||
pActor,
|
||||
"BehTryToSocialize",
|
||||
string.IsNullOrEmpty(target) ? "Tries to socialize" : "Tries to socialize with " + target,
|
||||
target);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehPlantCrops), nameof(BehPlantCrops.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixPlantCrops(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehPlantCrops", "Plants crops", "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehCityActorFertilizeCrop), nameof(BehCityActorFertilizeCrop.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFertilize(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehCityActorFertilizeCrop", "Fertilizes crops", "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehThrowResources), nameof(BehThrowResources.execute))]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixThrow(Actor pActor, out bool __state)
|
||||
{
|
||||
__state = false;
|
||||
try
|
||||
{
|
||||
__state = pActor != null && pActor.isCarryingResources();
|
||||
}
|
||||
catch
|
||||
{
|
||||
__state = false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehThrowResources), nameof(BehThrowResources.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixThrow(Actor pActor, bool __state)
|
||||
{
|
||||
if (pActor == null || !__state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string carrying = ActivityInterestTable.TryGetCarryingLabel(pActor);
|
||||
string fact = string.IsNullOrEmpty(carrying)
|
||||
? "Throws resources"
|
||||
: "Throws " + carrying;
|
||||
ActivityLog.NoteActionBeat(pActor, "BehThrowResources", fact, "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehBuildTarget), nameof(BehBuildTarget.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixBuild(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string target = ActivityInterestTable.TargetLabel(pActor);
|
||||
ActivityLog.NoteActionBeat(
|
||||
pActor,
|
||||
"BehBuildTarget",
|
||||
string.IsNullOrEmpty(target) ? "Works a build" : "Builds " + target,
|
||||
target);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehPoopOutside), nameof(BehPoopOutside.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixPoopOutside(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehPoopOutside", "Private moment outdoors", "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehPoopInside), nameof(BehPoopInside.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixPoopInside(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehPoopInside", "Private moment indoors", "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehBoatFishing), nameof(BehBoatFishing.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixBoatFish(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehBoatFishing", "Fishes from the boat", "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehBoatCollectFish), nameof(BehBoatCollectFish.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixBoatCollectFish(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(pActor, "BehBoatCollectFish", "Collects the catch", "");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehClaimZoneForCityActorBorder), nameof(BehClaimZoneForCityActorBorder.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixClaimBorder(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string place = "";
|
||||
try
|
||||
{
|
||||
if (pActor.city != null)
|
||||
{
|
||||
place = pActor.city.name ?? "";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
place = "";
|
||||
}
|
||||
|
||||
ActivityLog.NoteActionBeat(
|
||||
pActor,
|
||||
"BehClaimZoneForCityActorBorder",
|
||||
string.IsNullOrEmpty(place) ? "Claims border land" : "Claims border land for " + place,
|
||||
place);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,12 +408,37 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
string targetName = cmd.expect ?? "";
|
||||
string speciesId = cmd.value ?? "";
|
||||
string place = "";
|
||||
string carrying = "";
|
||||
// Optional: label "wheat@Riverhold" encodes carrying@place for unload tests.
|
||||
if (!string.IsNullOrEmpty(cmd.label) && cmd.label.IndexOf('@') >= 0
|
||||
&& (key.IndexOf("Unload", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| key.IndexOf("Throw", System.StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
string[] parts = cmd.label.Split(new[] { '@' }, 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
carrying = parts[0].Trim();
|
||||
place = parts[1].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
int n = Math.Max(1, cmd.count);
|
||||
int okN = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
string one = n > 1 ? $"{line} #{i + 1}" : line;
|
||||
if (ActivityLog.ForceNote(id, key, one, asBeat, actorName, targetName))
|
||||
if (ActivityLog.ForceNote(
|
||||
id,
|
||||
key,
|
||||
one,
|
||||
asBeat,
|
||||
actorName,
|
||||
targetName,
|
||||
speciesId,
|
||||
place,
|
||||
carrying))
|
||||
{
|
||||
okN++;
|
||||
}
|
||||
|
|
@ -1956,6 +1981,707 @@ public static class AgentHarness
|
|||
detail = $"plain='{plain}' actor='{actorName}'";
|
||||
break;
|
||||
}
|
||||
case "activity_prose_alive":
|
||||
{
|
||||
// Generic tasks must read as living diary lines, not "{name} wanders".
|
||||
string actorName = "Dorraa";
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"random_move",
|
||||
ActivityContext.ForHarness(actorName, "", "buffalo"),
|
||||
"Wandering",
|
||||
11,
|
||||
1,
|
||||
out string moveLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"wait",
|
||||
ActivityContext.ForHarness(actorName, "", "buffalo"),
|
||||
"Waiting",
|
||||
12,
|
||||
1,
|
||||
out string waitLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"city_idle_walking",
|
||||
ActivityContext.ForHarness(actorName, "", "human", place: "Riverhold", jobId: "farmer"),
|
||||
"Walking",
|
||||
13,
|
||||
1,
|
||||
out string walkLine,
|
||||
out _);
|
||||
|
||||
bool moveOk = !string.IsNullOrEmpty(moveLine)
|
||||
&& moveLine.Length >= 28
|
||||
&& !moveLine.Equals(actorName + " wanders", System.StringComparison.Ordinal)
|
||||
&& (moveLine.IndexOf("wander", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| moveLine.IndexOf("drift", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| moveLine.IndexOf("roam", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| moveLine.IndexOf("ambl", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| moveLine.IndexOf("trudg", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| moveLine.IndexOf("move", System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
bool waitOk = !string.IsNullOrEmpty(waitLine)
|
||||
&& waitLine.Length >= 18
|
||||
&& !waitLine.Equals(actorName + " waits", System.StringComparison.Ordinal);
|
||||
bool walkOk = !string.IsNullOrEmpty(walkLine)
|
||||
&& walkLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& walkLine.Length >= 28;
|
||||
pass = moveOk && waitOk && walkOk;
|
||||
detail =
|
||||
$"moveOk={moveOk} waitOk={waitOk} walkOk={walkOk} "
|
||||
+ $"move='{moveLine}' wait='{waitLine}' walk='{walkLine}'";
|
||||
break;
|
||||
}
|
||||
case "activity_prose_species":
|
||||
{
|
||||
// Species stays on the dossier nametag. Lines must diverge by manner / taxonomy
|
||||
// without spelling the species noun in the activity text.
|
||||
string actorName = "Tester";
|
||||
try
|
||||
{
|
||||
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||
{
|
||||
actorName = MoveCamera._focus_unit.getName() ?? "Tester";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
actorName = "Tester";
|
||||
}
|
||||
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness(actorName, "", "dog"),
|
||||
"Playing",
|
||||
1,
|
||||
1,
|
||||
out string dogLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness(actorName, "", "cat"),
|
||||
"Playing",
|
||||
2,
|
||||
1,
|
||||
out string catLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness(actorName, "", "human", place: "Riverhold"),
|
||||
"Playing",
|
||||
3,
|
||||
1,
|
||||
out string humanLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.ActionBeat,
|
||||
"BehUnloadResources",
|
||||
ActivityContext.ForHarness(
|
||||
actorName,
|
||||
"",
|
||||
"human",
|
||||
place: "Riverhold",
|
||||
carrying: "wheat"),
|
||||
"Unloads wheat",
|
||||
4,
|
||||
1,
|
||||
out string unloadLine,
|
||||
out _);
|
||||
|
||||
// Species must never leak into the place clause (wild kingdoms often = species id).
|
||||
ActivityContext buffaloBadPlace = ActivityContext.ForHarness(
|
||||
"Dorraa",
|
||||
"",
|
||||
"buffalo",
|
||||
place: "buffalo");
|
||||
buffaloBadPlace.PlaceLabel = ActivityInterestTable.PickPlaceLabel(
|
||||
"buffalo",
|
||||
"",
|
||||
"buffalo",
|
||||
"buffalo");
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
buffaloBadPlace,
|
||||
"Playing",
|
||||
5,
|
||||
1,
|
||||
out string buffaloLine,
|
||||
out _);
|
||||
|
||||
ActivityContext buffaloGoodPlace = ActivityContext.ForHarness(
|
||||
"Dorraa",
|
||||
"",
|
||||
"buffalo",
|
||||
place: "Riverhold");
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
buffaloGoodPlace,
|
||||
"Playing",
|
||||
6,
|
||||
1,
|
||||
out string buffaloTownLine,
|
||||
out _);
|
||||
|
||||
bool dogOk = !string.IsNullOrEmpty(dogLine)
|
||||
&& dogLine.Length >= 16
|
||||
&& dogLine.IndexOf(" the dog", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
bool catOk = !string.IsNullOrEmpty(catLine)
|
||||
&& catLine.Length >= 16
|
||||
&& catLine.IndexOf(" the cat", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
bool humanOk = !string.IsNullOrEmpty(humanLine)
|
||||
&& !humanLine.Equals(dogLine, System.StringComparison.Ordinal)
|
||||
&& !humanLine.Equals(catLine, System.StringComparison.Ordinal)
|
||||
&& humanLine.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
bool unloadOk = !string.IsNullOrEmpty(unloadLine)
|
||||
&& unloadLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& unloadLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool distinct = !dogLine.Equals(catLine, System.StringComparison.Ordinal);
|
||||
bool placeRejectOk = !string.IsNullOrEmpty(buffaloLine)
|
||||
&& buffaloLine.IndexOf(" in buffalo", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& buffaloLine.IndexOf(" the buffalo", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
bool placeKeepOk = !string.IsNullOrEmpty(buffaloTownLine)
|
||||
&& buffaloTownLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
// Taxonomy kingdoms like "insect" must never become place clauses.
|
||||
string insectPlace = ActivityInterestTable.PickPlaceLabel(
|
||||
"insect",
|
||||
"",
|
||||
"insect",
|
||||
"beetle");
|
||||
ActivityContext beetleCtx = ActivityContext.ForHarness("Eeeew", "", "beetle", place: insectPlace);
|
||||
beetleCtx.PlaceLabel = insectPlace;
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
beetleCtx,
|
||||
"Playing",
|
||||
7,
|
||||
1,
|
||||
out string beetleLine,
|
||||
out _);
|
||||
bool insectRejectOk = string.IsNullOrEmpty(insectPlace)
|
||||
&& !string.IsNullOrEmpty(beetleLine)
|
||||
&& beetleLine.IndexOf(" in insect", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& beetleLine.IndexOf(" the beetle", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness("Creaker", "", "frog"),
|
||||
"Playing",
|
||||
8,
|
||||
1,
|
||||
out string frogLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"child_random_jump",
|
||||
ActivityContext.ForHarness("Creaker", "", "frog"),
|
||||
"Jumping",
|
||||
9,
|
||||
1,
|
||||
out string frogJumpLine,
|
||||
out _);
|
||||
bool frogOk = !string.IsNullOrEmpty(frogLine)
|
||||
&& frogLine.Length >= 16
|
||||
&& frogLine.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& (frogLine.IndexOf("hop", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogLine.IndexOf("leap", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogLine.IndexOf("bounc", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogLine.IndexOf("splash", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogLine.IndexOf("play", System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
bool frogJumpOk = !string.IsNullOrEmpty(frogJumpLine)
|
||||
&& frogJumpLine.Length >= 16
|
||||
&& frogJumpLine.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& frogJumpLine.IndexOf("young", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
// Live ActorAsset taxonomy - not curated id maps.
|
||||
ActivityContext frogCtx = ActivityContext.ForHarness("Creaker", "", "frog");
|
||||
ActivityContext dogCtxLive = ActivityContext.ForHarness(actorName, "", "dog");
|
||||
bool liveTaxOk = ActivityAssetCatalog.IsLiveActorAssetId("frog")
|
||||
&& frogCtx.Voice.BaseBodyTag == ActivityBodyTag.Amphibian
|
||||
&& frogCtx.TaxonomicClass.Equals(
|
||||
"amphibia",
|
||||
System.StringComparison.OrdinalIgnoreCase)
|
||||
&& dogCtxLive.Family == ActivityVoiceFamilies.Canine
|
||||
&& dogCtxLive.TaxonomicFamily.Equals(
|
||||
"canidae",
|
||||
System.StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Species with no polish override still gets taxonomy manner without naming itself.
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness("Horn", "", "rhino"),
|
||||
"Playing",
|
||||
10,
|
||||
1,
|
||||
out string rhinoLine,
|
||||
out _);
|
||||
bool rhinoOk = !string.IsNullOrEmpty(rhinoLine)
|
||||
&& rhinoLine.Length >= 16
|
||||
&& rhinoLine.IndexOf(" the rhino", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
|
||||
pass = dogOk && catOk && humanOk && unloadOk && distinct && placeRejectOk && placeKeepOk
|
||||
&& insectRejectOk && frogOk && frogJumpOk && liveTaxOk && rhinoOk;
|
||||
detail =
|
||||
$"dogOk={dogOk} catOk={catOk} humanOk={humanOk} unloadOk={unloadOk} distinct={distinct} "
|
||||
+ $"placeRejectOk={placeRejectOk} placeKeepOk={placeKeepOk} insectRejectOk={insectRejectOk} "
|
||||
+ $"frogOk={frogOk} frogJumpOk={frogJumpOk} liveTaxOk={liveTaxOk} rhinoOk={rhinoOk} "
|
||||
+ $"frogFamily={frogCtx.Family} frogClass={frogCtx.TaxonomicClass} "
|
||||
+ $"dogFamily={dogCtxLive.Family} dogTaxFamily={dogCtxLive.TaxonomicFamily} "
|
||||
+ $"dog='{dogLine}' cat='{catLine}' human='{humanLine}' unload='{unloadLine}' "
|
||||
+ $"buffalo='{buffaloLine}' buffaloTown='{buffaloTownLine}' beetle='{beetleLine}' "
|
||||
+ $"frog='{frogLine}' frogJump='{frogJumpLine}' rhino='{rhinoLine}'";
|
||||
break;
|
||||
}
|
||||
case "activity_taxonomy_audit":
|
||||
{
|
||||
ActivityVoiceAuditResult audit = ActivityVoiceHarness.RunAudit(HarnessDir());
|
||||
pass = audit.Passed;
|
||||
detail = audit.Detail;
|
||||
break;
|
||||
}
|
||||
case "activity_prose_showcase":
|
||||
{
|
||||
// Taxonomy must map to real buckets - never fantasy kingdoms -> monster.
|
||||
string[] speciesIds = { "zombie", "angle", "lemon_snail", "dragon" };
|
||||
string[] wantFamily =
|
||||
{
|
||||
ActivityVoiceFamilies.Mammal,
|
||||
ActivityVoiceFamilies.Mythic,
|
||||
ActivityVoiceFamilies.Plant,
|
||||
ActivityVoiceFamilies.Mythic
|
||||
};
|
||||
ActivityBodyTag[] wantManner =
|
||||
{
|
||||
ActivityBodyTag.Undead,
|
||||
ActivityBodyTag.Mythic,
|
||||
ActivityBodyTag.Plant,
|
||||
ActivityBodyTag.Dragon
|
||||
};
|
||||
var verbs = new (string key, string fact, bool withTarget)[]
|
||||
{
|
||||
("task_unit_play", "Playing", false),
|
||||
("move", "Moving", false),
|
||||
("task_hunt", "Hunting", true),
|
||||
("task_sleep", "Sleeping", false),
|
||||
("task_eat", "Eating", false),
|
||||
("laugh", "Laughing", false),
|
||||
("task_attack", "Fighting", true)
|
||||
};
|
||||
|
||||
var dump = new StringBuilder(2048);
|
||||
int lineNo = 0;
|
||||
int okLines = 0;
|
||||
int leakLines = 0;
|
||||
int classOk = 0;
|
||||
for (int s = 0; s < speciesIds.Length; s++)
|
||||
{
|
||||
string sid = speciesIds[s];
|
||||
ActivityContext probe = ActivityContext.ForHarness("Tester", "", sid);
|
||||
bool mapped = probe.Family == wantFamily[s]
|
||||
&& probe.Voice.EffectiveActionTag == wantManner[s];
|
||||
if (mapped)
|
||||
{
|
||||
classOk++;
|
||||
}
|
||||
|
||||
dump.AppendLine(
|
||||
"=== " + sid
|
||||
+ " label='" + probe.SpeciesLabel + "'"
|
||||
+ " tax='" + probe.TaxonomicKingdom + "'"
|
||||
+ " family=" + probe.Family
|
||||
+ " manner=" + probe.MannerTag
|
||||
+ " wantFamily=" + wantFamily[s]
|
||||
+ " wantManner=" + wantManner[s]
|
||||
+ " mapped=" + mapped
|
||||
+ " live=" + ActivityAssetCatalog.IsLiveActorAssetId(sid)
|
||||
+ " ===");
|
||||
for (int v = 0; v < verbs.Length; v++)
|
||||
{
|
||||
string key = verbs[v].key;
|
||||
string fact = verbs[v].fact;
|
||||
bool withTarget = verbs[v].withTarget;
|
||||
lineNo++;
|
||||
string target = withTarget ? "Prey" : "";
|
||||
ActivityContext ctx = ActivityContext.ForHarness("Tester", target, sid);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
key,
|
||||
ctx,
|
||||
fact,
|
||||
9000 + s,
|
||||
lineNo,
|
||||
out string plain,
|
||||
out _);
|
||||
bool leak = !string.IsNullOrEmpty(probe.SpeciesLabel)
|
||||
&& plain.IndexOf(
|
||||
" the " + probe.SpeciesLabel,
|
||||
StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (leak)
|
||||
{
|
||||
leakLines++;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(plain) && plain.Length >= 8)
|
||||
{
|
||||
okLines++;
|
||||
}
|
||||
|
||||
dump.AppendLine(" " + key + ": " + plain);
|
||||
Debug.Log(
|
||||
"[IdleSpectator][SHOWCASE] " + sid + "|" + key + "|"
|
||||
+ probe.MannerTag + "|" + plain);
|
||||
}
|
||||
}
|
||||
|
||||
int kingdomFail = 0;
|
||||
string kingdomFailSample = "";
|
||||
List<string> actors = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
|
||||
for (int i = 0; i < actors.Count; i++)
|
||||
{
|
||||
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(actors[i]);
|
||||
if (asset == null || asset.civ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string tax = asset.name_taxonomic_kingdom ?? "";
|
||||
if (string.IsNullOrEmpty(tax))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string fam = ActivityVoiceResolver.Resolve(asset).BaseFamily;
|
||||
bool fantasyKingdom =
|
||||
tax.Equals("plantae", StringComparison.OrdinalIgnoreCase)
|
||||
|| tax.Equals("fungi", StringComparison.OrdinalIgnoreCase)
|
||||
|| tax.Equals("protista", StringComparison.OrdinalIgnoreCase)
|
||||
|| tax.Equals("machina", StringComparison.OrdinalIgnoreCase)
|
||||
|| tax.Equals("mythoria", StringComparison.OrdinalIgnoreCase)
|
||||
|| tax.Equals("gregalia", StringComparison.OrdinalIgnoreCase);
|
||||
if (fantasyKingdom && fam == ActivityVoiceFamilies.Default)
|
||||
{
|
||||
kingdomFail++;
|
||||
if (kingdomFailSample.Length < 180)
|
||||
{
|
||||
if (kingdomFailSample.Length > 0)
|
||||
{
|
||||
kingdomFailSample += "; ";
|
||||
}
|
||||
|
||||
kingdomFailSample += actors[i] + ":" + tax + "->" + fam;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string path = Path.Combine(HarnessDir(), "activity-prose-showcase.txt");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, dump.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[IdleSpectator][SHOWCASE] write failed: " + ex.Message);
|
||||
}
|
||||
|
||||
pass = okLines >= 20 && leakLines == 0 && classOk == speciesIds.Length && kingdomFail == 0;
|
||||
detail = "okLines=" + okLines
|
||||
+ " leakLines=" + leakLines
|
||||
+ " classOk=" + classOk
|
||||
+ " kingdomFail=" + kingdomFail
|
||||
+ " kingdomFailSample='" + kingdomFailSample + "'"
|
||||
+ " path=" + path;
|
||||
break;
|
||||
}
|
||||
case "activity_prose_grounding":
|
||||
{
|
||||
// Same verb, different observed clauses must diverge and mention those facts.
|
||||
const string verb = "BehUnloadResources";
|
||||
ActivityProse.Format(
|
||||
ActivityKind.ActionBeat,
|
||||
verb,
|
||||
ActivityContext.ForHarness(
|
||||
"Mira",
|
||||
"",
|
||||
"human",
|
||||
place: "Riverhold",
|
||||
carrying: "wheat",
|
||||
jobId: "farmer",
|
||||
traitId: "curious",
|
||||
isChild: true),
|
||||
"Unloads wheat",
|
||||
101,
|
||||
1,
|
||||
out string farmerLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.ActionBeat,
|
||||
verb,
|
||||
ActivityContext.ForHarness(
|
||||
"Mira",
|
||||
"",
|
||||
"human",
|
||||
place: "Ironford",
|
||||
carrying: "stone",
|
||||
jobId: "miner",
|
||||
traitId: "brave"),
|
||||
"Unloads stone",
|
||||
102,
|
||||
1,
|
||||
out string minerLine,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness(
|
||||
"Mira",
|
||||
"Barkley",
|
||||
"human",
|
||||
place: "Riverhold",
|
||||
jobId: "farmer",
|
||||
isKing: true,
|
||||
inCombat: true),
|
||||
"Playing",
|
||||
103,
|
||||
1,
|
||||
out string kingCombatLine,
|
||||
out _);
|
||||
|
||||
bool farmerOk = !string.IsNullOrEmpty(farmerLine)
|
||||
&& farmerLine.IndexOf("young", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& farmerLine.IndexOf("farmer", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& farmerLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& farmerLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& farmerLine.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& farmerLine.IndexOf("curious", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
bool minerOk = !string.IsNullOrEmpty(minerLine)
|
||||
&& minerLine.IndexOf("miner", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& minerLine.IndexOf("Ironford", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& minerLine.IndexOf("stone", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& minerLine.IndexOf("brave", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
bool divergeOk = !farmerLine.Equals(minerLine, System.StringComparison.Ordinal);
|
||||
bool kingOk = !string.IsNullOrEmpty(kingCombatLine)
|
||||
&& kingCombatLine.IndexOf("king", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& (kingCombatLine.IndexOf("combat", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| kingCombatLine.IndexOf("locked on", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| kingCombatLine.IndexOf("Barkley", System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
// Taxonomy manner divergence without SpeciesOverrides (rhino vs beetle vs frog).
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness("A", "", "beetle"),
|
||||
"Playing",
|
||||
104,
|
||||
1,
|
||||
out string beetlePlay,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness("B", "", "frog"),
|
||||
"Playing",
|
||||
105,
|
||||
1,
|
||||
out string frogPlay,
|
||||
out _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ActivityContext.ForHarness("C", "", "rhino"),
|
||||
"Playing",
|
||||
106,
|
||||
1,
|
||||
out string rhinoPlay,
|
||||
out _);
|
||||
bool taxDiverge = !string.IsNullOrEmpty(beetlePlay)
|
||||
&& !string.IsNullOrEmpty(frogPlay)
|
||||
&& !string.IsNullOrEmpty(rhinoPlay)
|
||||
&& !beetlePlay.Equals(frogPlay, System.StringComparison.Ordinal)
|
||||
&& !frogPlay.Equals(rhinoPlay, System.StringComparison.Ordinal)
|
||||
&& beetlePlay.IndexOf(" the beetle", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& frogPlay.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& rhinoPlay.IndexOf(" the rhino", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& (beetlePlay.IndexOf("scuttl", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| beetlePlay.IndexOf("skitter", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| beetlePlay.IndexOf("buzz", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| beetlePlay.IndexOf("tiny", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| beetlePlay.IndexOf("busy", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| beetlePlay.IndexOf("circuit", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
&& (frogPlay.IndexOf("hop", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogPlay.IndexOf("bounc", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogPlay.IndexOf("leap", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogPlay.IndexOf("splash", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogPlay.IndexOf("damp", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| frogPlay.IndexOf("jump", System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
|
||||
pass = farmerOk && minerOk && divergeOk && kingOk && taxDiverge;
|
||||
detail =
|
||||
$"farmerOk={farmerOk} minerOk={minerOk} divergeOk={divergeOk} kingOk={kingOk} taxDiverge={taxDiverge} "
|
||||
+ $"farmer='{farmerLine}' miner='{minerLine}' king='{kingCombatLine}' "
|
||||
+ $"beetle='{beetlePlay}' frog='{frogPlay}' rhino='{rhinoPlay}'";
|
||||
break;
|
||||
}
|
||||
case "activity_prose_library":
|
||||
{
|
||||
// Coverage from live libraries - not curated shortlists.
|
||||
List<string> actors = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
|
||||
List<string> tasks = ActivityVerbMap.EnumerateLiveTaskIds();
|
||||
int actorFail = 0;
|
||||
int actorChecked = 0;
|
||||
string actorFailSample = "";
|
||||
for (int i = 0; i < actors.Count; i++)
|
||||
{
|
||||
string sid = actors[i];
|
||||
ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(sid);
|
||||
if (asset == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stratified sample: every 3rd asset, plus always dog/cat/frog/beetle/rhino/human.
|
||||
bool force = sid == "dog" || sid == "cat" || sid == "frog" || sid == "beetle"
|
||||
|| sid == "rhino" || sid == "human";
|
||||
if (!force && (i % 3) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
actorChecked++;
|
||||
ActivityContext ctx = ActivityContext.ForHarness("Lib", "", sid);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_unit_play",
|
||||
ctx,
|
||||
"Playing",
|
||||
200 + i,
|
||||
1,
|
||||
out string line,
|
||||
out _);
|
||||
bool humanoid = ctx.IsCiv && ctx.IsHumanoid;
|
||||
bool ok = !string.IsNullOrEmpty(line) && line.Length >= 18;
|
||||
// Dossier shows species - activity lines must not spell "the {species}".
|
||||
string label = string.IsNullOrEmpty(ctx.SpeciesLabel) ? sid : ctx.SpeciesLabel;
|
||||
if (!string.IsNullOrEmpty(label))
|
||||
{
|
||||
ok = ok && line.IndexOf(" the " + label, System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
}
|
||||
|
||||
if (humanoid)
|
||||
{
|
||||
ok = ok && line.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
actorFail++;
|
||||
if (string.IsNullOrEmpty(actorFailSample))
|
||||
{
|
||||
actorFailSample = sid + ":'" + line + "'";
|
||||
}
|
||||
else if (actorFail <= 6 && actorFailSample.Length < 220)
|
||||
{
|
||||
actorFailSample += "; " + sid + ":'" + line + "'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int taskFail = 0;
|
||||
int taskChecked = 0;
|
||||
string taskFailSample = "";
|
||||
// Prefer live task library; fall back to snapshot file ids if library empty.
|
||||
List<string> taskIds = tasks;
|
||||
if (taskIds.Count == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = System.IO.Path.Combine(ModClass.ModFolder, ".harness", "actor_task_ids.txt");
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
foreach (string line in System.IO.File.ReadAllLines(path))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
taskIds.Add(line.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < taskIds.Count; i++)
|
||||
{
|
||||
string tid = taskIds[i];
|
||||
if (string.IsNullOrEmpty(tid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
taskChecked++;
|
||||
if (!ActivityVerbMap.HasActionCore(tid))
|
||||
{
|
||||
taskFail++;
|
||||
if (string.IsNullOrEmpty(taskFailSample))
|
||||
{
|
||||
taskFailSample = tid + ":no-core";
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
tid,
|
||||
ActivityContext.ForHarness("Lib", "", "dog", taskKey: tid),
|
||||
tid,
|
||||
300 + i,
|
||||
1,
|
||||
out string line,
|
||||
out _);
|
||||
bool ok = !string.IsNullOrEmpty(line)
|
||||
&& line.Length >= 16
|
||||
&& !line.Equals("Lib " + tid, System.StringComparison.OrdinalIgnoreCase)
|
||||
&& line.IndexOf("Lib", System.StringComparison.Ordinal) >= 0;
|
||||
// Reject bare gerund dead ends like "Lib laughing"
|
||||
string[] parts = line.Split(new[] { ' ' }, 3);
|
||||
if (parts.Length == 2 && parts[1].EndsWith("ing", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
taskFail++;
|
||||
if (taskFailSample.IndexOf(tid, System.StringComparison.Ordinal) < 0
|
||||
&& taskFailSample.Length < 120)
|
||||
{
|
||||
taskFailSample += (string.IsNullOrEmpty(taskFailSample) ? "" : ";")
|
||||
+ tid + ":'" + line + "'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool actorsOk = actors.Count >= 20 && actorFail == 0 && actorChecked >= 10;
|
||||
bool tasksOk = taskChecked >= 50 && taskFail == 0;
|
||||
pass = actorsOk && tasksOk;
|
||||
detail =
|
||||
$"actors={actors.Count} actorChecked={actorChecked} actorFail={actorFail} "
|
||||
+ $"tasksLive={tasks.Count} taskChecked={taskChecked} taskFail={taskFail} "
|
||||
+ $"actorFailSample='{actorFailSample}' taskFailSample='{taskFailSample}'";
|
||||
break;
|
||||
}
|
||||
case "lore_detail_pane":
|
||||
{
|
||||
string want = (cmd.value ?? "life").Trim().ToLowerInvariant();
|
||||
|
|
@ -2451,6 +3177,54 @@ public static class AgentHarness
|
|||
detail = $"top='{have}' want='{want}' tab={ChronicleHud.ActiveTab}";
|
||||
break;
|
||||
}
|
||||
case "fallen_list_top_not":
|
||||
{
|
||||
string reject = (cmd.value ?? cmd.label ?? "").Trim();
|
||||
string have = ChronicleHud.LastListTopTitle ?? "";
|
||||
pass = Chronicle.HudVisible
|
||||
&& ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen
|
||||
&& ChronicleHud.DetailUnitId == 0
|
||||
&& !string.IsNullOrEmpty(reject)
|
||||
&& have.IndexOf(reject, System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
detail = $"top='{have}' reject='{reject}' tab={ChronicleHud.ActiveTab}";
|
||||
break;
|
||||
}
|
||||
case "fallen_list_before":
|
||||
{
|
||||
string newer = (cmd.value ?? "").Trim();
|
||||
string older = (cmd.label ?? "").Trim();
|
||||
List<ChronicleCharacterSummary> rows = Chronicle.ListCharacters(
|
||||
max: 0,
|
||||
scope: Chronicle.CharacterListScope.Fallen);
|
||||
int newerIndex = -1;
|
||||
int olderIndex = -1;
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
string title = rows[i]?.Title ?? "";
|
||||
if (newerIndex < 0
|
||||
&& title.IndexOf(newer, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
newerIndex = i;
|
||||
}
|
||||
|
||||
if (olderIndex < 0
|
||||
&& title.IndexOf(older, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
olderIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
pass = Chronicle.HudVisible
|
||||
&& ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen
|
||||
&& !string.IsNullOrEmpty(newer)
|
||||
&& !string.IsNullOrEmpty(older)
|
||||
&& newerIndex >= 0
|
||||
&& olderIndex >= 0
|
||||
&& newerIndex < olderIndex;
|
||||
detail =
|
||||
$"newer='{newer}' newerIndex={newerIndex} older='{older}' olderIndex={olderIndex} rows={rows.Count}";
|
||||
break;
|
||||
}
|
||||
case "lore_list_stale":
|
||||
{
|
||||
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ internal static class HarnessScenarios
|
|||
case "activity":
|
||||
case "activity_idle_smoke":
|
||||
return ActivityIdleSmoke();
|
||||
case "activity_prose_showcase":
|
||||
return ActivityProseShowcase();
|
||||
case "activity_taxonomy_audit":
|
||||
return ActivityTaxonomyAudit();
|
||||
case "activity_rate":
|
||||
case "activity_rate_sample":
|
||||
return ActivityRateSample();
|
||||
|
|
@ -52,6 +56,30 @@ internal static class HarnessScenarios
|
|||
}
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> ActivityTaxonomyAudit()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("ata0", "wait_world"),
|
||||
Step("ata1", "pick_unit", asset: "auto"),
|
||||
Step("ata2", "focus", asset: "auto"),
|
||||
Step("ata3", "assert", expect: "activity_taxonomy_audit"),
|
||||
Step("ata4", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Dump sample activity lines for a few species (dev / product check).</summary>
|
||||
private static List<HarnessCommand> ActivityProseShowcase()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("aps0", "wait_world"),
|
||||
Step("aps1", "dismiss_windows"),
|
||||
Step("aps2", "assert", expect: "activity_prose_showcase"),
|
||||
Step("aps3", "snapshot")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timed Fallen/Living list rebuilds at rising subject counts.
|
||||
/// Picks the highest size under a 2ms Fallen avg budget (runtime cap only for that session).
|
||||
|
|
@ -453,11 +481,24 @@ internal static class HarnessScenarios
|
|||
Step("act12", "activity_probe"),
|
||||
Step("act13", "assert", expect: "activity_first_scoring"),
|
||||
Step("act13b", "assert", expect: "activity_prose_sentence"),
|
||||
Step("act13b2", "assert", expect: "activity_prose_alive"),
|
||||
Step("act13c", "assert", expect: "activity_prose_species"),
|
||||
Step("act13d", "assert", expect: "activity_prose_grounding"),
|
||||
Step("act13e", "assert", expect: "activity_prose_library"),
|
||||
Step("act13f", "assert", expect: "activity_taxonomy_audit"),
|
||||
Step("act14", "activity_force", asset: "laughing", label: "Laughing", count: 1),
|
||||
Step("act14b", "assert", expect: "activity_log_contains", value: "laugh"),
|
||||
Step("act14c", "activity_force", asset: "eating", label: "Taking a meal", count: 1),
|
||||
Step("act15", "activity_force", asset: "farm", label: "Tends the fields", count: 1),
|
||||
Step("act16", "activity_force", asset: "hunt", label: "Hunts", count: 1, expect: "Barkley"),
|
||||
Step("act16b", "activity_force", asset: "task_unit_play", label: "Playing", value: "dog"),
|
||||
Step("act16c", "assert", expect: "activity_log_contains", value: "play"),
|
||||
Step("act16d", "activity_force", asset: "task_unit_play", label: "Playing", value: "cat"),
|
||||
Step("act16e", "assert", expect: "activity_prose_variety", value: "2"),
|
||||
Step("act16f", "activity_force", asset: "BehUnloadResources", label: "wheat@Riverhold", tier: "beat", value: "human"),
|
||||
Step("act16g", "assert", expect: "activity_log_contains", value: "wheat"),
|
||||
Step("act16h", "activity_force", asset: "BehSocializeTalk", label: "Talks", count: 1, tier: "beat", expect: "Barkley"),
|
||||
Step("act16i", "assert", expect: "activity_log_contains", value: "Barkley"),
|
||||
Step("act17", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 3, tier: "beat"),
|
||||
Step("act17b", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1),
|
||||
Step("act18", "wait", wait: 0.25f),
|
||||
|
|
@ -753,7 +794,7 @@ internal static class HarnessScenarios
|
|||
Step("ch18ft2", "lore_tab", value: "fallen"),
|
||||
Step("ch18ft3", "wait", wait: 0.2f),
|
||||
Step("ch18ft4", "assert", expect: "lore_tab", value: "fallen"),
|
||||
Step("ch18ft5", "assert", expect: "fallen_list_top", value: "Slainbone"),
|
||||
Step("ch18ft5", "assert", expect: "fallen_list_before", value: "Slainbone", label: "Oldbone"),
|
||||
// Snapshot stays put until Refresh (new deaths do not auto-rebuild).
|
||||
Step("ch18ft5b", "assert", expect: "lore_list_stale", value: "false"),
|
||||
Step(
|
||||
|
|
@ -766,7 +807,7 @@ internal static class HarnessScenarios
|
|||
expect: "Age",
|
||||
tier: "place"),
|
||||
Step("ch18ft5d", "assert", expect: "lore_list_stale", value: "true"),
|
||||
Step("ch18ft5e", "assert", expect: "fallen_list_top", value: "Slainbone"),
|
||||
Step("ch18ft5e", "assert", expect: "fallen_list_top_not", value: "FreshFallen"),
|
||||
Step("ch18ft5f", "lore_refresh"),
|
||||
Step("ch18ft5g", "assert", expect: "lore_list_stale", value: "false"),
|
||||
Step("ch18ft5h", "assert", expect: "fallen_list_top", value: "FreshFallen"),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public static class InterestCollector
|
|||
|
||||
public static void OnWorldLogMessage(WorldLogMessage message)
|
||||
{
|
||||
if (message == null)
|
||||
if (message == null || AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public static class WatchCaption
|
|||
private const float TraitsH = 14f;
|
||||
private const float ReasonH = 14f;
|
||||
private const float HistoryLineMinH = 13f;
|
||||
private const float HistoryLineMaxH = 72f;
|
||||
private const float HistoryLineMaxH = 100f;
|
||||
private const float Gap = 2f;
|
||||
private const int HistoryPeekMax = 3;
|
||||
private const float BtnSize = 16f;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.12.30",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Full activity prose coverage for actor tasks.",
|
||||
"version": "0.12.70",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Detailed living activity prose by default.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue