Enhance Lore panel with Living and Fallen tabs, implementing character death tracking and improved HUD interactions. Added support for death manner classification and updated dossier handling for fallen units. Adjusted test scenarios to validate new features and behaviors.
This commit is contained in:
parent
b40f3f8621
commit
94b356c9c8
12 changed files with 1255 additions and 217 deletions
|
|
@ -27,6 +27,9 @@ public static class ActorChroniclePatches
|
|||
return;
|
||||
}
|
||||
|
||||
// Capture living dossier before die() clears state - used by Fallen archive.
|
||||
Chronicle.CaptureFinalDossier(__instance);
|
||||
|
||||
BaseSimObject attacker = __instance.attackedBy;
|
||||
if (attacker != null && !attacker.isRekt() && attacker.isActor() && attacker != __instance)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -395,9 +395,17 @@ public static class AgentHarness
|
|||
{
|
||||
Chronicle.ShowHud();
|
||||
string tabRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant();
|
||||
if (tabRaw == "characters" || tabRaw == "character" || tabRaw == "chars" || tabRaw == "history")
|
||||
if (tabRaw == "fallen" || tabRaw == "dead")
|
||||
{
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Fallen);
|
||||
}
|
||||
else if (tabRaw == "living"
|
||||
|| tabRaw == "characters"
|
||||
|| tabRaw == "character"
|
||||
|| tabRaw == "chars"
|
||||
|| tabRaw == "history")
|
||||
{
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -422,7 +430,7 @@ public static class AgentHarness
|
|||
case "lore_search":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
|
||||
ChronicleHud.SetSearch(cmd.value ?? cmd.label ?? "");
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"search='{ChronicleHud.SearchText}'");
|
||||
|
|
@ -432,7 +440,7 @@ public static class AgentHarness
|
|||
case "lore_favorites":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
|
||||
string v = (cmd.value ?? "true").Trim().ToLowerInvariant();
|
||||
bool on = v != "false" && v != "0" && v != "off";
|
||||
ChronicleHud.SetFavoritesOnly(on);
|
||||
|
|
@ -444,7 +452,7 @@ public static class AgentHarness
|
|||
case "lore_pick":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
|
||||
string needle = cmd.value ?? cmd.label ?? "";
|
||||
if (needle == "auto")
|
||||
{
|
||||
|
|
@ -489,7 +497,7 @@ public static class AgentHarness
|
|||
case "lore_pick_fallen":
|
||||
{
|
||||
Chronicle.ShowHud();
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Fallen);
|
||||
string needle = cmd.value ?? cmd.label ?? "";
|
||||
if (needle == "auto")
|
||||
{
|
||||
|
|
@ -508,7 +516,7 @@ public static class AgentHarness
|
|||
|
||||
Emit(cmd, ok,
|
||||
detail:
|
||||
$"picked={ok} detail={ChronicleHud.DetailUnitId} follow={ChronicleHud.FollowFocus} hist={Chronicle.HistoryCountFor(ChronicleHud.DetailUnitId)}");
|
||||
$"picked={ok} detail={ChronicleHud.DetailUnitId} follow={ChronicleHud.FollowFocus} hist={Chronicle.HistoryCountFor(ChronicleHud.DetailUnitId)} fallenFocus={Chronicle.LastFallenFocusDetail} manner={Chronicle.LatestDeathMannerFor(ChronicleHud.DetailUnitId)}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -518,7 +526,43 @@ public static class AgentHarness
|
|||
string name = !string.IsNullOrEmpty(cmd.label) ? cmd.label : "Orphan";
|
||||
string species = !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "creature";
|
||||
string prefix = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "Fallen";
|
||||
long id = Chronicle.ForceOrphanHistory(name, species, n, prefix);
|
||||
AttackType attackType = AttackType.Age;
|
||||
if (!string.IsNullOrEmpty(cmd.expect)
|
||||
&& System.Enum.TryParse(cmd.expect, ignoreCase: true, out AttackType parsedAttack))
|
||||
{
|
||||
attackType = parsedAttack;
|
||||
}
|
||||
|
||||
long killerId = 0;
|
||||
Vector3 deathPos = Vector3.zero;
|
||||
string mode = (cmd.tier ?? "").Trim().ToLowerInvariant();
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
if (mode == "killer" && focus != null && focus.isAlive())
|
||||
{
|
||||
killerId = focus.getID();
|
||||
deathPos = focus.current_position;
|
||||
}
|
||||
else if (mode == "place")
|
||||
{
|
||||
if (focus != null)
|
||||
{
|
||||
deathPos = focus.current_position;
|
||||
}
|
||||
else if (MoveCamera.instance != null)
|
||||
{
|
||||
deathPos = MoveCamera.instance.transform.position;
|
||||
deathPos.z = 0f;
|
||||
}
|
||||
|
||||
if (deathPos == Vector3.zero)
|
||||
{
|
||||
deathPos = new Vector3(48f, 48f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
long id = Chronicle.ForceOrphanHistory(
|
||||
name, species, n, prefix, attackType, killerId, deathPos);
|
||||
DeathManner manner = Chronicle.LatestDeathMannerFor(id);
|
||||
bool ok = id != 0 && Chronicle.HistoryCountFor(id) >= n;
|
||||
if (ok)
|
||||
{
|
||||
|
|
@ -529,7 +573,11 @@ public static class AgentHarness
|
|||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok, detail: $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n}");
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail:
|
||||
$"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n} manner={manner} killer={killerId} mode={mode}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1491,6 +1539,42 @@ public static class AgentHarness
|
|||
$"follow={have} want={want} detailId={ChronicleHud.DetailUnitId}";
|
||||
break;
|
||||
}
|
||||
case "fallen_focus":
|
||||
{
|
||||
string want = (cmd.value ?? "").Trim().ToLowerInvariant();
|
||||
string have = (Chronicle.LastFallenFocusDetail ?? "").Trim().ToLowerInvariant();
|
||||
if (want == "killer" || want.StartsWith("killer"))
|
||||
{
|
||||
pass = have.StartsWith("killer:");
|
||||
}
|
||||
else if (want == "place")
|
||||
{
|
||||
pass = have == "place";
|
||||
}
|
||||
else if (want == "none")
|
||||
{
|
||||
pass = have == "none" || string.IsNullOrEmpty(have);
|
||||
}
|
||||
else
|
||||
{
|
||||
pass = have == want;
|
||||
}
|
||||
|
||||
detail = $"fallenFocus={Chronicle.LastFallenFocusDetail} want={cmd.value}";
|
||||
break;
|
||||
}
|
||||
case "death_manner":
|
||||
{
|
||||
long id = ChronicleHud.DetailUnitId;
|
||||
DeathManner have = Chronicle.LatestDeathMannerFor(id);
|
||||
string haveLabel = Chronicle.DeathMannerLabel(have);
|
||||
string want = (cmd.value ?? "").Trim().ToLowerInvariant();
|
||||
pass = id != 0
|
||||
&& (haveLabel == want
|
||||
|| have.ToString().Equals(want, System.StringComparison.OrdinalIgnoreCase));
|
||||
detail = $"manner={have} label={haveLabel} want={cmd.value} detailId={id}";
|
||||
break;
|
||||
}
|
||||
case "lore_follows_focus":
|
||||
{
|
||||
long focusId = WatchCaption.CurrentUnitId;
|
||||
|
|
@ -1548,6 +1632,104 @@ public static class AgentHarness
|
|||
$"pinned={WatchCaption.PinnedWhilePaused} visible={WatchCaption.Visible} idle={SpectatorMode.Active} want={want}";
|
||||
break;
|
||||
}
|
||||
case "dossier_unit_matches_lore":
|
||||
{
|
||||
long dossierId = WatchCaption.CurrentUnitId;
|
||||
long loreId = ChronicleHud.DetailUnitId;
|
||||
pass = dossierId != 0 && loreId != 0 && dossierId == loreId;
|
||||
detail =
|
||||
$"dossierId={dossierId} loreId={loreId} headline={WatchCaption.LastHeadline}";
|
||||
break;
|
||||
}
|
||||
case "dossier_species":
|
||||
{
|
||||
string want = (cmd.value ?? cmd.asset ?? "").Trim();
|
||||
string have = WatchCaption.Current != null ? WatchCaption.Current.SpeciesId ?? "" : "";
|
||||
pass = !string.IsNullOrEmpty(want)
|
||||
&& have.Equals(want, System.StringComparison.OrdinalIgnoreCase);
|
||||
detail =
|
||||
$"species={have} want={want} headline={WatchCaption.LastHeadline} avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback}";
|
||||
break;
|
||||
}
|
||||
case "dossier_matches_focus":
|
||||
{
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
UnitDossier dossier = WatchCaption.Current;
|
||||
string focusSpecies = focus?.asset != null ? focus.asset.id : "";
|
||||
long focusId = 0;
|
||||
try
|
||||
{
|
||||
focusId = focus != null ? focus.getID() : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
focusId = 0;
|
||||
}
|
||||
|
||||
pass = focus != null
|
||||
&& focus.isAlive()
|
||||
&& dossier != null
|
||||
&& dossier.UnitId == focusId
|
||||
&& !string.IsNullOrEmpty(focusSpecies)
|
||||
&& focusSpecies.Equals(dossier.SpeciesId ?? "", System.StringComparison.OrdinalIgnoreCase)
|
||||
&& DossierAvatar.ShowingLiveAvatar
|
||||
&& !DossierAvatar.ShowingSpeciesFallback;
|
||||
detail =
|
||||
$"focusId={focusId} dossierId={(dossier != null ? dossier.UnitId : 0)} focusSpecies={focusSpecies} dossierSpecies={(dossier != null ? dossier.SpeciesId : "")} avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback}";
|
||||
break;
|
||||
}
|
||||
case "dossier_avatar_live":
|
||||
{
|
||||
pass = DossierAvatar.ShowingLiveAvatar && !DossierAvatar.ShowingSpeciesFallback;
|
||||
detail =
|
||||
$"avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback} dossierId={WatchCaption.CurrentUnitId}";
|
||||
break;
|
||||
}
|
||||
case "dossier_avatar_species":
|
||||
{
|
||||
pass = DossierAvatar.ShowingSpeciesFallback && !DossierAvatar.ShowingLiveAvatar;
|
||||
detail =
|
||||
$"avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback} dossierId={WatchCaption.CurrentUnitId}";
|
||||
break;
|
||||
}
|
||||
case "fallen_place_no_follow":
|
||||
{
|
||||
// Place focus must clear unit follow so the camera stays at the death site.
|
||||
pass = Chronicle.LastFallenFocusDetail == "place" && !MoveCamera.hasFocusUnit();
|
||||
detail =
|
||||
$"fallenFocus={Chronicle.LastFallenFocusDetail} hasFocus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastFallenFocusPosition}";
|
||||
break;
|
||||
}
|
||||
case "fallen_killer_camera":
|
||||
{
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
long focusId = 0;
|
||||
try
|
||||
{
|
||||
focusId = focus != null ? focus.getID() : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
focusId = 0;
|
||||
}
|
||||
|
||||
string detailStr = Chronicle.LastFallenFocusDetail ?? "";
|
||||
bool killerTag = detailStr.StartsWith("killer:");
|
||||
long killerId = 0;
|
||||
if (killerTag)
|
||||
{
|
||||
long.TryParse(detailStr.Substring("killer:".Length), out killerId);
|
||||
}
|
||||
|
||||
pass = killerTag
|
||||
&& killerId != 0
|
||||
&& focusId == killerId
|
||||
&& WatchCaption.CurrentUnitId != 0
|
||||
&& WatchCaption.CurrentUnitId != focusId;
|
||||
detail =
|
||||
$"fallenFocus={detailStr} focusId={focusId} dossierId={WatchCaption.CurrentUnitId}";
|
||||
break;
|
||||
}
|
||||
case "lore_zoom_blocked":
|
||||
{
|
||||
MoveCamera cam = MoveCamera.instance;
|
||||
|
|
@ -1619,15 +1801,37 @@ public static class AgentHarness
|
|||
case "lore_tab":
|
||||
{
|
||||
string wantRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant();
|
||||
ChronicleHud.LoreTab want = wantRaw == "characters" || wantRaw == "character"
|
||||
|| wantRaw == "chars" || wantRaw == "history"
|
||||
? ChronicleHud.LoreTab.Characters
|
||||
: ChronicleHud.LoreTab.World;
|
||||
ChronicleHud.LoreTab want = ChronicleHud.LoreTab.World;
|
||||
if (wantRaw == "fallen" || wantRaw == "dead")
|
||||
{
|
||||
want = ChronicleHud.LoreTab.Fallen;
|
||||
}
|
||||
else if (wantRaw == "living"
|
||||
|| wantRaw == "characters"
|
||||
|| wantRaw == "character"
|
||||
|| wantRaw == "chars"
|
||||
|| wantRaw == "history")
|
||||
{
|
||||
want = ChronicleHud.LoreTab.Living;
|
||||
}
|
||||
|
||||
pass = Chronicle.HudVisible && ChronicleHud.ActiveTab == want;
|
||||
detail =
|
||||
$"hud={Chronicle.HudVisible} tab={ChronicleHud.ActiveTab} want={want} recent={Chronicle.RecentFocusCount}";
|
||||
break;
|
||||
}
|
||||
case "fallen_list_top":
|
||||
{
|
||||
string want = (cmd.value ?? cmd.label ?? "").Trim();
|
||||
string have = ChronicleHud.LastListTopTitle ?? "";
|
||||
pass = Chronicle.HudVisible
|
||||
&& ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen
|
||||
&& ChronicleHud.DetailUnitId == 0
|
||||
&& !string.IsNullOrEmpty(want)
|
||||
&& have.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
detail = $"top='{have}' want='{want}' tab={ChronicleHud.ActiveTab}";
|
||||
break;
|
||||
}
|
||||
case "lore_recent":
|
||||
{
|
||||
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ public static class CameraDirector
|
|||
public static string LastWatchLabel { get; private set; } = "";
|
||||
public static string LastWatchAssetId { get; private set; } = "";
|
||||
|
||||
public static void FocusUnit(Actor actor)
|
||||
public static void FocusUnit(Actor actor, bool updateCaption = true)
|
||||
{
|
||||
if (actor != null && actor.isAlive())
|
||||
{
|
||||
RetargetFollow(actor);
|
||||
if (SpectatorMode.Active)
|
||||
if (updateCaption && SpectatorMode.Active)
|
||||
{
|
||||
WatchCaption.SetFromActor(actor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@ public enum ChronicleKind
|
|||
AgeChapter
|
||||
}
|
||||
|
||||
/// <summary>How a character died - used for Fallen list labels and icons.</summary>
|
||||
public enum DeathManner
|
||||
{
|
||||
None = 0,
|
||||
Slain,
|
||||
OldAge,
|
||||
Starvation,
|
||||
Drowned,
|
||||
Burned,
|
||||
Plague,
|
||||
Accident,
|
||||
Divine,
|
||||
Other
|
||||
}
|
||||
|
||||
public sealed class ChronicleEntry
|
||||
{
|
||||
public float CreatedAt;
|
||||
|
|
@ -33,17 +48,42 @@ public sealed class ChronicleEntry
|
|||
/// <summary>Chronicle-voice line for HUD; falls back to <see cref="Line"/>.</summary>
|
||||
public string LoreLine = "";
|
||||
public Vector3 Position;
|
||||
/// <summary>True when <see cref="Position"/> was recorded from a real world location.</summary>
|
||||
public bool HasWorldPosition;
|
||||
public string AssetId = "";
|
||||
public string AgeId = "";
|
||||
public string AgeName = "";
|
||||
/// <summary>True when this row is a curated character legend in World Memory.</summary>
|
||||
public bool IsLegend;
|
||||
/// <summary>Vanilla <see cref="AttackType"/> name when <see cref="Kind"/> is Death.</summary>
|
||||
public string AttackTypeName = "";
|
||||
/// <summary>Classified death manner for Fallen UI.</summary>
|
||||
public DeathManner DeathManner;
|
||||
|
||||
/// <summary>Back-compat for harness / jump helpers.</summary>
|
||||
public long UnitId => SubjectId;
|
||||
|
||||
public string HudLine => !string.IsNullOrEmpty(LoreLine) ? LoreLine : (Line ?? "");
|
||||
|
||||
/// <summary>Resolved death manner (stored, or inferred from line / killer).</summary>
|
||||
public DeathManner ResolvedDeathManner
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Kind != ChronicleKind.Death)
|
||||
{
|
||||
return DeathManner.None;
|
||||
}
|
||||
|
||||
if (DeathManner != DeathManner.None)
|
||||
{
|
||||
return DeathManner;
|
||||
}
|
||||
|
||||
return Chronicle.InferDeathManner(AttackTypeName, OtherId != 0, Line);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain date-prefixed line for logs / asserts (uses lore when present).</summary>
|
||||
public string DisplayLine
|
||||
{
|
||||
|
|
@ -103,6 +143,8 @@ public static class Chronicle
|
|||
private static readonly HashSet<string> LoverPairKeys = new HashSet<string>();
|
||||
private static readonly HashSet<string> FriendPairKeys = new HashSet<string>();
|
||||
private static readonly HashSet<long> DeathLogged = new HashSet<long>();
|
||||
private static readonly Dictionary<long, UnitDossier> FinalDossiers =
|
||||
new Dictionary<long, UnitDossier>();
|
||||
private static readonly HashSet<string> LandmarkDedupe = new HashSet<string>();
|
||||
private static readonly Dictionary<string, int> LandmarksPerAge = new Dictionary<string, int>();
|
||||
private static readonly List<long> RecentFocusIds = new List<long>();
|
||||
|
|
@ -350,6 +392,7 @@ public static class Chronicle
|
|||
LoverPairKeys.Clear();
|
||||
FriendPairKeys.Clear();
|
||||
DeathLogged.Clear();
|
||||
FinalDossiers.Clear();
|
||||
LandmarkDedupe.Clear();
|
||||
LandmarksPerAge.Clear();
|
||||
RecentFocusIds.Clear();
|
||||
|
|
@ -395,7 +438,7 @@ public static class Chronicle
|
|||
EnsureWindow();
|
||||
if (ChronicleHud.DetailUnitId == 0)
|
||||
{
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Characters);
|
||||
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
|
||||
}
|
||||
|
||||
ChronicleHud.SetVisible(true);
|
||||
|
|
@ -575,20 +618,31 @@ public static class Chronicle
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Which cast the Characters browser should show.</summary>
|
||||
public enum CharacterListScope
|
||||
{
|
||||
Living,
|
||||
Fallen
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Character History rows: recent focuses first, then other history subjects / favorites.
|
||||
/// Living: favorites, then recently watched, then by event count.
|
||||
/// Fallen: newest death / event first.
|
||||
/// </summary>
|
||||
public static List<ChronicleCharacterSummary> ListCharacters(
|
||||
string search = "",
|
||||
bool favoritesOnly = false,
|
||||
int max = 40)
|
||||
int max = 40,
|
||||
CharacterListScope scope = CharacterListScope.Living)
|
||||
{
|
||||
var byId = new Dictionary<long, ChronicleCharacterSummary>();
|
||||
string needle = (search ?? "").Trim().ToLowerInvariant();
|
||||
bool wantLiving = scope == CharacterListScope.Living;
|
||||
bool wantFallen = scope == CharacterListScope.Fallen;
|
||||
|
||||
void Upsert(Actor actor, bool recent, int recentIndex)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
if (!wantLiving || actor == null || !actor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -643,7 +697,8 @@ public static class Chronicle
|
|||
SpeciesId = species,
|
||||
HistoryCount = HistoryCountFor(id),
|
||||
IsFavorite = fav,
|
||||
IsAlive = true
|
||||
IsAlive = true,
|
||||
SortTime = LatestActivityTime(id)
|
||||
};
|
||||
byId[id] = row;
|
||||
}
|
||||
|
|
@ -659,135 +714,157 @@ public static class Chronicle
|
|||
|
||||
row.IsFavorite = fav;
|
||||
row.HistoryCount = HistoryCountFor(id);
|
||||
row.SortTime = LatestActivityTime(id);
|
||||
}
|
||||
|
||||
List<long> recentCopy;
|
||||
lock (RecentFocusIds)
|
||||
if (wantLiving)
|
||||
{
|
||||
recentCopy = new List<long>(RecentFocusIds);
|
||||
}
|
||||
|
||||
for (int i = 0; i < recentCopy.Count; i++)
|
||||
{
|
||||
Upsert(FindUnitById(recentCopy[i]), recent: true, recentIndex: i);
|
||||
}
|
||||
|
||||
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
List<long> recentCopy;
|
||||
lock (RecentFocusIds)
|
||||
{
|
||||
continue;
|
||||
recentCopy = new List<long>(RecentFocusIds);
|
||||
}
|
||||
|
||||
bool fav = false;
|
||||
try
|
||||
for (int i = 0; i < recentCopy.Count; i++)
|
||||
{
|
||||
fav = actor.isFavorite();
|
||||
}
|
||||
catch
|
||||
{
|
||||
fav = false;
|
||||
Upsert(FindUnitById(recentCopy[i]), recent: true, recentIndex: i);
|
||||
}
|
||||
|
||||
long id = 0;
|
||||
try
|
||||
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
|
||||
{
|
||||
id = actor.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasHist = HistoryCountFor(id) > 0;
|
||||
if (!fav && !hasHist && !byId.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Upsert(actor, recent: false, recentIndex: -1);
|
||||
}
|
||||
|
||||
// History subjects that may still be named from entries even if not matched above.
|
||||
List<long> histIds;
|
||||
lock (Histories)
|
||||
{
|
||||
histIds = new List<long>(Histories.Keys);
|
||||
}
|
||||
|
||||
for (int i = 0; i < histIds.Count; i++)
|
||||
{
|
||||
long id = histIds[i];
|
||||
if (byId.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Actor live = FindUnitById(id);
|
||||
if (live != null)
|
||||
{
|
||||
Upsert(live, recent: false, recentIndex: -1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (favoritesOnly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(id);
|
||||
if (snap == null || snap.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ChronicleEntry last = snap[snap.Count - 1];
|
||||
string name = last?.Name ?? "";
|
||||
string species = last?.SpeciesId ?? "";
|
||||
if (!string.IsNullOrEmpty(needle))
|
||||
{
|
||||
string hay = (name + " " + species).ToLowerInvariant();
|
||||
if (hay.IndexOf(needle, System.StringComparison.Ordinal) < 0)
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool fav = false;
|
||||
try
|
||||
{
|
||||
fav = actor.isFavorite();
|
||||
}
|
||||
catch
|
||||
{
|
||||
fav = false;
|
||||
}
|
||||
|
||||
long id = 0;
|
||||
try
|
||||
{
|
||||
id = actor.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasHist = HistoryCountFor(id) > 0;
|
||||
if (!fav && !hasHist && !byId.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Upsert(actor, recent: false, recentIndex: -1);
|
||||
}
|
||||
}
|
||||
|
||||
if (wantFallen)
|
||||
{
|
||||
// History subjects that may still be named from entries even if not matched above.
|
||||
List<long> histIds;
|
||||
lock (Histories)
|
||||
{
|
||||
histIds = new List<long>(Histories.Keys);
|
||||
}
|
||||
|
||||
byId[id] = new ChronicleCharacterSummary
|
||||
for (int i = 0; i < histIds.Count; i++)
|
||||
{
|
||||
UnitId = id,
|
||||
Name = name,
|
||||
SpeciesId = species,
|
||||
HistoryCount = snap.Count,
|
||||
IsFavorite = false,
|
||||
IsAlive = false,
|
||||
IsRecent = false
|
||||
};
|
||||
long id = histIds[i];
|
||||
if (byId.ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Actor live = FindUnitById(id);
|
||||
if (live != null && live.isAlive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (favoritesOnly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(id);
|
||||
if (snap == null || snap.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ChronicleEntry last = snap[snap.Count - 1];
|
||||
string name = last?.Name ?? "";
|
||||
string species = last?.SpeciesId ?? "";
|
||||
if (!string.IsNullOrEmpty(needle))
|
||||
{
|
||||
string hay = (name + " " + species).ToLowerInvariant();
|
||||
if (hay.IndexOf(needle, System.StringComparison.Ordinal) < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
byId[id] = new ChronicleCharacterSummary
|
||||
{
|
||||
UnitId = id,
|
||||
Name = name,
|
||||
SpeciesId = species,
|
||||
HistoryCount = snap.Count,
|
||||
IsFavorite = false,
|
||||
IsAlive = false,
|
||||
IsRecent = false,
|
||||
DeathManner = LatestDeathMannerFor(id),
|
||||
SortTime = LatestActivityTime(id)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var list = new List<ChronicleCharacterSummary>(byId.Values);
|
||||
list.Sort((a, b) =>
|
||||
{
|
||||
if (a.IsAlive != b.IsAlive)
|
||||
if (wantFallen || (!a.IsAlive && !b.IsAlive))
|
||||
{
|
||||
return a.IsAlive ? -1 : 1;
|
||||
}
|
||||
|
||||
if (a.IsRecent != b.IsRecent)
|
||||
{
|
||||
return a.IsRecent ? -1 : 1;
|
||||
}
|
||||
|
||||
if (a.IsRecent && b.IsRecent)
|
||||
{
|
||||
return a.RecentIndex.CompareTo(b.RecentIndex);
|
||||
// Fallen: most recent event first.
|
||||
if (a.SortTime != b.SortTime)
|
||||
{
|
||||
return b.SortTime.CompareTo(a.SortTime);
|
||||
}
|
||||
|
||||
if (a.HistoryCount != b.HistoryCount)
|
||||
{
|
||||
return b.HistoryCount.CompareTo(a.HistoryCount);
|
||||
}
|
||||
|
||||
return string.Compare(a.Title, b.Title, System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Living: favorites, recently watched, event count, name.
|
||||
if (a.IsFavorite != b.IsFavorite)
|
||||
{
|
||||
return a.IsFavorite ? -1 : 1;
|
||||
}
|
||||
|
||||
int ra = a.IsRecent && a.RecentIndex >= 0 ? a.RecentIndex : int.MaxValue;
|
||||
int rb = b.IsRecent && b.RecentIndex >= 0 ? b.RecentIndex : int.MaxValue;
|
||||
if (ra != rb)
|
||||
{
|
||||
return ra.CompareTo(rb);
|
||||
}
|
||||
|
||||
if (a.HistoryCount != b.HistoryCount)
|
||||
{
|
||||
return b.HistoryCount.CompareTo(a.HistoryCount);
|
||||
}
|
||||
|
||||
return string.Compare(a.Title, b.Title, System.StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
|
|
@ -799,6 +876,22 @@ public static class Chronicle
|
|||
return list;
|
||||
}
|
||||
|
||||
private static float LatestActivityTime(long subjectId)
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(subjectId);
|
||||
float best = 0f;
|
||||
for (int i = 0; i < snap.Count; i++)
|
||||
{
|
||||
ChronicleEntry e = snap[i];
|
||||
if (e != null && e.CreatedAt > best)
|
||||
{
|
||||
best = e.CreatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (!ModSettings.ChronicleEnabled || !Config.game_loaded || World.world == null)
|
||||
|
|
@ -867,7 +960,7 @@ public static class Chronicle
|
|||
}
|
||||
|
||||
string line = FormatDeathCause(victim, attackType, killer);
|
||||
AppendHistory(ChronicleKind.Death, victim, killer, line);
|
||||
AppendHistory(ChronicleKind.Death, victim, killer, line, attackType);
|
||||
}
|
||||
|
||||
public static void NoteLovers(Actor a, Actor b)
|
||||
|
|
@ -976,7 +1069,14 @@ public static class Chronicle
|
|||
/// <summary>
|
||||
/// Harness: inject history for a subject id with no living unit (Fallen list).
|
||||
/// </summary>
|
||||
public static long ForceOrphanHistory(string name, string species, int count, string labelPrefix = "Fallen")
|
||||
public static long ForceOrphanHistory(
|
||||
string name,
|
||||
string species,
|
||||
int count,
|
||||
string labelPrefix = "Fallen",
|
||||
AttackType attackType = AttackType.Age,
|
||||
long killerId = 0,
|
||||
Vector3 deathPos = default)
|
||||
{
|
||||
count = Mathf.Clamp(count, 1, MaxHistoryPerUnit);
|
||||
long id;
|
||||
|
|
@ -988,6 +1088,40 @@ public static class Chronicle
|
|||
string who = string.IsNullOrEmpty(name) ? "Nameless" : name;
|
||||
string sp = string.IsNullOrEmpty(species) ? "creature" : species;
|
||||
string prefix = string.IsNullOrEmpty(labelPrefix) ? "Fallen" : labelPrefix;
|
||||
bool hasKiller = killerId != 0;
|
||||
DeathManner manner = ClassifyDeathManner(attackType, hasKiller);
|
||||
string baseCause;
|
||||
switch (manner)
|
||||
{
|
||||
case DeathManner.Slain:
|
||||
baseCause = hasKiller ? "Killed by foe" : "Killed in combat";
|
||||
break;
|
||||
case DeathManner.OldAge:
|
||||
baseCause = "Died of old age";
|
||||
break;
|
||||
case DeathManner.Starvation:
|
||||
baseCause = "Starved to death";
|
||||
break;
|
||||
case DeathManner.Drowned:
|
||||
baseCause = "Drowned";
|
||||
break;
|
||||
case DeathManner.Burned:
|
||||
baseCause = "Burned to death";
|
||||
break;
|
||||
case DeathManner.Plague:
|
||||
baseCause = "Died of plague";
|
||||
break;
|
||||
case DeathManner.Divine:
|
||||
baseCause = "Struck down by divine power";
|
||||
break;
|
||||
case DeathManner.Accident:
|
||||
baseCause = "Died in an explosion";
|
||||
break;
|
||||
default:
|
||||
baseCause = "Died";
|
||||
break;
|
||||
}
|
||||
|
||||
RefreshAgeChapter(force: false);
|
||||
StampWorldDate(out double worldTime, out string dateLabel);
|
||||
|
||||
|
|
@ -1001,7 +1135,8 @@ public static class Chronicle
|
|||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string line = count > 1 ? $"{prefix} #{i + 1}" : prefix;
|
||||
string line = count > 1 ? $"{baseCause} ({prefix} #{i + 1})" : baseCause;
|
||||
string lore = LoreProse.Rewrite(ChronicleKind.Death, line);
|
||||
list.Add(new ChronicleEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
|
|
@ -1009,12 +1144,17 @@ public static class Chronicle
|
|||
DateLabel = dateLabel,
|
||||
Kind = ChronicleKind.Death,
|
||||
SubjectId = id,
|
||||
OtherId = killerId,
|
||||
Name = who,
|
||||
SpeciesId = sp,
|
||||
Line = line,
|
||||
LoreLine = line,
|
||||
LoreLine = lore,
|
||||
Position = deathPos,
|
||||
HasWorldPosition = deathPos.sqrMagnitude > 0.0001f,
|
||||
AgeId = _currentAgeId,
|
||||
AgeName = _currentAgeName
|
||||
AgeName = _currentAgeName,
|
||||
AttackTypeName = attackType.ToString(),
|
||||
DeathManner = manner
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1025,7 +1165,10 @@ public static class Chronicle
|
|||
}
|
||||
|
||||
NoteFocusId(id);
|
||||
LogService.LogInfo($"[IdleSpectator][CHRONICLE] orphan history id={id} count={count} name={who}");
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator][CHRONICLE] orphan history id={id} count={count} name={who} manner={manner} killer={killerId}");
|
||||
|
||||
FinalDossiers[id] = UnitDossier.FromFallenArchive(id, who, sp, manner);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
|
@ -1064,10 +1207,318 @@ public static class Chronicle
|
|||
|
||||
Actor victim = MoveCamera._focus_unit;
|
||||
string line = FormatDeathCause(victim, attackType, killer);
|
||||
AppendHistory(ChronicleKind.Death, victim, killer, line);
|
||||
AppendHistory(ChronicleKind.Death, victim, killer, line, attackType);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static DeathManner ClassifyDeathManner(AttackType attackType, bool hasKiller)
|
||||
{
|
||||
switch (attackType)
|
||||
{
|
||||
case AttackType.Age:
|
||||
return DeathManner.OldAge;
|
||||
case AttackType.Starvation:
|
||||
return DeathManner.Starvation;
|
||||
case AttackType.Drowning:
|
||||
case AttackType.Water:
|
||||
return DeathManner.Drowned;
|
||||
case AttackType.Fire:
|
||||
return DeathManner.Burned;
|
||||
case AttackType.Plague:
|
||||
case AttackType.Infection:
|
||||
case AttackType.Poison:
|
||||
case AttackType.Tumor:
|
||||
case AttackType.AshFever:
|
||||
return DeathManner.Plague;
|
||||
case AttackType.Divine:
|
||||
return DeathManner.Divine;
|
||||
case AttackType.Gravity:
|
||||
case AttackType.Explosion:
|
||||
case AttackType.Acid:
|
||||
case AttackType.Metamorphosis:
|
||||
return DeathManner.Accident;
|
||||
case AttackType.Eaten:
|
||||
case AttackType.Weapon:
|
||||
case AttackType.Smile:
|
||||
case AttackType.Other:
|
||||
return hasKiller ? DeathManner.Slain : DeathManner.Other;
|
||||
case AttackType.None:
|
||||
return hasKiller ? DeathManner.Slain : DeathManner.Other;
|
||||
default:
|
||||
return hasKiller ? DeathManner.Slain : DeathManner.Other;
|
||||
}
|
||||
}
|
||||
|
||||
public static DeathManner InferDeathManner(string attackTypeName, bool hasKiller, string line)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(attackTypeName)
|
||||
&& System.Enum.TryParse(attackTypeName, ignoreCase: true, out AttackType parsed))
|
||||
{
|
||||
return ClassifyDeathManner(parsed, hasKiller);
|
||||
}
|
||||
|
||||
string raw = line ?? "";
|
||||
if (raw.IndexOf("old age", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.OldAge;
|
||||
}
|
||||
|
||||
if (raw.IndexOf("Starved", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Starvation;
|
||||
}
|
||||
|
||||
if (raw.IndexOf("Drown", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Drowned;
|
||||
}
|
||||
|
||||
if (raw.IndexOf("Burn", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("lava", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Burned;
|
||||
}
|
||||
|
||||
if (raw.IndexOf("plague", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("poison", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("infection", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("tumor", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("ash fever", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Plague;
|
||||
}
|
||||
|
||||
if (raw.IndexOf("divine", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Divine;
|
||||
}
|
||||
|
||||
if (raw.IndexOf("explosion", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("acid", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("Fell to", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Accident;
|
||||
}
|
||||
|
||||
if (hasKiller
|
||||
|| raw.IndexOf("Killed by", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("Eaten by", System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("Was eaten", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return DeathManner.Slain;
|
||||
}
|
||||
|
||||
return DeathManner.Other;
|
||||
}
|
||||
|
||||
public static string DeathMannerLabel(DeathManner manner)
|
||||
{
|
||||
switch (manner)
|
||||
{
|
||||
case DeathManner.Slain:
|
||||
return "slain";
|
||||
case DeathManner.OldAge:
|
||||
return "old age";
|
||||
case DeathManner.Starvation:
|
||||
return "starved";
|
||||
case DeathManner.Drowned:
|
||||
return "drowned";
|
||||
case DeathManner.Burned:
|
||||
return "burned";
|
||||
case DeathManner.Plague:
|
||||
return "plague";
|
||||
case DeathManner.Accident:
|
||||
return "accident";
|
||||
case DeathManner.Divine:
|
||||
return "divine";
|
||||
case DeathManner.Other:
|
||||
return "dead";
|
||||
default:
|
||||
return "fallen";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Newest death event for a subject, if any.</summary>
|
||||
public static ChronicleEntry LatestDeathEntry(long subjectId)
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(subjectId);
|
||||
for (int i = snap.Count - 1; i >= 0; i--)
|
||||
{
|
||||
ChronicleEntry e = snap[i];
|
||||
if (e != null && e.Kind == ChronicleKind.Death)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static DeathManner LatestDeathMannerFor(long subjectId)
|
||||
{
|
||||
ChronicleEntry death = LatestDeathEntry(subjectId);
|
||||
return death != null ? death.ResolvedDeathManner : DeathManner.None;
|
||||
}
|
||||
|
||||
/// <summary>Harness: how the last fallen focus resolved (killer:id | place | none).</summary>
|
||||
public static string LastFallenFocusDetail { get; private set; } = "";
|
||||
|
||||
/// <summary>Harness: world position used for the last place-style fallen focus.</summary>
|
||||
public static Vector3 LastFallenFocusPosition { get; private set; } = Vector3.zero;
|
||||
|
||||
/// <summary>Snapshot living dossier just before die() - for Fallen archive UI.</summary>
|
||||
public static void CaptureFinalDossier(Actor victim)
|
||||
{
|
||||
if (victim == null || !victim.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long id = 0;
|
||||
try
|
||||
{
|
||||
id = victim.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnitDossier dossier = UnitDossier.FromActor(victim);
|
||||
if (dossier == null || dossier.UnitId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FinalDossiers[id] = dossier;
|
||||
}
|
||||
|
||||
/// <summary>Last living dossier for a fallen subject, or a minimal archive rebuild.</summary>
|
||||
public static UnitDossier ResolveFallenDossier(long unitId)
|
||||
{
|
||||
if (unitId == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (FinalDossiers.TryGetValue(unitId, out UnitDossier cached) && cached != null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
ChronicleEntry death = LatestDeathEntry(unitId);
|
||||
ChronicleEntry any = death;
|
||||
if (any == null)
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(unitId);
|
||||
if (snap != null && snap.Count > 0)
|
||||
{
|
||||
any = snap[snap.Count - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (any == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DeathManner manner = death != null ? death.ResolvedDeathManner : DeathManner.None;
|
||||
UnitDossier rebuilt = UnitDossier.FromFallenArchive(unitId, any.Name, any.SpeciesId, manner);
|
||||
FinalDossiers[unitId] = rebuilt;
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Camera for a dead subject: living killer if possible, else last death / history position.
|
||||
/// Dossier always shows the fallen unit's last living state (not the killer).
|
||||
/// </summary>
|
||||
public static bool FocusFallenSubject(long unitId)
|
||||
{
|
||||
LastFallenFocusDetail = "none";
|
||||
LastFallenFocusPosition = Vector3.zero;
|
||||
if (unitId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UnitDossier fallen = ResolveFallenDossier(unitId);
|
||||
if (fallen != null)
|
||||
{
|
||||
WatchCaption.PinWhilePaused();
|
||||
WatchCaption.SetFromDossier(fallen);
|
||||
}
|
||||
|
||||
ChronicleEntry death = LatestDeathEntry(unitId);
|
||||
if (death != null && death.OtherId != 0)
|
||||
{
|
||||
Actor killer = FindUnitById(death.OtherId);
|
||||
if (killer != null && killer.isAlive())
|
||||
{
|
||||
CameraDirector.FocusUnit(killer, updateCaption: false);
|
||||
LastFallenFocusDetail = "killer:" + death.OtherId;
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator][LORE] fallen focus killer id={death.OtherId} victim={unitId}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (TryResolveFallenPlace(unitId, death, out Vector3 pos))
|
||||
{
|
||||
// Drop any prior follow target or the camera eases back away from the death site.
|
||||
CameraDirector.ClearFollow();
|
||||
CameraDirector.PanTo(pos);
|
||||
LastFallenFocusDetail = "place";
|
||||
LastFallenFocusPosition = pos;
|
||||
LogService.LogInfo($"[IdleSpectator][LORE] fallen focus place victim={unitId} pos={pos}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return fallen != null;
|
||||
}
|
||||
|
||||
private static bool EntryHasWorldPosition(ChronicleEntry e)
|
||||
{
|
||||
if (e == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.HasWorldPosition)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy session entries before HasWorldPosition existed.
|
||||
return e.Position.sqrMagnitude > 0.0001f;
|
||||
}
|
||||
|
||||
private static bool TryResolveFallenPlace(long unitId, ChronicleEntry death, out Vector3 pos)
|
||||
{
|
||||
pos = Vector3.zero;
|
||||
if (EntryHasWorldPosition(death))
|
||||
{
|
||||
pos = death.Position;
|
||||
return true;
|
||||
}
|
||||
|
||||
IReadOnlyList<ChronicleEntry> snap = SnapshotForSubject(unitId);
|
||||
for (int i = snap.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (EntryHasWorldPosition(snap[i]))
|
||||
{
|
||||
pos = snap[i].Position;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string FormatDeathCause(Actor victim, AttackType attackType, Actor killer)
|
||||
{
|
||||
string killerLabel = killer != null
|
||||
|
|
@ -1140,7 +1591,12 @@ public static class Chronicle
|
|||
}
|
||||
}
|
||||
|
||||
private static void AppendHistory(ChronicleKind kind, Actor subject, Actor other, string line)
|
||||
private static void AppendHistory(
|
||||
ChronicleKind kind,
|
||||
Actor subject,
|
||||
Actor other,
|
||||
string line,
|
||||
AttackType? attackType = null)
|
||||
{
|
||||
if (subject == null || string.IsNullOrEmpty(line))
|
||||
{
|
||||
|
|
@ -1169,10 +1625,25 @@ public static class Chronicle
|
|||
Line = line,
|
||||
LoreLine = lore,
|
||||
Position = subject.current_position,
|
||||
HasWorldPosition = true,
|
||||
AgeId = _currentAgeId,
|
||||
AgeName = _currentAgeName
|
||||
};
|
||||
|
||||
if (kind == ChronicleKind.Death)
|
||||
{
|
||||
bool hasKiller = other != null;
|
||||
if (attackType.HasValue)
|
||||
{
|
||||
entry.AttackTypeName = attackType.Value.ToString();
|
||||
entry.DeathManner = ClassifyDeathManner(attackType.Value, hasKiller);
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.DeathManner = InferDeathManner("", hasKiller, line);
|
||||
}
|
||||
}
|
||||
|
||||
lock (Histories)
|
||||
{
|
||||
if (!Histories.TryGetValue(subjectId, out List<ChronicleEntry> list))
|
||||
|
|
@ -1281,6 +1752,7 @@ public static class Chronicle
|
|||
Line = rawLine ?? "",
|
||||
LoreLine = string.IsNullOrEmpty(loreLine) ? (rawLine ?? "") : loreLine,
|
||||
Position = subject != null ? subject.current_position : fallbackPosition,
|
||||
HasWorldPosition = subject != null || fallbackPosition.sqrMagnitude > 0.0001f,
|
||||
AssetId = assetId ?? "",
|
||||
AgeId = ageId,
|
||||
AgeName = _currentAgeName,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>One row in the Lore panel Character History tab.</summary>
|
||||
/// <summary>One row in the Lore panel Living / Fallen tabs.</summary>
|
||||
public sealed class ChronicleCharacterSummary
|
||||
{
|
||||
public long UnitId;
|
||||
|
|
@ -11,6 +11,9 @@ public sealed class ChronicleCharacterSummary
|
|||
public bool IsAlive;
|
||||
public bool IsRecent;
|
||||
public int RecentIndex = -1;
|
||||
public DeathManner DeathManner;
|
||||
/// <summary>Newest event time for Fallen sort (unscaledTime).</summary>
|
||||
public float SortTime;
|
||||
|
||||
public string Title
|
||||
{
|
||||
|
|
@ -30,14 +33,17 @@ public sealed class ChronicleCharacterSummary
|
|||
}
|
||||
}
|
||||
|
||||
public string DeathLabel =>
|
||||
!IsAlive ? Chronicle.DeathMannerLabel(DeathManner) : "";
|
||||
|
||||
public string Detail
|
||||
{
|
||||
get
|
||||
{
|
||||
string hist = HistoryCount <= 0 ? "no history" : HistoryCount + " events";
|
||||
if (IsRecent)
|
||||
if (!IsAlive && DeathManner != DeathManner.None)
|
||||
{
|
||||
return "recent · " + hist;
|
||||
return DeathLabel + " · " + hist;
|
||||
}
|
||||
|
||||
return hist;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using UnityEngine.UI;
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Bottom-right Lore panel (L): World Memory + Character History tabs.
|
||||
/// Bottom-right Lore panel (L): World Memory + Living + Fallen tabs.
|
||||
/// Sized in canvas units (same scaler as the dossier) to stay clear of top-right.
|
||||
/// </summary>
|
||||
public static class ChronicleHud
|
||||
|
|
@ -16,7 +16,8 @@ public static class ChronicleHud
|
|||
public enum LoreTab
|
||||
{
|
||||
World = 0,
|
||||
Characters = 1
|
||||
Living = 1,
|
||||
Fallen = 2
|
||||
}
|
||||
|
||||
private const float PanelWidth = 268f;
|
||||
|
|
@ -32,7 +33,7 @@ public static class ChronicleHud
|
|||
private const float TabH = 15f;
|
||||
private const float CharToolsH = 17f;
|
||||
private const float ScrollSensitivity = 55f;
|
||||
private const string ChromeMarker = "LoreV4";
|
||||
private const string ChromeMarker = "LoreV5";
|
||||
|
||||
private static GameObject _root;
|
||||
private static RectTransform _rootRt;
|
||||
|
|
@ -50,7 +51,8 @@ public static class ChronicleHud
|
|||
|
||||
private static GameObject _tabsBar;
|
||||
private static GameObject _worldTabBtn;
|
||||
private static GameObject _charsTabBtn;
|
||||
private static GameObject _livingTabBtn;
|
||||
private static GameObject _fallenTabBtn;
|
||||
private static GameObject _charTools;
|
||||
private static InputField _searchField;
|
||||
private static Button _favFilterBtn;
|
||||
|
|
@ -63,9 +65,12 @@ public static class ChronicleHud
|
|||
private static GameObject _backBtn;
|
||||
private static Text _backBtnLabel;
|
||||
|
||||
/// <summary>Rows actually built in the last Characters detail rebuild (harness).</summary>
|
||||
/// <summary>Rows actually built in the last character detail rebuild (harness).</summary>
|
||||
public static int LastDetailHistoryRows { get; private set; }
|
||||
|
||||
/// <summary>Title of the first row in the last Living/Fallen list rebuild (harness).</summary>
|
||||
public static string LastListTopTitle { get; private set; } = "";
|
||||
|
||||
public static bool Visible => _visible && _root != null && _root.activeSelf;
|
||||
|
||||
public static bool IsReady => _root != null;
|
||||
|
|
@ -78,6 +83,8 @@ public static class ChronicleHud
|
|||
|
||||
public static long DetailUnitId => _detailUnitId;
|
||||
|
||||
private static bool IsCastTab => _tab == LoreTab.Living || _tab == LoreTab.Fallen;
|
||||
|
||||
/// <summary>True when Lore detail was opened via L/books and should track idle focus.</summary>
|
||||
public static bool FollowFocus => _followFocus;
|
||||
|
||||
|
|
@ -188,7 +195,7 @@ public static class ChronicleHud
|
|||
_root.SetActive(false);
|
||||
_visible = false;
|
||||
ApplyTabChrome();
|
||||
LogService.LogInfo("[IdleSpectator] Lore HUD ready (L, World + Characters)");
|
||||
LogService.LogInfo("[IdleSpectator] Lore HUD ready (L, World + Living + Fallen)");
|
||||
}
|
||||
|
||||
private static void BuildTabs()
|
||||
|
|
@ -203,10 +210,12 @@ public static class ChronicleHud
|
|||
tabsRt.sizeDelta = new Vector2(-12f, TabH);
|
||||
|
||||
_worldTabBtn = BuildTabButton(_tabsBar.transform, "WorldTab", "World", () => SetTab(LoreTab.World));
|
||||
_charsTabBtn = BuildTabButton(_tabsBar.transform, "CharsTab", "Characters", () => SetTab(LoreTab.Characters));
|
||||
_livingTabBtn = BuildTabButton(_tabsBar.transform, "LivingTab", "Living", () => SetTab(LoreTab.Living));
|
||||
_fallenTabBtn = BuildTabButton(_tabsBar.transform, "FallenTab", "Fallen", () => SetTab(LoreTab.Fallen));
|
||||
|
||||
PlaceTab(_worldTabBtn, 0f);
|
||||
PlaceTab(_charsTabBtn, 1f);
|
||||
PlaceTab(_worldTabBtn, 0f, 3f);
|
||||
PlaceTab(_livingTabBtn, 1f, 3f);
|
||||
PlaceTab(_fallenTabBtn, 2f, 3f);
|
||||
}
|
||||
|
||||
private static GameObject BuildTabButton(Transform parent, string name, string label, UnityAction onClick)
|
||||
|
|
@ -240,14 +249,14 @@ public static class ChronicleHud
|
|||
return go;
|
||||
}
|
||||
|
||||
private static void PlaceTab(GameObject tab, float index)
|
||||
private static void PlaceTab(GameObject tab, float index, float count = 3f)
|
||||
{
|
||||
RectTransform rt = tab.GetComponent<RectTransform>();
|
||||
float w = 0.5f;
|
||||
float w = 1f / Mathf.Max(1f, count);
|
||||
rt.anchorMin = new Vector2(index * w, 0f);
|
||||
rt.anchorMax = new Vector2((index + 1f) * w, 1f);
|
||||
rt.offsetMin = new Vector2(index == 0f ? 0f : 1f, 0f);
|
||||
rt.offsetMax = new Vector2(index == 0f ? -1f : 0f, 0f);
|
||||
rt.offsetMax = new Vector2(index >= count - 1f ? 0f : -1f, 0f);
|
||||
}
|
||||
|
||||
private static void BuildCharTools()
|
||||
|
|
@ -306,7 +315,7 @@ public static class ChronicleHud
|
|||
favRt.sizeDelta = new Vector2(18f, 0f);
|
||||
favRt.anchoredPosition = Vector2.zero;
|
||||
|
||||
_backBtn = BuildTabButton(_charTools.transform, "BackBtn", "← Characters", ClearUnitDetail);
|
||||
_backBtn = BuildTabButton(_charTools.transform, "BackBtn", "← Living", ClearUnitDetail);
|
||||
_backBtnLabel = _backBtn.transform.Find("Label")?.GetComponent<Text>();
|
||||
RectTransform backRt = _backBtn.GetComponent<RectTransform>();
|
||||
backRt.anchorMin = new Vector2(0f, 0f);
|
||||
|
|
@ -400,7 +409,7 @@ public static class ChronicleHud
|
|||
return;
|
||||
}
|
||||
|
||||
bool chars = _tab == LoreTab.Characters;
|
||||
bool chars = IsCastTab;
|
||||
bool detail = chars && _detailUnitId != 0;
|
||||
float top = TitleBandH + 2f;
|
||||
if (!detail)
|
||||
|
|
@ -586,7 +595,7 @@ public static class ChronicleHud
|
|||
|
||||
_detailUnitId = 0;
|
||||
_followFocus = false;
|
||||
_tab = LoreTab.Characters;
|
||||
_tab = LoreTab.Living;
|
||||
SetVisible(true);
|
||||
}
|
||||
|
||||
|
|
@ -612,7 +621,7 @@ public static class ChronicleHud
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show one unit's full chronicle in Lore Characters.
|
||||
/// Show one unit's full chronicle in Lore (Living or Fallen tab).
|
||||
/// <paramref name="followFocus"/> true (L/books) retargets with idle; false (list) pins the archive.
|
||||
/// </summary>
|
||||
public static void OpenUnitHistory(long unitId, bool pauseIdle = true, bool followFocus = false)
|
||||
|
|
@ -625,7 +634,6 @@ public static class ChronicleHud
|
|||
EnsureBuilt();
|
||||
_detailUnitId = unitId;
|
||||
_followFocus = followFocus;
|
||||
_tab = LoreTab.Characters;
|
||||
|
||||
if (pauseIdle && SpectatorMode.Active)
|
||||
{
|
||||
|
|
@ -633,14 +641,16 @@ public static class ChronicleHud
|
|||
}
|
||||
|
||||
bool live = Chronicle.FocusCameraForBrowse(unitId);
|
||||
_tab = live ? LoreTab.Living : LoreTab.Fallen;
|
||||
if (live)
|
||||
{
|
||||
WatchCaption.PinWhilePaused();
|
||||
}
|
||||
else if (pauseIdle)
|
||||
else
|
||||
{
|
||||
// Dead / missing: keep current dossier if any; still pause idle for archive reading.
|
||||
// Dead / missing: keep dossier if any; pan to killer or death place.
|
||||
WatchCaption.PinWhilePaused();
|
||||
Chronicle.FocusFallenSubject(unitId);
|
||||
}
|
||||
|
||||
if (pauseIdle)
|
||||
|
|
@ -660,7 +670,7 @@ public static class ChronicleHud
|
|||
_followFocus = false;
|
||||
_lastCharFingerprint = int.MinValue;
|
||||
ApplyTabChrome();
|
||||
if (Visible && _tab == LoreTab.Characters)
|
||||
if (Visible && IsCastTab)
|
||||
{
|
||||
Rebuild(force: true);
|
||||
}
|
||||
|
|
@ -675,7 +685,7 @@ public static class ChronicleHud
|
|||
|
||||
_favoritesOnly = on;
|
||||
RefreshFavFilterVisual();
|
||||
if (_tab == LoreTab.Characters)
|
||||
if (IsCastTab)
|
||||
{
|
||||
Rebuild(force: true);
|
||||
}
|
||||
|
|
@ -693,7 +703,7 @@ public static class ChronicleHud
|
|||
}
|
||||
|
||||
_searchText = next;
|
||||
if (_tab == LoreTab.Characters)
|
||||
if (IsCastTab)
|
||||
{
|
||||
Rebuild(force: true);
|
||||
}
|
||||
|
|
@ -704,13 +714,14 @@ public static class ChronicleHud
|
|||
{
|
||||
EnsureBuilt();
|
||||
ClearUnitDetail();
|
||||
SetTab(LoreTab.Characters);
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, _favoritesOnly);
|
||||
SetTab(LoreTab.Living);
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(
|
||||
_searchText, _favoritesOnly, scope: Chronicle.CharacterListScope.Living);
|
||||
string needle = (nameNeedle ?? "").Trim().ToLowerInvariant();
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
ChronicleCharacterSummary row = list[i];
|
||||
if (row == null || !row.IsAlive)
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -733,13 +744,14 @@ public static class ChronicleHud
|
|||
{
|
||||
EnsureBuilt();
|
||||
ClearUnitDetail();
|
||||
SetTab(LoreTab.Characters);
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, _favoritesOnly);
|
||||
SetTab(LoreTab.Fallen);
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(
|
||||
_searchText, favoritesOnly: false, scope: Chronicle.CharacterListScope.Fallen);
|
||||
string needle = (nameNeedle ?? "").Trim().ToLowerInvariant();
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
ChronicleCharacterSummary row = list[i];
|
||||
if (row == null || row.IsAlive || row.HistoryCount <= 0)
|
||||
if (row == null || row.HistoryCount <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -765,7 +777,7 @@ public static class ChronicleHud
|
|||
private static void OnSearchChanged(string value)
|
||||
{
|
||||
_searchText = value ?? "";
|
||||
if (_tab == LoreTab.Characters)
|
||||
if (IsCastTab)
|
||||
{
|
||||
Rebuild(force: true);
|
||||
}
|
||||
|
|
@ -773,7 +785,7 @@ public static class ChronicleHud
|
|||
|
||||
private static void ApplyTabChrome()
|
||||
{
|
||||
bool chars = _tab == LoreTab.Characters;
|
||||
bool chars = IsCastTab;
|
||||
bool detail = chars && _detailUnitId != 0;
|
||||
if (_tabsBar != null)
|
||||
{
|
||||
|
|
@ -799,7 +811,8 @@ public static class ChronicleHud
|
|||
|
||||
if (_favFilterBtn != null)
|
||||
{
|
||||
_favFilterBtn.gameObject.SetActive(chars && !detail);
|
||||
// Favorites filter is Living-only.
|
||||
_favFilterBtn.gameObject.SetActive(_tab == LoreTab.Living && !detail);
|
||||
}
|
||||
|
||||
if (_backBtn != null)
|
||||
|
|
@ -807,12 +820,13 @@ public static class ChronicleHud
|
|||
_backBtn.SetActive(detail);
|
||||
if (_backBtnLabel != null)
|
||||
{
|
||||
_backBtnLabel.text = "← Characters";
|
||||
_backBtnLabel.text = _tab == LoreTab.Fallen ? "← Fallen" : "← Living";
|
||||
}
|
||||
}
|
||||
|
||||
StyleTab(_worldTabBtn, _tab == LoreTab.World);
|
||||
StyleTab(_charsTabBtn, _tab == LoreTab.Characters);
|
||||
StyleTab(_livingTabBtn, _tab == LoreTab.Living);
|
||||
StyleTab(_fallenTabBtn, _tab == LoreTab.Fallen);
|
||||
ApplyScrollInsets();
|
||||
RefreshTitle();
|
||||
}
|
||||
|
|
@ -824,7 +838,7 @@ public static class ChronicleHud
|
|||
return;
|
||||
}
|
||||
|
||||
if (_tab == LoreTab.Characters)
|
||||
if (IsCastTab)
|
||||
{
|
||||
if (_detailUnitId != 0)
|
||||
{
|
||||
|
|
@ -833,7 +847,7 @@ public static class ChronicleHud
|
|||
return;
|
||||
}
|
||||
|
||||
_titleText.text = "Characters";
|
||||
_titleText.text = _tab == LoreTab.Fallen ? "Fallen" : "Living";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -927,7 +941,7 @@ public static class ChronicleHud
|
|||
/// </summary>
|
||||
private static bool TryRetargetFollowFocus()
|
||||
{
|
||||
if (!_followFocus || _detailUnitId == 0 || _tab != LoreTab.Characters)
|
||||
if (!_followFocus || _detailUnitId == 0 || !IsCastTab)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -960,7 +974,7 @@ public static class ChronicleHud
|
|||
/// </summary>
|
||||
public static void OnIdleResumed()
|
||||
{
|
||||
if (!Visible || _tab != LoreTab.Characters || _detailUnitId == 0)
|
||||
if (!Visible || !IsCastTab || _detailUnitId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -994,7 +1008,14 @@ public static class ChronicleHud
|
|||
return hash;
|
||||
}
|
||||
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, _favoritesOnly, max: 12);
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(
|
||||
_searchText,
|
||||
_tab == LoreTab.Living && _favoritesOnly,
|
||||
max: 12,
|
||||
scope: _tab == LoreTab.Fallen
|
||||
? Chronicle.CharacterListScope.Fallen
|
||||
: Chronicle.CharacterListScope.Living);
|
||||
hash = (hash * 31) + (int)_tab;
|
||||
hash = (hash * 31) + list.Count;
|
||||
for (int i = 0; i < list.Count && i < 8; i++)
|
||||
{
|
||||
|
|
@ -1018,7 +1039,7 @@ public static class ChronicleHud
|
|||
ApplyTabChrome();
|
||||
ClearRows();
|
||||
|
||||
if (_tab == LoreTab.Characters)
|
||||
if (IsCastTab)
|
||||
{
|
||||
RebuildCharacters();
|
||||
return;
|
||||
|
|
@ -1066,24 +1087,37 @@ public static class ChronicleHud
|
|||
private static void RebuildCharacters()
|
||||
{
|
||||
RefreshTitle();
|
||||
LastListTopTitle = "";
|
||||
if (_detailUnitId != 0)
|
||||
{
|
||||
RebuildUnitHistoryDetail(_detailUnitId);
|
||||
return;
|
||||
}
|
||||
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, _favoritesOnly);
|
||||
Chronicle.CharacterListScope scope = _tab == LoreTab.Fallen
|
||||
? Chronicle.CharacterListScope.Fallen
|
||||
: Chronicle.CharacterListScope.Living;
|
||||
bool favOnly = scope == Chronicle.CharacterListScope.Living && _favoritesOnly;
|
||||
List<ChronicleCharacterSummary> list = Chronicle.ListCharacters(_searchText, favOnly, scope: scope);
|
||||
_lastCharFingerprint = CharacterFingerprint();
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
AddEmpty(_favoritesOnly ? "No favorites match" : "No characters yet");
|
||||
if (scope == Chronicle.CharacterListScope.Fallen)
|
||||
{
|
||||
AddEmpty("No fallen yet");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddEmpty(favOnly ? "No favorites match" : "No living yet");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool wroteRecentHeader = false;
|
||||
bool wroteAliveHeader = false;
|
||||
bool wroteFallenHeader = false;
|
||||
LastListTopTitle = list[0]?.Title ?? "";
|
||||
|
||||
bool wroteFavoritesHeader = false;
|
||||
int shown = 0;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
|
|
@ -1093,26 +1127,12 @@ public static class ChronicleHud
|
|||
continue;
|
||||
}
|
||||
|
||||
if (row.IsRecent)
|
||||
if (scope == Chronicle.CharacterListScope.Living
|
||||
&& (favOnly || row.IsFavorite)
|
||||
&& !wroteFavoritesHeader)
|
||||
{
|
||||
if (!wroteRecentHeader)
|
||||
{
|
||||
AddSection("Recent");
|
||||
wroteRecentHeader = true;
|
||||
}
|
||||
}
|
||||
else if (row.IsAlive)
|
||||
{
|
||||
if (!wroteAliveHeader)
|
||||
{
|
||||
AddSection(_favoritesOnly ? "Favorites" : "History");
|
||||
wroteAliveHeader = true;
|
||||
}
|
||||
}
|
||||
else if (!wroteFallenHeader)
|
||||
{
|
||||
AddSection("Fallen");
|
||||
wroteFallenHeader = true;
|
||||
AddSection("Favorites");
|
||||
wroteFavoritesHeader = true;
|
||||
}
|
||||
|
||||
GameObject go = BuildCharacterRow(row, shown);
|
||||
|
|
@ -1137,7 +1157,14 @@ public static class ChronicleHud
|
|||
return;
|
||||
}
|
||||
|
||||
AddSection(entries.Count + " events");
|
||||
string section = entries.Count + " events";
|
||||
ChronicleEntry death = Chronicle.LatestDeathEntry(unitId);
|
||||
if (death != null)
|
||||
{
|
||||
section = Chronicle.DeathMannerLabel(death.ResolvedDeathManner) + " · " + entries.Count + " events";
|
||||
}
|
||||
|
||||
AddSection(section);
|
||||
int shown = 0;
|
||||
int cap = Chronicle.MaxHistoryPerUnit;
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
|
|
@ -1195,7 +1222,11 @@ public static class ChronicleHud
|
|||
kindRt.pivot = new Vector2(0.5f, 1f);
|
||||
kindRt.anchoredPosition = new Vector2(9f, -5f);
|
||||
kindIcon.raycastTarget = false;
|
||||
HudIcons.Apply(kindIcon, HudIcons.ForChronicleKind(entry.Kind));
|
||||
HudIcons.Apply(
|
||||
kindIcon,
|
||||
entry.Kind == ChronicleKind.Death
|
||||
? HudIcons.ForDeathManner(entry.ResolvedDeathManner)
|
||||
: HudIcons.ForChronicleKind(entry.Kind));
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", entry.DisplayLineRich, 8);
|
||||
RectTransform labelRt = label.GetComponent<RectTransform>();
|
||||
|
|
@ -1204,7 +1235,9 @@ public static class ChronicleHud
|
|||
labelRt.offsetMin = new Vector2(18f, 2f);
|
||||
labelRt.offsetMax = new Vector2(-4f, -2f);
|
||||
label.alignment = TextAnchor.UpperLeft;
|
||||
label.color = ColorForKind(entry.Kind);
|
||||
label.color = entry.Kind == ChronicleKind.Death
|
||||
? ColorForDeathManner(entry.ResolvedDeathManner, listRow: false)
|
||||
: ColorForKind(entry.Kind);
|
||||
label.supportRichText = true;
|
||||
label.resizeTextForBestFit = false;
|
||||
label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
|
|
@ -1397,7 +1430,7 @@ public static class ChronicleHud
|
|||
? new Color(0.95f, 0.85f, 0.45f, 0.07f)
|
||||
: (summary.IsAlive
|
||||
? new Color(1f, 1f, 1f, 0.03f)
|
||||
: new Color(0.55f, 0.45f, 0.5f, 0.06f));
|
||||
: ColorForDeathManner(summary.DeathManner, listRow: true));
|
||||
bg.raycastTarget = true;
|
||||
|
||||
Image kindIcon = HudCanvas.MakeIcon(go.transform, "Kind", KindIconSize);
|
||||
|
|
@ -1411,12 +1444,15 @@ public static class ChronicleHud
|
|||
kindIcon,
|
||||
summary.IsFavorite
|
||||
? HudIcons.Favorite()
|
||||
: (HudIcons.FromUiIcon("iconUnit") ?? HudIcons.FromUiIcon("iconPopulation")));
|
||||
: (summary.IsAlive
|
||||
? (HudIcons.FromUiIcon("iconUnit") ?? HudIcons.FromUiIcon("iconPopulation"))
|
||||
: HudIcons.ForDeathManner(summary.DeathManner)));
|
||||
|
||||
string title = summary.Title;
|
||||
if (!summary.IsAlive && !string.IsNullOrEmpty(title))
|
||||
{
|
||||
title += " · fallen";
|
||||
string manner = summary.DeathLabel;
|
||||
title += string.IsNullOrEmpty(manner) ? " · fallen" : " · " + manner;
|
||||
}
|
||||
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", title, 8);
|
||||
|
|
@ -1428,7 +1464,7 @@ public static class ChronicleHud
|
|||
label.alignment = TextAnchor.MiddleLeft;
|
||||
label.color = summary.IsAlive
|
||||
? new Color(0.9f, 0.9f, 0.92f, 1f)
|
||||
: new Color(0.62f, 0.58f, 0.64f, 0.95f);
|
||||
: ColorForDeathManner(summary.DeathManner, listRow: false);
|
||||
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
label.raycastTarget = false;
|
||||
|
||||
|
|
@ -1541,6 +1577,49 @@ public static class ChronicleHud
|
|||
}
|
||||
}
|
||||
|
||||
private static Color ColorForDeathManner(DeathManner manner, bool listRow)
|
||||
{
|
||||
float a = listRow ? 0.08f : 1f;
|
||||
Color c;
|
||||
switch (manner)
|
||||
{
|
||||
case DeathManner.Slain:
|
||||
c = new Color(0.95f, 0.45f, 0.42f, a);
|
||||
break;
|
||||
case DeathManner.OldAge:
|
||||
c = new Color(0.72f, 0.7f, 0.78f, a);
|
||||
break;
|
||||
case DeathManner.Starvation:
|
||||
c = new Color(0.85f, 0.7f, 0.45f, a);
|
||||
break;
|
||||
case DeathManner.Drowned:
|
||||
c = new Color(0.45f, 0.65f, 0.9f, a);
|
||||
break;
|
||||
case DeathManner.Burned:
|
||||
c = new Color(0.95f, 0.55f, 0.28f, a);
|
||||
break;
|
||||
case DeathManner.Plague:
|
||||
c = new Color(0.55f, 0.85f, 0.4f, a);
|
||||
break;
|
||||
case DeathManner.Divine:
|
||||
c = new Color(0.95f, 0.9f, 0.45f, a);
|
||||
break;
|
||||
case DeathManner.Accident:
|
||||
c = new Color(0.75f, 0.55f, 0.85f, a);
|
||||
break;
|
||||
default:
|
||||
c = new Color(0.62f, 0.58f, 0.64f, a);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!listRow)
|
||||
{
|
||||
c.a = 0.95f;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void StretchFull(RectTransform rt, float pad)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ public static class DossierAvatar
|
|||
|
||||
public static bool UsingVanilla => _usingVanilla;
|
||||
|
||||
/// <summary>Live unit portrait is active (not the fallen species fallback).</summary>
|
||||
public static bool ShowingLiveAvatar => _shownActorId > 0;
|
||||
|
||||
/// <summary>Fallen archive species-icon portrait is showing.</summary>
|
||||
public static bool ShowingSpeciesFallback => _shownActorId == -2;
|
||||
|
||||
/// <summary>True if <paramref name="screenPoint"/> is inside the live portrait rect.</summary>
|
||||
public static bool ContainsScreenPoint(Vector2 screenPoint)
|
||||
{
|
||||
|
|
@ -113,6 +119,8 @@ public static class DossierAvatar
|
|||
long id = actor.getID();
|
||||
if (_usingVanilla && _instance != null && _shownActorId == id && _instance.gameObject.activeSelf)
|
||||
{
|
||||
// Always clear species fallback - it can linger above the live portrait.
|
||||
HideFallback();
|
||||
try
|
||||
{
|
||||
_instance.updateTileSprite();
|
||||
|
|
@ -129,6 +137,8 @@ public static class DossierAvatar
|
|||
{
|
||||
try
|
||||
{
|
||||
HideFallback();
|
||||
_instance.gameObject.SetActive(true);
|
||||
_instance.show_banner_kingdom = false;
|
||||
_instance.show_banner_clan = false;
|
||||
if (_instance.kingdomBanner != null)
|
||||
|
|
@ -167,6 +177,54 @@ public static class DossierAvatar
|
|||
public static void ClearActor()
|
||||
{
|
||||
_shownActorId = -1;
|
||||
HideVanillaInstance();
|
||||
HideFallback();
|
||||
}
|
||||
|
||||
/// <summary>Species icon portrait when the unit is gone (Fallen archive).</summary>
|
||||
public static void ShowSpecies(string speciesId)
|
||||
{
|
||||
if (_host == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Sprite sprite = HudIcons.FromSpeciesId(speciesId);
|
||||
HideVanillaInstance();
|
||||
|
||||
if (_fallbackSprite == null)
|
||||
{
|
||||
BuildFallback(_host.sizeDelta.x > 1f ? _host.sizeDelta.x : 44f);
|
||||
}
|
||||
|
||||
if (_fallbackBg != null)
|
||||
{
|
||||
_fallbackBg.gameObject.SetActive(true);
|
||||
_fallbackBg.sprite = null;
|
||||
_fallbackBg.color = new Color(0.04f, 0.045f, 0.055f, 0.95f);
|
||||
_fallbackBg.type = Image.Type.Simple;
|
||||
}
|
||||
|
||||
if (_fallbackSprite != null)
|
||||
{
|
||||
_fallbackSprite.gameObject.SetActive(true);
|
||||
HudIcons.Apply(_fallbackSprite, sprite, preserveAspect: true);
|
||||
_fallbackSprite.enabled = sprite != null;
|
||||
if (sprite != null)
|
||||
{
|
||||
float w = Mathf.Max(1f, sprite.rect.width);
|
||||
float h = Mathf.Max(1f, sprite.rect.height);
|
||||
float max = _host.sizeDelta.x;
|
||||
float scale = (max * 0.85f) / Mathf.Max(w, h);
|
||||
_fallbackSprite.rectTransform.sizeDelta = new Vector2(w * scale, h * scale);
|
||||
}
|
||||
}
|
||||
|
||||
_shownActorId = -2;
|
||||
}
|
||||
|
||||
private static void HideVanillaInstance()
|
||||
{
|
||||
if (_usingVanilla && _instance != null)
|
||||
{
|
||||
try
|
||||
|
|
@ -178,10 +236,19 @@ public static class DossierAvatar
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HideFallback()
|
||||
{
|
||||
if (_fallbackSprite != null)
|
||||
{
|
||||
_fallbackSprite.enabled = false;
|
||||
_fallbackSprite.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_fallbackBg != null)
|
||||
{
|
||||
_fallbackBg.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,8 +410,10 @@ public static class DossierAvatar
|
|||
|
||||
if (_fallbackSprite != null)
|
||||
{
|
||||
_fallbackSprite.gameObject.SetActive(true);
|
||||
Sprite live = HudIcons.FromActorLive(actor);
|
||||
HudIcons.Apply(_fallbackSprite, live, preserveAspect: true);
|
||||
_fallbackSprite.enabled = live != null;
|
||||
if (live != null)
|
||||
{
|
||||
float w = Mathf.Max(1f, live.rect.width);
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ internal static class HarnessScenarios
|
|||
Step("ch18m", "lore_open_focus"),
|
||||
Step("ch18n", "wait", wait: 0.3f),
|
||||
Step("ch18o", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18o2", "assert", expect: "lore_tab", value: "characters"),
|
||||
Step("ch18o2", "assert", expect: "lore_tab", value: "living"),
|
||||
Step("ch18o3", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18o3b", "assert", expect: "lore_follow", value: "true"),
|
||||
Step("ch18o4", "assert", expect: "lore_history_shown", value: "40", label: "min"),
|
||||
|
|
@ -518,26 +518,98 @@ internal static class HarnessScenarios
|
|||
Step("ch18p11", "assert", expect: "dossier_lore_history_match"),
|
||||
Step("ch18p12", "screenshot", value: "hud-lore-empty-history.png"),
|
||||
Step("ch18p13", "wait", wait: 0.55f),
|
||||
// Fallen (dead) archive: pinned, not follow-mode.
|
||||
// Fallen (dead) archive: pinned; old age → death place.
|
||||
Step("ch18d1", "lore_back"),
|
||||
Step("ch18d2", "chronicle_orphan", label: "Oldbone", asset: "skeleton", value: "Fallen", count: 5),
|
||||
Step("ch18d1b", "focus", asset: "auto"),
|
||||
Step("ch18d1c", "wait", wait: 0.2f),
|
||||
Step(
|
||||
"ch18d2",
|
||||
"chronicle_orphan",
|
||||
label: "Oldbone",
|
||||
asset: "skeleton",
|
||||
value: "Fallen",
|
||||
count: 5,
|
||||
expect: "Age",
|
||||
tier: "place"),
|
||||
Step("ch18d3", "lore_pick_fallen", value: "Oldbone"),
|
||||
Step("ch18d4", "wait", wait: 0.25f),
|
||||
Step("ch18d5", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18d6", "assert", expect: "lore_follow", value: "false"),
|
||||
Step("ch18d7", "assert", expect: "lore_history_shown", value: "5", label: "exact"),
|
||||
Step("ch18d7b", "assert", expect: "fallen_focus", value: "place"),
|
||||
Step("ch18d7c", "assert", expect: "death_manner", value: "old age"),
|
||||
Step("ch18d7d", "assert", expect: "dossier_contains", value: "Oldbone"),
|
||||
Step("ch18d7e", "assert", expect: "dossier_unit_matches_lore"),
|
||||
Step("ch18d7f", "assert", expect: "dossier_species", value: "skeleton"),
|
||||
Step("ch18d7g", "assert", expect: "dossier_avatar_species"),
|
||||
Step("ch18d7h", "assert", expect: "fallen_place_no_follow"),
|
||||
Step("ch18d8", "screenshot", value: "hud-lore-fallen.png"),
|
||||
Step("ch18d9", "wait", wait: 0.55f),
|
||||
// Character list browse (pinned archive).
|
||||
Step("ch18q", "lore_tab", value: "characters"),
|
||||
// Fallen slain → living killer focus.
|
||||
Step("ch18k1", "lore_back"),
|
||||
Step("ch18k2", "spawn", asset: "human", count: 1),
|
||||
Step("ch18k3", "focus", asset: "auto"),
|
||||
Step("ch18k4", "wait", wait: 0.25f),
|
||||
Step(
|
||||
"ch18k5",
|
||||
"chronicle_orphan",
|
||||
label: "Slainbone",
|
||||
asset: "skeleton",
|
||||
value: "Murder",
|
||||
count: 3,
|
||||
expect: "Weapon",
|
||||
tier: "killer"),
|
||||
Step("ch18k6", "lore_pick_fallen", value: "Slainbone"),
|
||||
Step("ch18k7", "wait", wait: 0.25f),
|
||||
Step("ch18k8", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18k9", "assert", expect: "lore_follow", value: "false"),
|
||||
Step("ch18k10", "assert", expect: "fallen_focus", value: "killer"),
|
||||
Step("ch18k11", "assert", expect: "death_manner", value: "slain"),
|
||||
Step("ch18k11b", "assert", expect: "dossier_contains", value: "Slainbone"),
|
||||
Step("ch18k11c", "assert", expect: "dossier_unit_matches_lore"),
|
||||
Step("ch18k11d", "assert", expect: "dossier_species", value: "skeleton"),
|
||||
Step("ch18k11e", "assert", expect: "dossier_avatar_species"),
|
||||
Step("ch18k11f", "assert", expect: "fallen_killer_camera"),
|
||||
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.
|
||||
Step("ch18av1", "lore_back"),
|
||||
Step("ch18av1b", "lore_close"),
|
||||
Step("ch18av1c", "spectator", value: "on"),
|
||||
Step("ch18av1d", "wait", wait: 0.2f),
|
||||
Step("ch18av2", "spawn", asset: "human", count: 1),
|
||||
Step("ch18av3", "focus", asset: "auto"),
|
||||
Step("ch18av4", "wait", wait: 0.25f),
|
||||
Step("ch18av5", "assert", expect: "dossier_matches_focus"),
|
||||
Step("ch18av6", "assert", expect: "dossier_species", value: "human"),
|
||||
Step("ch18av7", "assert", expect: "dossier_avatar_live"),
|
||||
Step("ch18av8", "screenshot", value: "hud-dossier-after-fallen.png"),
|
||||
Step("ch18av9", "wait", wait: 0.55f),
|
||||
// Also verify list-pick living after a fallen species portrait.
|
||||
Step("ch18av10", "lore_pick_fallen", value: "Oldbone"),
|
||||
Step("ch18av11", "wait", wait: 0.2f),
|
||||
Step("ch18av12", "assert", expect: "dossier_avatar_species"),
|
||||
Step("ch18av13", "lore_back"),
|
||||
Step("ch18av14", "lore_pick", value: "auto"),
|
||||
Step("ch18av15", "wait", wait: 0.25f),
|
||||
Step("ch18av16", "assert", expect: "dossier_matches_focus"),
|
||||
Step("ch18av17", "assert", expect: "dossier_avatar_live"),
|
||||
Step("ch18av18", "screenshot", value: "hud-dossier-after-fallen-pick.png"),
|
||||
Step("ch18av19", "wait", wait: 0.55f),
|
||||
// Character list browse (pinned archive) - Living tab.
|
||||
Step("ch18q", "lore_tab", value: "living"),
|
||||
Step("ch18q0", "lore_back"),
|
||||
Step("ch18q2", "assert", expect: "lore_tab", value: "characters"),
|
||||
Step("ch18q2", "assert", expect: "lore_tab", value: "living"),
|
||||
Step("ch18q3", "assert", expect: "lore_recent", value: "1", label: "min"),
|
||||
Step("ch18q4", "lore_pick", value: "auto"),
|
||||
Step("ch18q5", "wait", wait: 0.25f),
|
||||
Step("ch18q6", "assert", expect: "idle", value: "false"),
|
||||
Step("ch18q7", "assert", expect: "lore_detail", value: "true"),
|
||||
Step("ch18q7b", "assert", expect: "lore_follow", value: "false"),
|
||||
Step("ch18q7c", "assert", expect: "dossier_unit_matches_lore"),
|
||||
Step("ch18q7d", "assert", expect: "dossier_matches_focus"),
|
||||
Step("ch18q7e", "assert", expect: "dossier_avatar_live"),
|
||||
Step("ch18q7f", "assert", expect: "lore_tab", value: "living"),
|
||||
Step("ch18q8", "screenshot", value: "hud-lore-characters.png"),
|
||||
Step("ch18q9", "wait", wait: 0.55f),
|
||||
// Resume idle: pinned list pick flips back to Follow mode.
|
||||
|
|
@ -548,6 +620,14 @@ internal static class HarnessScenarios
|
|||
Step("ch18q11d", "focus", asset: "auto"),
|
||||
Step("ch18q11e", "wait", wait: 0.35f),
|
||||
Step("ch18q11f", "assert", expect: "lore_follows_focus"),
|
||||
// Fallen tab: newest death first (Slainbone after Oldbone).
|
||||
Step("ch18ft1", "lore_back"),
|
||||
Step("ch18ft2", "lore_tab", value: "fallen"),
|
||||
Step("ch18ft3", "wait", wait: 0.2f),
|
||||
Step("ch18ft4", "assert", expect: "lore_tab", value: "fallen"),
|
||||
Step("ch18ft5", "assert", expect: "fallen_list_top", value: "Slainbone"),
|
||||
Step("ch18ft6", "screenshot", value: "hud-lore-fallen-tab.png"),
|
||||
Step("ch18ft7", "wait", wait: 0.55f),
|
||||
Step("ch18q12", "lore_tab", value: "world"),
|
||||
Step("ch19", "assert", expect: "focus", value: "true"),
|
||||
Step("ch20", "assert", expect: "health"),
|
||||
|
|
|
|||
|
|
@ -76,6 +76,29 @@ public static class HudIcons
|
|||
return FromAsset(actor.asset);
|
||||
}
|
||||
|
||||
public static Sprite FromSpeciesId(string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId))
|
||||
{
|
||||
return FromUiIcon("iconQuestionMark");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ActorAsset asset = AssetManager.actor_library?.get(speciesId);
|
||||
if (asset != null)
|
||||
{
|
||||
return FromAsset(asset);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through
|
||||
}
|
||||
|
||||
return FromUiIcon("iconQuestionMark");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live unit sprite as shown in inspect UI (phenotype/kingdom tint), not the species icon.
|
||||
/// </summary>
|
||||
|
|
@ -138,6 +161,31 @@ public static class HudIcons
|
|||
}
|
||||
}
|
||||
|
||||
public static Sprite ForDeathManner(DeathManner manner)
|
||||
{
|
||||
switch (manner)
|
||||
{
|
||||
case DeathManner.Slain:
|
||||
return FromUiIcon("iconKills") ?? FromUiIcon("iconAttack") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.OldAge:
|
||||
return FromUiIcon("iconAge") ?? FromUiIcon("iconClock") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.Starvation:
|
||||
return FromUiIcon("iconHunger") ?? FromUiIcon("iconFood") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.Drowned:
|
||||
return FromUiIcon("iconRain") ?? FromUiIcon("iconWater") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.Burned:
|
||||
return FromUiIcon("iconFire") ?? FromUiIcon("iconLava") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.Plague:
|
||||
return FromUiIcon("iconPlague") ?? FromUiIcon("iconDisease") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.Divine:
|
||||
return FromUiIcon("iconDivineLight") ?? FromUiIcon("iconGodFinger") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
case DeathManner.Accident:
|
||||
return FromUiIcon("iconBomb") ?? FromUiIcon("iconExplosion") ?? ForChronicleKind(ChronicleKind.Death);
|
||||
default:
|
||||
return ForChronicleKind(ChronicleKind.Death);
|
||||
}
|
||||
}
|
||||
|
||||
public static Sprite Favorite() =>
|
||||
FromUiIcon("iconFavorite") ?? FromUiIcon("iconFavoriteStar") ?? FromUiIcon("iconStar");
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,31 @@ public sealed class UnitDossier
|
|||
public ActorTrait Trait;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal dossier for archive subjects with no living unit (harness orphans / legacy deaths).
|
||||
/// </summary>
|
||||
public static UnitDossier FromFallenArchive(
|
||||
long unitId,
|
||||
string name,
|
||||
string speciesId,
|
||||
DeathManner manner = DeathManner.None)
|
||||
{
|
||||
UnitDossier d = new UnitDossier();
|
||||
d.UnitId = unitId;
|
||||
d.Name = string.IsNullOrEmpty(name) ? "Nameless" : name;
|
||||
d.SpeciesId = string.IsNullOrEmpty(speciesId) ? "creature" : speciesId;
|
||||
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
|
||||
? d.Name
|
||||
: $"{d.Name} ({d.SpeciesId})";
|
||||
string mannerLabel = Chronicle.DeathMannerLabel(manner);
|
||||
d.ReasonLine = string.IsNullOrEmpty(mannerLabel) || manner == DeathManner.None
|
||||
? "Fallen"
|
||||
: char.ToUpperInvariant(mannerLabel[0]) + mannerLabel.Substring(1);
|
||||
d.DetailLine = "";
|
||||
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
|
||||
return d;
|
||||
}
|
||||
|
||||
public static UnitDossier FromActor(Actor actor, InterestTier? watchTier = null, string watchLabel = null)
|
||||
{
|
||||
UnitDossier d = new UnitDossier();
|
||||
|
|
|
|||
|
|
@ -412,6 +412,33 @@ public static class WatchCaption
|
|||
LogService.LogInfo("[IdleSpectator][CAPTION] " + LastCaptionText.Replace("\n", " | "));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show a stored dossier with no living actor (Fallen archive last living state).
|
||||
/// </summary>
|
||||
public static void SetFromDossier(UnitDossier dossier)
|
||||
{
|
||||
if (dossier == null || dossier.UnitId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_pinnedWhilePaused)
|
||||
{
|
||||
ClearStatusBanner();
|
||||
}
|
||||
|
||||
_current = dossier;
|
||||
_boundActor = null;
|
||||
LastHeadline = dossier.Headline ?? "";
|
||||
LastDetail = dossier.DetailLine ?? "";
|
||||
LastCaptionText = dossier.CaptionText ?? "";
|
||||
EnsureBuilt();
|
||||
ApplyVisual(null, dossier);
|
||||
SetVisible(ModSettings.ShowDossierCaption && (SpectatorMode.Active || _pinnedWhilePaused));
|
||||
LogService.LogInfo(
|
||||
"[IdleSpectator][CAPTION] fallen dossier " + LastCaptionText.Replace("\n", " | "));
|
||||
}
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
// History / manual pause: keep dossier pinned even after the banner timer expires.
|
||||
|
|
@ -604,7 +631,7 @@ public static class WatchCaption
|
|||
|
||||
int shown = FillHistory(id);
|
||||
// Re-run layout only when history presence flips or line count changes.
|
||||
bool hasBody = _boundActor != null;
|
||||
bool hasBody = _current != null;
|
||||
bool hasTask = _taskText != null && _taskText.gameObject.activeSelf;
|
||||
bool hasReason = _reasonText != null && _reasonText.gameObject.activeSelf;
|
||||
int traits = 0;
|
||||
|
|
@ -630,11 +657,25 @@ public static class WatchCaption
|
|||
return;
|
||||
}
|
||||
|
||||
_boundActor = actor;
|
||||
bool hasLive = actor != null
|
||||
&& actor.isAlive()
|
||||
&& dossier != null
|
||||
&& actor.getID() == dossier.UnitId;
|
||||
_boundActor = hasLive ? actor : null;
|
||||
|
||||
if (_speciesIcon != null)
|
||||
{
|
||||
HudIcons.Apply(_speciesIcon, actor != null ? HudIcons.FromActor(actor) : null);
|
||||
Sprite species = null;
|
||||
if (hasLive)
|
||||
{
|
||||
species = HudIcons.FromActor(actor);
|
||||
}
|
||||
else if (dossier != null)
|
||||
{
|
||||
species = HudIcons.FromSpeciesId(dossier.SpeciesId);
|
||||
}
|
||||
|
||||
HudIcons.Apply(_speciesIcon, species);
|
||||
}
|
||||
|
||||
bool hasSex = false;
|
||||
|
|
@ -665,10 +706,11 @@ public static class WatchCaption
|
|||
: "";
|
||||
}
|
||||
|
||||
bool hasBody = dossier != null && actor != null;
|
||||
bool hasBody = dossier != null && dossier.UnitId != 0;
|
||||
DossierAvatar.SetActive(hasBody);
|
||||
|
||||
bool hasLevel = hasBody;
|
||||
// Live units always show level; archive snapshots hide a blank 0.
|
||||
bool hasLevel = hasBody && (hasLive || dossier.Level > 0);
|
||||
if (_levelIcon != null)
|
||||
{
|
||||
HudIcons.Apply(_levelIcon, hasLevel ? HudIcons.Level() : null);
|
||||
|
|
@ -681,10 +723,14 @@ public static class WatchCaption
|
|||
_levelValue.gameObject.SetActive(hasLevel);
|
||||
}
|
||||
|
||||
if (hasBody)
|
||||
if (hasLive)
|
||||
{
|
||||
DossierAvatar.Show(actor);
|
||||
}
|
||||
else if (hasBody)
|
||||
{
|
||||
DossierAvatar.ShowSpecies(dossier.SpeciesId);
|
||||
}
|
||||
else
|
||||
{
|
||||
DossierAvatar.ClearActor();
|
||||
|
|
@ -729,7 +775,13 @@ public static class WatchCaption
|
|||
{
|
||||
UnitDossier.TraitChip chip = dossier.TopTraits[i];
|
||||
slot.Root.SetActive(true);
|
||||
HudIcons.Apply(slot.Icon, HudIcons.FromTrait(chip.Trait));
|
||||
Sprite traitSprite = chip != null ? HudIcons.FromTrait(chip.Trait) : null;
|
||||
if (traitSprite == null && chip != null && !string.IsNullOrEmpty(chip.Id))
|
||||
{
|
||||
traitSprite = HudIcons.FromUiIcon(chip.Id) ?? HudIcons.FromUiIcon("icon" + chip.Id);
|
||||
}
|
||||
|
||||
HudIcons.Apply(slot.Icon, traitSprite);
|
||||
string name = chip.Name ?? "";
|
||||
if (slot.Label != null)
|
||||
{
|
||||
|
|
@ -782,7 +834,7 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
Relayout(hasBody, traitCount, hasTask, hasReason, historyCount);
|
||||
RefreshFavoriteVisual(actor);
|
||||
RefreshFavoriteVisual(hasLive ? actor : null);
|
||||
}
|
||||
|
||||
private static int FillHistory(long unitId)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.12.6",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). L/books follow focus; Characters list is a pinned archive including Fallen.",
|
||||
"version": "0.12.14",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Tabs: World / Living / Fallen (newest first).",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue