Enhance ActivityLog and ActivityInterestTable with detailed target resolution and activity tracking. Implemented new methods for resolving actor targets and formatting activity statistics, including colored display options. Updated HUD interactions to reflect these changes and added test scenarios for improved activity logging and display functionality.
This commit is contained in:
parent
beb08265ec
commit
63bf44cae5
8 changed files with 1208 additions and 230 deletions
|
|
@ -213,13 +213,47 @@ public static class ActivityInterestTable
|
|||
|
||||
public static string TargetLabel(Actor actor)
|
||||
{
|
||||
ResolveTarget(actor, out string name, out _, out string place);
|
||||
return !string.IsNullOrEmpty(name) ? name : place;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefer live actor targets (by name). Places/buildings stay as type labels.
|
||||
/// </summary>
|
||||
public static void ResolveTarget(
|
||||
Actor actor,
|
||||
out string targetName,
|
||||
out bool targetIsActor,
|
||||
out string placeLabel)
|
||||
{
|
||||
targetName = "";
|
||||
targetIsActor = false;
|
||||
placeLabel = "";
|
||||
if (actor == null)
|
||||
{
|
||||
return "";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (actor.has_attack_target && actor.attack_target != null && actor.attack_target.isAlive())
|
||||
{
|
||||
if (actor.attack_target.isActor())
|
||||
{
|
||||
Actor foe = actor.attack_target.a;
|
||||
if (foe != null)
|
||||
{
|
||||
string n = foe.getName();
|
||||
if (!string.IsNullOrEmpty(n))
|
||||
{
|
||||
targetName = n;
|
||||
targetIsActor = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actor.beh_actor_target != null && actor.beh_actor_target.isAlive())
|
||||
{
|
||||
if (actor.beh_actor_target.isActor())
|
||||
|
|
@ -228,11 +262,17 @@ public static class ActivityInterestTable
|
|||
if (other != null)
|
||||
{
|
||||
string n = other.getName();
|
||||
return string.IsNullOrEmpty(n) ? "foe" : n;
|
||||
if (!string.IsNullOrEmpty(n))
|
||||
{
|
||||
targetName = n;
|
||||
targetIsActor = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "target";
|
||||
placeLabel = "their mark";
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor.beh_building_target != null && actor.beh_building_target.isAlive())
|
||||
|
|
@ -242,23 +282,41 @@ public static class ActivityInterestTable
|
|||
string id = b.asset != null ? b.asset.id : "";
|
||||
if (!string.IsNullOrEmpty(type))
|
||||
{
|
||||
return type.Replace("type_", "");
|
||||
placeLabel = type.Replace("type_", "");
|
||||
return;
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(id) ? "building" : id;
|
||||
placeLabel = string.IsNullOrEmpty(id) ? "a building" : id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor.beh_tile_target != null)
|
||||
{
|
||||
return "tile";
|
||||
placeLabel = "the wilds";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
public static string ActorName(Actor actor)
|
||||
{
|
||||
if (actor == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string n = actor.getName();
|
||||
return string.IsNullOrEmpty(n) ? "" : n;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string hay, params string[] needles)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ public sealed class ActivityEntry
|
|||
public string TargetLabel = "";
|
||||
/// <summary>Stable fact line for asserts.</summary>
|
||||
public string Line = "";
|
||||
/// <summary>Varied display prose for HUD.</summary>
|
||||
/// <summary>Varied display prose for HUD (plain text).</summary>
|
||||
public string DisplayLine = "";
|
||||
/// <summary>Display prose with colored names (Unity rich text).</summary>
|
||||
public string DisplayLineRich = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -38,7 +40,17 @@ public static class ActivityLog
|
|||
private static readonly Dictionary<long, float> LastBeatAt = new Dictionary<long, float>();
|
||||
private static readonly Dictionary<long, int> LineIndex = new Dictionary<long, int>();
|
||||
|
||||
private static int _notesSinceClear;
|
||||
private static int _tasksSinceClear;
|
||||
private static int _beatsSinceClear;
|
||||
private static float _clearedAt = -1f;
|
||||
private static readonly Dictionary<string, int> TaskHitsSinceClear =
|
||||
new Dictionary<string, int>();
|
||||
|
||||
public static int SubjectCount => BySubject.Count;
|
||||
public static int NotesSinceClear => _notesSinceClear;
|
||||
public static int TasksSinceClear => _tasksSinceClear;
|
||||
public static int BeatsSinceClear => _beatsSinceClear;
|
||||
|
||||
public static void ClearSession()
|
||||
{
|
||||
|
|
@ -46,6 +58,52 @@ public static class ActivityLog
|
|||
LastTaskId.Clear();
|
||||
LastBeatAt.Clear();
|
||||
LineIndex.Clear();
|
||||
ResetSampleCounters();
|
||||
}
|
||||
|
||||
/// <summary>Harness: reset rate counters without wiping the ring.</summary>
|
||||
public static void ResetSampleCounters()
|
||||
{
|
||||
_notesSinceClear = 0;
|
||||
_tasksSinceClear = 0;
|
||||
_beatsSinceClear = 0;
|
||||
_clearedAt = Time.unscaledTime;
|
||||
TaskHitsSinceClear.Clear();
|
||||
}
|
||||
|
||||
public static string FormatSampleStats()
|
||||
{
|
||||
float elapsed = _clearedAt < 0f ? 0f : Mathf.Max(0.01f, Time.unscaledTime - _clearedAt);
|
||||
float perSec = _notesSinceClear / elapsed;
|
||||
float perMin = perSec * 60f;
|
||||
int lines = 0;
|
||||
foreach (var kv in BySubject)
|
||||
{
|
||||
lines += kv.Value != null ? kv.Value.Count : 0;
|
||||
}
|
||||
|
||||
string top = "";
|
||||
if (TaskHitsSinceClear.Count > 0)
|
||||
{
|
||||
var ranked = new List<KeyValuePair<string, int>>(TaskHitsSinceClear);
|
||||
ranked.Sort((a, b) => b.Value.CompareTo(a.Value));
|
||||
int n = Mathf.Min(8, ranked.Count);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
top += ", ";
|
||||
}
|
||||
|
||||
top += ranked[i].Key + "=" + ranked[i].Value;
|
||||
}
|
||||
}
|
||||
|
||||
float perSubjectPerMin = BySubject.Count > 0 ? perMin / BySubject.Count : 0f;
|
||||
return
|
||||
$"elapsed={elapsed:0.0}s notes={_notesSinceClear} tasks={_tasksSinceClear} beats={_beatsSinceClear} "
|
||||
+ $"per_sec={perSec:0.00} per_min={perMin:0.0} per_subject_per_min={perSubjectPerMin:0.00} "
|
||||
+ $"subjects={BySubject.Count} ring_lines={lines} top=[{top}]";
|
||||
}
|
||||
|
||||
public static int CountFor(long subjectId)
|
||||
|
|
@ -110,7 +168,9 @@ public static class ActivityLog
|
|||
|
||||
LastTaskId[id] = taskId;
|
||||
string jobId = ActivityInterestTable.SafeJobId(actor);
|
||||
string target = ActivityInterestTable.TargetLabel(actor);
|
||||
ActivityInterestTable.ResolveTarget(
|
||||
actor, out string targetName, out bool targetIsActor, out string place);
|
||||
string actorName = ActivityInterestTable.ActorName(actor);
|
||||
string loc = "";
|
||||
try
|
||||
{
|
||||
|
|
@ -124,15 +184,27 @@ public static class ActivityLog
|
|||
// ignore
|
||||
}
|
||||
|
||||
string targetLabel = !string.IsNullOrEmpty(targetName) ? targetName : place;
|
||||
string raw = string.IsNullOrEmpty(loc) ? ("Task: " + taskId) : loc;
|
||||
if (!string.IsNullOrEmpty(target))
|
||||
if (!string.IsNullOrEmpty(targetLabel))
|
||||
{
|
||||
raw = raw + " → " + target;
|
||||
raw = raw + " → " + targetLabel;
|
||||
}
|
||||
|
||||
int idx = NextIndex(id);
|
||||
string display = ActivityProse.Format(
|
||||
ActivityKind.TaskStart, taskId, raw, target, id, idx);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.TaskStart,
|
||||
taskId,
|
||||
actorName,
|
||||
targetName,
|
||||
targetIsActor,
|
||||
place,
|
||||
raw,
|
||||
id,
|
||||
idx,
|
||||
out string display,
|
||||
out string displayRich);
|
||||
RecordSample(taskId, isBeat: false);
|
||||
Append(id, new ActivityEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
|
|
@ -140,12 +212,32 @@ public static class ActivityLog
|
|||
Kind = ActivityKind.TaskStart,
|
||||
TaskId = taskId,
|
||||
JobId = jobId,
|
||||
TargetLabel = target,
|
||||
TargetLabel = targetLabel,
|
||||
Line = raw,
|
||||
DisplayLine = display
|
||||
DisplayLine = display,
|
||||
DisplayLineRich = displayRich
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture safety net: if the live task id differs from the last logged id, note it.
|
||||
/// </summary>
|
||||
public static void EnsureCurrentTask(Actor actor)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string taskId = ActivityInterestTable.SafeTaskId(actor);
|
||||
if (string.IsNullOrEmpty(taskId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NoteTask(actor, taskId);
|
||||
}
|
||||
|
||||
public static void NoteActionBeat(Actor actor, string actionKey, string rawFact, string target)
|
||||
{
|
||||
if (actor == null || !actor.isAlive() || string.IsNullOrEmpty(actionKey))
|
||||
|
|
@ -177,10 +269,30 @@ public static class ActivityLog
|
|||
LastBeatAt[id] = now;
|
||||
string taskId = ActivityInterestTable.SafeTaskId(actor);
|
||||
string jobId = ActivityInterestTable.SafeJobId(actor);
|
||||
string actorName = ActivityInterestTable.ActorName(actor);
|
||||
ActivityInterestTable.ResolveTarget(
|
||||
actor, out string resolvedName, out bool targetIsActor, out string place);
|
||||
if (string.IsNullOrEmpty(resolvedName) && !string.IsNullOrEmpty(target))
|
||||
{
|
||||
resolvedName = target;
|
||||
targetIsActor = true;
|
||||
}
|
||||
|
||||
string fact = string.IsNullOrEmpty(rawFact) ? actionKey : rawFact;
|
||||
int idx = NextIndex(id);
|
||||
string display = ActivityProse.Format(
|
||||
ActivityKind.ActionBeat, actionKey, fact, target ?? "", id, idx);
|
||||
ActivityProse.Format(
|
||||
ActivityKind.ActionBeat,
|
||||
actionKey,
|
||||
actorName,
|
||||
resolvedName,
|
||||
targetIsActor,
|
||||
place,
|
||||
fact,
|
||||
id,
|
||||
idx,
|
||||
out string display,
|
||||
out string displayRich);
|
||||
RecordSample(actionKey, isBeat: true);
|
||||
Append(id, new ActivityEntry
|
||||
{
|
||||
CreatedAt = now,
|
||||
|
|
@ -188,14 +300,21 @@ public static class ActivityLog
|
|||
Kind = ActivityKind.ActionBeat,
|
||||
TaskId = string.IsNullOrEmpty(taskId) ? actionKey : taskId,
|
||||
JobId = jobId,
|
||||
TargetLabel = target ?? "",
|
||||
TargetLabel = !string.IsNullOrEmpty(resolvedName) ? resolvedName : (place ?? ""),
|
||||
Line = fact,
|
||||
DisplayLine = display
|
||||
DisplayLine = display,
|
||||
DisplayLineRich = displayRich
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Harness: inject activity without a live AI transition.</summary>
|
||||
public static bool ForceNote(long subjectId, string taskOrAction, string rawLine, bool asBeat = false)
|
||||
public static bool ForceNote(
|
||||
long subjectId,
|
||||
string taskOrAction,
|
||||
string rawLine,
|
||||
bool asBeat = false,
|
||||
string actorName = "",
|
||||
string targetName = "")
|
||||
{
|
||||
if (subjectId == 0 || string.IsNullOrEmpty(taskOrAction))
|
||||
{
|
||||
|
|
@ -205,19 +324,54 @@ public static class ActivityLog
|
|||
int idx = NextIndex(subjectId);
|
||||
string fact = string.IsNullOrEmpty(rawLine) ? taskOrAction : rawLine;
|
||||
ActivityKind kind = asBeat ? ActivityKind.ActionBeat : ActivityKind.TaskStart;
|
||||
string display = ActivityProse.Format(kind, taskOrAction, fact, "", subjectId, idx);
|
||||
ActivityProse.Format(
|
||||
kind,
|
||||
taskOrAction,
|
||||
actorName,
|
||||
targetName,
|
||||
!string.IsNullOrEmpty(targetName),
|
||||
"",
|
||||
fact,
|
||||
subjectId,
|
||||
idx,
|
||||
out string display,
|
||||
out string displayRich);
|
||||
Append(subjectId, new ActivityEntry
|
||||
{
|
||||
CreatedAt = Time.unscaledTime,
|
||||
SubjectId = subjectId,
|
||||
Kind = kind,
|
||||
TaskId = taskOrAction,
|
||||
TargetLabel = targetName ?? "",
|
||||
Line = fact,
|
||||
DisplayLine = display
|
||||
DisplayLine = display,
|
||||
DisplayLineRich = displayRich
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void RecordSample(string key, bool isBeat)
|
||||
{
|
||||
if (_clearedAt < 0f)
|
||||
{
|
||||
_clearedAt = Time.unscaledTime;
|
||||
}
|
||||
|
||||
_notesSinceClear++;
|
||||
if (isBeat)
|
||||
{
|
||||
_beatsSinceClear++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_tasksSinceClear++;
|
||||
}
|
||||
|
||||
string k = string.IsNullOrEmpty(key) ? "?" : key;
|
||||
TaskHitsSinceClear.TryGetValue(k, out int n);
|
||||
TaskHitsSinceClear[k] = n + 1;
|
||||
}
|
||||
|
||||
private static int NextIndex(long id)
|
||||
{
|
||||
LineIndex.TryGetValue(id, out int idx);
|
||||
|
|
|
|||
|
|
@ -3,196 +3,347 @@ using System.Collections.Generic;
|
|||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Fact-preserving multi-variant activity lines (anti-stale dossier feed).
|
||||
/// Fact-preserving activity sentences with colored actor/target names.
|
||||
/// </summary>
|
||||
public static class ActivityProse
|
||||
{
|
||||
public const string NameColorHex = "#F0C14A";
|
||||
|
||||
/// <summary>
|
||||
/// Templates use {actor} and optional {target}. Never invent outcomes.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string[]> Variants = new Dictionary<string, string[]>
|
||||
{
|
||||
["fighting"] = new[]
|
||||
["attack"] = new[]
|
||||
{
|
||||
"Locked in combat",
|
||||
"Trading blows",
|
||||
"In the fray",
|
||||
"Clash of steel"
|
||||
"{actor} strikes at {target}",
|
||||
"{actor} presses the attack on {target}",
|
||||
"{actor} lunges toward {target}"
|
||||
},
|
||||
["fight"] = new[]
|
||||
{
|
||||
"Locked in combat",
|
||||
"Trading blows",
|
||||
"In the fray"
|
||||
"{actor} fights {target}",
|
||||
"{actor} clashes with {target}",
|
||||
"{actor} trades blows with {target}"
|
||||
},
|
||||
["attack"] = new[]
|
||||
["fighting"] = new[]
|
||||
{
|
||||
"Strikes at {target}",
|
||||
"Presses the attack on {target}",
|
||||
"Lunges toward {target}"
|
||||
"{actor} fights {target}",
|
||||
"{actor} is locked in combat with {target}",
|
||||
"{actor} clashes with {target}"
|
||||
},
|
||||
["hunt"] = new[]
|
||||
{
|
||||
"Hunts {target}",
|
||||
"Stalks {target}",
|
||||
"On the trail of {target}"
|
||||
},
|
||||
["eating"] = new[]
|
||||
{
|
||||
"Taking a meal",
|
||||
"Eating",
|
||||
"Breaking hunger"
|
||||
},
|
||||
["eat"] = new[]
|
||||
{
|
||||
"Taking a meal",
|
||||
"Eating",
|
||||
"Breaking hunger"
|
||||
},
|
||||
["sleep"] = new[]
|
||||
{
|
||||
"Sleeping",
|
||||
"Resting",
|
||||
"Deep in slumber"
|
||||
},
|
||||
["random_move"] = new[]
|
||||
{
|
||||
"Wandering",
|
||||
"Roaming aimlessly",
|
||||
"Drifting along"
|
||||
},
|
||||
["pollinate"] = new[]
|
||||
{
|
||||
"Dusts the blossom",
|
||||
"Works the flower",
|
||||
"Leaves pollen behind",
|
||||
"Humming over petals"
|
||||
},
|
||||
["farm"] = new[]
|
||||
{
|
||||
"Tends the fields",
|
||||
"Works the soil",
|
||||
"Farming the plot"
|
||||
},
|
||||
["harvest"] = new[]
|
||||
{
|
||||
"Harvests the crop",
|
||||
"Gathers the yield",
|
||||
"Cuts the ripe fields"
|
||||
},
|
||||
["mine"] = new[]
|
||||
{
|
||||
"Delves the mine",
|
||||
"Chipping ore",
|
||||
"Deep in the dig"
|
||||
},
|
||||
["build"] = new[]
|
||||
{
|
||||
"Building",
|
||||
"Raising walls",
|
||||
"At the worksite"
|
||||
},
|
||||
["gather"] = new[]
|
||||
{
|
||||
"Gathering stores",
|
||||
"Foraging supplies",
|
||||
"Collecting goods"
|
||||
},
|
||||
["woodcut"] = new[]
|
||||
{
|
||||
"Felling timber",
|
||||
"Cutting wood",
|
||||
"Axe to the trees"
|
||||
},
|
||||
["fire"] = new[]
|
||||
{
|
||||
"Fighting the blaze",
|
||||
"Dousing flames",
|
||||
"Racing the fire"
|
||||
},
|
||||
["fireman"] = new[]
|
||||
{
|
||||
"Fighting the blaze",
|
||||
"Dousing flames",
|
||||
"Racing the fire"
|
||||
},
|
||||
["social"] = new[]
|
||||
{
|
||||
"Socializing",
|
||||
"In conversation",
|
||||
"Among companions"
|
||||
},
|
||||
["lover"] = new[]
|
||||
{
|
||||
"Seeking a lover",
|
||||
"Drawn to {target}",
|
||||
"Heart on the path"
|
||||
},
|
||||
["unload"] = new[]
|
||||
{
|
||||
"Unloads goods in town",
|
||||
"Delivers the haul",
|
||||
"Empties the pack at home"
|
||||
},
|
||||
["store"] = new[]
|
||||
{
|
||||
"Stores supplies",
|
||||
"Stocking the village",
|
||||
"Puts goods away"
|
||||
},
|
||||
["fish"] = new[]
|
||||
{
|
||||
"Fishing",
|
||||
"Casting for fish",
|
||||
"Working the waters"
|
||||
},
|
||||
["trade"] = new[]
|
||||
{
|
||||
"Trading",
|
||||
"Bartering goods",
|
||||
"On a trade run"
|
||||
},
|
||||
["run_away"] = new[]
|
||||
{
|
||||
"Fleeing",
|
||||
"Running for safety",
|
||||
"In retreat"
|
||||
},
|
||||
["make_decision"] = new[]
|
||||
{
|
||||
"Considering next steps",
|
||||
"Weighing a choice",
|
||||
"Paused in thought"
|
||||
},
|
||||
["wait"] = new[]
|
||||
{
|
||||
"Waiting",
|
||||
"Holding still",
|
||||
"Biding time"
|
||||
},
|
||||
["BehPollinate"] = new[]
|
||||
{
|
||||
"Dusts the blossom",
|
||||
"Works the flower",
|
||||
"Leaves pollen behind"
|
||||
},
|
||||
["BehUnloadResources"] = new[]
|
||||
{
|
||||
"Unloads goods in town",
|
||||
"Delivers the haul",
|
||||
"Empties the pack at home"
|
||||
"{actor} hunts {target}",
|
||||
"{actor} stalks {target}",
|
||||
"{actor} closes on {target}"
|
||||
},
|
||||
["BehAttackActorHuntingTarget"] = new[]
|
||||
{
|
||||
"Hunts {target}",
|
||||
"Strikes prey",
|
||||
"Closes on the quarry"
|
||||
"{actor} hunts {target}",
|
||||
"{actor} strikes at {target}",
|
||||
"{actor} closes on {target}"
|
||||
},
|
||||
["heal"] = new[]
|
||||
{
|
||||
"{actor} tends to {target}",
|
||||
"{actor} heals {target}",
|
||||
"{actor} binds {target}'s wounds"
|
||||
},
|
||||
["check_heal"] = new[]
|
||||
{
|
||||
"{actor} checks on wounds",
|
||||
"{actor} looks over injuries",
|
||||
"{actor} takes stock of pain"
|
||||
},
|
||||
["cure"] = new[]
|
||||
{
|
||||
"{actor} seeks a cure",
|
||||
"{actor} works to recover",
|
||||
"{actor} fights sickness"
|
||||
},
|
||||
["eat"] = new[]
|
||||
{
|
||||
"{actor} takes a meal",
|
||||
"{actor} eats",
|
||||
"{actor} breaks hunger"
|
||||
},
|
||||
["eating"] = new[]
|
||||
{
|
||||
"{actor} takes a meal",
|
||||
"{actor} eats",
|
||||
"{actor} breaks hunger"
|
||||
},
|
||||
["sleep"] = new[]
|
||||
{
|
||||
"{actor} sleeps",
|
||||
"{actor} rests",
|
||||
"{actor} settles in to sleep"
|
||||
},
|
||||
["decide_where_to_sleep"] = new[]
|
||||
{
|
||||
"{actor} looks for a place to sleep",
|
||||
"{actor} decides where to rest",
|
||||
"{actor} seeks shelter for the night"
|
||||
},
|
||||
["random_move"] = new[]
|
||||
{
|
||||
"{actor} wanders",
|
||||
"{actor} roams nearby",
|
||||
"{actor} drifts along"
|
||||
},
|
||||
["city_idle_walking"] = new[]
|
||||
{
|
||||
"{actor} walks the streets",
|
||||
"{actor} strolls through town",
|
||||
"{actor} paces the settlement"
|
||||
},
|
||||
["move"] = new[]
|
||||
{
|
||||
"{actor} is on the move",
|
||||
"{actor} travels onward",
|
||||
"{actor} heads out"
|
||||
},
|
||||
["pollinate"] = new[]
|
||||
{
|
||||
"{actor} dusts the blossom",
|
||||
"{actor} works the flower",
|
||||
"{actor} leaves pollen behind"
|
||||
},
|
||||
["BehPollinate"] = new[]
|
||||
{
|
||||
"{actor} dusts the blossom",
|
||||
"{actor} works the flower",
|
||||
"{actor} leaves pollen behind"
|
||||
},
|
||||
["farm"] = new[]
|
||||
{
|
||||
"{actor} tends the fields",
|
||||
"{actor} works the soil",
|
||||
"{actor} farms the plot"
|
||||
},
|
||||
["harvest"] = new[]
|
||||
{
|
||||
"{actor} harvests the crop",
|
||||
"{actor} gathers the yield",
|
||||
"{actor} cuts the ripe fields"
|
||||
},
|
||||
["forag"] = new[]
|
||||
{
|
||||
"{actor} forages",
|
||||
"{actor} gathers wild food",
|
||||
"{actor} searches for forage"
|
||||
},
|
||||
["gather"] = new[]
|
||||
{
|
||||
"{actor} gathers stores",
|
||||
"{actor} collects goods",
|
||||
"{actor} forages supplies"
|
||||
},
|
||||
["mine"] = new[]
|
||||
{
|
||||
"{actor} delves the mine",
|
||||
"{actor} chips at ore",
|
||||
"{actor} works the dig"
|
||||
},
|
||||
["build"] = new[]
|
||||
{
|
||||
"{actor} builds",
|
||||
"{actor} raises walls",
|
||||
"{actor} works the construction"
|
||||
},
|
||||
["woodcut"] = new[]
|
||||
{
|
||||
"{actor} fells timber",
|
||||
"{actor} cuts wood",
|
||||
"{actor} takes an axe to the trees"
|
||||
},
|
||||
["fire"] = new[]
|
||||
{
|
||||
"{actor} fights the blaze",
|
||||
"{actor} douses flames",
|
||||
"{actor} races the fire"
|
||||
},
|
||||
["BehCityActorRemoveFire"] = new[]
|
||||
{
|
||||
"Dousing flames",
|
||||
"Fighting the blaze",
|
||||
"Beats back the fire"
|
||||
"{actor} fights the blaze",
|
||||
"{actor} douses flames",
|
||||
"{actor} beats back the fire"
|
||||
},
|
||||
["social"] = new[]
|
||||
{
|
||||
"{actor} talks with {target}",
|
||||
"{actor} socializes with {target}",
|
||||
"{actor} converses with {target}"
|
||||
},
|
||||
["socialize"] = new[]
|
||||
{
|
||||
"{actor} talks with {target}",
|
||||
"{actor} socializes with {target}",
|
||||
"{actor} joins company near {target}"
|
||||
},
|
||||
["lover"] = new[]
|
||||
{
|
||||
"{actor} seeks out {target}",
|
||||
"{actor} is drawn to {target}",
|
||||
"{actor} pursues {target}'s affection"
|
||||
},
|
||||
["breed"] = new[]
|
||||
{
|
||||
"{actor} tries to start a family with {target}",
|
||||
"{actor} courts {target}",
|
||||
"{actor} seeks offspring with {target}"
|
||||
},
|
||||
["sexual_reproduction"] = new[]
|
||||
{
|
||||
"{actor} tries to start a family with {target}",
|
||||
"{actor} courts {target}",
|
||||
"{actor} seeks offspring with {target}"
|
||||
},
|
||||
["unload"] = new[]
|
||||
{
|
||||
"{actor} unloads goods in town",
|
||||
"{actor} delivers the haul",
|
||||
"{actor} empties the pack at home"
|
||||
},
|
||||
["BehUnloadResources"] = new[]
|
||||
{
|
||||
"{actor} unloads goods in town",
|
||||
"{actor} delivers the haul",
|
||||
"{actor} empties the pack at home"
|
||||
},
|
||||
["job"] = new[]
|
||||
{
|
||||
"{actor} seeks work",
|
||||
"{actor} looks for a job",
|
||||
"{actor} searches for employment"
|
||||
},
|
||||
["find_city_job"] = new[]
|
||||
{
|
||||
"{actor} seeks work in town",
|
||||
"{actor} looks for a city job",
|
||||
"{actor} searches for employment"
|
||||
},
|
||||
["find_house"] = new[]
|
||||
{
|
||||
"{actor} looks for a house",
|
||||
"{actor} seeks a home",
|
||||
"{actor} searches for shelter"
|
||||
},
|
||||
["claim"] = new[]
|
||||
{
|
||||
"{actor} claims land",
|
||||
"{actor} stakes a claim",
|
||||
"{actor} takes claim of ground"
|
||||
},
|
||||
["warrior"] = new[]
|
||||
{
|
||||
"{actor} follows the army",
|
||||
"{actor} marches with the warband",
|
||||
"{actor} keeps formation"
|
||||
},
|
||||
["army"] = new[]
|
||||
{
|
||||
"{actor} follows the army",
|
||||
"{actor} marches with the warband",
|
||||
"{actor} keeps formation"
|
||||
},
|
||||
["wait"] = new[]
|
||||
{
|
||||
"{actor} waits",
|
||||
"{actor} holds still",
|
||||
"{actor} bides time"
|
||||
},
|
||||
["make_decision"] = new[]
|
||||
{
|
||||
"{actor} considers next steps",
|
||||
"{actor} weighs a choice",
|
||||
"{actor} pauses in thought"
|
||||
},
|
||||
["reflection"] = new[]
|
||||
{
|
||||
"{actor} reflects",
|
||||
"{actor} takes a quiet moment",
|
||||
"{actor} thinks things over"
|
||||
},
|
||||
["run_away"] = new[]
|
||||
{
|
||||
"{actor} flees",
|
||||
"{actor} runs for safety",
|
||||
"{actor} retreats"
|
||||
},
|
||||
["trade"] = new[]
|
||||
{
|
||||
"{actor} trades",
|
||||
"{actor} barters goods",
|
||||
"{actor} is on a trade run"
|
||||
},
|
||||
["fish"] = new[]
|
||||
{
|
||||
"{actor} fishes",
|
||||
"{actor} casts for fish",
|
||||
"{actor} works the waters"
|
||||
},
|
||||
["family_group"] = new[]
|
||||
{
|
||||
"{actor} joins the herd",
|
||||
"{actor} seeks the family group",
|
||||
"{actor} gathers with kin"
|
||||
},
|
||||
["generate_loot"] = new[]
|
||||
{
|
||||
"{actor} gathers loot",
|
||||
"{actor} claims spoils",
|
||||
"{actor} picks through loot"
|
||||
}
|
||||
};
|
||||
|
||||
public static string ColorName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return "<color=" + NameColorHex + ">" + name + "</color>";
|
||||
}
|
||||
|
||||
public static void Format(
|
||||
ActivityKind kind,
|
||||
string key,
|
||||
string actorName,
|
||||
string targetName,
|
||||
bool targetIsActor,
|
||||
string placeOrFallback,
|
||||
string rawFact,
|
||||
long subjectId,
|
||||
int lineIndex,
|
||||
out string plain,
|
||||
out string rich)
|
||||
{
|
||||
plain = "";
|
||||
rich = "";
|
||||
string actor = string.IsNullOrEmpty(actorName) ? "Someone" : actorName;
|
||||
string targetPlain = ResolveTargetPlain(targetName, targetIsActor, placeOrFallback);
|
||||
string template = PickTemplate(key, subjectId, lineIndex, !string.IsNullOrEmpty(targetPlain));
|
||||
|
||||
if (string.IsNullOrEmpty(template))
|
||||
{
|
||||
plain = BuildFallbackPlain(actor, targetPlain, rawFact, key);
|
||||
rich = BuildFallbackRich(actor, targetPlain, targetIsActor, rawFact, key);
|
||||
return;
|
||||
}
|
||||
|
||||
plain = Apply(template, actor, targetPlain);
|
||||
rich = Apply(
|
||||
template,
|
||||
ColorName(actor),
|
||||
targetIsActor && !string.IsNullOrEmpty(targetName)
|
||||
? ColorName(targetName)
|
||||
: targetPlain);
|
||||
}
|
||||
|
||||
/// <summary>Legacy helper used by older call sites / harness.</summary>
|
||||
public static string Format(
|
||||
ActivityKind kind,
|
||||
string key,
|
||||
|
|
@ -201,22 +352,123 @@ public static class ActivityProse
|
|||
long subjectId,
|
||||
int lineIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rawFact) && string.IsNullOrEmpty(key))
|
||||
Format(
|
||||
kind,
|
||||
key,
|
||||
actorName: "",
|
||||
targetName: target,
|
||||
targetIsActor: !string.IsNullOrEmpty(target),
|
||||
placeOrFallback: target,
|
||||
rawFact: rawFact,
|
||||
subjectId: subjectId,
|
||||
lineIndex: lineIndex,
|
||||
out string plain,
|
||||
out _);
|
||||
return plain;
|
||||
}
|
||||
|
||||
private static string ResolveTargetPlain(string targetName, bool targetIsActor, string placeOrFallback)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(targetName))
|
||||
{
|
||||
return targetName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(placeOrFallback))
|
||||
{
|
||||
return placeOrFallback;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static string Apply(string template, string actor, string target)
|
||||
{
|
||||
string t = template.Replace("{actor}", actor ?? "");
|
||||
if (string.IsNullOrEmpty(target))
|
||||
{
|
||||
// Drop dangling " with {target}" style if target missing - templates should
|
||||
// prefer no-target variants via PickTemplate, but scrub leftovers.
|
||||
t = t.Replace(" with {target}", "")
|
||||
.Replace(" on {target}", "")
|
||||
.Replace(" toward {target}", "")
|
||||
.Replace(" at {target}", "")
|
||||
.Replace(" {target}", "")
|
||||
.Replace("{target}", "someone");
|
||||
}
|
||||
else
|
||||
{
|
||||
t = t.Replace("{target}", target);
|
||||
}
|
||||
|
||||
return t.Trim();
|
||||
}
|
||||
|
||||
private static string BuildFallbackPlain(string actor, string target, string rawFact, string key)
|
||||
{
|
||||
string action = !string.IsNullOrEmpty(rawFact) ? rawFact : HumanizeKey(key);
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(target) && action.IndexOf(target, System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return actor + " " + LowerFirst(action) + " → " + target;
|
||||
}
|
||||
|
||||
return actor + " " + LowerFirst(action);
|
||||
}
|
||||
|
||||
private static string BuildFallbackRich(
|
||||
string actor,
|
||||
string target,
|
||||
bool targetIsActor,
|
||||
string rawFact,
|
||||
string key)
|
||||
{
|
||||
string action = !string.IsNullOrEmpty(rawFact) ? rawFact : HumanizeKey(key);
|
||||
string a = ColorName(actor);
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(target) && action.IndexOf(target, System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
string t = targetIsActor ? ColorName(target) : target;
|
||||
return a + " " + LowerFirst(action) + " → " + t;
|
||||
}
|
||||
|
||||
return a + " " + LowerFirst(action);
|
||||
}
|
||||
|
||||
private static string LowerFirst(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
if (s.Length == 1)
|
||||
{
|
||||
return s.ToLowerInvariant();
|
||||
}
|
||||
|
||||
return char.ToLowerInvariant(s[0]) + s.Substring(1);
|
||||
}
|
||||
|
||||
private static string HumanizeKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string template = PickTemplate(key, subjectId, lineIndex);
|
||||
if (string.IsNullOrEmpty(template))
|
||||
{
|
||||
return rawFact ?? key ?? "";
|
||||
}
|
||||
|
||||
string t = string.IsNullOrEmpty(target) ? "their mark" : target;
|
||||
return template.Replace("{target}", t);
|
||||
return key.Replace('_', ' ').Replace("Beh", "").Trim();
|
||||
}
|
||||
|
||||
private static string PickTemplate(string key, long subjectId, int lineIndex)
|
||||
private static string PickTemplate(string key, long subjectId, int lineIndex, bool hasTarget)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
|
|
@ -226,21 +478,61 @@ public static class ActivityProse
|
|||
string[] bag = null;
|
||||
if (Variants.TryGetValue(key, out bag) && bag != null && bag.Length > 0)
|
||||
{
|
||||
return bag[PickIndex(subjectId, key, lineIndex, bag.Length)];
|
||||
return PreferTargetAware(bag, hasTarget, subjectId, key, lineIndex);
|
||||
}
|
||||
|
||||
string lower = key.ToLowerInvariant();
|
||||
string bestKey = null;
|
||||
string[] bestBag = null;
|
||||
int bestLen = -1;
|
||||
foreach (KeyValuePair<string, string[]> kv in Variants)
|
||||
{
|
||||
if (lower.IndexOf(kv.Key, System.StringComparison.Ordinal) >= 0
|
||||
&& kv.Value != null
|
||||
&& kv.Value.Length > 0)
|
||||
if (kv.Value == null || kv.Value.Length == 0)
|
||||
{
|
||||
return kv.Value[PickIndex(subjectId, kv.Key, lineIndex, kv.Value.Length)];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lower.IndexOf(kv.Key, System.StringComparison.Ordinal) >= 0 && kv.Key.Length > bestLen)
|
||||
{
|
||||
bestLen = kv.Key.Length;
|
||||
bestKey = kv.Key;
|
||||
bestBag = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
if (bestBag == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return PreferTargetAware(bestBag, hasTarget, subjectId, bestKey, lineIndex);
|
||||
}
|
||||
|
||||
private static string PreferTargetAware(
|
||||
string[] bag,
|
||||
bool hasTarget,
|
||||
long subjectId,
|
||||
string key,
|
||||
int lineIndex)
|
||||
{
|
||||
var suited = new List<string>(bag.Length);
|
||||
for (int i = 0; i < bag.Length; i++)
|
||||
{
|
||||
string line = bag[i];
|
||||
bool needsTarget = line.IndexOf("{target}", System.StringComparison.Ordinal) >= 0;
|
||||
if (hasTarget || !needsTarget)
|
||||
{
|
||||
suited.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (suited.Count == 0)
|
||||
{
|
||||
// Fall back to any template; Apply() will scrub missing targets.
|
||||
suited.AddRange(bag);
|
||||
}
|
||||
|
||||
return suited[PickIndex(subjectId, key, lineIndex, suited.Count)];
|
||||
}
|
||||
|
||||
private static int PickIndex(long subjectId, string key, int lineIndex, int count)
|
||||
|
|
@ -253,7 +545,6 @@ public static class ActivityProse
|
|||
unchecked
|
||||
{
|
||||
int h = (int)(subjectId * 397) ^ (key?.GetHashCode() ?? 0) ^ (lineIndex * 31);
|
||||
// Extra jitter so back-to-back repeats of the same task shift.
|
||||
h ^= (int)(UnityEngine.Time.unscaledTime * 10f);
|
||||
if (h < 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -394,12 +394,26 @@ public static class AgentHarness
|
|||
: (!string.IsNullOrEmpty(cmd.value) ? cmd.value : "farm");
|
||||
string line = !string.IsNullOrEmpty(cmd.label) ? cmd.label : key;
|
||||
bool asBeat = (cmd.tier ?? "").IndexOf("beat", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
string actorName = "";
|
||||
try
|
||||
{
|
||||
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||
{
|
||||
actorName = MoveCamera._focus_unit.getName() ?? "";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
actorName = "";
|
||||
}
|
||||
|
||||
string targetName = cmd.expect ?? "";
|
||||
int n = Math.Max(1, cmd.count);
|
||||
int okN = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
string one = n > 1 ? $"{line} #{i + 1}" : line;
|
||||
if (ActivityLog.ForceNote(id, key, one, asBeat))
|
||||
if (ActivityLog.ForceNote(id, key, one, asBeat, actorName, targetName))
|
||||
{
|
||||
okN++;
|
||||
}
|
||||
|
|
@ -430,6 +444,46 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
|
||||
case "activity_sample_reset":
|
||||
{
|
||||
ActivityLog.ResetSampleCounters();
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: ActivityLog.FormatSampleStats());
|
||||
break;
|
||||
}
|
||||
|
||||
case "activity_stats":
|
||||
{
|
||||
string stats = ActivityLog.FormatSampleStats();
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: stats);
|
||||
break;
|
||||
}
|
||||
|
||||
case "lore_detail_pane":
|
||||
{
|
||||
string raw = (cmd.value ?? cmd.label ?? "life").Trim().ToLowerInvariant();
|
||||
ChronicleHud.DetailPane pane = raw.StartsWith("act")
|
||||
? ChronicleHud.DetailPane.Activity
|
||||
: ChronicleHud.DetailPane.Life;
|
||||
ChronicleHud.SetDetailPane(pane);
|
||||
bool ok = ChronicleHud.DetailUnitId != 0
|
||||
&& ChronicleHud.ActiveDetailPane == pane;
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok, detail:
|
||||
$"pane={ChronicleHud.ActiveDetailPane} detailId={ChronicleHud.DetailUnitId} "
|
||||
+ $"actRows={ChronicleHud.LastDetailActivityRows} lifeRows={ChronicleHud.LastDetailHistoryRows}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "chronicle_world_force":
|
||||
{
|
||||
string label = !string.IsNullOrEmpty(cmd.label)
|
||||
|
|
@ -1803,6 +1857,90 @@ public static class AgentHarness
|
|||
detail = scoreDetail;
|
||||
break;
|
||||
}
|
||||
case "activity_names_colored":
|
||||
{
|
||||
long id = WatchCaption.CurrentUnitId;
|
||||
IReadOnlyList<ActivityEntry> entries = ActivityLog.LatestForSubject(id, 8);
|
||||
bool any = false;
|
||||
bool colored = false;
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
ActivityEntry e = entries[i];
|
||||
if (e == null || string.IsNullOrEmpty(e.DisplayLineRich))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
if (e.DisplayLineRich.IndexOf("<color=", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
colored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pass = any && colored;
|
||||
detail = $"any={any} colored={colored} preview='{WatchCaption.LastHistoryPreview}'";
|
||||
break;
|
||||
}
|
||||
case "activity_log_contains":
|
||||
{
|
||||
long id = WatchCaption.CurrentUnitId;
|
||||
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||
IReadOnlyList<ActivityEntry> entries =
|
||||
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
|
||||
bool hit = false;
|
||||
string sample = "";
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
ActivityEntry e = entries[i];
|
||||
if (e == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string line = e.DisplayLine ?? e.Line ?? "";
|
||||
if (string.IsNullOrEmpty(sample))
|
||||
{
|
||||
sample = line;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(needle)
|
||||
&& line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
hit = true;
|
||||
sample = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pass = hit;
|
||||
detail = $"hit={hit} needle='{needle}' sample='{sample}' count={entries.Count}";
|
||||
break;
|
||||
}
|
||||
case "lore_detail_pane":
|
||||
{
|
||||
string want = (cmd.value ?? "life").Trim().ToLowerInvariant();
|
||||
ChronicleHud.DetailPane have = ChronicleHud.ActiveDetailPane;
|
||||
bool match = want.StartsWith("act")
|
||||
? have == ChronicleHud.DetailPane.Activity
|
||||
: have == ChronicleHud.DetailPane.Life;
|
||||
pass = Chronicle.HudVisible && ChronicleHud.DetailUnitId != 0 && match;
|
||||
detail =
|
||||
$"pane={have} want={want} actRows={ChronicleHud.LastDetailActivityRows} lifeRows={ChronicleHud.LastDetailHistoryRows}";
|
||||
break;
|
||||
}
|
||||
case "lore_activity_shown":
|
||||
{
|
||||
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||
int have = ChronicleHud.LastDetailActivityRows;
|
||||
string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant();
|
||||
pass = Chronicle.HudVisible
|
||||
&& ChronicleHud.ActiveDetailPane == ChronicleHud.DetailPane.Activity
|
||||
&& (cmp == "exact" || cmp == "==" ? have == want : have >= want);
|
||||
detail = $"actRows={have} want={want} pane={ChronicleHud.ActiveDetailPane}";
|
||||
break;
|
||||
}
|
||||
case "activity_band_cold_focus":
|
||||
{
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ public static class ChronicleHud
|
|||
Fallen = 2
|
||||
}
|
||||
|
||||
public enum DetailPane
|
||||
{
|
||||
Life = 0,
|
||||
Activity = 1
|
||||
}
|
||||
|
||||
private const float PanelWidth = 268f;
|
||||
private const float PanelHeight = 188f;
|
||||
private const float ScreenInset = 12f;
|
||||
|
|
@ -33,7 +39,7 @@ public static class ChronicleHud
|
|||
private const float TabH = 15f;
|
||||
private const float CharToolsH = 17f;
|
||||
private const float ScrollSensitivity = 55f;
|
||||
private const string ChromeMarker = "LoreV5";
|
||||
private const string ChromeMarker = "LoreV6";
|
||||
|
||||
private static GameObject _root;
|
||||
private static RectTransform _rootRt;
|
||||
|
|
@ -68,10 +74,20 @@ public static class ChronicleHud
|
|||
private static bool _followFocus;
|
||||
private static GameObject _backBtn;
|
||||
private static Text _backBtnLabel;
|
||||
private static GameObject _detailPaneBar;
|
||||
private static GameObject _detailActivityBtn;
|
||||
private static GameObject _detailLifeBtn;
|
||||
private static DetailPane _detailPane = DetailPane.Life;
|
||||
|
||||
/// <summary>Rows actually built in the last character detail rebuild (harness).</summary>
|
||||
public static int LastDetailHistoryRows { get; private set; }
|
||||
|
||||
/// <summary>Activity rows in the last Activity-pane rebuild (harness).</summary>
|
||||
public static int LastDetailActivityRows { get; private set; }
|
||||
|
||||
/// <summary>Active detail sub-pane when a unit history is open.</summary>
|
||||
public static DetailPane ActiveDetailPane => _detailPane;
|
||||
|
||||
/// <summary>Title of the first row in the last Living/Fallen list rebuild (harness).</summary>
|
||||
public static string LastListTopTitle { get; private set; } = "";
|
||||
|
||||
|
|
@ -128,8 +144,12 @@ public static class ChronicleHud
|
|||
_searchField = null;
|
||||
_backBtn = null;
|
||||
_backBtnLabel = null;
|
||||
_detailPaneBar = null;
|
||||
_detailActivityBtn = null;
|
||||
_detailLifeBtn = null;
|
||||
_detailUnitId = 0;
|
||||
_followFocus = false;
|
||||
_detailPane = DetailPane.Life;
|
||||
}
|
||||
|
||||
if (_root != null)
|
||||
|
|
@ -337,16 +357,46 @@ public static class ChronicleHud
|
|||
_backBtnLabel = _backBtn.transform.Find("Label")?.GetComponent<Text>();
|
||||
RectTransform backRt = _backBtn.GetComponent<RectTransform>();
|
||||
backRt.anchorMin = new Vector2(0f, 0f);
|
||||
backRt.anchorMax = new Vector2(0.42f, 1f);
|
||||
backRt.anchorMax = new Vector2(0.36f, 1f);
|
||||
backRt.offsetMin = Vector2.zero;
|
||||
backRt.offsetMax = Vector2.zero;
|
||||
_backBtn.SetActive(false);
|
||||
|
||||
_detailPaneBar = new GameObject("DetailPanes", typeof(RectTransform));
|
||||
_detailPaneBar.transform.SetParent(_charTools.transform, false);
|
||||
RectTransform paneRt = _detailPaneBar.GetComponent<RectTransform>();
|
||||
paneRt.anchorMin = new Vector2(0.38f, 0f);
|
||||
paneRt.anchorMax = new Vector2(1f, 1f);
|
||||
paneRt.offsetMin = Vector2.zero;
|
||||
paneRt.offsetMax = Vector2.zero;
|
||||
_detailActivityBtn = BuildTabButton(
|
||||
_detailPaneBar.transform, "ActivityPane", "Activity", () => SetDetailPane(DetailPane.Activity));
|
||||
_detailLifeBtn = BuildTabButton(
|
||||
_detailPaneBar.transform, "LifePane", "Life", () => SetDetailPane(DetailPane.Life));
|
||||
PlaceDetailPane(_detailActivityBtn, 0f);
|
||||
PlaceDetailPane(_detailLifeBtn, 1f);
|
||||
_detailPaneBar.SetActive(false);
|
||||
|
||||
_charTools.SetActive(false);
|
||||
RefreshFavFilterVisual();
|
||||
RefreshStaleVisual();
|
||||
}
|
||||
|
||||
private static void PlaceDetailPane(GameObject tab, float index)
|
||||
{
|
||||
if (tab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform rt = tab.GetComponent<RectTransform>();
|
||||
float w = 0.5f;
|
||||
rt.anchorMin = new Vector2(index * w, 0f);
|
||||
rt.anchorMax = new Vector2(index * w + w, 1f);
|
||||
rt.offsetMin = new Vector2(1f, 1f);
|
||||
rt.offsetMax = new Vector2(-1f, -1f);
|
||||
}
|
||||
|
||||
private static Button BuildTextTool(Transform parent, string name, string label, UnityAction onClick)
|
||||
{
|
||||
GameObject go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
|
|
@ -671,6 +721,7 @@ public static class ChronicleHud
|
|||
EnsureBuilt();
|
||||
_detailUnitId = unitId;
|
||||
_followFocus = followFocus;
|
||||
_detailPane = DetailPane.Life;
|
||||
|
||||
if (pauseIdle && SpectatorMode.Active)
|
||||
{
|
||||
|
|
@ -705,6 +756,7 @@ public static class ChronicleHud
|
|||
{
|
||||
_detailUnitId = 0;
|
||||
_followFocus = false;
|
||||
_detailPane = DetailPane.Life;
|
||||
_lastCharFingerprint = int.MinValue;
|
||||
ApplyTabChrome();
|
||||
if (Visible && IsCastTab)
|
||||
|
|
@ -713,6 +765,24 @@ public static class ChronicleHud
|
|||
}
|
||||
}
|
||||
|
||||
public static void SetDetailPane(DetailPane pane)
|
||||
{
|
||||
if (_detailUnitId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_detailPane == pane)
|
||||
{
|
||||
StyleDetailPanes();
|
||||
return;
|
||||
}
|
||||
|
||||
_detailPane = pane;
|
||||
StyleDetailPanes();
|
||||
Rebuild(force: true);
|
||||
}
|
||||
|
||||
public static void SetFavoritesOnly(bool on)
|
||||
{
|
||||
if (_favoritesOnly == on)
|
||||
|
|
@ -871,6 +941,12 @@ public static class ChronicleHud
|
|||
}
|
||||
}
|
||||
|
||||
if (_detailPaneBar != null)
|
||||
{
|
||||
_detailPaneBar.SetActive(detail);
|
||||
StyleDetailPanes();
|
||||
}
|
||||
|
||||
StyleTab(_worldTabBtn, _tab == LoreTab.World);
|
||||
StyleTab(_livingTabBtn, _tab == LoreTab.Living);
|
||||
StyleTab(_fallenTabBtn, _tab == LoreTab.Fallen);
|
||||
|
|
@ -879,6 +955,12 @@ public static class ChronicleHud
|
|||
RefreshTitle();
|
||||
}
|
||||
|
||||
private static void StyleDetailPanes()
|
||||
{
|
||||
StyleTab(_detailActivityBtn, _detailPane == DetailPane.Activity);
|
||||
StyleTab(_detailLifeBtn, _detailPane == DetailPane.Life);
|
||||
}
|
||||
|
||||
private static void RefreshTitle()
|
||||
{
|
||||
if (_titleText == null)
|
||||
|
|
@ -1086,6 +1168,8 @@ public static class ChronicleHud
|
|||
if (_detailUnitId != 0)
|
||||
{
|
||||
hash = (hash * 31) + Chronicle.HistoryCountFor(_detailUnitId);
|
||||
hash = (hash * 31) + ActivityLog.CountFor(_detailUnitId);
|
||||
hash = (hash * 31) + (int)_detailPane;
|
||||
}
|
||||
|
||||
return hash;
|
||||
|
|
@ -1155,7 +1239,15 @@ public static class ChronicleHud
|
|||
LastListTopTitle = "";
|
||||
if (_detailUnitId != 0)
|
||||
{
|
||||
RebuildUnitHistoryDetail(_detailUnitId);
|
||||
if (_detailPane == DetailPane.Activity)
|
||||
{
|
||||
RebuildActivityDetail(_detailUnitId);
|
||||
}
|
||||
else
|
||||
{
|
||||
RebuildUnitHistoryDetail(_detailUnitId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1211,6 +1303,103 @@ public static class ChronicleHud
|
|||
}
|
||||
}
|
||||
|
||||
private static void RebuildActivityDetail(long unitId)
|
||||
{
|
||||
IReadOnlyList<ActivityEntry> entries =
|
||||
ActivityLog.LatestForSubject(unitId, ActivityLog.MaxLinesPerSubject);
|
||||
_lastCharFingerprint = CharacterFingerprint();
|
||||
_builtAtRevision = Chronicle.HistoryRevision;
|
||||
RefreshStaleVisual();
|
||||
LastDetailHistoryRows = 0;
|
||||
if (entries == null || entries.Count == 0)
|
||||
{
|
||||
LastDetailActivityRows = 0;
|
||||
AddEmpty("No activity yet");
|
||||
return;
|
||||
}
|
||||
|
||||
AddSection(entries.Count + " actions");
|
||||
int shown = 0;
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
ActivityEntry entry = entries[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObject row = BuildActivityDetailRow(entry, shown);
|
||||
row.transform.SetParent(_content, false);
|
||||
Rows.Add(row);
|
||||
shown++;
|
||||
}
|
||||
|
||||
LastDetailActivityRows = shown;
|
||||
if (_scroll != null)
|
||||
{
|
||||
_scroll.verticalNormalizedPosition = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject BuildActivityDetailRow(ActivityEntry entry, int index)
|
||||
{
|
||||
GameObject go = new GameObject(
|
||||
$"ActDetail_{index}",
|
||||
typeof(RectTransform),
|
||||
typeof(Image),
|
||||
typeof(LayoutElement),
|
||||
typeof(ContentSizeFitter));
|
||||
RectTransform rt = go.GetComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(PanelWidth - 14f, RowHeightMin);
|
||||
LayoutElement le = go.GetComponent<LayoutElement>();
|
||||
le.minHeight = RowHeightMin;
|
||||
le.preferredHeight = -1f;
|
||||
ContentSizeFitter fit = go.GetComponent<ContentSizeFitter>();
|
||||
fit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
fit.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||
|
||||
Image bg = go.GetComponent<Image>();
|
||||
bg.color = index % 2 == 0
|
||||
? new Color(1f, 1f, 1f, 0.04f)
|
||||
: new Color(1f, 1f, 1f, 0.015f);
|
||||
bg.raycastTarget = false;
|
||||
|
||||
Image kindIcon = HudCanvas.MakeIcon(go.transform, "Kind", KindIconSize);
|
||||
RectTransform kindRt = kindIcon.GetComponent<RectTransform>();
|
||||
kindRt.anchorMin = new Vector2(0f, 1f);
|
||||
kindRt.anchorMax = new Vector2(0f, 1f);
|
||||
kindRt.pivot = new Vector2(0.5f, 1f);
|
||||
kindRt.anchoredPosition = new Vector2(9f, -5f);
|
||||
kindIcon.raycastTarget = false;
|
||||
HudIcons.Apply(
|
||||
kindIcon,
|
||||
HudIcons.FromUiIcon("iconClock")
|
||||
?? HudIcons.FromUiIcon("iconTask")
|
||||
?? HudIcons.ForChronicleKind(ChronicleKind.Other));
|
||||
|
||||
string text = !string.IsNullOrEmpty(entry.DisplayLineRich)
|
||||
? entry.DisplayLineRich
|
||||
: (!string.IsNullOrEmpty(entry.DisplayLine) ? entry.DisplayLine : entry.Line);
|
||||
Text label = HudCanvas.MakeText(go.transform, "Text", text ?? "", 8);
|
||||
RectTransform labelRt = label.GetComponent<RectTransform>();
|
||||
labelRt.anchorMin = new Vector2(0f, 0f);
|
||||
labelRt.anchorMax = new Vector2(1f, 1f);
|
||||
labelRt.offsetMin = new Vector2(18f, 2f);
|
||||
labelRt.offsetMax = new Vector2(-4f, -2f);
|
||||
label.alignment = TextAnchor.UpperLeft;
|
||||
label.color = new Color(0.88f, 0.9f, 0.78f, 1f);
|
||||
label.supportRichText = true;
|
||||
label.resizeTextForBestFit = false;
|
||||
label.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
label.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
label.raycastTarget = false;
|
||||
|
||||
ContentSizeFitter textFit = label.gameObject.AddComponent<ContentSizeFitter>();
|
||||
textFit.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
textFit.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||
return go;
|
||||
}
|
||||
|
||||
private static void RebuildUnitHistoryDetail(long unitId)
|
||||
{
|
||||
IReadOnlyList<ChronicleEntry> entries = Chronicle.LatestForSubject(unitId, Chronicle.MaxHistoryPerUnit);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ internal static class HarnessScenarios
|
|||
case "activity":
|
||||
case "activity_idle_smoke":
|
||||
return ActivityIdleSmoke();
|
||||
case "activity_rate":
|
||||
case "activity_rate_sample":
|
||||
return ActivityRateSample();
|
||||
case "chronicle_subject_bench":
|
||||
case "subject_bench":
|
||||
return ChronicleSubjectBench();
|
||||
|
|
@ -419,6 +422,18 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> ActivityRateSample()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("ar0", "wait_world"),
|
||||
Step("ar1", "activity_sample_reset"),
|
||||
Step("ar2", "wait", wait: 20f),
|
||||
Step("ar3", "activity_stats"),
|
||||
Step("ar4", "snapshot"),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> ActivityIdleSmoke()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
|
|
@ -439,12 +454,14 @@ internal static class HarnessScenarios
|
|||
Step("act13", "assert", expect: "activity_first_scoring"),
|
||||
Step("act14", "activity_force", asset: "eating", label: "Taking a meal", count: 1),
|
||||
Step("act15", "activity_force", asset: "farm", label: "Tends the fields", count: 1),
|
||||
Step("act16", "activity_force", asset: "pollinate", label: "Dusts the blossom", count: 1),
|
||||
Step("act17", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 4, tier: "beat"),
|
||||
Step("act16", "activity_force", asset: "hunt", label: "Hunts", count: 1, expect: "Barkley"),
|
||||
Step("act17", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 3, tier: "beat"),
|
||||
Step("act17b", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1),
|
||||
Step("act18", "wait", wait: 0.25f),
|
||||
Step("act19", "assert", expect: "activity_count", value: "4", label: "min"),
|
||||
Step("act20", "assert", expect: "activity_prose_variety", value: "2"),
|
||||
Step("act20b", "assert", expect: "activity_names_colored"),
|
||||
Step("act20c", "assert", expect: "activity_log_contains", value: "Barkley"),
|
||||
Step("act21", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "activity"),
|
||||
Step("act22", "assert", expect: "dossier_history_contains", value: "Harness pollen"),
|
||||
Step("act23", "assert", expect: "caption_layout_ok"),
|
||||
|
|
@ -455,9 +472,23 @@ internal static class HarnessScenarios
|
|||
Step("act27", "wait", wait: 0.2f),
|
||||
Step("act28", "assert", expect: "dossier_history_contains", value: "serpent"),
|
||||
Step("act29", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "life"),
|
||||
Step("act30", "assert", expect: "caption_layout_ok"),
|
||||
Step("act31", "assert", expect: "health"),
|
||||
Step("act32", "assert", expect: "no_bad"),
|
||||
Step("act29b", "screenshot", value: "hud-activity-life-split.png"),
|
||||
Step("act29c", "wait", wait: 0.45f),
|
||||
// Books: Activity / Life tabs.
|
||||
Step("act40", "lore_open_focus"),
|
||||
Step("act41", "wait", wait: 0.25f),
|
||||
Step("act42", "assert", expect: "lore_detail_pane", value: "life"),
|
||||
Step("act43", "lore_detail_pane", value: "activity"),
|
||||
Step("act44", "wait", wait: 0.2f),
|
||||
Step("act45", "assert", expect: "lore_detail_pane", value: "activity"),
|
||||
Step("act46", "assert", expect: "lore_activity_shown", value: "4", label: "min"),
|
||||
Step("act47", "screenshot", value: "hud-lore-activity-tab.png"),
|
||||
Step("act48", "wait", wait: 0.45f),
|
||||
Step("act49", "lore_detail_pane", value: "life"),
|
||||
Step("act50", "assert", expect: "lore_history_shown", value: "1", label: "min"),
|
||||
Step("act51", "assert", expect: "idle", value: "false"),
|
||||
Step("act52", "assert", expect: "caption_layout_ok"),
|
||||
Step("act53", "assert", expect: "no_bad"),
|
||||
Step("act99", "snapshot"),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,15 @@ public static class WatchCaption
|
|||
private static GameObject _traitsRow;
|
||||
|
||||
private static GameObject _historyCol;
|
||||
private static Image _activityBoxBg;
|
||||
private static RectTransform _lifeSep;
|
||||
private static readonly HistorySlot[] _historySlots = new HistorySlot[HistoryPeekMax];
|
||||
private static readonly float[] _historySlotHeights = new float[HistoryPeekMax];
|
||||
private static readonly bool[] _historySlotIsActivity = new bool[HistoryPeekMax];
|
||||
private static float _historyColW = HistoryColMinW;
|
||||
private static int _lastHistoryCount = -1;
|
||||
private static long _lastHistorySubjectId;
|
||||
private const float LifeSepH = 3f;
|
||||
|
||||
private static Button _favoriteBtn;
|
||||
private static Image _favoriteIcon;
|
||||
|
|
@ -773,6 +777,7 @@ public static class WatchCaption
|
|||
if (hasLive)
|
||||
{
|
||||
DossierAvatar.Show(actor);
|
||||
ActivityLog.EnsureCurrentTask(actor);
|
||||
}
|
||||
else if (hasBody)
|
||||
{
|
||||
|
|
@ -983,7 +988,7 @@ public static class WatchCaption
|
|||
|
||||
_historyCol.SetActive(true);
|
||||
|
||||
var lines = new List<(string text, bool isActivity, ChronicleKind? kind)>(HistoryPeekMax);
|
||||
var lines = new List<(string rich, string plain, bool isActivity, ChronicleKind? kind)>(HistoryPeekMax);
|
||||
if (activity != null)
|
||||
{
|
||||
for (int i = 0; i < activity.Count && lines.Count < HistoryPeekMax; i++)
|
||||
|
|
@ -994,13 +999,14 @@ public static class WatchCaption
|
|||
continue;
|
||||
}
|
||||
|
||||
string t = !string.IsNullOrEmpty(e.DisplayLine) ? e.DisplayLine : e.Line;
|
||||
if (string.IsNullOrEmpty(t))
|
||||
string plain = !string.IsNullOrEmpty(e.DisplayLine) ? e.DisplayLine : e.Line;
|
||||
if (string.IsNullOrEmpty(plain))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.Add((t, true, null));
|
||||
string rich = !string.IsNullOrEmpty(e.DisplayLineRich) ? e.DisplayLineRich : plain;
|
||||
lines.Add((rich, plain, true, null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1014,13 +1020,14 @@ public static class WatchCaption
|
|||
continue;
|
||||
}
|
||||
|
||||
string t = e.DisplayLineRich ?? e.DisplayLine ?? "";
|
||||
if (string.IsNullOrEmpty(t))
|
||||
string plain = e.DisplayLine ?? e.HudLine ?? "";
|
||||
string rich = e.DisplayLineRich ?? plain;
|
||||
if (string.IsNullOrEmpty(plain) && string.IsNullOrEmpty(rich))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.Add((t, false, e.Kind));
|
||||
lines.Add((rich, plain, false, e.Kind));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1039,7 +1046,7 @@ public static class WatchCaption
|
|||
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
label.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
label.alignment = TextAnchor.UpperLeft;
|
||||
label.text = lines[i].text;
|
||||
label.text = lines[i].plain;
|
||||
Canvas.ForceUpdateCanvases();
|
||||
try
|
||||
{
|
||||
|
|
@ -1047,7 +1054,7 @@ public static class WatchCaption
|
|||
}
|
||||
catch
|
||||
{
|
||||
widestLabel = Mathf.Max(widestLabel, lines[i].text.Length * 6.2f);
|
||||
widestLabel = Mathf.Max(widestLabel, lines[i].plain.Length * 6.2f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1061,7 +1068,7 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
private static void ApplyCombinedSlotContents(
|
||||
List<(string text, bool isActivity, ChronicleKind? kind)> lines,
|
||||
List<(string rich, string plain, bool isActivity, ChronicleKind? kind)> lines,
|
||||
float colW)
|
||||
{
|
||||
float labelW = Mathf.Max(24f, colW - HistoryIcon - 2f);
|
||||
|
|
@ -1083,10 +1090,12 @@ public static class WatchCaption
|
|||
{
|
||||
slot.Root.SetActive(false);
|
||||
_historySlotHeights[i] = 0f;
|
||||
_historySlotIsActivity[i] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var row = lines[i];
|
||||
_historySlotIsActivity[i] = row.isActivity;
|
||||
slot.Root.SetActive(true);
|
||||
Sprite icon = row.isActivity
|
||||
? (HudIcons.FromUiIcon("iconClock")
|
||||
|
|
@ -1103,7 +1112,7 @@ public static class WatchCaption
|
|||
slot.Label.color = row.isActivity
|
||||
? new Color(0.88f, 0.9f, 0.78f, 1f)
|
||||
: HistoryTextColor;
|
||||
slot.Label.text = row.text ?? "";
|
||||
slot.Label.text = row.rich ?? row.plain ?? "";
|
||||
RectTransform lrt = slot.Label.GetComponent<RectTransform>();
|
||||
lrt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, labelW);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
|
@ -1114,7 +1123,7 @@ public static class WatchCaption
|
|||
}
|
||||
catch
|
||||
{
|
||||
int chars = (row.text ?? "").Length;
|
||||
int chars = (row.plain ?? "").Length;
|
||||
int perLine = Mathf.Max(12, (int)(labelW / 6.2f));
|
||||
int wrapLines = Mathf.Max(1, (chars + perLine - 1) / perLine);
|
||||
needed = HistoryLineMinH * wrapLines;
|
||||
|
|
@ -1136,7 +1145,7 @@ public static class WatchCaption
|
|||
LastLifeShown++;
|
||||
}
|
||||
|
||||
string plain = (row.text ?? "").Replace("\n", " ");
|
||||
string plain = (row.plain ?? "").Replace("\n", " ");
|
||||
joined.Add(plain);
|
||||
if (string.IsNullOrEmpty(LastHistoryPreview))
|
||||
{
|
||||
|
|
@ -1185,6 +1194,8 @@ public static class WatchCaption
|
|||
|
||||
float total = 0f;
|
||||
int used = 0;
|
||||
bool hasAct = false;
|
||||
bool hasLife = false;
|
||||
for (int i = 0; i < _historySlotHeights.Length && used < historyCount; i++)
|
||||
{
|
||||
if (_historySlotHeights[i] <= 0.01f)
|
||||
|
|
@ -1193,9 +1204,23 @@ public static class WatchCaption
|
|||
}
|
||||
|
||||
total += _historySlotHeights[i];
|
||||
if (_historySlotIsActivity[i])
|
||||
{
|
||||
hasAct = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasLife = true;
|
||||
}
|
||||
|
||||
used++;
|
||||
}
|
||||
|
||||
if (hasAct && hasLife)
|
||||
{
|
||||
total += LifeSepH;
|
||||
}
|
||||
|
||||
return Mathf.Max(BodyH, total);
|
||||
}
|
||||
|
||||
|
|
@ -1521,6 +1546,16 @@ public static class WatchCaption
|
|||
if (historyCount <= 0)
|
||||
{
|
||||
_historyCol.SetActive(false);
|
||||
if (_activityBoxBg != null)
|
||||
{
|
||||
_activityBoxBg.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_lifeSep != null)
|
||||
{
|
||||
_lifeSep.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1533,6 +1568,11 @@ public static class WatchCaption
|
|||
col.sizeDelta = new Vector2(_historyColW, bodyH);
|
||||
|
||||
float top = 0f;
|
||||
float activityTop = -1f;
|
||||
float activityBottom = 0f;
|
||||
bool sawLife = false;
|
||||
bool placedSep = false;
|
||||
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
HistorySlot slot = _historySlots[i];
|
||||
|
|
@ -1541,7 +1581,41 @@ public static class WatchCaption
|
|||
continue;
|
||||
}
|
||||
|
||||
bool isAct = _historySlotIsActivity[i];
|
||||
if (!isAct && !placedSep && activityTop >= 0f)
|
||||
{
|
||||
// Thin rule between Activity box and Life.
|
||||
if (_lifeSep != null)
|
||||
{
|
||||
_lifeSep.gameObject.SetActive(true);
|
||||
_lifeSep.SetParent(_historyCol.transform, false);
|
||||
_lifeSep.anchorMin = new Vector2(0f, 1f);
|
||||
_lifeSep.anchorMax = new Vector2(1f, 1f);
|
||||
_lifeSep.pivot = new Vector2(0.5f, 1f);
|
||||
_lifeSep.offsetMin = new Vector2(2f, -(top + LifeSepH));
|
||||
_lifeSep.offsetMax = new Vector2(-2f, -top);
|
||||
}
|
||||
|
||||
top += LifeSepH;
|
||||
placedSep = true;
|
||||
}
|
||||
|
||||
if (!isAct)
|
||||
{
|
||||
sawLife = true;
|
||||
}
|
||||
|
||||
float lineH = _historySlotHeights[i] > 0.01f ? _historySlotHeights[i] : HistoryLineMinH;
|
||||
if (isAct)
|
||||
{
|
||||
if (activityTop < 0f)
|
||||
{
|
||||
activityTop = top;
|
||||
}
|
||||
|
||||
activityBottom = top + lineH;
|
||||
}
|
||||
|
||||
RectTransform rt = slot.Root.GetComponent<RectTransform>();
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
|
|
@ -1569,6 +1643,31 @@ public static class WatchCaption
|
|||
lrt.offsetMax = new Vector2(0f, -1f);
|
||||
}
|
||||
}
|
||||
|
||||
if (_lifeSep != null && (!sawLife || activityTop < 0f))
|
||||
{
|
||||
_lifeSep.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (_activityBoxBg != null)
|
||||
{
|
||||
if (activityTop >= 0f && activityBottom > activityTop)
|
||||
{
|
||||
_activityBoxBg.gameObject.SetActive(true);
|
||||
RectTransform box = _activityBoxBg.rectTransform;
|
||||
box.SetParent(_historyCol.transform, false);
|
||||
box.SetAsFirstSibling();
|
||||
box.anchorMin = new Vector2(0f, 1f);
|
||||
box.anchorMax = new Vector2(1f, 1f);
|
||||
box.pivot = new Vector2(0.5f, 1f);
|
||||
box.offsetMin = new Vector2(0f, -activityBottom);
|
||||
box.offsetMax = new Vector2(0f, -activityTop);
|
||||
}
|
||||
else
|
||||
{
|
||||
_activityBoxBg.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float PlaceTraits(float yFromTop, int traitCount)
|
||||
|
|
@ -1768,6 +1867,8 @@ public static class WatchCaption
|
|||
_taskText = null;
|
||||
_traitsRow = null;
|
||||
_historyCol = null;
|
||||
_activityBoxBg = null;
|
||||
_lifeSep = null;
|
||||
_favoriteBtn = null;
|
||||
_favoriteIcon = null;
|
||||
_historyBtn = null;
|
||||
|
|
@ -1808,6 +1909,21 @@ public static class WatchCaption
|
|||
_historyCol = new GameObject("HistoryCol", typeof(RectTransform));
|
||||
_historyCol.transform.SetParent(_root.transform, false);
|
||||
|
||||
GameObject boxGo = new GameObject("ActivityBox", typeof(RectTransform), typeof(Image));
|
||||
boxGo.transform.SetParent(_historyCol.transform, false);
|
||||
_activityBoxBg = boxGo.GetComponent<Image>();
|
||||
_activityBoxBg.color = new Color(0.14f, 0.16f, 0.14f, 0.55f);
|
||||
_activityBoxBg.raycastTarget = false;
|
||||
boxGo.SetActive(false);
|
||||
|
||||
GameObject sepGo = new GameObject("LifeSep", typeof(RectTransform), typeof(Image));
|
||||
sepGo.transform.SetParent(_historyCol.transform, false);
|
||||
_lifeSep = sepGo.GetComponent<RectTransform>();
|
||||
Image sepImg = sepGo.GetComponent<Image>();
|
||||
sepImg.color = new Color(0.55f, 0.58f, 0.62f, 0.55f);
|
||||
sepImg.raycastTarget = false;
|
||||
sepGo.SetActive(false);
|
||||
|
||||
for (int i = 0; i < _historySlots.Length; i++)
|
||||
{
|
||||
GameObject slotGo = new GameObject("Hist" + i, typeof(RectTransform));
|
||||
|
|
@ -1817,6 +1933,7 @@ public static class WatchCaption
|
|||
label.color = HistoryTextColor;
|
||||
label.alignment = TextAnchor.MiddleLeft;
|
||||
label.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
label.supportRichText = true;
|
||||
label.resizeTextMinSize = 6;
|
||||
label.resizeTextMaxSize = 8;
|
||||
_historySlots[i] = new HistorySlot { Root = slotGo, Icon = icon, Label = label };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.12.25",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Activity feed + activity-first idle scoring.",
|
||||
"version": "0.12.29",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Detailed activity prose, Activity/Life tabs.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue