Enhance dossier interactions and improve Lore click handling.
- Add related unit tracking for activity logs and dossiers - Implement colorized names for related units in reason lines - Update harness scenarios to validate new interactions - Increment version to 0.26.1 in mod.json
This commit is contained in:
parent
d1be1d9f00
commit
90d5b1604f
16 changed files with 1581 additions and 83 deletions
|
|
@ -212,7 +212,8 @@ Use `fast_timing` / `director_run` instead.
|
|||
- `set_setting` (`expect=enabled|show_watch_reasons|show_dossier_caption|chronicle_enabled`)
|
||||
- `discovery_reset`, `discovery_note`, `discovery_mark`, `discovery_drain`
|
||||
- `chronicle_force`, `chronicle_open`, `chronicle_jump`
|
||||
- `assert` expects: `idle`, `health`, `power_bar`, `no_bad`, `tip_matches_unit`, `unit_asset`, `tip_contains`, `enabled`, `show_watch_reasons`, `current_tier`, `would_accept_curiosity`, `focus_same`, `presented`, `pending_discovery`, `pending_interest`, `in_grace`, `dossier_contains`, `caption_contains`, `dossier_traits_ok`, `dossier_statuses_ok`, `citizen_job_labels_ok`, `caption_layout_ok`, `dossier_history_contains`, `chronicle_count`, `chronicle_latest_contains`, `chronicle_latest_dated`, `focus_arrows`
|
||||
- `assert` expects: `idle`, `health`, `power_bar`, `no_bad`, `tip_matches_unit`, `unit_asset`, `tip_contains`, `enabled`, `show_watch_reasons`, `current_tier`, `would_accept_curiosity`, `focus_same`, `presented`, `pending_discovery`, `pending_interest`, `in_grace`, `dossier_contains`, `caption_contains`, `dossier_traits_ok`, `dossier_statuses_ok`, `citizen_job_labels_ok`, `tooltip_library_ok`, `history_other_rows`, `lore_detail_is_other`, `reason_names_colored`, `fallen_killer_banner`, `caption_layout_ok`, `dossier_history_contains`, `chronicle_count`, `chronicle_latest_contains`, `chronicle_latest_dated`, `focus_arrows`
|
||||
- `dossier_click_history_other`, `dossier_force_named_reason` - dossier row → Lore other; gold names in orange reason
|
||||
- `snapshot`, `reset_counters`, `scenario`
|
||||
|
||||
## Process safety
|
||||
|
|
|
|||
19
.cursorrules
Normal file
19
.cursorrules
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Commit message generation (SCM sparkle button)
|
||||
|
||||
When Cursor auto-generates a Git commit message for this repository:
|
||||
|
||||
1. Write a short imperative subject line (why, ~72 chars).
|
||||
2. Add a blank line.
|
||||
3. Summarize the main changes as bullet points (2-6 bullets).
|
||||
4. Do not put everything in one line.
|
||||
5. Do not add co-author trailers or agent signatures.
|
||||
|
||||
Example shape:
|
||||
|
||||
```
|
||||
Fix Lore row clicks dropping camera follow.
|
||||
|
||||
- Pause idle before focusing the browse unit
|
||||
- Assert camera follow matches Lore detail after row click
|
||||
- Leave status banners non-clickable
|
||||
```
|
||||
|
|
@ -232,6 +232,44 @@ public static class ActivityInterestTable
|
|||
return isActor ? name : "";
|
||||
}
|
||||
|
||||
/// <summary>Living interactee unit id when <see cref="ActorTargetName"/> resolves (0 otherwise).</summary>
|
||||
public static long ActorTargetId(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (actor.has_attack_target && actor.attack_target != null && actor.attack_target.isAlive()
|
||||
&& actor.attack_target.isActor())
|
||||
{
|
||||
Actor foe = actor.attack_target.a;
|
||||
if (foe != null && foe.isAlive())
|
||||
{
|
||||
return EventFeedUtil.SafeId(foe);
|
||||
}
|
||||
}
|
||||
|
||||
if (actor.beh_actor_target != null && actor.beh_actor_target.isAlive()
|
||||
&& actor.beh_actor_target.isActor())
|
||||
{
|
||||
Actor other = actor.beh_actor_target.a;
|
||||
if (other != null && other.isAlive())
|
||||
{
|
||||
return EventFeedUtil.SafeId(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Building/tile/settlement label only - never an actor name.</summary>
|
||||
public static string PlaceOrObjectLabel(Actor actor)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public sealed class ActivityEntry
|
|||
{
|
||||
public float CreatedAt;
|
||||
public long SubjectId;
|
||||
/// <summary>Other living/fallen unit named by this line (0 when none / unresolved).</summary>
|
||||
public long RelatedId;
|
||||
public ActivityKind Kind;
|
||||
public string TaskId = "";
|
||||
public string JobId = "";
|
||||
|
|
@ -419,7 +421,8 @@ public static class ActivityLog
|
|||
string actionKey,
|
||||
string rawFact,
|
||||
string actorTarget = "",
|
||||
string placeHint = "")
|
||||
string placeHint = "",
|
||||
long relatedId = 0)
|
||||
{
|
||||
if (actor == null || !actor.isAlive() || string.IsNullOrEmpty(actionKey))
|
||||
{
|
||||
|
|
@ -504,6 +507,7 @@ public static class ActivityLog
|
|||
{
|
||||
CreatedAt = now,
|
||||
SubjectId = id,
|
||||
RelatedId = relatedId,
|
||||
Kind = ActivityKind.ActionBeat,
|
||||
TaskId = actionKey,
|
||||
JobId = jobId,
|
||||
|
|
@ -519,7 +523,12 @@ public static class ActivityLog
|
|||
/// <summary>
|
||||
/// Life milestone mirrored into Activity (no beat cooldown). Death may use a dead actor.
|
||||
/// </summary>
|
||||
public static void NoteMilestone(Actor actor, string actionKey, string rawLine, string target = "")
|
||||
public static void NoteMilestone(
|
||||
Actor actor,
|
||||
string actionKey,
|
||||
string rawLine,
|
||||
string target = "",
|
||||
long relatedId = 0)
|
||||
{
|
||||
if (actor == null || string.IsNullOrEmpty(actionKey) || string.IsNullOrEmpty(rawLine))
|
||||
{
|
||||
|
|
@ -612,6 +621,7 @@ public static class ActivityLog
|
|||
{
|
||||
CreatedAt = now,
|
||||
SubjectId = id,
|
||||
RelatedId = relatedId,
|
||||
Kind = ActivityKind.ActionBeat,
|
||||
TaskId = actionKey,
|
||||
JobId = "",
|
||||
|
|
@ -673,7 +683,8 @@ public static class ActivityLog
|
|||
actorName = "creature";
|
||||
}
|
||||
|
||||
ForceStatusNote(id, statusId.Trim(), gained, actorName);
|
||||
ResolveStatusRelated(actor, out long relatedId, out string relatedName);
|
||||
ForceStatusNote(id, statusId.Trim(), gained, actorName, relatedId, relatedName);
|
||||
try
|
||||
{
|
||||
HappinessEventRouter.NoteStatusPhase(id, statusId.Trim());
|
||||
|
|
@ -693,12 +704,50 @@ public static class ActivityLog
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Related unit only when a happiness/relationship hook stamped the context bag
|
||||
/// (e.g. becomeLoversWith → fell_in_love). Do not invent lover links for every status.
|
||||
/// </summary>
|
||||
private static void ResolveStatusRelated(Actor actor, out long relatedId, out string relatedName)
|
||||
{
|
||||
relatedId = 0;
|
||||
relatedName = "";
|
||||
if (actor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!HappinessContextBag.TryGetRelated(actor, out Actor bagRelated, out _)
|
||||
|| bagRelated == null
|
||||
|| !bagRelated.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
relatedId = EventFeedUtil.SafeId(bagRelated);
|
||||
relatedName = bagRelated.getName() ?? "";
|
||||
if (string.IsNullOrEmpty(relatedName) && bagRelated.asset != null)
|
||||
{
|
||||
relatedName = bagRelated.asset.id ?? "";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
relatedId = 0;
|
||||
relatedName = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Harness / forced path for status lines without a live transition.</summary>
|
||||
public static bool ForceStatusNote(
|
||||
long subjectId,
|
||||
string statusId,
|
||||
bool gained,
|
||||
string actorName = "")
|
||||
string actorName = "",
|
||||
long relatedId = 0,
|
||||
string relatedName = "")
|
||||
{
|
||||
if (subjectId == 0 || string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
|
|
@ -709,9 +758,30 @@ public static class ActivityLog
|
|||
string name = string.IsNullOrEmpty(actorName) ? "Someone" : actorName;
|
||||
string phrase = ActivityStatusProse.Phrase(id, gained, out _);
|
||||
string clause = LowerFirst(phrase);
|
||||
if (gained
|
||||
&& !string.IsNullOrEmpty(relatedName)
|
||||
&& !string.IsNullOrEmpty(clause)
|
||||
&& clause.IndexOf(relatedName, System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
// Keep short status verbs readable when we know who they are about.
|
||||
clause = clause + " with " + relatedName;
|
||||
}
|
||||
|
||||
string display = string.IsNullOrEmpty(clause) ? name : name + " " + clause;
|
||||
string displayRich = ActivityProse.ColorName(name)
|
||||
+ (string.IsNullOrEmpty(clause) ? "" : " " + clause);
|
||||
string displayRich;
|
||||
if (!string.IsNullOrEmpty(relatedName)
|
||||
&& !string.IsNullOrEmpty(clause)
|
||||
&& clause.IndexOf(relatedName, System.StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
displayRich = ActivityProse.ColorName(name) + " "
|
||||
+ clause.Replace(relatedName, ActivityProse.ColorName(relatedName));
|
||||
}
|
||||
else
|
||||
{
|
||||
displayRich = ActivityProse.ColorName(name)
|
||||
+ (string.IsNullOrEmpty(clause) ? "" : " " + clause);
|
||||
}
|
||||
|
||||
string key = ActivityStatusProse.TaskKey(id, gained);
|
||||
string fact = (gained ? "Gained " : "Lost ") + id;
|
||||
|
||||
|
|
@ -732,15 +802,21 @@ public static class ActivityLog
|
|||
}
|
||||
}
|
||||
|
||||
if (relatedId == 0 && !string.IsNullOrEmpty(relatedName))
|
||||
{
|
||||
relatedId = ResolveLivingUnitIdByName(relatedName, subjectId);
|
||||
}
|
||||
|
||||
RecordSample(key, isBeat: true);
|
||||
Append(subjectId, new ActivityEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
SubjectId = subjectId,
|
||||
RelatedId = relatedId != subjectId ? relatedId : 0,
|
||||
Kind = gained ? ActivityKind.StatusGain : ActivityKind.StatusLoss,
|
||||
TaskId = key,
|
||||
JobId = "",
|
||||
TargetLabel = id,
|
||||
TargetLabel = !string.IsNullOrEmpty(relatedName) ? relatedName : id,
|
||||
Line = fact,
|
||||
DisplayLine = display,
|
||||
DisplayLineRich = displayRich
|
||||
|
|
@ -772,10 +848,17 @@ public static class ActivityLog
|
|||
}
|
||||
|
||||
RecordSample(key, isBeat: true);
|
||||
long relatedId = occ.RelatedId;
|
||||
if (relatedId == 0 && !string.IsNullOrEmpty(occ.RelatedName))
|
||||
{
|
||||
relatedId = ResolveLivingUnitIdByName(occ.RelatedName, occ.SubjectId);
|
||||
}
|
||||
|
||||
Append(occ.SubjectId, new ActivityEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
SubjectId = occ.SubjectId,
|
||||
RelatedId = relatedId != occ.SubjectId ? relatedId : 0,
|
||||
Kind = ActivityKind.Happiness,
|
||||
TaskId = key,
|
||||
JobId = occ.Amount.ToString(),
|
||||
|
|
@ -1032,10 +1115,17 @@ public static class ActivityLog
|
|||
idx,
|
||||
out string display,
|
||||
out string displayRich);
|
||||
long relatedId = 0;
|
||||
if (ctx != null && ctx.TargetIsActor && !string.IsNullOrEmpty(ctx.TargetName))
|
||||
{
|
||||
relatedId = ResolveLivingUnitIdByName(ctx.TargetName, subjectId);
|
||||
}
|
||||
|
||||
Append(subjectId, new ActivityEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
SubjectId = subjectId,
|
||||
RelatedId = relatedId,
|
||||
Kind = kind,
|
||||
TaskId = taskOrAction,
|
||||
JobId = ctx.JobId ?? "",
|
||||
|
|
@ -1047,6 +1137,45 @@ public static class ActivityLog
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Living unit id with exact name match, or 0. Used by dossier row links.</summary>
|
||||
public static long ResolveLivingUnitIdByName(string name, long excludeId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || World.world?.units == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (Actor unit in World.world.units)
|
||||
{
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
long id = EventFeedUtil.SafeId(unit);
|
||||
if (id == 0 || id == excludeId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string n = unit.getName();
|
||||
if (!string.IsNullOrEmpty(n)
|
||||
&& string.Equals(n, name, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void RecordSample(string key, bool isBeat)
|
||||
{
|
||||
if (_clearedAt < 0f)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ namespace IdleSpectator;
|
|||
/// </summary>
|
||||
public static class ActivityProse
|
||||
{
|
||||
public const string NameColorHex = "#F0C14A";
|
||||
/// <summary>
|
||||
/// Bright lemon for person names - must read clearly on dark dossier chrome and
|
||||
/// against orange reason text (<c>#F2B861</c>).
|
||||
/// </summary>
|
||||
public const string NameColorHex = "#FFE566";
|
||||
|
||||
public static string ColorName(string name)
|
||||
{
|
||||
|
|
@ -15,7 +19,76 @@ public static class ActivityProse
|
|||
return "";
|
||||
}
|
||||
|
||||
return "<color=" + NameColorHex + ">" + name + "</color>";
|
||||
return "<b><color=" + NameColorHex + ">" + name + "</color></b>";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap known person names in gold for orange reason beats (rest inherits ReasonColor).
|
||||
/// Longer names first so partial overlaps do not corrupt replacements.
|
||||
/// </summary>
|
||||
public static string ColorizePersonNames(string plain, params string[] names)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plain) || names == null || names.Length == 0)
|
||||
{
|
||||
return plain ?? "";
|
||||
}
|
||||
|
||||
var unique = new System.Collections.Generic.List<string>();
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
string n = names[i];
|
||||
if (string.IsNullOrEmpty(n))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool seen = false;
|
||||
for (int j = 0; j < unique.Count; j++)
|
||||
{
|
||||
if (string.Equals(unique[j], n, System.StringComparison.Ordinal))
|
||||
{
|
||||
seen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!seen)
|
||||
{
|
||||
unique.Add(n);
|
||||
}
|
||||
}
|
||||
|
||||
unique.Sort((a, b) => b.Length.CompareTo(a.Length));
|
||||
string result = plain;
|
||||
for (int i = 0; i < unique.Count; i++)
|
||||
{
|
||||
string n = unique[i];
|
||||
// Skip if already wrapped from a prior pass.
|
||||
if (result.IndexOf(NameColorHex + ">" + n + "</color>", System.StringComparison.Ordinal) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result = ReplaceNameLiteral(result, n, ColorName(n));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ReplaceNameLiteral(string haystack, string name, string rich)
|
||||
{
|
||||
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(name))
|
||||
{
|
||||
return haystack;
|
||||
}
|
||||
|
||||
int idx = haystack.IndexOf(name, System.StringComparison.Ordinal);
|
||||
if (idx < 0)
|
||||
{
|
||||
return haystack;
|
||||
}
|
||||
|
||||
return haystack.Substring(0, idx) + rich + haystack.Substring(idx + name.Length);
|
||||
}
|
||||
|
||||
public static void Format(
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ public static class ActorActivityPatches
|
|||
pActor,
|
||||
"BehAttackActorHuntingTarget",
|
||||
string.IsNullOrEmpty(actorTarget) ? "Hunts prey" : "Hunts " + actorTarget,
|
||||
actorTarget: actorTarget);
|
||||
actorTarget: actorTarget,
|
||||
relatedId: ActivityInterestTable.ActorTargetId(pActor));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehCityActorRemoveFire), nameof(BehCityActorRemoveFire.execute))]
|
||||
|
|
@ -126,7 +127,8 @@ public static class ActorActivityPatches
|
|||
pActor,
|
||||
"BehTryToSocialize",
|
||||
string.IsNullOrEmpty(actorTarget) ? "Tries to socialize" : "Tries to socialize with " + actorTarget,
|
||||
actorTarget: actorTarget);
|
||||
actorTarget: actorTarget,
|
||||
relatedId: ActivityInterestTable.ActorTargetId(pActor));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehPlantCrops), nameof(BehPlantCrops.execute))]
|
||||
|
|
|
|||
|
|
@ -412,6 +412,7 @@ public static class AgentHarness
|
|||
bool ok = Chronicle.ForceLifeMilestoneOnFocus(kind, other);
|
||||
if (ok)
|
||||
{
|
||||
WatchCaption.ForceRefreshHistory();
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
|
|
@ -421,7 +422,7 @@ public static class AgentHarness
|
|||
|
||||
long id = Chronicle.CurrentHistorySubjectId();
|
||||
Emit(cmd, ok, detail:
|
||||
$"milestone={kind} history={Chronicle.HistoryCountFor(id)} activity={ActivityLog.CountFor(id)}");
|
||||
$"milestone={kind} history={Chronicle.HistoryCountFor(id)} activity={ActivityLog.CountFor(id)} otherRows={WatchCaption.CountHistoryOtherRows()}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -569,6 +570,124 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
|
||||
case "dossier_force_named_reason":
|
||||
{
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
string name = "";
|
||||
try
|
||||
{
|
||||
name = focus != null ? (focus.getName() ?? "") : "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
name = "";
|
||||
}
|
||||
|
||||
string clause = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "is fighting";
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, false, detail: "no_focus_name");
|
||||
break;
|
||||
}
|
||||
|
||||
// Optional related: label/expect name, else remembered happiness partner.
|
||||
string relatedName = !string.IsNullOrEmpty(cmd.expect)
|
||||
? cmd.expect.Trim()
|
||||
: (!string.IsNullOrEmpty(cmd.label) ? cmd.label.Trim() : "");
|
||||
long relatedId = 0;
|
||||
Actor relatedActor = null;
|
||||
if (string.Equals(relatedName, "auto", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| string.IsNullOrEmpty(relatedName))
|
||||
{
|
||||
relatedActor = _happinessPartner != null && _happinessPartner.isAlive()
|
||||
? _happinessPartner
|
||||
: null;
|
||||
if (relatedActor != null)
|
||||
{
|
||||
relatedId = EventFeedUtil.SafeId(relatedActor);
|
||||
relatedName = SafeName(relatedActor);
|
||||
}
|
||||
else
|
||||
{
|
||||
relatedName = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
relatedId = ActivityLog.ResolveLivingUnitIdByName(
|
||||
relatedName,
|
||||
EventFeedUtil.SafeId(focus));
|
||||
}
|
||||
|
||||
string reason = name + " " + clause.Trim();
|
||||
if (!string.IsNullOrEmpty(relatedName)
|
||||
&& reason.IndexOf(relatedName, System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
reason = reason + " " + relatedName;
|
||||
}
|
||||
|
||||
bool ok = WatchCaption.ForceJobLabelOnFocus(
|
||||
!string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "farmer",
|
||||
reason,
|
||||
relatedId,
|
||||
relatedName);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail:
|
||||
$"reason='{WatchCaption.Current?.ReasonLine}' relatedId={relatedId} clickable={WatchCaption.ReasonOtherClickable}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "dossier_click_reason_other":
|
||||
{
|
||||
bool ok = WatchCaption.ClickReasonOther(out long otherId);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail: $"clicked={ok} otherId={otherId} lore={ChronicleHud.DetailUnitId}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "dossier_click_history_other":
|
||||
{
|
||||
WatchCaption.ForceRefreshHistory();
|
||||
bool ok = WatchCaption.ClickFirstHistoryOtherRow(out long otherId);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail: $"clicked={ok} otherId={otherId} otherRows={WatchCaption.CountHistoryOtherRows()} lore={ChronicleHud.DetailUnitId}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "dossier_clear_job":
|
||||
{
|
||||
WatchCaption.ClearHarnessJobOverrides();
|
||||
|
|
@ -684,6 +803,7 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
bool expectBlocked = (cmd.expect ?? "").IndexOf("block", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool optional = (cmd.expect ?? "").IndexOf("optional", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool alive = false;
|
||||
try
|
||||
{
|
||||
|
|
@ -695,7 +815,19 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
bool applied = alive && StatusGameApi.TryAddStatus(focus, statusId, timer);
|
||||
// Some units briefly refuse a status (immunity / race); one retry covers that class.
|
||||
if (!applied && alive && !expectBlocked)
|
||||
{
|
||||
applied = StatusGameApi.TryAddStatus(focus, statusId, timer);
|
||||
}
|
||||
|
||||
bool ok = expectBlocked ? !applied : applied;
|
||||
if (optional)
|
||||
{
|
||||
// Seed-only second statuses: dossier only needs one chip for the assert.
|
||||
ok = true;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
|
|
@ -710,7 +842,7 @@ public static class AgentHarness
|
|||
cmd,
|
||||
ok,
|
||||
detail:
|
||||
$"status={statusId} applied={applied} expectBlocked={expectBlocked} alive={alive} "
|
||||
$"status={statusId} applied={applied} expectBlocked={expectBlocked} optional={optional} alive={alive} "
|
||||
+ $"gains={ActivityLog.StatusGainsSinceClear} refreshes={ActivityLog.StatusRefreshesSinceClear} "
|
||||
+ $"active={StatusGameApi.HasStatus(focus, statusId)}");
|
||||
break;
|
||||
|
|
@ -5942,6 +6074,139 @@ public static class AgentHarness
|
|||
detail = $"needle='{needle}' caption='{caption.Replace("\n", " | ")}' dossier={(dossier != null)} reason='{dossier?.ReasonLine}'";
|
||||
break;
|
||||
}
|
||||
case "tooltip_library_ok":
|
||||
{
|
||||
pass = DossierAssetTips.RunTooltipInventoryAudit(HarnessDir(), out string tipDetail);
|
||||
detail = tipDetail;
|
||||
break;
|
||||
}
|
||||
case "history_other_rows":
|
||||
{
|
||||
WatchCaption.ForceRefreshHistory();
|
||||
int have = WatchCaption.CountHistoryOtherRows();
|
||||
int want = 1;
|
||||
if (!string.IsNullOrEmpty(cmd.value)
|
||||
&& int.TryParse(cmd.value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
|
||||
{
|
||||
want = parsed;
|
||||
}
|
||||
|
||||
string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant();
|
||||
pass = cmp == "exact" ? have == want : have >= want;
|
||||
long subjectId = WatchCaption.CurrentUnitId;
|
||||
if (subjectId == 0)
|
||||
{
|
||||
subjectId = ResolveActivitySubjectId();
|
||||
}
|
||||
|
||||
int withRelated = 0;
|
||||
var bits = new System.Text.StringBuilder();
|
||||
IReadOnlyList<ActivityEntry> ring = ActivityLog.LatestForSubject(subjectId, 8);
|
||||
for (int i = 0; i < ring.Count; i++)
|
||||
{
|
||||
ActivityEntry e = ring[i];
|
||||
if (e == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.RelatedId != 0 && e.RelatedId != subjectId)
|
||||
{
|
||||
withRelated++;
|
||||
}
|
||||
|
||||
if (bits.Length > 0)
|
||||
{
|
||||
bits.Append(';');
|
||||
}
|
||||
|
||||
bits.Append(e.TaskId ?? "")
|
||||
.Append(':')
|
||||
.Append(e.RelatedId)
|
||||
.Append(':')
|
||||
.Append(e.TargetLabel ?? "");
|
||||
}
|
||||
|
||||
detail =
|
||||
$"otherRows={have} want={want} cmp={cmp} subject={subjectId} visible={WatchCaption.Visible} "
|
||||
+ $"ringRelated={withRelated} ring=[{bits}] hist='{WatchCaption.LastHistoryJoined}'";
|
||||
break;
|
||||
}
|
||||
case "lore_detail_is_other":
|
||||
{
|
||||
long loreId = ChronicleHud.DetailUnitId;
|
||||
long dossierId = WatchCaption.CurrentUnitId;
|
||||
long cameraId = 0;
|
||||
bool cameraFollow = false;
|
||||
try
|
||||
{
|
||||
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||
{
|
||||
cameraId = MoveCamera._focus_unit.getID();
|
||||
cameraFollow = MoveCamera.isCameraFollowingUnit(MoveCamera._focus_unit);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
cameraId = 0;
|
||||
cameraFollow = false;
|
||||
}
|
||||
|
||||
bool fallenKiller = (Chronicle.LastFallenFocusDetail ?? "")
|
||||
.StartsWith("killer:", System.StringComparison.Ordinal);
|
||||
// After row-click OpenUnitHistory(other): Lore + dossier + camera on that living unit.
|
||||
bool dossierOk = loreId != 0 && loreId == dossierId;
|
||||
bool cameraOk = fallenKiller
|
||||
|| (cameraId != 0 && cameraId == loreId && cameraFollow);
|
||||
pass = dossierOk && cameraOk;
|
||||
detail =
|
||||
$"loreId={loreId} dossierId={dossierId} cameraId={cameraId} cameraFollow={cameraFollow} "
|
||||
+ $"follow={ChronicleHud.FollowFocus} fallenKiller={fallenKiller}";
|
||||
break;
|
||||
}
|
||||
case "reason_names_colored":
|
||||
{
|
||||
string reason = WatchCaption.Current?.ReasonLine ?? "";
|
||||
bool hasColor = reason.IndexOf(ActivityProse.NameColorHex, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| reason.IndexOf("<color=", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
pass = hasColor && !string.IsNullOrEmpty(reason);
|
||||
detail = $"colored={hasColor} reason='{reason}'";
|
||||
break;
|
||||
}
|
||||
case "reason_other_clickable":
|
||||
{
|
||||
bool want = string.IsNullOrEmpty(cmd.value)
|
||||
|| !string.Equals(cmd.value.Trim(), "false", System.StringComparison.OrdinalIgnoreCase);
|
||||
bool have = WatchCaption.ReasonOtherClickable;
|
||||
pass = have == want;
|
||||
detail =
|
||||
$"clickable={have} want={want} relatedId={WatchCaption.Current?.ReasonRelatedId ?? 0} "
|
||||
+ $"reason='{WatchCaption.Current?.ReasonLine}'";
|
||||
break;
|
||||
}
|
||||
case "fallen_killer_banner":
|
||||
{
|
||||
string banner = WatchCaption.LastCaptionText ?? "";
|
||||
string reason = "";
|
||||
try
|
||||
{
|
||||
// Banner is shown on reason text while status banner is active.
|
||||
reason = WatchCaption.Current?.ReasonLine ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
reason = "";
|
||||
}
|
||||
|
||||
string hay = (banner + " " + reason).ToLowerInvariant();
|
||||
bool has = hay.IndexOf("camera on killer", System.StringComparison.Ordinal) >= 0;
|
||||
bool killerFocus = (Chronicle.LastFallenFocusDetail ?? "")
|
||||
.StartsWith("killer:", System.StringComparison.Ordinal);
|
||||
pass = killerFocus && has;
|
||||
detail =
|
||||
$"fallenFocus={Chronicle.LastFallenFocusDetail} bannerHit={has} caption='{banner}'";
|
||||
break;
|
||||
}
|
||||
case "citizen_job_labels_ok":
|
||||
{
|
||||
// Full live citizen_job_library: non-empty Title Case labels; resource-role
|
||||
|
|
|
|||
|
|
@ -1227,7 +1227,8 @@ public static class Chronicle
|
|||
|
||||
string victimName = SafeName(victim);
|
||||
string line = $"Killed {victimName} ({SpeciesOf(victim)})";
|
||||
ActivityLog.NoteMilestone(killer, "milestone_kill", line, victimName);
|
||||
long victimId = EventFeedUtil.SafeId(victim);
|
||||
ActivityLog.NoteMilestone(killer, "milestone_kill", line, victimName, victimId);
|
||||
try
|
||||
{
|
||||
HappinessEventRouter.NoteCanonicalMilestone(killer.getID(), "milestone_kill");
|
||||
|
|
@ -1276,7 +1277,8 @@ public static class Chronicle
|
|||
|
||||
string line = FormatDeathCause(victim, attackType, killer);
|
||||
string killerName = killer != null ? SafeName(killer) : "";
|
||||
ActivityLog.NoteMilestone(victim, "milestone_death", line, killerName);
|
||||
long killerId = EventFeedUtil.SafeId(killer);
|
||||
ActivityLog.NoteMilestone(victim, "milestone_death", line, killerName, killerId);
|
||||
if (ModSettings.ChronicleEnabled)
|
||||
{
|
||||
AppendHistory(ChronicleKind.Death, victim, killer, line, attackType);
|
||||
|
|
@ -1302,8 +1304,8 @@ public static class Chronicle
|
|||
string nameB = SafeName(b);
|
||||
string lineA = $"Became lovers with {nameB}";
|
||||
string lineB = $"Became lovers with {nameA}";
|
||||
ActivityLog.NoteMilestone(a, "milestone_lover", lineA, nameB);
|
||||
ActivityLog.NoteMilestone(b, "milestone_lover", lineB, nameA);
|
||||
ActivityLog.NoteMilestone(a, "milestone_lover", lineA, nameB, EventFeedUtil.SafeId(b));
|
||||
ActivityLog.NoteMilestone(b, "milestone_lover", lineB, nameA, EventFeedUtil.SafeId(a));
|
||||
try
|
||||
{
|
||||
HappinessEventRouter.NoteCanonicalMilestone(a.getID(), "milestone_lover");
|
||||
|
|
@ -1348,8 +1350,8 @@ public static class Chronicle
|
|||
string nameB = SafeName(b);
|
||||
string lineA = $"Befriended {nameB}";
|
||||
string lineB = $"Befriended {nameA}";
|
||||
ActivityLog.NoteMilestone(a, "milestone_friend", lineA, nameB);
|
||||
ActivityLog.NoteMilestone(b, "milestone_friend", lineB, nameA);
|
||||
ActivityLog.NoteMilestone(a, "milestone_friend", lineA, nameB, EventFeedUtil.SafeId(b));
|
||||
ActivityLog.NoteMilestone(b, "milestone_friend", lineB, nameA, EventFeedUtil.SafeId(a));
|
||||
try
|
||||
{
|
||||
HappinessEventRouter.NoteCanonicalMilestone(a.getID(), "milestone_friend");
|
||||
|
|
@ -1931,6 +1933,37 @@ public static class Chronicle
|
|||
/// <summary>Harness: world position used for the last place-style fallen focus.</summary>
|
||||
public static Vector3 LastFallenFocusPosition { get; private set; } = Vector3.zero;
|
||||
|
||||
/// <summary>
|
||||
/// Pause banner after fallen Lore open - must say when camera is on the killer.
|
||||
/// </summary>
|
||||
public static string FallenPauseBanner()
|
||||
{
|
||||
string detail = LastFallenFocusDetail ?? "";
|
||||
if (detail.StartsWith("killer:", System.StringComparison.Ordinal))
|
||||
{
|
||||
string killerLabel = "";
|
||||
if (detail.Length > 7
|
||||
&& long.TryParse(detail.Substring(7), out long killerId)
|
||||
&& killerId != 0)
|
||||
{
|
||||
Actor killer = FindUnitById(killerId);
|
||||
if (killer != null)
|
||||
{
|
||||
killerLabel = SafeName(killer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(killerLabel))
|
||||
{
|
||||
return "Paused (viewing fallen - camera on killer " + killerLabel + ")";
|
||||
}
|
||||
|
||||
return "Paused (viewing fallen - camera on killer)";
|
||||
}
|
||||
|
||||
return "Paused (viewing fallen)";
|
||||
}
|
||||
|
||||
/// <summary>Snapshot living dossier just before die() - for Fallen archive UI.</summary>
|
||||
public static void CaptureFinalDossier(Actor victim)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -759,6 +759,8 @@ public static class ChronicleHud
|
|||
_followFocus = followFocus;
|
||||
_detailPane = DetailPane.Life;
|
||||
|
||||
// Pause idle first: SetActive(false) calls ClearFollow(). Focusing before that
|
||||
// would instantly drop the camera off the browse subject.
|
||||
if (pauseIdle && SpectatorMode.Active)
|
||||
{
|
||||
_pausedIdleForLore = true;
|
||||
|
|
@ -771,22 +773,18 @@ public static class ChronicleHud
|
|||
}
|
||||
|
||||
bool live = Chronicle.FocusCameraForBrowse(unitId);
|
||||
_tab = live ? LoreTab.Living : LoreTab.Fallen;
|
||||
if (live)
|
||||
if (!live)
|
||||
{
|
||||
WatchCaption.PinWhilePaused();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dead / missing: keep dossier if any; pan to killer or death place.
|
||||
WatchCaption.PinWhilePaused();
|
||||
Chronicle.FocusFallenSubject(unitId);
|
||||
}
|
||||
|
||||
_tab = live ? LoreTab.Living : LoreTab.Fallen;
|
||||
WatchCaption.PinWhilePaused();
|
||||
|
||||
if (pauseIdle)
|
||||
{
|
||||
WatchCaption.ShowStatusBanner(
|
||||
live ? "Paused (viewing history)" : "Paused (viewing fallen)");
|
||||
live ? "Paused (viewing history)" : Chronicle.FallenPauseBanner());
|
||||
}
|
||||
|
||||
SetVisible(true);
|
||||
|
|
|
|||
321
IdleSpectator/DossierAssetTips.cs
Normal file
321
IdleSpectator/DossierAssetTips.cs
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Vanilla trait/status tooltips for dossier chips.
|
||||
/// Tip types come from live <see cref="AssetManager.tooltips"/> (not invented keys).
|
||||
/// </summary>
|
||||
public static class DossierAssetTips
|
||||
{
|
||||
public const string TraitTipType = "trait";
|
||||
public const string StatusTipType = "status";
|
||||
|
||||
private static HashSet<string> _liveTipTypes;
|
||||
private static int _liveTipTypesCount = -1;
|
||||
|
||||
public static List<string> EnumerateLiveTooltipTypeIds()
|
||||
{
|
||||
var ids = new List<string>();
|
||||
try
|
||||
{
|
||||
TooltipLibrary lib = AssetManager.tooltips;
|
||||
if (lib?.list == null)
|
||||
{
|
||||
return ids;
|
||||
}
|
||||
|
||||
for (int i = 0; i < lib.list.Count; i++)
|
||||
{
|
||||
TooltipAsset 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 bool HasLiveTooltipType(string tipType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tipType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureLiveTipTypes();
|
||||
return _liveTipTypes != null && _liveTipTypes.Contains(tipType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Harness audit: dump live tooltip ids; require trait + status tip types exist.
|
||||
/// </summary>
|
||||
public static bool RunTooltipInventoryAudit(string outputDirectory, out string detail)
|
||||
{
|
||||
List<string> ids = EnumerateLiveTooltipTypeIds();
|
||||
var lines = new List<string>(ids.Count + 1) { "id" };
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
{
|
||||
lines.Add(ids[i]);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
File.WriteAllLines(Path.Combine(outputDirectory, "tooltip-library-ids.tsv"), lines);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// diagnostic dump only
|
||||
}
|
||||
|
||||
bool hasTrait = HasLiveTooltipType(TraitTipType);
|
||||
bool hasStatus = HasLiveTooltipType(StatusTipType);
|
||||
bool pass = ids.Count > 0 && hasTrait && hasStatus;
|
||||
detail = $"tips={ids.Count} trait={hasTrait} status={hasStatus}";
|
||||
return pass;
|
||||
}
|
||||
|
||||
public static void ShowTraitTip(GameObject host, ActorTrait trait)
|
||||
{
|
||||
if (host == null || trait == null || !HasLiveTooltipType(TraitTipType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Config.tooltips_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Tooltip.show(host, TraitTipType, new TooltipData { trait = trait });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Tip surface can be unavailable mid-load.
|
||||
}
|
||||
}
|
||||
|
||||
public static void ShowStatusTip(GameObject host, Actor actor, StatusAsset asset, string statusId)
|
||||
{
|
||||
if (host == null || !HasLiveTooltipType(StatusTipType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StatusAsset resolved = asset;
|
||||
if (resolved == null && !string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
resolved = ActivityAssetCatalog.TryGetStatusAsset(statusId);
|
||||
}
|
||||
|
||||
if (resolved == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Status live = FindLiveStatus(actor, resolved.id ?? statusId);
|
||||
if (live == null)
|
||||
{
|
||||
// Vanilla status tip needs a Status instance (time bar + stats).
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Config.tooltips_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string locale = "";
|
||||
string description = "";
|
||||
try
|
||||
{
|
||||
locale = resolved.getLocaleID() ?? "";
|
||||
description = resolved.getDescriptionID() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through with empty locale keys - showStatus still uses status.asset stats
|
||||
}
|
||||
|
||||
Tooltip.show(host, StatusTipType, new TooltipData
|
||||
{
|
||||
tip_name = locale,
|
||||
tip_description = description,
|
||||
status = live
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Tip surface can be unavailable mid-load.
|
||||
}
|
||||
}
|
||||
|
||||
public static void HideTip()
|
||||
{
|
||||
try
|
||||
{
|
||||
Tooltip.hideTooltip();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private static Status FindLiveStatus(Actor actor, string statusId)
|
||||
{
|
||||
if (actor == null || string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!actor.isAlive())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (Status status in actor.getStatuses())
|
||||
{
|
||||
if (status?.asset != null
|
||||
&& string.Equals(status.asset.id, statusId, StringComparison.Ordinal))
|
||||
{
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void EnsureLiveTipTypes()
|
||||
{
|
||||
List<string> ids = EnumerateLiveTooltipTypeIds();
|
||||
if (_liveTipTypes != null && _liveTipTypesCount == ids.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_liveTipTypes = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ids[i]))
|
||||
{
|
||||
_liveTipTypes.Add(ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_liveTipTypesCount = ids.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pointer hover on a dossier trait/status chip root.</summary>
|
||||
public sealed class DossierChipTip : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
public enum Kind
|
||||
{
|
||||
Trait,
|
||||
Status
|
||||
}
|
||||
|
||||
public Kind ChipKind;
|
||||
public string AssetId = "";
|
||||
public ActorTrait Trait;
|
||||
public StatusAsset Status;
|
||||
|
||||
public void BindTrait(ActorTrait trait, string id)
|
||||
{
|
||||
ChipKind = Kind.Trait;
|
||||
Trait = trait;
|
||||
Status = null;
|
||||
AssetId = id ?? (trait != null ? trait.id : "");
|
||||
EnsureHitTarget();
|
||||
}
|
||||
|
||||
public void BindStatus(StatusAsset status, string id)
|
||||
{
|
||||
ChipKind = Kind.Status;
|
||||
Status = status;
|
||||
Trait = null;
|
||||
AssetId = id ?? (status != null ? status.id : "");
|
||||
EnsureHitTarget();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Trait = null;
|
||||
Status = null;
|
||||
AssetId = "";
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (ChipKind == Kind.Trait)
|
||||
{
|
||||
ActorTrait trait = Trait;
|
||||
if (trait == null && !string.IsNullOrEmpty(AssetId))
|
||||
{
|
||||
try
|
||||
{
|
||||
trait = AssetManager.traits?.get(AssetId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
trait = null;
|
||||
}
|
||||
}
|
||||
|
||||
DossierAssetTips.ShowTraitTip(gameObject, trait);
|
||||
return;
|
||||
}
|
||||
|
||||
Actor actor = WatchCaption.BoundActor;
|
||||
DossierAssetTips.ShowStatusTip(gameObject, actor, Status, AssetId);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
DossierAssetTips.HideTip();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DossierAssetTips.HideTip();
|
||||
}
|
||||
|
||||
private void EnsureHitTarget()
|
||||
{
|
||||
Image hit = GetComponent<Image>();
|
||||
if (hit == null)
|
||||
{
|
||||
hit = gameObject.AddComponent<Image>();
|
||||
}
|
||||
|
||||
hit.color = new Color(1f, 1f, 1f, 0.001f);
|
||||
hit.raycastTarget = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ public static class DossierAvatar
|
|||
private static bool _usingVanilla;
|
||||
private static bool _loggedMode;
|
||||
private static long _shownActorId = -1;
|
||||
/// <summary>Egg/baby/adult + species - same unit id can change visual form.</summary>
|
||||
private static string _shownFormKey = "";
|
||||
|
||||
public static bool UsingVanilla => _usingVanilla;
|
||||
|
||||
|
|
@ -48,6 +50,7 @@ public static class DossierAvatar
|
|||
_usingVanilla = false;
|
||||
_loggedMode = false;
|
||||
_shownActorId = -1;
|
||||
_shownFormKey = "";
|
||||
}
|
||||
|
||||
public static void EnsureHost(Transform parent, float size)
|
||||
|
|
@ -117,9 +120,11 @@ public static class DossierAvatar
|
|||
}
|
||||
|
||||
long id = actor.getID();
|
||||
if (_usingVanilla && _instance != null && _shownActorId == id && _instance.gameObject.activeSelf)
|
||||
string formKey = ActorFormKey(actor);
|
||||
if (_usingVanilla && _instance != null && _shownActorId == id && _instance.gameObject.activeSelf
|
||||
&& formKey == _shownFormKey)
|
||||
{
|
||||
// Always clear species fallback - it can linger above the live portrait.
|
||||
// Same unit + same life stage: only refresh the floor tile underfoot.
|
||||
HideFallback();
|
||||
try
|
||||
{
|
||||
|
|
@ -157,8 +162,10 @@ public static class DossierAvatar
|
|||
_instance.avatarLoader.avatarSize = 0.55f;
|
||||
}
|
||||
|
||||
// Re-load avatar whenever form changes (egg → chick) even if unit id is unchanged.
|
||||
_instance.show(actor);
|
||||
_shownActorId = id;
|
||||
_shownFormKey = formKey;
|
||||
DisableInteractions(_instance.gameObject);
|
||||
return;
|
||||
}
|
||||
|
|
@ -166,17 +173,65 @@ public static class DossierAvatar
|
|||
{
|
||||
LogService.LogInfo("[IdleSpectator] Vanilla avatar show failed, using fallback: " + ex.Message);
|
||||
FallbackToSprite(actor);
|
||||
_shownActorId = id;
|
||||
_shownFormKey = formKey;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FallbackToSprite(actor);
|
||||
_shownActorId = id;
|
||||
_shownFormKey = formKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visual form key for the live portrait. Same actor id can change (egg → baby → adult).
|
||||
/// </summary>
|
||||
private static string ActorFormKey(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string species = "";
|
||||
try
|
||||
{
|
||||
species = actor.asset != null ? (actor.asset.id ?? "") : "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
species = "";
|
||||
}
|
||||
|
||||
string stage = "adult";
|
||||
try
|
||||
{
|
||||
if (actor.isEgg())
|
||||
{
|
||||
stage = "egg";
|
||||
}
|
||||
else if (actor.isBaby())
|
||||
{
|
||||
stage = "baby";
|
||||
}
|
||||
else if (!actor.isAdult())
|
||||
{
|
||||
stage = "young";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
stage = "unknown";
|
||||
}
|
||||
|
||||
return species + "|" + stage;
|
||||
}
|
||||
|
||||
public static void ClearActor()
|
||||
{
|
||||
_shownActorId = -1;
|
||||
_shownFormKey = "";
|
||||
HideVanillaInstance();
|
||||
HideFallback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,14 @@ internal static class FocusTooltipPatches
|
|||
return true;
|
||||
}
|
||||
|
||||
// Dossier chrome (not the sprite): no tip.
|
||||
// Trait/status chips: DossierChipTip owns the tip; skip gameplay actor tip / hide.
|
||||
if (WatchCaption.IsPointerOverAssetChip())
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dossier chrome (not the sprite / chips): no tip.
|
||||
if (WatchCaption.IsPointerOverPanel())
|
||||
{
|
||||
Tooltip.hideTooltip();
|
||||
|
|
|
|||
|
|
@ -296,8 +296,10 @@ internal static class HarnessScenarios
|
|||
Step("hp32", "assert", expect: "activity_log_contains", value: "Ava"),
|
||||
Step("hp33", "assert", expect: "activity_happiness_task_id", value: "happiness:death_child"),
|
||||
|
||||
// Real lover grief path
|
||||
Step("hp40", "spawn", asset: "human"),
|
||||
// Real lover grief path (+ dossier row → other Lore).
|
||||
// Idle must be on *before* the bond: first spectator enable can ClearSession the ring.
|
||||
Step("hp40", "spectator", value: "on"),
|
||||
Step("hp40b", "spawn", asset: "human"),
|
||||
Step("hp41", "happiness_remember_partner"),
|
||||
Step("hp42", "spawn", asset: "human"),
|
||||
Step("hp43", "focus", asset: "auto"),
|
||||
|
|
@ -306,6 +308,16 @@ internal static class HarnessScenarios
|
|||
Step("hp46", "happiness_bond_lovers"),
|
||||
Step("hp46b", "assert", expect: "activity_happiness_count", value: "2", label: "min"),
|
||||
Step("hp46c", "assert", expect: "activity_log_contains", value: "love"),
|
||||
Step("hp46d", "focus", asset: "auto"),
|
||||
Step("hp46e", "wait", wait: 0.2f),
|
||||
Step("hp46f", "assert", expect: "history_other_rows", value: "1", label: "min"),
|
||||
Step("hp46g", "remember_focus"),
|
||||
Step("hp46h", "dossier_click_history_other"),
|
||||
Step("hp46i", "wait", wait: 0.2f),
|
||||
Step("hp46j", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("hp46k", "assert", expect: "lore_detail_is_other"),
|
||||
Step("hp46l", "lore_close"),
|
||||
Step("hp46m", "spectator", value: "off"),
|
||||
Step("hp47", "wait", value: "0.5"),
|
||||
Step("hp48", "happiness_kill_partner"),
|
||||
Step("hp49", "wait", value: "0.5"),
|
||||
|
|
@ -2145,7 +2157,8 @@ internal static class HarnessScenarios
|
|||
Step("act16r", "dossier_clear_job"),
|
||||
// Statuses row (priority, max 4).
|
||||
Step("act16s", "status_apply", value: "cursed", label: "20"),
|
||||
Step("act16t", "status_apply", value: "poisoned", label: "20"),
|
||||
// Optional second chip - some units refuse poison; cursed alone still gates the row.
|
||||
Step("act16t", "status_apply", value: "poisoned", label: "20", expect: "optional"),
|
||||
Step("act16u", "wait", wait: 0.25f),
|
||||
Step("act16v", "assert", expect: "dossier_statuses_ok", value: "1", label: "min"),
|
||||
Step("act16w", "assert", expect: "dossier_contains", value: "Cursed"),
|
||||
|
|
@ -2180,6 +2193,42 @@ internal static class HarnessScenarios
|
|||
Step("act20g", "chronicle_milestone", value: "lover"),
|
||||
Step("act20h", "assert", expect: "activity_log_contains", value: "Became lovers"),
|
||||
Step("act20i", "assert", expect: "chronicle_latest_contains", value: "Became lovers"),
|
||||
// Whole-row other-person click → Lore biography (then restore original focus).
|
||||
Step("act20j", "assert", expect: "tooltip_library_ok"),
|
||||
Step("act20j2", "remember_focus"),
|
||||
Step("act20k", "assert", expect: "history_other_rows", value: "1", label: "min"),
|
||||
Step("act20l", "dossier_click_history_other"),
|
||||
Step("act20m", "wait", wait: 0.3f),
|
||||
Step("act20n", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("act20o", "assert", expect: "lore_detail_is_other"),
|
||||
Step("act20p", "lore_close"),
|
||||
Step("act20q", "spectator", value: "on"),
|
||||
Step("act20r", "wait", wait: 0.2f),
|
||||
// Return to a living subject so later activity-count asserts stay valid.
|
||||
// Orange reason click → related unit Lore (same contract as history rows).
|
||||
Step("act20r2", "spawn", asset: "human", count: 1),
|
||||
Step("act20r2b", "happiness_remember_partner"),
|
||||
Step("act20r3", "spawn", asset: "human", count: 1),
|
||||
Step("act20r4", "pick_unit", asset: "human"),
|
||||
Step("act20r5", "focus", asset: "auto"),
|
||||
Step("act20r6", "wait", wait: 0.2f),
|
||||
Step("act20s", "dossier_force_named_reason", value: "is fighting", expect: "auto"),
|
||||
Step("act20t", "assert", expect: "reason_names_colored"),
|
||||
Step("act20t2", "assert", expect: "reason_other_clickable"),
|
||||
Step("act20t3", "remember_focus"),
|
||||
Step("act20t4", "dossier_click_reason_other"),
|
||||
Step("act20t5", "wait", wait: 0.2f),
|
||||
Step("act20t6", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("act20t7", "assert", expect: "lore_detail_is_other"),
|
||||
Step("act20t8", "lore_close"),
|
||||
Step("act20t9", "spectator", value: "on"),
|
||||
Step("act20t10", "focus", asset: "auto"),
|
||||
Step("act20u", "dossier_clear_job"),
|
||||
// Re-seed activity ring after the other-person Lore detour.
|
||||
Step("act20v", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 2, tier: "beat"),
|
||||
Step("act20w", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1),
|
||||
Step("act20x", "activity_force", asset: "BehSocializeTalk", label: "Talks", count: 1, tier: "beat",
|
||||
expect: "Barkley"),
|
||||
Step("act21", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "activity"),
|
||||
Step("act22", "assert", expect: "dossier_history_not_contains", value: "Harness pollen"),
|
||||
Step("act23", "assert", expect: "caption_layout_ok"),
|
||||
|
|
@ -2433,6 +2482,7 @@ internal static class HarnessScenarios
|
|||
Step("ch18k11d", "assert", expect: "dossier_species", value: "skeleton"),
|
||||
Step("ch18k11e", "assert", expect: "dossier_avatar_species"),
|
||||
Step("ch18k11f", "assert", expect: "fallen_killer_camera"),
|
||||
Step("ch18k11g", "assert", expect: "fallen_killer_banner"),
|
||||
Step("ch18k12", "screenshot", value: "hud-lore-fallen-slain.png"),
|
||||
Step("ch18k13", "wait", wait: 0.55f),
|
||||
// Regression: fallen species portrait must not stick onto the next living dossier.
|
||||
|
|
@ -2490,6 +2540,8 @@ internal static class HarnessScenarios
|
|||
Step("ch18ft4", "assert", expect: "lore_tab", value: "fallen"),
|
||||
Step("ch18ft5", "assert", expect: "fallen_list_before", value: "Slainbone", label: "Oldbone"),
|
||||
// Snapshot stays put until Refresh (new deaths do not auto-rebuild).
|
||||
// Refresh first so live world deaths during the long scenario do not leave stale=true.
|
||||
Step("ch18ft5a", "lore_refresh"),
|
||||
Step("ch18ft5b", "assert", expect: "lore_list_stale", value: "false"),
|
||||
Step(
|
||||
"ch18ft5c",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public sealed class UnitDossier
|
|||
public string SpeciesId = "";
|
||||
public string Headline = "";
|
||||
public string ReasonLine = "";
|
||||
/// <summary>Other unit named by the orange reason (0 when none).</summary>
|
||||
public long ReasonRelatedId;
|
||||
public string DetailLine = "";
|
||||
public string CaptionText = "";
|
||||
public string JobLabel = "";
|
||||
|
|
@ -62,10 +64,18 @@ public sealed class UnitDossier
|
|||
/// <summary>Harness: optional watch-reason fragment used with <see cref="HarnessJobLabelOverride"/>.</summary>
|
||||
public static string HarnessReasonOverride = "";
|
||||
|
||||
/// <summary>Harness: related unit for forced reason clicks / gold names.</summary>
|
||||
public static long HarnessReasonRelatedId;
|
||||
|
||||
/// <summary>Harness: related display name for forced reason prose.</summary>
|
||||
public static string HarnessReasonRelatedName = "";
|
||||
|
||||
public static void ClearHarnessOverrides()
|
||||
{
|
||||
HarnessJobLabelOverride = "";
|
||||
HarnessReasonOverride = "";
|
||||
HarnessReasonRelatedId = 0;
|
||||
HarnessReasonRelatedName = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -321,7 +331,23 @@ public sealed class UnitDossier
|
|||
{
|
||||
if (!string.IsNullOrEmpty(HarnessReasonOverride))
|
||||
{
|
||||
return HarnessReasonOverride.Trim();
|
||||
string plain = HarnessReasonOverride.Trim();
|
||||
string subject = d != null ? (d.Name ?? "") : "";
|
||||
if (string.IsNullOrEmpty(subject) && actor != null)
|
||||
{
|
||||
subject = EventFeedUtil.SafeName(actor);
|
||||
}
|
||||
|
||||
string relatedName = HarnessReasonRelatedName ?? "";
|
||||
if (d != null)
|
||||
{
|
||||
d.ReasonRelatedId = ResolveReasonRelatedId(
|
||||
actor,
|
||||
HarnessReasonRelatedId,
|
||||
relatedName);
|
||||
}
|
||||
|
||||
return ActivityProse.ColorizePersonNames(plain, subject, relatedName);
|
||||
}
|
||||
|
||||
return OwnedEventReason(actor, d);
|
||||
|
|
@ -341,6 +367,11 @@ public sealed class UnitDossier
|
|||
InterestCandidate scene = InterestDirector.TryGetOwnedReasonCandidate(actor);
|
||||
if (scene == null)
|
||||
{
|
||||
if (dossier != null)
|
||||
{
|
||||
dossier.ReasonRelatedId = 0;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +379,11 @@ public sealed class UnitDossier
|
|||
if (scene.LeadKind == InterestLeadKind.CharacterLed
|
||||
&& !InterestScoring.IsCombatAction(scene))
|
||||
{
|
||||
if (dossier != null)
|
||||
{
|
||||
dossier.ReasonRelatedId = 0;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -367,7 +403,51 @@ public sealed class UnitDossier
|
|||
}
|
||||
}
|
||||
|
||||
return EvaluateSceneLabel(scene, d, recordDrops: true) ?? "";
|
||||
string beat = EvaluateSceneLabel(scene, d, recordDrops: true) ?? "";
|
||||
if (string.IsNullOrEmpty(beat))
|
||||
{
|
||||
d.ReasonRelatedId = 0;
|
||||
return "";
|
||||
}
|
||||
|
||||
// Gold names inside the orange reason row (same palette as history/activity).
|
||||
string subjectName = d != null ? (d.Name ?? "") : "";
|
||||
if (string.IsNullOrEmpty(subjectName))
|
||||
{
|
||||
subjectName = EventFeedUtil.SafeName(actor);
|
||||
}
|
||||
|
||||
string relatedName = EventFeedUtil.SafeName(scene.RelatedUnit);
|
||||
long relatedId = scene.RelatedId != 0
|
||||
? scene.RelatedId
|
||||
: EventFeedUtil.SafeId(scene.RelatedUnit);
|
||||
d.ReasonRelatedId = ResolveReasonRelatedId(actor, relatedId, relatedName);
|
||||
return ActivityProse.ColorizePersonNames(beat, subjectName, relatedName);
|
||||
}
|
||||
|
||||
private static long ResolveReasonRelatedId(Actor subject, long relatedId, string relatedName)
|
||||
{
|
||||
long subjectId = 0;
|
||||
try
|
||||
{
|
||||
subjectId = subject != null ? subject.getID() : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
subjectId = 0;
|
||||
}
|
||||
|
||||
if (relatedId != 0 && relatedId != subjectId)
|
||||
{
|
||||
return relatedId;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(relatedName))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ActivityLog.ResolveLivingUnitIdByName(relatedName, subjectId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ public static class WatchCaption
|
|||
private static Image _sexIcon;
|
||||
private static Text _nameText;
|
||||
private static Text _reasonText;
|
||||
private static long _reasonOtherId;
|
||||
|
||||
private static Image _levelIcon;
|
||||
private static Text _levelValue;
|
||||
|
|
@ -103,6 +104,7 @@ public static class WatchCaption
|
|||
public GameObject Root;
|
||||
public Image Icon;
|
||||
public Text Label;
|
||||
public DossierChipTip Tip;
|
||||
}
|
||||
|
||||
private sealed class HistorySlot
|
||||
|
|
@ -110,10 +112,16 @@ public static class WatchCaption
|
|||
public GameObject Root;
|
||||
public Image Icon;
|
||||
public Text Label;
|
||||
public Button Button;
|
||||
public Image Hit;
|
||||
public long OtherId;
|
||||
}
|
||||
|
||||
public static UnitDossier Current => _current;
|
||||
|
||||
/// <summary>Living actor currently bound to the dossier (null for fallen archive).</summary>
|
||||
public static Actor BoundActor => _boundActor;
|
||||
|
||||
public static string LastHeadline { get; private set; } = "";
|
||||
|
||||
public static string LastDetail { get; private set; } = "";
|
||||
|
|
@ -209,6 +217,45 @@ public static class WatchCaption
|
|||
return DossierAvatar.ContainsScreenPoint(Input.mousePosition);
|
||||
}
|
||||
|
||||
/// <summary>True if the mouse is over a trait/status chip (vanilla tip host).</summary>
|
||||
public static bool IsPointerOverAssetChip()
|
||||
{
|
||||
if (!_visible || _root == null || !_root.activeInHierarchy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 mouse = Input.mousePosition;
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
if (IsPointerOverChipRoot(_traitSlots[i]?.Root, mouse))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < _statusSlots.Length; i++)
|
||||
{
|
||||
if (IsPointerOverChipRoot(_statusSlots[i]?.Root, mouse))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPointerOverChipRoot(GameObject root, Vector2 mouse)
|
||||
{
|
||||
if (root == null || !root.activeInHierarchy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RectTransform rt = root.GetComponent<RectTransform>();
|
||||
return rt != null && RectTransformUtility.RectangleContainsScreenPoint(rt, mouse, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Favorite control (and the panel chrome around it).
|
||||
/// Used so idle does not treat those clicks as "manual pause".
|
||||
|
|
@ -402,6 +449,7 @@ public static class WatchCaption
|
|||
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
_reasonText.gameObject.SetActive(true);
|
||||
WireReasonClick(0);
|
||||
}
|
||||
|
||||
LastCaptionText = (LastHeadline ?? "Idle Spectator") + " | " + message;
|
||||
|
|
@ -731,7 +779,30 @@ public static class WatchCaption
|
|||
return;
|
||||
}
|
||||
|
||||
string next = UnitDossier.OwnedEventReason(actor, _current) ?? "";
|
||||
// Honor harness reason override the same way FromActor/BuildStoryBeat does.
|
||||
string next;
|
||||
if (!string.IsNullOrEmpty(UnitDossier.HarnessReasonOverride))
|
||||
{
|
||||
string subject = _current.Name ?? "";
|
||||
if (string.IsNullOrEmpty(subject))
|
||||
{
|
||||
subject = EventFeedUtil.SafeName(actor);
|
||||
}
|
||||
|
||||
string relatedName = UnitDossier.HarnessReasonRelatedName ?? "";
|
||||
_current.ReasonRelatedId = UnitDossier.HarnessReasonRelatedId != 0
|
||||
? UnitDossier.HarnessReasonRelatedId
|
||||
: ActivityLog.ResolveLivingUnitIdByName(relatedName, _current.UnitId);
|
||||
next = ActivityProse.ColorizePersonNames(
|
||||
UnitDossier.HarnessReasonOverride.Trim(),
|
||||
subject,
|
||||
relatedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
next = UnitDossier.OwnedEventReason(actor, _current) ?? "";
|
||||
}
|
||||
|
||||
string prev = _current.ReasonLine ?? "";
|
||||
if (next == prev)
|
||||
{
|
||||
|
|
@ -745,8 +816,7 @@ public static class WatchCaption
|
|||
bool hasReason = !string.IsNullOrEmpty(next);
|
||||
if (_reasonText != null)
|
||||
{
|
||||
_reasonText.text = hasReason ? next : "";
|
||||
_reasonText.gameObject.SetActive(hasReason);
|
||||
ApplyReasonText(hasReason ? next : "", hasReason ? _current.ReasonRelatedId : 0);
|
||||
}
|
||||
|
||||
bool hasTask = !string.IsNullOrEmpty(_current.TaskText);
|
||||
|
|
@ -1122,6 +1192,11 @@ public static class WatchCaption
|
|||
{
|
||||
_lastHistoryCount = -1;
|
||||
_lastLiveHistFingerprint = int.MinValue;
|
||||
if (_current == null && MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||
{
|
||||
SetFromActor(MoveCamera._focus_unit);
|
||||
}
|
||||
|
||||
if (!_visible || _current == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -1133,7 +1208,11 @@ public static class WatchCaption
|
|||
/// <summary>
|
||||
/// Harness: force JobLabel onto the nametag identity tag (and optional orange reason beat).
|
||||
/// </summary>
|
||||
public static bool ForceJobLabelOnFocus(string jobLabel, string reasonOverride = "")
|
||||
public static bool ForceJobLabelOnFocus(
|
||||
string jobLabel,
|
||||
string reasonOverride = "",
|
||||
long reasonRelatedId = 0,
|
||||
string reasonRelatedName = "")
|
||||
{
|
||||
if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null)
|
||||
{
|
||||
|
|
@ -1142,6 +1221,8 @@ public static class WatchCaption
|
|||
|
||||
UnitDossier.HarnessJobLabelOverride = jobLabel ?? "";
|
||||
UnitDossier.HarnessReasonOverride = reasonOverride ?? "";
|
||||
UnitDossier.HarnessReasonRelatedId = reasonRelatedId;
|
||||
UnitDossier.HarnessReasonRelatedName = reasonRelatedName ?? "";
|
||||
SetFromActor(MoveCamera._focus_unit);
|
||||
return _current != null
|
||||
&& !string.IsNullOrEmpty(_current.JobLabel)
|
||||
|
|
@ -1303,17 +1384,82 @@ public static class WatchCaption
|
|||
bool hasReason = dossier != null && !string.IsNullOrEmpty(dossier.ReasonLine);
|
||||
if (_reasonText != null)
|
||||
{
|
||||
_reasonText.text = hasReason ? dossier.ReasonLine : "";
|
||||
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
_reasonText.resizeTextForBestFit = false;
|
||||
_reasonText.gameObject.SetActive(hasReason);
|
||||
ApplyReasonText(
|
||||
hasReason ? dossier.ReasonLine : "",
|
||||
hasReason && dossier != null ? dossier.ReasonRelatedId : 0);
|
||||
}
|
||||
|
||||
Relayout(hasBody, traitCount, statusCount, hasTask, hasReason, historyCount);
|
||||
RefreshFavoriteVisual(hasLive ? actor : null);
|
||||
}
|
||||
|
||||
/// <summary>Orange story beat with rich-text gold names; empty hides the row.</summary>
|
||||
private static void ApplyReasonText(string reasonRichOrPlain, long otherId = 0)
|
||||
{
|
||||
if (_reasonText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasReason = !string.IsNullOrEmpty(reasonRichOrPlain);
|
||||
_reasonText.supportRichText = true;
|
||||
_reasonText.color = ReasonColor;
|
||||
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
_reasonText.resizeTextForBestFit = false;
|
||||
_reasonText.text = hasReason ? reasonRichOrPlain : "";
|
||||
_reasonText.gameObject.SetActive(hasReason);
|
||||
WireReasonClick(hasReason ? otherId : 0);
|
||||
}
|
||||
|
||||
private static void WireReasonClick(long otherId)
|
||||
{
|
||||
long subjectId = CurrentUnitId;
|
||||
_reasonOtherId = otherId != 0 && otherId != subjectId ? otherId : 0;
|
||||
if (_reasonText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = _reasonText.gameObject.GetComponent<UnityEngine.EventSystems.EventTrigger>();
|
||||
if (entry == null)
|
||||
{
|
||||
entry = _reasonText.gameObject.AddComponent<UnityEngine.EventSystems.EventTrigger>();
|
||||
}
|
||||
|
||||
entry.triggers.Clear();
|
||||
bool clickable = _reasonOtherId != 0 && _reasonText.gameObject.activeSelf;
|
||||
_reasonText.raycastTarget = clickable;
|
||||
if (!clickable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long captured = _reasonOtherId;
|
||||
var down = new UnityEngine.EventSystems.EventTrigger.Entry
|
||||
{
|
||||
eventID = UnityEngine.EventSystems.EventTriggerType.PointerDown
|
||||
};
|
||||
down.callback.AddListener(_ => OpenHistoryOtherInLore(captured));
|
||||
entry.triggers.Add(down);
|
||||
}
|
||||
|
||||
/// <summary>Harness: orange reason names another living unit.</summary>
|
||||
public static bool ReasonOtherClickable => _reasonOtherId != 0;
|
||||
|
||||
/// <summary>Harness: click the orange reason to open the related unit in Lore.</summary>
|
||||
public static bool ClickReasonOther(out long otherId)
|
||||
{
|
||||
otherId = _reasonOtherId;
|
||||
if (otherId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OpenHistoryOtherInLore(otherId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int ApplyTraitChips(UnitDossier dossier)
|
||||
{
|
||||
int traitCount = 0;
|
||||
|
|
@ -1356,6 +1502,20 @@ public static class WatchCaption
|
|||
slot.Label.text = name;
|
||||
}
|
||||
|
||||
if (slot.Tip != null)
|
||||
{
|
||||
if (chip != null && (chip.Trait != null || !string.IsNullOrEmpty(chip.Id)))
|
||||
{
|
||||
slot.Tip.BindTrait(chip.Trait, chip.Id);
|
||||
slot.Tip.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.Tip.Clear();
|
||||
slot.Tip.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (traitPreview.Length > 0)
|
||||
{
|
||||
traitPreview.Append(", ");
|
||||
|
|
@ -1366,6 +1526,12 @@ public static class WatchCaption
|
|||
}
|
||||
else
|
||||
{
|
||||
if (slot.Tip != null)
|
||||
{
|
||||
slot.Tip.Clear();
|
||||
slot.Tip.enabled = false;
|
||||
}
|
||||
|
||||
slot.Root.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1422,6 +1588,20 @@ public static class WatchCaption
|
|||
slot.Label.color = StatusNameColor;
|
||||
}
|
||||
|
||||
if (slot.Tip != null)
|
||||
{
|
||||
if (chip != null && (chip.Status != null || !string.IsNullOrEmpty(chip.Id)))
|
||||
{
|
||||
slot.Tip.BindStatus(chip.Status, chip.Id);
|
||||
slot.Tip.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.Tip.Clear();
|
||||
slot.Tip.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (preview.Length > 0)
|
||||
{
|
||||
preview.Append(", ");
|
||||
|
|
@ -1432,6 +1612,12 @@ public static class WatchCaption
|
|||
}
|
||||
else
|
||||
{
|
||||
if (slot.Tip != null)
|
||||
{
|
||||
slot.Tip.Clear();
|
||||
slot.Tip.enabled = false;
|
||||
}
|
||||
|
||||
slot.Root.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1559,7 +1745,8 @@ public static class WatchCaption
|
|||
|
||||
_historyCol.SetActive(true);
|
||||
|
||||
var lines = new List<(string rich, string plain, bool isActivity, ChronicleKind? kind, string activityKey)>(HistoryPeekMax);
|
||||
var lines = new List<(string rich, string plain, bool isActivity, ChronicleKind? kind, string activityKey, long otherId)>(
|
||||
HistoryPeekMax);
|
||||
if (activity != null)
|
||||
{
|
||||
for (int i = 0; i < activity.Count && lines.Count < HistoryPeekMax; i++)
|
||||
|
|
@ -1577,7 +1764,8 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
string rich = !string.IsNullOrEmpty(e.DisplayLineRich) ? e.DisplayLineRich : plain;
|
||||
lines.Add((rich, plain, true, null, e.TaskId ?? ""));
|
||||
long otherId = ResolveHistoryOtherId(unitId, e.RelatedId, e.TargetLabel);
|
||||
lines.Add((rich, plain, true, null, e.TaskId ?? "", otherId));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1598,7 +1786,8 @@ public static class WatchCaption
|
|||
continue;
|
||||
}
|
||||
|
||||
lines.Add((rich, plain, false, e.Kind, ""));
|
||||
long otherId = e.OtherId != 0 && e.OtherId != unitId ? e.OtherId : 0;
|
||||
lines.Add((rich, plain, false, e.Kind, "", otherId));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1639,7 +1828,7 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
private static void ApplyCombinedSlotContents(
|
||||
List<(string rich, string plain, bool isActivity, ChronicleKind? kind, string activityKey)> lines,
|
||||
List<(string rich, string plain, bool isActivity, ChronicleKind? kind, string activityKey, long otherId)> lines,
|
||||
float colW)
|
||||
{
|
||||
float labelW = Mathf.Max(24f, colW - HistoryIcon - 2f);
|
||||
|
|
@ -1660,6 +1849,8 @@ public static class WatchCaption
|
|||
if (i >= lines.Count)
|
||||
{
|
||||
slot.Root.SetActive(false);
|
||||
slot.OtherId = 0;
|
||||
WireHistoryRowClick(slot, 0);
|
||||
_historySlotHeights[i] = 0f;
|
||||
_historySlotIsActivity[i] = false;
|
||||
continue;
|
||||
|
|
@ -1668,6 +1859,7 @@ public static class WatchCaption
|
|||
var row = lines[i];
|
||||
_historySlotIsActivity[i] = row.isActivity;
|
||||
slot.Root.SetActive(true);
|
||||
slot.OtherId = row.otherId;
|
||||
Sprite icon = row.isActivity
|
||||
? (HudIcons.ForActivityKey(row.activityKey)
|
||||
?? HudIcons.FromUiIcon("iconClock")
|
||||
|
|
@ -1707,6 +1899,8 @@ public static class WatchCaption
|
|||
_historySlotHeights[i] = HistoryLineMinH;
|
||||
}
|
||||
|
||||
WireHistoryRowClick(slot, row.otherId);
|
||||
|
||||
if (row.isActivity)
|
||||
{
|
||||
LastActivityShown++;
|
||||
|
|
@ -1727,6 +1921,132 @@ public static class WatchCaption
|
|||
LastHistoryJoined = string.Join(" | ", joined);
|
||||
}
|
||||
|
||||
private static void WireHistoryRowClick(HistorySlot slot, long otherId)
|
||||
{
|
||||
if (slot?.Root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
slot.OtherId = otherId;
|
||||
if (slot.Hit == null)
|
||||
{
|
||||
slot.Hit = slot.Root.GetComponent<Image>();
|
||||
if (slot.Hit == null)
|
||||
{
|
||||
slot.Hit = slot.Root.AddComponent<Image>();
|
||||
}
|
||||
|
||||
slot.Hit.color = new Color(1f, 1f, 1f, 0.001f);
|
||||
}
|
||||
|
||||
if (slot.Button == null)
|
||||
{
|
||||
slot.Button = slot.Root.GetComponent<Button>();
|
||||
if (slot.Button == null)
|
||||
{
|
||||
slot.Button = slot.Root.AddComponent<Button>();
|
||||
}
|
||||
|
||||
ColorBlock colors = slot.Button.colors;
|
||||
colors.normalColor = Color.white;
|
||||
colors.highlightedColor = new Color(1.15f, 1.15f, 1.2f, 1f);
|
||||
colors.pressedColor = new Color(0.85f, 0.85f, 0.9f, 1f);
|
||||
slot.Button.colors = colors;
|
||||
slot.Button.transition = Selectable.Transition.ColorTint;
|
||||
slot.Button.targetGraphic = slot.Hit;
|
||||
}
|
||||
|
||||
slot.Button.onClick.RemoveAllListeners();
|
||||
bool clickable = otherId != 0;
|
||||
slot.Hit.raycastTarget = clickable;
|
||||
slot.Button.interactable = clickable;
|
||||
var entry = slot.Root.GetComponent<UnityEngine.EventSystems.EventTrigger>();
|
||||
if (entry == null)
|
||||
{
|
||||
entry = slot.Root.AddComponent<UnityEngine.EventSystems.EventTrigger>();
|
||||
}
|
||||
|
||||
entry.triggers.Clear();
|
||||
if (!clickable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// PointerDown (not Button release) so Lore opens on the same frame as the press.
|
||||
long captured = otherId;
|
||||
var down = new UnityEngine.EventSystems.EventTrigger.Entry
|
||||
{
|
||||
eventID = UnityEngine.EventSystems.EventTriggerType.PointerDown
|
||||
};
|
||||
down.callback.AddListener(_ => OpenHistoryOtherInLore(captured));
|
||||
entry.triggers.Add(down);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefer stored related id; if missing (older happiness rings), resolve by living name.
|
||||
/// </summary>
|
||||
private static long ResolveHistoryOtherId(long subjectId, long relatedId, string relatedName)
|
||||
{
|
||||
if (relatedId != 0 && relatedId != subjectId)
|
||||
{
|
||||
return relatedId;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(relatedName))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ActivityLog.ResolveLivingUnitIdByName(relatedName, subjectId);
|
||||
}
|
||||
|
||||
/// <summary>Whole-row click: focus other unit (or fallen path) and open Lore biography.</summary>
|
||||
public static void OpenHistoryOtherInLore(long otherId)
|
||||
{
|
||||
if (otherId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ChronicleHud.OpenUnitHistory(otherId, pauseIdle: true, followFocus: true);
|
||||
}
|
||||
|
||||
/// <summary>Harness: click first peek row that names another unit.</summary>
|
||||
public static bool ClickFirstHistoryOtherRow(out long otherId)
|
||||
{
|
||||
otherId = 0;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
if (slot?.Root == null || !slot.Root.activeSelf || slot.OtherId == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
otherId = slot.OtherId;
|
||||
OpenHistoryOtherInLore(otherId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int CountHistoryOtherRows()
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
if (slot?.Root != null && slot.Root.activeSelf && slot.OtherId != 0)
|
||||
{
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
private static void RemeasureHistoryForWidth(float colW)
|
||||
{
|
||||
_historyColW = Mathf.Clamp(colW, HistoryColMinW, HistoryColMaxW);
|
||||
|
|
@ -1838,16 +2158,19 @@ public static class WatchCaption
|
|||
int historyCount)
|
||||
{
|
||||
float headerW = MeasureHeaderWidth(hasTask);
|
||||
float traitsW = 0f;
|
||||
// Traits beside avatar only when history and statuses leave the body-right slot free.
|
||||
bool traitsBeside = hasBody && historyCount <= 0 && statusCount <= 0 && traitCount > 0;
|
||||
if (traitCount > 0 && !traitsBeside)
|
||||
float traitsRowW = traitCount > 0 && !traitsBeside ? MeasureTraitsRowWidth() : 0f;
|
||||
float traitsBesideW = traitsBeside ? MeasureTraitsBesideWidth() : 0f;
|
||||
float statusesRowW = statusCount > 0 ? MeasureStatusesRowWidth() : 0f;
|
||||
float traitsW = 0f;
|
||||
if (traitsBeside)
|
||||
{
|
||||
traitsW = HistoryColMinW;
|
||||
traitsW = Mathf.Max(HistoryColMinW, traitsBesideW);
|
||||
}
|
||||
else if (traitsBeside)
|
||||
else if (traitCount > 0)
|
||||
{
|
||||
traitsW = HistoryColMinW;
|
||||
traitsW = Mathf.Max(HistoryColMinW, traitsRowW);
|
||||
}
|
||||
|
||||
float bodyW = 0f;
|
||||
|
|
@ -1867,8 +2190,9 @@ public static class WatchCaption
|
|||
}
|
||||
}
|
||||
|
||||
// Chip rows must widen the panel - PlaceTraits/PlaceStatuses lay out natural widths.
|
||||
_panelWidth = Mathf.Clamp(
|
||||
PadX * 2f + Mathf.Max(headerW, bodyW, traitsW, LiveMax),
|
||||
PadX * 2f + Mathf.Max(headerW, bodyW, traitsW, statusesRowW, LiveMax),
|
||||
LiveMax + PadX * 2f,
|
||||
PanelWidthMax);
|
||||
|
||||
|
|
@ -1913,13 +2237,15 @@ public static class WatchCaption
|
|||
|
||||
if (statusCount > 0)
|
||||
{
|
||||
PlaceStatuses(y, statusCount);
|
||||
float statusesUsed = PlaceStatuses(y, statusCount);
|
||||
WidenPanelForChipRow(statusesUsed);
|
||||
y += StatusesH + Gap;
|
||||
}
|
||||
|
||||
if (traitCount > 0 && !traitsBeside)
|
||||
{
|
||||
PlaceTraits(y, traitCount);
|
||||
float traitsUsed = PlaceTraits(y, traitCount);
|
||||
WidenPanelForChipRow(traitsUsed);
|
||||
y += TraitsH + Gap;
|
||||
}
|
||||
|
||||
|
|
@ -2340,6 +2666,93 @@ public static class WatchCaption
|
|||
}
|
||||
}
|
||||
|
||||
private static float MeasureChipRowWidth(TraitSlot[] slots, float fallbackLabelW)
|
||||
{
|
||||
if (slots == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Canvas.ForceUpdateCanvases();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
float x = 0f;
|
||||
int n = 0;
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
TraitSlot slot = slots[i];
|
||||
if (slot == null || slot.Root == null || !slot.Root.activeSelf)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float labelW = slot.Label != null
|
||||
? Mathf.Max(MeasureTextWidth(slot.Label, fallbackLabelW), 18f)
|
||||
: fallbackLabelW;
|
||||
// Prefer preferredWidth; fall back to character estimate so long labels
|
||||
// still widen the panel before Unity finishes a layout pass.
|
||||
if (slot.Label != null && !string.IsNullOrEmpty(slot.Label.text)
|
||||
&& labelW <= fallbackLabelW + 0.5f)
|
||||
{
|
||||
labelW = Mathf.Max(labelW, slot.Label.text.Length * 6.2f);
|
||||
}
|
||||
|
||||
float slotW = TraitIcon + 2f + labelW;
|
||||
x += slotW + 3f;
|
||||
n++;
|
||||
}
|
||||
|
||||
return n > 0 ? x - 3f : 0f;
|
||||
}
|
||||
|
||||
private static float MeasureStatusesRowWidth() => MeasureChipRowWidth(_statusSlots, 28f);
|
||||
|
||||
private static float MeasureTraitsRowWidth() => MeasureChipRowWidth(_traitSlots, 28f);
|
||||
|
||||
/// <summary>Grow the dossier when chip rows need more width than the pre-pass estimate.</summary>
|
||||
private static void WidenPanelForChipRow(float rowContentW)
|
||||
{
|
||||
if (rowContentW < 1f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float need = PadX * 2f + rowContentW;
|
||||
if (need > _panelWidth + 0.5f)
|
||||
{
|
||||
_panelWidth = Mathf.Clamp(need, LiveMax + PadX * 2f, PanelWidthMax);
|
||||
}
|
||||
}
|
||||
|
||||
private static float MeasureTraitsBesideWidth()
|
||||
{
|
||||
float colW = 0f;
|
||||
if (_traitSlots == null)
|
||||
{
|
||||
return 56f;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
TraitSlot slot = _traitSlots[i];
|
||||
if (slot == null || slot.Root == null || !slot.Root.activeSelf || slot.Label == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float labelW = Mathf.Max(MeasureTextWidth(slot.Label, 36f), 24f);
|
||||
colW = Mathf.Max(colW, TraitIcon + 2f + labelW);
|
||||
}
|
||||
|
||||
return colW > 1f ? colW : 56f;
|
||||
}
|
||||
|
||||
private static float PlaceStatuses(float yFromTop, int statusCount)
|
||||
{
|
||||
if (_statusesRow == null)
|
||||
|
|
@ -2416,23 +2829,7 @@ public static class WatchCaption
|
|||
return 0f;
|
||||
}
|
||||
|
||||
float colW = 0f;
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
TraitSlot slot = _traitSlots[i];
|
||||
if (slot == null || slot.Root == null || !slot.Root.activeSelf || slot.Label == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float labelW = Mathf.Max(MeasureTextWidth(slot.Label, 36f), 24f);
|
||||
colW = Mathf.Max(colW, TraitIcon + 2f + labelW);
|
||||
}
|
||||
|
||||
if (colW < 1f)
|
||||
{
|
||||
colW = 56f;
|
||||
}
|
||||
float colW = MeasureTraitsBesideWidth();
|
||||
|
||||
RectTransform row = _traitsRow.GetComponent<RectTransform>();
|
||||
row.anchorMin = new Vector2(0f, 1f);
|
||||
|
|
@ -2569,6 +2966,7 @@ public static class WatchCaption
|
|||
_sexIcon = null;
|
||||
_nameText = null;
|
||||
_reasonText = null;
|
||||
_reasonOtherId = 0;
|
||||
_levelIcon = null;
|
||||
_levelValue = null;
|
||||
_taskIcon = null;
|
||||
|
|
@ -2633,8 +3031,15 @@ public static class WatchCaption
|
|||
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
GameObject slotGo = new GameObject("Hist" + i, typeof(RectTransform));
|
||||
GameObject slotGo = new GameObject("Hist" + i, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
slotGo.transform.SetParent(_historyCol.transform, false);
|
||||
Image hit = slotGo.GetComponent<Image>();
|
||||
hit.color = new Color(1f, 1f, 1f, 0.001f);
|
||||
hit.raycastTarget = false;
|
||||
Button button = slotGo.GetComponent<Button>();
|
||||
button.transition = Selectable.Transition.ColorTint;
|
||||
button.targetGraphic = hit;
|
||||
button.interactable = false;
|
||||
Image icon = HudCanvas.MakeIcon(slotGo.transform, "Icon", HistoryIcon);
|
||||
Text label = HudCanvas.MakeText(slotGo.transform, "Label", "", 8);
|
||||
label.color = HistoryTextColor;
|
||||
|
|
@ -2643,7 +3048,15 @@ public static class WatchCaption
|
|||
label.supportRichText = true;
|
||||
label.resizeTextMinSize = 6;
|
||||
label.resizeTextMaxSize = 8;
|
||||
_historySlots[i] = new HistorySlot { Root = slotGo, Icon = icon, Label = label };
|
||||
_historySlots[i] = new HistorySlot
|
||||
{
|
||||
Root = slotGo,
|
||||
Icon = icon,
|
||||
Label = label,
|
||||
Button = button,
|
||||
Hit = hit,
|
||||
OtherId = 0
|
||||
};
|
||||
slotGo.SetActive(false);
|
||||
}
|
||||
|
||||
|
|
@ -2651,8 +3064,11 @@ public static class WatchCaption
|
|||
_statusesRow.transform.SetParent(_root.transform, false);
|
||||
for (int i = 0; i < _statusSlots.Length; i++)
|
||||
{
|
||||
GameObject slotGo = new GameObject("Status" + i, typeof(RectTransform));
|
||||
GameObject slotGo = new GameObject("Status" + i, typeof(RectTransform), typeof(Image));
|
||||
slotGo.transform.SetParent(_statusesRow.transform, false);
|
||||
Image hit = slotGo.GetComponent<Image>();
|
||||
hit.color = new Color(1f, 1f, 1f, 0.001f);
|
||||
hit.raycastTarget = true;
|
||||
Image icon = HudCanvas.MakeIcon(slotGo.transform, "Icon", TraitIcon);
|
||||
Text label = HudCanvas.MakeText(slotGo.transform, "Label", "", 8);
|
||||
label.color = StatusNameColor;
|
||||
|
|
@ -2660,7 +3076,9 @@ public static class WatchCaption
|
|||
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
label.resizeTextMinSize = 6;
|
||||
label.resizeTextMaxSize = 8;
|
||||
_statusSlots[i] = new TraitSlot { Root = slotGo, Icon = icon, Label = label };
|
||||
DossierChipTip tip = slotGo.AddComponent<DossierChipTip>();
|
||||
tip.enabled = false;
|
||||
_statusSlots[i] = new TraitSlot { Root = slotGo, Icon = icon, Label = label, Tip = tip };
|
||||
slotGo.SetActive(false);
|
||||
}
|
||||
|
||||
|
|
@ -2668,8 +3086,11 @@ public static class WatchCaption
|
|||
_traitsRow.transform.SetParent(_root.transform, false);
|
||||
for (int i = 0; i < _traitSlots.Length; i++)
|
||||
{
|
||||
GameObject slotGo = new GameObject("Trait" + i, typeof(RectTransform));
|
||||
GameObject slotGo = new GameObject("Trait" + i, typeof(RectTransform), typeof(Image));
|
||||
slotGo.transform.SetParent(_traitsRow.transform, false);
|
||||
Image hit = slotGo.GetComponent<Image>();
|
||||
hit.color = new Color(1f, 1f, 1f, 0.001f);
|
||||
hit.raycastTarget = true;
|
||||
Image icon = HudCanvas.MakeIcon(slotGo.transform, "Icon", TraitIcon);
|
||||
Text label = HudCanvas.MakeText(slotGo.transform, "Label", "", 8);
|
||||
label.color = TraitNameColor;
|
||||
|
|
@ -2677,16 +3098,20 @@ public static class WatchCaption
|
|||
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
label.resizeTextMinSize = 6;
|
||||
label.resizeTextMaxSize = 8;
|
||||
_traitSlots[i] = new TraitSlot { Root = slotGo, Icon = icon, Label = label };
|
||||
DossierChipTip tip = slotGo.AddComponent<DossierChipTip>();
|
||||
tip.enabled = false;
|
||||
_traitSlots[i] = new TraitSlot { Root = slotGo, Icon = icon, Label = label, Tip = tip };
|
||||
slotGo.SetActive(false);
|
||||
}
|
||||
|
||||
_reasonText = HudCanvas.MakeText(_root.transform, "Reason", "", 8);
|
||||
_reasonText.color = ReasonColor;
|
||||
_reasonText.supportRichText = true;
|
||||
_reasonText.alignment = TextAnchor.UpperLeft;
|
||||
_reasonText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
_reasonText.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
_reasonText.resizeTextForBestFit = false;
|
||||
WireReasonClick(0);
|
||||
|
||||
_speciesIcon = HudCanvas.MakeIcon(_root.transform, "SpeciesIcon", SpeciesSize);
|
||||
_sexIcon = HudCanvas.MakeIcon(_root.transform, "SexIcon", SexSize);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.25.93",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Citizen job labels from live library discovery.",
|
||||
"version": "0.26.1",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Fix Lore click camera follow (pause before focus).",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue