Compare commits
2 commits
0dfe3ab832
...
7439b3e7e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 7439b3e7e6 | |||
| 88a0c38c00 |
19 changed files with 4606 additions and 302 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 along{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 ahead in a trail of flame{place_at}", "flows forward in a rolling blaze{place_at}" },
|
||||
"wait" => new[] { "flickers in place{place_at}", "burns steadily while waiting{place_at}" },
|
||||
"play" => new[] { "flares and darts through a wild game{place_at}", "dances in playful arcs of flame{place_at}" },
|
||||
"eat" => new[] { "feeds the inner flame{place_at}", "draws fuel into the flames{place_at}" },
|
||||
"sleep" => new[] { "dims into a banked rest{place_at}", "settles into a low, steady 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 as flames flicker and rise{place_at}"),
|
||||
"fight" => Targeted(hasTarget, "crashes into {target} in a burst of flame{place_at}", "flares against {target}{place_at}", "fights in a storm of sparks{place_at}", "clashes amid sparks and flame{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 ahead in soft, uneven steps{place_at}", "creeps forward as spores drift{place_at}" },
|
||||
"wait" => new[] { "rests while spores drift around it{place_at}", "waits while faint spores drift{place_at}" },
|
||||
"play" => new[] { "puffs through an odd little game{place_at}", "bobs about in playful bursts of spores{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, quiet rest{place_at}", "folds down among drifting spores{place_at}" },
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Tumor:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "pulses forward as its mass shifts{place_at}", "lurches ahead in uneven surges{place_at}" },
|
||||
"wait" => new[] { "throbs in unsettling stillness{place_at}", "quivers without moving on{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 food into the growing mass{place_at}" },
|
||||
"fight" => Targeted(hasTarget, "lashes into {target} with terrible force{place_at}", "surges against {target}{place_at}", "fights in a thrashing fury{place_at}", "clashes in a violent rush{place_at}"),
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Alien:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "glides ahead without a sound{place_at}", "tracks a precise, unfamiliar course{place_at}" },
|
||||
"wait" => new[] { "observes in unreadable stillness{place_at}", "stands motionless and watches{place_at}" },
|
||||
"play" => new[] { "tests each movement like a game{place_at}", "experiments with a strange little game{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "tracks {target} without a sound{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 uncanny precision{place_at}", "clashes with {target} in sudden bursts{place_at}", "fights with unreadable intent{place_at}", "attacks in a precise rush{place_at}"),
|
||||
_ => GenericBag(verb, hasTarget)
|
||||
};
|
||||
case ActivityModifier.Construct:
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "moves in measured, jointed steps{place_at}", "shifts forward with deliberate precision{place_at}" },
|
||||
"wait" => new[] { "idles in rigid watchfulness{place_at}", "stays motionless, mechanisms quiet{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 ahead with a stiff, uneven gait{place_at}", "sniffs the ground and shambles on{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "slinks ahead in eerie silence{place_at}", "pads forward with stiff precision{place_at}" },
|
||||
ActivityBodyTag.Insect => new[] { "skitters ahead on stiff little legs{place_at}", "crawls forward in twitching bursts{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "creeps ahead in withered motion{place_at}", "drags dead growth across the ground{place_at}" },
|
||||
ActivityBodyTag.Dragon => new[] { "lumbers ahead with the weight of a fallen giant{place_at}", "advances like ruin given motion{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "flutters ahead on lifeless wings{place_at}", "hops forward in ragged, uneven motions{place_at}" },
|
||||
_ => new[] { "shambles forward over uneven ground{place_at}", "drifts along in restless undeath{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
return verb switch
|
||||
{
|
||||
"wait" => new[] { "waits in hollow stillness{place_at}", "stands in restless silence{place_at}" },
|
||||
"play" => new[] { "lurches through a strange game{place_at}", "mimics a game with jerking motions{place_at}" },
|
||||
"eat" => new[] { "feeds with deathless hunger{place_at}", "tears into a meal with endless hunger{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, "lunges at {target} without fear{place_at}", "clashes with {target} in relentless fury{place_at}", "fights without fear or hesitation{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}", "stands beside {target} without a word{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 at an unhurried pace{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}", "devours a meal at leisure{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}", "watches {target} from above{place_at}", "circles in search of prey{place_at}", "hunts across a wide range{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 ahead at an unhurried pace{place_at}", "follows a slow, winding trail{place_at}" },
|
||||
"play" => new[] { "turns in slow, playful circles{place_at}", "sways through a gentle game{place_at}" },
|
||||
"eat" => new[] { "nibbles through a meal slowly{place_at}", "takes a slow, careful meal{place_at}" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "cat")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "pads softly along{place_at}", "slinks ahead, watching the way{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 steady weight{place_at}", "ambles ahead in slow, heavy steps{place_at}" },
|
||||
"play" => new[] { "charges about in rough play{place_at}", "stomps through a clumsy game{place_at}" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (profile == "crabzilla")
|
||||
{
|
||||
return verb switch
|
||||
{
|
||||
"move" => new[] { "takes enormous sideways strides{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 siege engine{place_at}", "fights with city-shaking force{place_at}", "clashes like a moving 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 along, taking in the surroundings{place_at}", "wanders ahead at an easy pace{place_at}" },
|
||||
ActivityBodyTag.Canine => new[] { "trots along without hurry{place_at}", "roams ahead with nose to the ground{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "pads softly along{place_at}", "slinks ahead at an easy pace{place_at}" },
|
||||
ActivityBodyTag.Bird => new[] { "flutters onward through the air{place_at}", "hops and flaps 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 from patch to patch{place_at}", "bounces along in damp, springy steps{place_at}" },
|
||||
ActivityBodyTag.Aquatic => new[] { "glides through the water{place_at}", "swims onward with easy strokes{place_at}" },
|
||||
ActivityBodyTag.Reptile => new[] { "slides along without hurry{place_at}", "creeps forward close to the ground{place_at}" },
|
||||
ActivityBodyTag.Livestock => new[] { "ambles along without hurry{place_at}", "wanders in slow, heavy steps{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "creeps along as stems and leaves sway{place_at}", "eases forward in slow, leafy steps{place_at}" },
|
||||
ActivityBodyTag.Fungi => new[] { "drifts along in soft, uneven steps{place_at}", "spreads forward as spores trail behind{place_at}" },
|
||||
ActivityBodyTag.Protist => new[] { "oozes onward without hurry{place_at}", "pulses along in slow ripples{place_at}" },
|
||||
ActivityBodyTag.Machina => new[] { "moves ahead in measured steps{place_at}", "tracks a precise course{place_at}" },
|
||||
ActivityBodyTag.Mythic => new[] { "drifts forward as if barely touching the ground{place_at}", "moves with an uncanny grace{place_at}" },
|
||||
ActivityBodyTag.Gregalia => new[] { "lopes onward with an uneven gait{place_at}", "wanders in twitchy, restless steps{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 forward on its belly{place_at}", "bounds ahead with a sleek, rolling motion{place_at}" },
|
||||
ActivityBodyTag.Procyonid => new[] { "pads onward, nosing around{place_at}", "scampers ahead in nimble steps{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 forward across the ground{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 in stiff, glittering steps{place_at}" },
|
||||
ActivityBodyTag.Crab => new[] { "scuttles sideways across the ground{place_at}", "sidesteps onward behind a hard shell{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 through a tiny game{place_at}", "darts about in quick bursts of play{place_at}" },
|
||||
ActivityBodyTag.Arachnid => new[] { "skitters through a restless game{place_at}", "darts about 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}", "darts and circles in play{place_at}" },
|
||||
ActivityBodyTag.Reptile => new[] { "slides and coils in play{place_at}", "plays in slow, winding motions{place_at}" },
|
||||
ActivityBodyTag.Livestock => new[] { "stomps about in clumsy play{place_at}", "frolics in heavy, eager bursts{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "sways through a slow game{place_at}", "twists and rustles in 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[] { "finds a way to amuse itself{place_at}", "plays with restless enthusiasm{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 low and waits{place_at}", "sits still, ready to spring{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "settles into a watchful crouch{place_at}", "waits without looking away{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}", "drifts almost motionless{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}", "stands still with leaves barely stirring{place_at}" },
|
||||
ActivityBodyTag.Machina => new[] { "idles in a watchful hold{place_at}", "stays still and scans the surroundings{place_at}" },
|
||||
_ => new[] { "waits in watchful quiet{place_at}", "stands quietly and watches{place_at}" }
|
||||
};
|
||||
}
|
||||
|
||||
if (verb == "hunt")
|
||||
{
|
||||
return body switch
|
||||
{
|
||||
ActivityBodyTag.Canine => Targeted(hasTarget, "hunts {target} with nose to the ground{place_at}", "follows {target}'s trail{place_at}", "follows the scent of prey{place_at}", "hunts with nose to the ground{place_at}"),
|
||||
ActivityBodyTag.Feline => Targeted(hasTarget, "hunts {target} in silence{place_at}", "stalks {target} from cover{place_at}", "hunts without making a sound{place_at}", "stalks prey from cover{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}", "darts toward {target}{place_at}", "searches for prey in quick bursts{place_at}", "scuttles after prey{place_at}"),
|
||||
ActivityBodyTag.Reptile => Targeted(hasTarget, "slides after {target}{place_at}", "creeps closer to {target}{place_at}", "waits low for prey to approach{place_at}", "stalks prey close to the ground{place_at}"),
|
||||
_ => Targeted(hasTarget, "pursues {target}{place_at}", "closes in on {target}{place_at}", "searches for prey{place_at}", "moves quietly on the hunt{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[] { "gulps down a meal{place_at}", "tears into a meal{place_at}" },
|
||||
ActivityBodyTag.Feline => new[] { "eats in small, careful bites{place_at}", "takes its time over a meal{place_at}" },
|
||||
ActivityBodyTag.Insect => new[] { "nibbles rapidly at a meal{place_at}", "eats in quick little bites{place_at}" },
|
||||
ActivityBodyTag.Plant => new[] { "draws nourishment slowly{place_at}", "feeds without moving from the spot{place_at}" },
|
||||
ActivityBodyTag.Livestock => new[] { "grazes with steady appetite{place_at}", "settles in to feed{place_at}" },
|
||||
_ => new[] { "sits down to eat{place_at}", "finishes a meal{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 completely still{place_at}" },
|
||||
_ => new[] { "settles into deep sleep{place_at}", "rests after the day's demands{place_at}" }
|
||||
},
|
||||
"social" => Targeted(hasTarget, "spends time with {target}{place_at}", "talks and lingers with {target}{place_at}", "looks for company{place_at}", "lingers near others{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}", "raises a clear 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[] { "continues working on the water{place_at}", "keeps steadily to its course{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 from row to row{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 carefully among the blossoms{place_at}", "moves steadily from flower to flower{place_at}" },
|
||||
"fish" => new[] { "works the water for fish{place_at}{job_as}", "waits patiently for a bite{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[] { "bolts away from danger{place_at}", "retreats before danger closes in{place_at}" },
|
||||
"work" => new[] { "works steadily at the task{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[] { "handles civic business{place_at}{job_as}", "sees to matters around the settlement{place_at}" },
|
||||
"warrior" => Targeted(hasTarget, "holds formation against {target}{place_at}", "drills for war near {target}{place_at}", "stands ready for battle{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 companionship{place_at}"),
|
||||
"gather_life" => new[] { "gathers with kin{place_at}", "moves among the group{place_at}" },
|
||||
"move" => new[] { "wanders without hurry{place_at}", "roams nearby, looking around{place_at}" },
|
||||
"wait" => new[] { "waits in watchful quiet{place_at}", "stands still and watches{place_at}" },
|
||||
"play" => new[] { "finds a way to amuse itself{place_at}", "turns idle time into a game{place_at}" },
|
||||
"eat" => new[] { "sits down to eat{place_at}", "finishes a meal{place_at}" },
|
||||
"sleep" => new[] { "settles into deep sleep{place_at}", "rests after the day's demands{place_at}" },
|
||||
"hunt" => Targeted(hasTarget, "pursues {target}{place_at}", "closes in on {target}{place_at}", "searches for prey{place_at}", "moves quietly on the hunt{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, "spends time with {target}{place_at}", "talks and lingers with {target}{place_at}", "looks for company{place_at}", "lingers near others{place_at}"),
|
||||
_ => hasTarget
|
||||
? new[] { "attends to " + verb.Replace('_', ' ') + " with {target}{place_at}", "works on " + verb.Replace('_', ' ') + " near {target}{place_at}" }
|
||||
: new[] { "attends to " + verb.Replace('_', ' ') + "{place_at}", "works through " + verb.Replace('_', ' ') + "{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());
|
||||
}
|
||||
}
|
||||
}
|
||||
393
IdleSpectator/ActivityClauseAssembler.cs
Normal file
393
IdleSpectator/ActivityClauseAssembler.cs
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
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 "";
|
||||
}
|
||||
|
||||
// An action that already names the observed target or reads as combat-aware
|
||||
// does not need a second, generic combat suffix.
|
||||
if (action.IndexOf("{target}", StringComparison.Ordinal) >= 0
|
||||
|| action.IndexOf("fight", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("clash", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("attack", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("blow", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("hunt", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("stalk", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("strike", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| action.IndexOf("close on", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (ctx.TargetIsActor && !string.IsNullOrEmpty(ctx.TargetName))
|
||||
{
|
||||
return " while keeping {target} in sight";
|
||||
}
|
||||
|
||||
return " while danger presses close";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
429
IdleSpectator/ActivityVoiceHarness.cs
Normal file
429
IdleSpectator/ActivityVoiceHarness.cs
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
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"
|
||||
};
|
||||
|
||||
private static readonly string[] AwkwardProseFragments =
|
||||
{
|
||||
"under combat pressure",
|
||||
"through the living world",
|
||||
"through the living moment",
|
||||
"in a living rush",
|
||||
"living energy",
|
||||
"proper meal",
|
||||
"focused appetite",
|
||||
"careful focus",
|
||||
"constructed steps",
|
||||
"spores about",
|
||||
"gregs onward",
|
||||
"through the wet",
|
||||
"quiet purpose",
|
||||
"deathly little steps",
|
||||
"mythic calm",
|
||||
"otherworldly purpose",
|
||||
"patient citrus"
|
||||
};
|
||||
|
||||
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;
|
||||
int awkwardProse = 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;
|
||||
string awkward = FindAwkwardPhrase(fingerprint);
|
||||
if (!string.IsNullOrEmpty(awkward))
|
||||
{
|
||||
awkwardProse++;
|
||||
AddSample(ref sample, id + ":awkward=" + awkward);
|
||||
}
|
||||
|
||||
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
|
||||
&& awkwardProse == 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
|
||||
+ " awkwardProse=" + awkwardProse
|
||||
+ " pairFail=" + pairFail
|
||||
+ " liveContextOk=" + liveContextOk
|
||||
+ " sample='" + sample + "'"
|
||||
};
|
||||
}
|
||||
|
||||
private static string FindAwkwardPhrase(string fingerprint)
|
||||
{
|
||||
string text = fingerprint ?? "";
|
||||
for (int i = 0; i < AwkwardProseFragments.Length; i++)
|
||||
{
|
||||
string phrase = AwkwardProseFragments[i];
|
||||
if (text.IndexOf(phrase, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return phrase;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
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,729 @@ 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 _);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"task_hunt",
|
||||
ActivityContext.ForHarness(
|
||||
"Gogith",
|
||||
"Boortkin",
|
||||
"human",
|
||||
place: "Thasthuk",
|
||||
isKing: true,
|
||||
inCombat: true),
|
||||
"Hunting",
|
||||
107,
|
||||
1,
|
||||
out string huntCombatLine,
|
||||
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("keeping", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| kingCombatLine.IndexOf("Barkley", System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
bool huntPressureOk = !string.IsNullOrEmpty(huntCombatLine)
|
||||
&& huntCombatLine.IndexOf("Boortkin", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& huntCombatLine.IndexOf("Thasthuk", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& huntCombatLine.IndexOf(
|
||||
"under combat pressure",
|
||||
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 && huntPressureOk && taxDiverge;
|
||||
detail =
|
||||
$"farmerOk={farmerOk} minerOk={minerOk} divergeOk={divergeOk} kingOk={kingOk} "
|
||||
+ $"huntPressureOk={huntPressureOk} taxDiverge={taxDiverge} "
|
||||
+ $"farmer='{farmerLine}' miner='{minerLine}' king='{kingCombatLine}' hunt='{huntCombatLine}' "
|
||||
+ $"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 +3199,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.72",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Detailed living activity prose by default.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue