Enhance activity context management by introducing place handling in ActivityContext and ActivityClauseAssembler. Implement logic to determine if a place is an object, improving action phrasing in ActivityProse and related methods. Update ActivityLog to reflect new place handling, ensuring accurate context in activity descriptions. Increment version in mod.json to reflect these changes.
This commit is contained in:
parent
72a4a2e164
commit
3f56cf457e
7 changed files with 156 additions and 23 deletions
|
|
@ -57,12 +57,72 @@ public static class ActivityClauseAssembler
|
|||
// Traits stay on the dossier chips - they only salt variety below, never named here.
|
||||
|
||||
string target = BuildObservedTargetClause(ctx, action, verb);
|
||||
action = EnsurePlaceClause(action, ctx, verb, key);
|
||||
string carrying = BuildCarryingClause(ctx, action);
|
||||
string pressure = BuildPressureClause(ctx, action + target);
|
||||
string line = subject + " " + action + target + carrying + pressure;
|
||||
return CollapseSpaces(line).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach {place_at} when the action still lacks a place token.
|
||||
/// Objects (bonfire, house, …) appear on site-work verbs; settlements stay settle/trade/farm/unload.
|
||||
/// </summary>
|
||||
private static string EnsurePlaceClause(string action, ActivityContext ctx, string verb, string key)
|
||||
{
|
||||
if (ctx == null
|
||||
|| string.IsNullOrEmpty(ctx.PlaceLabel)
|
||||
|| string.IsNullOrEmpty(action)
|
||||
|| action.IndexOf("{place_at}", StringComparison.Ordinal) >= 0
|
||||
|| action.IndexOf("{place}", StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
return action;
|
||||
}
|
||||
|
||||
if (ctx.PlaceIsObject)
|
||||
{
|
||||
if (WantsObjectPlace(verb) || IsUnloadKey(key))
|
||||
{
|
||||
return action + "{place_at}";
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
if (WantsSettlementPlace(verb) || IsUnloadKey(key))
|
||||
{
|
||||
return action + "{place_at}";
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private static bool IsUnloadKey(string key)
|
||||
{
|
||||
return !string.IsNullOrEmpty(key)
|
||||
&& (key.Equals("BehUnloadResources", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.Equals("BehThrowResources", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool WantsSettlementPlace(string verb)
|
||||
{
|
||||
return verb == "settle" || verb == "trade" || verb == "farm";
|
||||
}
|
||||
|
||||
private static bool WantsObjectPlace(string verb)
|
||||
{
|
||||
return verb == "work"
|
||||
|| verb == "haul"
|
||||
|| verb == "farm"
|
||||
|| verb == "trade"
|
||||
|| verb == "settle"
|
||||
|| verb == "fire"
|
||||
|| verb == "pollinate"
|
||||
|| verb == "fish"
|
||||
|| verb == "heal"
|
||||
|| verb == "read";
|
||||
}
|
||||
|
||||
private static bool IsCompanionVerb(string verb)
|
||||
{
|
||||
return verb == "social"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ public sealed class ActivityContext
|
|||
public bool TargetIsActor;
|
||||
/// <summary>Building, city, kingdom, or soft place label for clauses.</summary>
|
||||
public string PlaceLabel = "";
|
||||
/// <summary>
|
||||
/// True when <see cref="PlaceLabel"/> is a concrete building/object (bonfire, house, …).
|
||||
/// False for settlements, realms, and soft places like "the wilds".
|
||||
/// </summary>
|
||||
public bool PlaceIsObject;
|
||||
/// <summary>Observed terrain under the actor when this snapshot was captured.</summary>
|
||||
public ActivityTerrain Terrain;
|
||||
public string CarryingLabel = "";
|
||||
|
|
@ -78,7 +83,8 @@ public sealed class ActivityContext
|
|||
bool isKing = false,
|
||||
bool isLeader = false,
|
||||
bool isWarrior = false,
|
||||
ActivityTerrain terrain = ActivityTerrain.Unknown)
|
||||
ActivityTerrain terrain = ActivityTerrain.Unknown,
|
||||
bool placeIsObject = false)
|
||||
{
|
||||
ActivityContext ctx = new ActivityContext
|
||||
{
|
||||
|
|
@ -87,6 +93,7 @@ public sealed class ActivityContext
|
|||
TargetIsActor = !string.IsNullOrEmpty(targetName),
|
||||
SpeciesId = speciesId ?? "",
|
||||
PlaceLabel = place ?? "",
|
||||
PlaceIsObject = placeIsObject,
|
||||
CarryingLabel = carrying ?? "",
|
||||
JobId = jobId ?? "",
|
||||
TaskKey = taskKey ?? "",
|
||||
|
|
@ -130,6 +137,7 @@ public sealed class ActivityContext
|
|||
&& ActivityInterestTable.LooksLikeSpeciesPublic(ctx.PlaceLabel, ctx.SpeciesId))
|
||||
{
|
||||
ctx.PlaceLabel = "";
|
||||
ctx.PlaceIsObject = false;
|
||||
}
|
||||
|
||||
ctx.RoleLabel = ActivityInterestTable.DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior);
|
||||
|
|
|
|||
|
|
@ -214,36 +214,39 @@ public static class ActivityInterestTable
|
|||
|
||||
public static string TargetLabel(Actor actor)
|
||||
{
|
||||
ResolveTarget(actor, out string name, out _, out string place);
|
||||
ResolveTarget(actor, out string name, out _, out string place, out _);
|
||||
return !string.IsNullOrEmpty(name) ? name : place;
|
||||
}
|
||||
|
||||
/// <summary>Living actor interactee name only - never a building/place label.</summary>
|
||||
public static string ActorTargetName(Actor actor)
|
||||
{
|
||||
ResolveTarget(actor, out string name, out bool isActor, out _);
|
||||
ResolveTarget(actor, out string name, out bool isActor, out _, out _);
|
||||
return isActor ? name : "";
|
||||
}
|
||||
|
||||
/// <summary>Building/tile/settlement label only - never an actor name.</summary>
|
||||
public static string PlaceOrObjectLabel(Actor actor)
|
||||
{
|
||||
ResolveTarget(actor, out _, out bool isActor, out string place);
|
||||
ResolveTarget(actor, out _, out bool isActor, out string place, out _);
|
||||
return isActor ? "" : (place ?? "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefer live actor targets (by name). Places/buildings stay as type labels.
|
||||
/// <paramref name="placeIsObject"/> is true only for concrete building targets.
|
||||
/// </summary>
|
||||
public static void ResolveTarget(
|
||||
Actor actor,
|
||||
out string targetName,
|
||||
out bool targetIsActor,
|
||||
out string placeLabel)
|
||||
out string placeLabel,
|
||||
out bool placeIsObject)
|
||||
{
|
||||
targetName = "";
|
||||
targetIsActor = false;
|
||||
placeLabel = "";
|
||||
placeIsObject = false;
|
||||
if (actor == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -298,10 +301,12 @@ public static class ActivityInterestTable
|
|||
if (!string.IsNullOrEmpty(type))
|
||||
{
|
||||
placeLabel = type.Replace("type_", "");
|
||||
placeIsObject = true;
|
||||
return;
|
||||
}
|
||||
|
||||
placeLabel = string.IsNullOrEmpty(id) ? "a building" : id;
|
||||
placeIsObject = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -466,10 +471,15 @@ public static class ActivityInterestTable
|
|||
ctx.InCombat = ctx.HasAttackTarget;
|
||||
}
|
||||
|
||||
ResolveTarget(actor, out string targetName, out bool targetIsActor, out string place);
|
||||
ResolveTarget(actor, out string targetName, out bool targetIsActor, out string place, out bool placeIsObject);
|
||||
ctx.TargetName = targetName;
|
||||
ctx.TargetIsActor = targetIsActor;
|
||||
ctx.PlaceLabel = PickPlaceLabel(place, ctx.CityName, ctx.KingdomName, ctx.SpeciesId);
|
||||
string picked = PickPlaceLabel(place, ctx.CityName, ctx.KingdomName, ctx.SpeciesId);
|
||||
ctx.PlaceLabel = picked;
|
||||
ctx.PlaceIsObject = placeIsObject
|
||||
&& !string.IsNullOrEmpty(picked)
|
||||
&& !string.IsNullOrEmpty(place)
|
||||
&& picked.Equals(place.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
ctx.CarryingLabel = TryGetCarryingLabel(actor);
|
||||
ctx.TopTraitId = TryTopInterestingTraitId(actor);
|
||||
|
|
@ -565,8 +575,8 @@ public static class ActivityInterestTable
|
|||
}
|
||||
|
||||
/// <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.
|
||||
/// Prefer a concrete resolved object/building from AI targets over settlement names.
|
||||
/// City/kingdom fill in only when there is no usable object/place from ResolveTarget.
|
||||
/// </summary>
|
||||
public static string PickPlaceLabel(
|
||||
string resolvedPlace,
|
||||
|
|
@ -574,16 +584,16 @@ public static class ActivityInterestTable
|
|||
string kingdomName,
|
||||
string speciesId)
|
||||
{
|
||||
if (IsUsablePlace(cityName, speciesId))
|
||||
{
|
||||
return cityName.Trim();
|
||||
}
|
||||
|
||||
if (IsConcreteResolvedPlace(resolvedPlace, speciesId))
|
||||
{
|
||||
return resolvedPlace.Trim();
|
||||
}
|
||||
|
||||
if (IsUsablePlace(cityName, speciesId))
|
||||
{
|
||||
return cityName.Trim();
|
||||
}
|
||||
|
||||
// Kingdom only when it reads like a settlement/realm name, not a species bucket.
|
||||
if (IsUsablePlace(kingdomName, speciesId) && LooksLikeRealmName(kingdomName))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ public static class ActivityLog
|
|||
if (string.IsNullOrEmpty(ctx.PlaceLabel) && !string.IsNullOrEmpty(placeHint))
|
||||
{
|
||||
ctx.PlaceLabel = placeHint;
|
||||
ctx.PlaceIsObject = true;
|
||||
}
|
||||
|
||||
if (actionKey == "BehUnloadResources" || actionKey == "BehThrowResources")
|
||||
|
|
@ -304,6 +305,7 @@ public static class ActivityLog
|
|||
if (string.IsNullOrEmpty(ctx.PlaceLabel))
|
||||
{
|
||||
ctx.PlaceLabel = !string.IsNullOrEmpty(ctx.CityName) ? ctx.CityName : "town";
|
||||
ctx.PlaceIsObject = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -555,6 +557,8 @@ public static class ActivityLog
|
|||
if (!string.IsNullOrEmpty(place))
|
||||
{
|
||||
ctx.PlaceLabel = place;
|
||||
// Explicit harness place strings are settlement/realm names, not building objects.
|
||||
ctx.PlaceIsObject = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(carrying))
|
||||
|
|
|
|||
|
|
@ -35,9 +35,14 @@ public static class ActivityProse
|
|||
actorName,
|
||||
targetIsActor ? targetName : "",
|
||||
place: placeOrFallback);
|
||||
if (!targetIsActor && !string.IsNullOrEmpty(targetName) && string.IsNullOrEmpty(ctx.PlaceLabel))
|
||||
if (!targetIsActor && !string.IsNullOrEmpty(targetName))
|
||||
{
|
||||
ctx.PlaceLabel = targetName;
|
||||
if (string.IsNullOrEmpty(ctx.PlaceLabel))
|
||||
{
|
||||
ctx.PlaceLabel = targetName;
|
||||
}
|
||||
|
||||
ctx.PlaceIsObject = true;
|
||||
}
|
||||
|
||||
if (targetIsActor)
|
||||
|
|
@ -144,6 +149,47 @@ public static class ActivityProse
|
|||
return HumanizeJob(jobId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settlement / realm / wilds → " in X". Building/object → " at the X" (or " at a/an/the X").
|
||||
/// </summary>
|
||||
public static string FormatPlaceAtClause(string place, bool placeIsObject)
|
||||
{
|
||||
if (string.IsNullOrEmpty(place))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string noun = place.Trim().Replace('_', ' ');
|
||||
if (string.IsNullOrEmpty(noun))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (placeIsObject)
|
||||
{
|
||||
if (HasLeadingArticle(noun))
|
||||
{
|
||||
return " at " + noun;
|
||||
}
|
||||
|
||||
return " at the " + LowerFirst(noun);
|
||||
}
|
||||
|
||||
return " in " + noun;
|
||||
}
|
||||
|
||||
private static bool HasLeadingArticle(string noun)
|
||||
{
|
||||
if (string.IsNullOrEmpty(noun))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return noun.StartsWith("a ", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| noun.StartsWith("an ", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| noun.StartsWith("the ", System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Legacy helper used by older call sites / harness.</summary>
|
||||
public static string Format(
|
||||
ActivityKind kind,
|
||||
|
|
@ -193,8 +239,8 @@ public static class ActivityProse
|
|||
string carrying = string.IsNullOrEmpty(ctx.CarryingLabel) ? "goods" : ctx.CarryingLabel;
|
||||
|
||||
string withClause = string.IsNullOrEmpty(target) ? "" : (" with " + target);
|
||||
// Empty place → empty clause (no forced "in town"). Job never appears in Activity.
|
||||
string placeAt = string.IsNullOrEmpty(place) ? "" : (" in " + place);
|
||||
// Empty place → empty clause. Objects use "at the", settlements use "in".
|
||||
string placeAt = FormatPlaceAtClause(place, ctx.PlaceIsObject);
|
||||
|
||||
string t = template;
|
||||
t = t.Replace("{actor}", actor);
|
||||
|
|
|
|||
|
|
@ -2297,11 +2297,13 @@ public static class AgentHarness
|
|||
socialBindOk = socialBindOk && withTomi <= 1;
|
||||
|
||||
// Building/place labels must never become companion "with …" on work/unload.
|
||||
// Objects use "at the …"; settlements use "in …".
|
||||
ActivityContext bonfireCtx = ActivityContext.ForHarness(
|
||||
actorName,
|
||||
"bonfire",
|
||||
"",
|
||||
"human",
|
||||
place: "bonfire");
|
||||
place: "bonfire",
|
||||
placeIsObject: true);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
"type_bonfire",
|
||||
|
|
@ -2312,11 +2314,13 @@ public static class AgentHarness
|
|||
out string bonfireLine,
|
||||
out _);
|
||||
bool bonfireOk = !string.IsNullOrEmpty(bonfireLine)
|
||||
&& bonfireLine.IndexOf("with bonfire", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
&& bonfireLine.IndexOf("at the bonfire", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& bonfireLine.IndexOf("with bonfire", System.StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& bonfireLine.IndexOf("in bonfire", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
|
||||
ActivityContext unloadTownCtx = ActivityContext.ForHarness(
|
||||
actorName,
|
||||
"town",
|
||||
"",
|
||||
"human",
|
||||
place: "Riverhold",
|
||||
carrying: "wheat");
|
||||
|
|
@ -2331,6 +2335,7 @@ public static class AgentHarness
|
|||
out _);
|
||||
bool unloadTownOk = !string.IsNullOrEmpty(unloadTownLine)
|
||||
&& unloadTownLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& unloadTownLine.IndexOf("in Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
&& unloadTownLine.IndexOf("with town", System.StringComparison.OrdinalIgnoreCase) < 0;
|
||||
|
||||
// Taxonomy kingdoms like "insect" must never become place clauses.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.12.91",
|
||||
"version": "0.12.93",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Detailed living activity prose by default.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue