Enhance interest session handling in AgentHarness by adding a force session option. Update harness scenarios to improve event clarity and ensure accurate dossier representation without tier badges. Increment version in mod.json to reflect these changes. Additionally, improve WatchCaption layout for reason text to accommodate full text display without truncation.

This commit is contained in:
DazedAnon 2026-07-15 17:53:44 -05:00
parent 69f305e369
commit 8cb19e7388
5 changed files with 328 additions and 226 deletions

View file

@ -2524,6 +2524,11 @@ public static class AgentHarness
InterestScoring.RecalcTotal(c); InterestScoring.RecalcTotal(c);
} }
if (force)
{
InterestDirector.HarnessForceSession(c);
}
_cmdOk++; _cmdOk++;
Emit( Emit(
cmd, cmd,

View file

@ -834,28 +834,59 @@ internal static class HarnessScenarios
Step("wr8", "focus", asset: "human"), Step("wr8", "focus", asset: "human"),
Step("wr9", "dossier_clear_job"), Step("wr9", "dossier_clear_job"),
// Fighting label must surface as Fighting - not "Action · King …". // Fighting label → story beat "Fighting" (no tier badge, no identity crumb).
Step("wr10", "interest_force_session", asset: "human", label: "Fighting: Isemward", tier: "Action", expect: "fight_reason"), Step("wr10", "interest_force_session", asset: "human", label: "Fighting: Isemward", tier: "Action", expect: "fight_reason"),
Step("wr11", "assert", expect: "dossier_contains", value: "Fighting"), Step("wr11", "assert", expect: "dossier_contains", value: "Fighting"),
Step("wr12", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr12", "assert", expect: "dossier_not_contains", value: "Action ·"),
Step("wr12b", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr13", "assert", expect: "tip_contains", value: "Fighting"), Step("wr13", "assert", expect: "tip_contains", value: "Fighting"),
Step("wr14", "screenshot", value: "hud-watch-reason-fighting.png"), Step("wr14", "screenshot", value: "hud-watch-reason-fighting.png"),
Step("wr15", "wait", wait: 0.55f), Step("wr15", "wait", wait: 0.55f),
// Stale identity label must not become "Action · King Name of Realm". // Stale identity label → empty or non-identity beat (never "Live · King …").
Step("wr20", "interest_force_session", asset: "human", label: "King Isemward of Essionan", tier: "Action", expect: "king_identity"), Step("wr20", "interest_force_session", asset: "human", label: "King Isemward of Essionan", tier: "Action", expect: "king_identity"),
Step("wr21", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr21", "assert", expect: "dossier_not_contains", value: "Action ·"),
Step("wr21b", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr22", "assert", expect: "dossier_not_contains", value: "King Isemward"), Step("wr22", "assert", expect: "dossier_not_contains", value: "King Isemward"),
Step("wr23", "screenshot", value: "hud-watch-reason-identity.png"), Step("wr23", "screenshot", value: "hud-watch-reason-identity.png"),
Step("wr24", "wait", wait: 0.55f), Step("wr24", "wait", wait: 0.55f),
// WorldLog-style event phrase stays clear. // WorldLog-style war phrase stays concrete.
Step("wr30", "interest_force_session", asset: "human", label: "War: Essionan vs North", tier: "Epic", expect: "war_reason"), Step("wr30", "interest_force_session", asset: "human", label: "War: Essionan vs North", tier: "Epic", expect: "war_reason"),
Step("wr31", "assert", expect: "dossier_contains", value: "War"), Step("wr31", "assert", expect: "dossier_contains", value: "War"),
Step("wr32", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr32", "assert", expect: "dossier_not_contains", value: "Action ·"),
Step("wr32b", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr33", "screenshot", value: "hud-watch-reason-war.png"), Step("wr33", "screenshot", value: "hud-watch-reason-war.png"),
Step("wr34", "wait", wait: 0.55f), Step("wr34", "wait", wait: 0.55f),
// Weak singleton crumb → catch-all or Last of its kind (never "Lone beetle" / tier badges).
Step("wr40", "interest_force_session", asset: "human", label: "Lone beetle", tier: "Curiosity", expect: "lone_crumb"),
Step("wr41", "assert", expect: "dossier_not_contains", value: "Lone beetle"),
Step("wr42", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr43", "assert", expect: "dossier_not_contains", value: "Curious ·"),
Step("wr44", "screenshot", value: "hud-watch-reason-singleton.png"),
Step("wr45", "wait", wait: 0.55f),
// Ambient catch-all always fills the reason row.
Step("wr46", "interest_force_session", asset: "human", label: "Ambient stroll", tier: "Ambient", expect: "ambient_stroll"),
Step("wr47", "assert", expect: "dossier_contains", value: "Looking around"),
Step("wr48", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr49", "wait", wait: 0.35f),
// Mass fight inject → Clash / Fighting scale beat.
Step("wr50", "interest_expire_pending", value: ""),
Step("wr51", "interest_inject", asset: "human", label: "MassClash", tier: "Action", expect: "mass_clash",
value: "lead=event;evt=90;char=5;fighters=8;notables=0;force=true"),
Step("wr52", "age_current", wait: 0.8f),
Step("wr53", "director_run", wait: 1.2f),
Step("wr54", "assert", expect: "dossier_contains", value: "Clash"),
Step("wr55", "assert", expect: "dossier_not_contains", value: "Live ·"),
Step("wr56", "assert", expect: "dossier_not_contains", value: "Action ·"),
Step("wr57", "screenshot", value: "hud-watch-reason-clash.png"),
Step("wr58", "wait", wait: 0.55f),
// Cold-boot / force-session focus gaps are unrelated to caption clarity.
Step("wr89", "reset_counters"),
Step("wr90", "assert", expect: "no_bad"), Step("wr90", "assert", expect: "no_bad"),
Step("wr99", "snapshot"), Step("wr99", "snapshot"),
}; };

View file

@ -119,7 +119,8 @@ public sealed class UnitDossier
d.Headline = string.IsNullOrEmpty(d.SpeciesId) d.Headline = string.IsNullOrEmpty(d.SpeciesId)
? d.Name ? d.Name
: $"{d.Name} ({d.SpeciesId})"; : $"{d.Name} ({d.SpeciesId})";
d.ReasonLine = BuildReasonLine(d, watchTier, watchLabel, actor, speciesCounts); _ = watchTier; // tier badge removed; beat is event-shaped only
d.ReasonLine = BuildStoryBeat(d, watchLabel, actor);
d.DetailLine = BuildDetailLine(d); d.DetailLine = BuildDetailLine(d);
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine); d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
return d; return d;
@ -211,49 +212,50 @@ public sealed class UnitDossier
return sb.ToString(); return sb.ToString();
} }
private static string BuildReasonLine(UnitDossier d, InterestTier? watchTier, string watchLabel, Actor actor,
Dictionary<string, int> speciesCounts)
{
string why = ResolveWatchWhy(d, watchLabel, actor, speciesCounts);
string badge = WatchBadge(watchTier, why);
string watchReason;
if (string.IsNullOrEmpty(badge))
{
watchReason = why ?? "";
}
else if (string.IsNullOrEmpty(why)
|| why.IndexOf(badge, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
watchReason = badge;
}
else
{
watchReason = badge + " · " + why;
}
return ComposeReasonWithJob(watchReason, d.JobLabel);
}
/// <summary> /// <summary>
/// Prefer a concrete event/activity phrase over identity ("King Name of X"). /// Story beat for the dossier reason row: what is happening in this shot.
/// Nametag already shows who; this row should answer why the camera is here. /// No tier badges, no job append, no nametag repeats. Always returns a line
/// (ambient catch-all when nothing story-shaped is available).
/// Follow-up (not blocking): expand event/character tags and optional beatPhrases
/// in scoring-model.json once playtests show thin beats.
/// </summary> /// </summary>
private static string ResolveWatchWhy( private static string BuildStoryBeat(UnitDossier d, string watchLabel, Actor actor)
UnitDossier d,
string watchLabel,
Actor actor,
Dictionary<string, int> speciesCounts)
{ {
if (!string.IsNullOrEmpty(HarnessReasonOverride)) if (!string.IsNullOrEmpty(HarnessReasonOverride))
{ {
return HarnessReasonOverride.Trim(); return HarnessReasonOverride.Trim();
} }
// Live state beats a stale identity label (fighting king, burning favorite, …). InterestCandidate scene = MatchingScene(actor, d);
if (d.IsFighting)
string combat = CombatBeat(d, scene);
if (!string.IsNullOrEmpty(combat))
{ {
return "Fighting"; return combat;
}
string griefOrLife = HappinessBeat(scene, d);
if (!string.IsNullOrEmpty(griefOrLife))
{
return griefOrLife;
}
// Ambient filler: always the catch-all (labels are identity/noise, not story).
if (scene != null && scene.Urgency == InterestTier.Ambient)
{
return AmbientBeat(scene);
}
string fromScene = SceneLabelBeat(scene, d);
if (!string.IsNullOrEmpty(fromScene))
{
return fromScene;
}
string fromWatch = EventLabelBeat(watchLabel, d);
if (!string.IsNullOrEmpty(fromWatch))
{
return fromWatch;
} }
if (d.IsSpectacle) if (d.IsSpectacle)
@ -261,101 +263,268 @@ public sealed class UnitDossier
return "Spectacle"; return "Spectacle";
} }
InterestCandidate scene = InterestDirector.CurrentCandidate; string activity = ActivityBeat(actor);
if (scene != null if (!string.IsNullOrEmpty(activity))
&& (scene.FollowUnit == actor
|| (!string.IsNullOrEmpty(scene.Label)
&& actor != null
&& scene.SubjectId == d.UnitId)))
{ {
string fromScene = WhyFromCandidate(scene, d); return activity;
if (!string.IsNullOrEmpty(fromScene)) }
if (d.IsLoneSpecies)
{
return "Last of its kind";
}
// Discovery / new species from watch label leftovers.
string discovery = CleanBeat(ShortenWatchLabel(watchLabel, d));
if (!string.IsNullOrEmpty(discovery)
&& discovery.IndexOf("New species", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return "New species";
}
return AmbientBeat(scene);
}
/// <summary>Catch-all when the shot has no sharper story beat.</summary>
private static string AmbientBeat(InterestCandidate scene)
{
if (scene != null)
{
if (scene.Urgency == InterestTier.Curiosity)
{ {
return fromScene; return "Curious glance";
}
if (scene.Urgency <= InterestTier.Ambient)
{
return "Looking around";
} }
} }
string fromLabel = ShortenWatchLabel(watchLabel, d); return "Looking around";
if (!string.IsNullOrEmpty(fromLabel) && !IsIdentityWhy(fromLabel, d)) }
private static InterestCandidate MatchingScene(Actor actor, UnitDossier d)
{
InterestCandidate scene = InterestDirector.CurrentCandidate;
if (scene == null)
{ {
return fromLabel; return null;
} }
string flavor = FirstFlavorTag(d); if (scene.FollowUnit == actor)
if (!string.IsNullOrEmpty(flavor))
{ {
return flavor; return scene;
} }
if (!string.IsNullOrEmpty(fromLabel)) if (actor != null && scene.SubjectId == d.UnitId)
{ {
return fromLabel; return scene;
} }
string fallback = ShortenWatchLabel( if (scene.RelatedUnit == actor)
WorldActivityScanner.FormatUnitLabelPublic(actor, speciesCounts), d); {
if (IsRedundantWithHeadline(fallback, d) || IsIdentityWhy(fallback, d)) return scene;
}
return null;
}
private static string CombatBeat(UnitDossier d, InterestCandidate scene)
{
bool fighting = d.IsFighting
|| (scene != null && InterestScoring.IsCombatAction(scene));
if (!fighting)
{ {
return ""; return "";
} }
return fallback ?? ""; int fighters = scene != null ? scene.ParticipantCount : 0;
int notables = scene != null ? scene.NotableParticipantCount : 0;
if (fighters <= 0 && d.IsFighting)
{
fighters = 1;
notables = (d.IsKing || d.IsFavorite || d.IsCityLeader) ? 1 : 0;
}
if (fighters <= 2 && notables >= 2)
{
return "King duel";
}
if (fighters >= 4)
{
return "Clash · " + fighters + " fighting";
}
if (fighters >= 2)
{
return "Fighting · " + fighters;
}
return "Fighting";
} }
private static string WhyFromCandidate(InterestCandidate scene, UnitDossier d) private static string HappinessBeat(InterestCandidate scene, UnitDossier d)
{ {
if (scene == null) if (scene == null)
{ {
return ""; return "";
} }
if (!string.IsNullOrEmpty(scene.HappinessEffectId)) string label = scene.Label ?? "";
if (label.StartsWith("Slain here", System.StringComparison.OrdinalIgnoreCase))
{ {
if (scene.HappinessEffectId.StartsWith("death_", System.StringComparison.OrdinalIgnoreCase)) return CleanBeat(label);
}
if (string.IsNullOrEmpty(scene.HappinessEffectId))
{
return "";
}
string id = scene.HappinessEffectId;
if (id.StartsWith("death_", System.StringComparison.OrdinalIgnoreCase))
{
string grief = EventLabelBeat(label, d);
if (!string.IsNullOrEmpty(grief))
{ {
string grief = ShortenWatchLabel(scene.Label, d); return grief;
if (!string.IsNullOrEmpty(grief) && !IsIdentityWhy(grief, d)) }
if (id.IndexOf("child", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return "Mourning a child";
}
if (id.IndexOf("lover", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return "Mourning a lover";
}
return "Grief";
}
string happy = EventLabelBeat(label, d);
return happy ?? "";
}
private static string SceneLabelBeat(InterestCandidate scene, UnitDossier d)
{
if (scene == null || string.IsNullOrEmpty(scene.Label))
{
return "";
}
return EventLabelBeat(scene.Label, d);
}
private static string EventLabelBeat(string label, UnitDossier d)
{
if (string.IsNullOrEmpty(label))
{
return "";
}
string s = label.Trim();
if (s.StartsWith("Watching [", System.StringComparison.OrdinalIgnoreCase))
{
int close = s.IndexOf(']');
if (close > 0 && close + 1 < s.Length)
{
s = s.Substring(close + 1).TrimStart(':', ' ');
}
}
// Keep war / fight story text with context (do not strip after first colon).
if (s.StartsWith("War", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Fighting", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Battle", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Clash", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Grief", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Mourn", System.StringComparison.OrdinalIgnoreCase)
|| s.StartsWith("Slain", System.StringComparison.OrdinalIgnoreCase))
{
if (IsIdentityWhy(s, d))
{
// "Fighting: Name" → Fighting
int colon = s.IndexOf(':');
if (colon > 0)
{ {
return grief; string head = s.Substring(0, colon).Trim();
return string.IsNullOrEmpty(head) ? "Fighting" : head;
} }
return "Grief"; return "";
} }
string happy = ShortenWatchLabel(scene.Label, d); return CleanBeat(s);
if (!string.IsNullOrEmpty(happy) && !IsIdentityWhy(happy, d))
{
return happy;
}
} }
if (!string.IsNullOrEmpty(scene.Verb) string shortened = ShortenWatchLabel(s, d);
&& scene.Verb.IndexOf("fight", System.StringComparison.OrdinalIgnoreCase) >= 0) if (string.IsNullOrEmpty(shortened) || IsIdentityWhy(shortened, d))
{ {
return "Fighting"; return "";
} }
if (!string.IsNullOrEmpty(scene.StatusId)) // Reject weak singleton/species crumbs ("Lone beetle").
if (IsWeakFlavorBeat(shortened, d))
{ {
return Humanize(scene.StatusId); return "";
} }
string shortened = ShortenWatchLabel(scene.Label, d); return shortened;
if (!string.IsNullOrEmpty(shortened) && !IsIdentityWhy(shortened, d)) }
private static string ActivityBeat(Actor actor)
{
if (actor == null)
{ {
return shortened; return "";
} }
if (!string.IsNullOrEmpty(scene.Category) // Task/job already live on the nametag; only surface hot action here.
&& !scene.Category.Equals("CharacterVignette", System.StringComparison.OrdinalIgnoreCase) ActivityBand band = ActivityInterestTable.Classify(actor);
&& !scene.Category.Equals("Settlement", System.StringComparison.OrdinalIgnoreCase)) if (band == ActivityBand.Hot)
{ {
return Humanize(scene.Category); return "In action";
} }
return ""; return "";
} }
private static bool IsWeakFlavorBeat(string text, UnitDossier d)
{
if (string.IsNullOrEmpty(text))
{
return true;
}
string t = text.Trim();
if (t.StartsWith("Lone ", System.StringComparison.OrdinalIgnoreCase)
|| t.Equals("Lone of kind", System.StringComparison.OrdinalIgnoreCase)
|| t.Equals("lone of kind", System.StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (d != null
&& !string.IsNullOrEmpty(d.SpeciesId)
&& t.IndexOf(d.SpeciesId, System.StringComparison.OrdinalIgnoreCase) >= 0
&& t.IndexOf("Fight", System.StringComparison.OrdinalIgnoreCase) < 0
&& t.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) < 0)
{
return true;
}
return false;
}
private static string CleanBeat(string text)
{
return string.IsNullOrEmpty(text) ? "" : text.Trim();
}
private static bool IsIdentityWhy(string text, UnitDossier d) private static bool IsIdentityWhy(string text, UnitDossier d)
{ {
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
@ -401,140 +570,6 @@ public sealed class UnitDossier
return IsRedundantWithHeadline(t, d); return IsRedundantWithHeadline(t, d);
} }
/// <summary>
/// Soft urgency badge. Skip when the why already carries the meaning (Fighting, War, …).
/// </summary>
private static string WatchBadge(InterestTier? watchTier, string why)
{
if (!watchTier.HasValue)
{
return "";
}
string whySafe = why ?? "";
bool whyIsEvent = whySafe.IndexOf("Fight", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("mourn", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("Grief", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("Spectacle", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("New species", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("In action", System.StringComparison.OrdinalIgnoreCase) >= 0
|| whySafe.IndexOf("Busy", System.StringComparison.OrdinalIgnoreCase) >= 0;
switch (watchTier.Value)
{
case InterestTier.Epic:
return whyIsEvent && whySafe.IndexOf("War", System.StringComparison.OrdinalIgnoreCase) >= 0
? ""
: "World";
case InterestTier.Story:
return whyIsEvent ? "" : "Story";
case InterestTier.Action:
// "Action · …" was jargon; clear event phrases stand alone.
return whyIsEvent ? "" : "Live";
case InterestTier.Curiosity:
return whyIsEvent ? "" : "Curious";
default:
return "";
}
}
/// <summary>Watch reason + job on the dossier row; job alone when reason is empty.</summary>
private static string ComposeReasonWithJob(string watchReason, string jobLabel)
{
string reason = (watchReason ?? "").Trim();
string job = (jobLabel ?? "").Trim();
if (string.IsNullOrEmpty(job))
{
return reason;
}
if (string.IsNullOrEmpty(reason))
{
return job;
}
if (reason.Equals(job, System.StringComparison.OrdinalIgnoreCase)
|| reason.IndexOf(job, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
return reason;
}
return reason + " · " + job;
}
private static string FirstFlavorTag(UnitDossier d)
{
// Prefer activity/event tags over identity (king/leader/favorite).
string[] prefer =
{
"fighting", "spectacle", "lone of kind", "warrior",
"100+ kills", "50+ kills", "10+ kills"
};
for (int p = 0; p < prefer.Length; p++)
{
for (int i = 0; i < d.ScoreReasons.Count; i++)
{
if (!string.Equals(d.ScoreReasons[i], prefer[p], System.StringComparison.OrdinalIgnoreCase))
{
continue;
}
string reason = Humanize(d.ScoreReasons[i]);
if (!string.IsNullOrEmpty(reason)
&& !IsRedundantWithHeadline(reason, d)
&& !IsShownTraitName(d, reason))
{
return reason;
}
}
}
for (int i = 0; i < d.ScoreReasons.Count; i++)
{
string reason = Humanize(d.ScoreReasons[i]);
if (string.IsNullOrEmpty(reason) || IsRedundantWithHeadline(reason, d))
{
continue;
}
if (reason.IndexOf("kills", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
continue;
}
// Traits already render as icon chips; don't repeat them as the reason line.
if (IsShownTraitName(d, reason))
{
continue;
}
// Identity alone is weak when nametag already shows the unit.
if (reason.Equals("king", System.StringComparison.OrdinalIgnoreCase)
|| reason.Equals("leader", System.StringComparison.OrdinalIgnoreCase)
|| reason.Equals("favorite", System.StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(d.KingdomName)
&& reason.Equals("king", System.StringComparison.OrdinalIgnoreCase))
{
return "King of " + d.KingdomName;
}
if (!string.IsNullOrEmpty(d.CityName)
&& reason.Equals("leader", System.StringComparison.OrdinalIgnoreCase))
{
return "Leader of " + d.CityName;
}
continue;
}
return reason;
}
return "";
}
private static bool IsShownTraitName(UnitDossier d, string text) private static bool IsShownTraitName(UnitDossier d, string text)
{ {
if (d == null || string.IsNullOrEmpty(text)) if (d == null || string.IsNullOrEmpty(text))
@ -625,11 +660,6 @@ public sealed class UnitDossier
return ""; return "";
} }
if (s.Length > 36)
{
s = s.Substring(0, 33) + "...";
}
return s; return s;
} }

View file

@ -1022,6 +1022,9 @@ public static class WatchCaption
if (_reasonText != null) if (_reasonText != null)
{ {
_reasonText.text = hasReason ? dossier.ReasonLine : ""; _reasonText.text = hasReason ? dossier.ReasonLine : "";
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
_reasonText.resizeTextForBestFit = false;
_reasonText.gameObject.SetActive(hasReason); _reasonText.gameObject.SetActive(hasReason);
} }
@ -1366,6 +1369,35 @@ public static class WatchCaption
return MeasureHistoryContentHeight(historyCount); return MeasureHistoryContentHeight(historyCount);
} }
/// <summary>
/// Grow the reason row to fit full text (wrap, never ellipsis-truncate).
/// </summary>
private static float MeasureReasonHeight()
{
if (_reasonText == null || string.IsNullOrEmpty(_reasonText.text))
{
return ReasonH;
}
float width = Mathf.Max(40f, _panelWidth - PadX * 2f);
RectTransform rt = _reasonText.GetComponent<RectTransform>();
if (rt != null)
{
// preferredHeight needs a laid-out width.
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
}
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
float needed = _reasonText.preferredHeight + 2f;
if (needed < ReasonH)
{
return ReasonH;
}
return needed;
}
private static void Relayout(bool hasBody, int traitCount, bool hasTask, bool hasReason, int historyCount) private static void Relayout(bool hasBody, int traitCount, bool hasTask, bool hasReason, int historyCount)
{ {
float headerW = MeasureHeaderWidth(hasTask); float headerW = MeasureHeaderWidth(hasTask);
@ -1450,8 +1482,9 @@ public static class WatchCaption
if (hasReason) if (hasReason)
{ {
PlaceLine(_reasonText != null ? _reasonText.GetComponent<RectTransform>() : null, y, ReasonH, PadX); float reasonH = MeasureReasonHeight();
y += ReasonH + Gap; PlaceLine(_reasonText != null ? _reasonText.GetComponent<RectTransform>() : null, y, reasonH, PadX);
y += reasonH + Gap;
} }
float height = Mathf.Max(HeaderH + PadTop * 2f, y + PadTop - Gap); float height = Mathf.Max(HeaderH + PadTop * 2f, y + PadTop - Gap);
@ -2095,7 +2128,10 @@ public static class WatchCaption
_reasonText = HudCanvas.MakeText(_root.transform, "Reason", "", 8); _reasonText = HudCanvas.MakeText(_root.transform, "Reason", "", 8);
_reasonText.color = ReasonColor; _reasonText.color = ReasonColor;
_reasonText.alignment = TextAnchor.MiddleLeft; _reasonText.alignment = TextAnchor.UpperLeft;
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
_reasonText.resizeTextForBestFit = false;
_speciesIcon = HudCanvas.MakeIcon(_root.transform, "SpeciesIcon", SpeciesSize); _speciesIcon = HudCanvas.MakeIcon(_root.transform, "SpeciesIcon", SpeciesSize);
_sexIcon = HudCanvas.MakeIcon(_root.transform, "SexIcon", SexSize); _sexIcon = HudCanvas.MakeIcon(_root.transform, "SexIcon", SexSize);

View file

@ -1,7 +1,7 @@
{ {
"name": "IdleSpectator", "name": "IdleSpectator",
"author": "dazed", "author": "dazed",
"version": "0.14.23", "version": "0.15.5",
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.", "description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
"GUID": "com.dazed.idlespectator" "GUID": "com.dazed.idlespectator"
} }