feat(saga): escalate hard-arc admit and add soak probes
- Grow climax admit heat with arc age and re-stamp while theater holds - Let prolonged stranger fights challenge Cap so Active chrome can light - Add soak_probe that dumps roster vs focus without wiping saga state - Gate with saga_hard_arc_escalates; bump mod to 0.29.59
This commit is contained in:
parent
3941dc33e5
commit
dafe79dfa2
7 changed files with 350 additions and 20 deletions
|
|
@ -215,6 +215,8 @@ Use `fast_timing` / `director_run` instead.
|
|||
- `assert` expects: `idle`, `health`, `power_bar`, `no_bad`, `tip_matches_unit`, `unit_asset`, `tip_contains`, `enabled`, `show_watch_reasons`, `current_tier`, `would_accept_curiosity`, `focus_same`, `presented`, `pending_discovery`, `pending_interest`, `in_grace`, `dossier_contains`, `caption_contains`, `dossier_traits_ok`, `dossier_statuses_ok`, `citizen_job_labels_ok`, `tooltip_library_ok`, `history_other_rows`, `lore_detail_is_other`, `reason_names_colored`, `fallen_killer_banner`, `caption_layout_ok`, `dossier_history_contains`, `chronicle_count`, `chronicle_latest_contains`, `chronicle_latest_dated`, `focus_arrows`
|
||||
- `dossier_click_history_other`, `dossier_force_named_reason` - dossier row → Lore other; gold names in orange reason
|
||||
- `snapshot`, `reset_counters`, `scenario`
|
||||
- `soak_probe` / `saga_soak_probe` - read-only AFK dump (roster vs focus, Active/Involved, rivals); **preserves** saga roster+memory on batch end
|
||||
- `snapshot` with `value=preserve` (or `soak` / `keep`) also skips the batch-end saga wipe
|
||||
|
||||
## Process safety
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ public static class AgentHarness
|
|||
private static Actor _happinessPartner;
|
||||
private static long _activitySubjectId;
|
||||
private static bool _directorRunActive;
|
||||
/// <summary>
|
||||
/// When set, batch-end skips roster/memory wipe so AFK soak probes stay non-destructive.
|
||||
/// Scenario batches never set this; only <c>soak_probe</c> / preserve snapshots do.
|
||||
/// </summary>
|
||||
private static bool _preserveSagaAfterBatch;
|
||||
private static string _rememberedFocusKey = "";
|
||||
private static string _rememberedTip = "";
|
||||
private static long _rememberedRelatedId;
|
||||
|
|
@ -2972,6 +2977,50 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
|
||||
case "saga_fill_cap":
|
||||
{
|
||||
// Force-admit living units until Cap (skips Prefer partner / current focus when asked).
|
||||
string assetId = string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto"
|
||||
? "human"
|
||||
: cmd.asset;
|
||||
Actor skip = _happinessPartner;
|
||||
int added = 0;
|
||||
int guard = 0;
|
||||
while (LifeSagaRoster.Count < LifeSagaRoster.Cap && guard++ < 40)
|
||||
{
|
||||
Actor unit = FindOtherAliveUnitPreferOffRoster(skip, assetId, preferOffRoster: true);
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!LifeSagaRoster.HarnessForceAdmit(unit))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
added++;
|
||||
skip = unit;
|
||||
}
|
||||
|
||||
bool ok = LifeSagaRoster.Count >= LifeSagaRoster.Cap;
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail:
|
||||
$"filled added={added} roster={LifeSagaRoster.Count} cap={LifeSagaRoster.Cap}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "saga_force_admit_partner":
|
||||
{
|
||||
Actor partner = _happinessPartner;
|
||||
|
|
@ -3133,24 +3182,77 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
|
||||
case "saga_stiffen_roster":
|
||||
{
|
||||
// Keep Cap competitive via Heat (RefreshSlot rewrites Standing from live BaseInterest).
|
||||
float now = Time.unscaledTime;
|
||||
float heat = LifeSagaRoster.HeatCap;
|
||||
if (!string.IsNullOrEmpty(cmd.value)
|
||||
&& float.TryParse(
|
||||
cmd.value.Trim(),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out float parsed)
|
||||
&& parsed > 0f)
|
||||
{
|
||||
heat = parsed;
|
||||
}
|
||||
|
||||
var board = new System.Collections.Generic.List<LifeSagaSlot>(LifeSagaRoster.Cap);
|
||||
LifeSagaRoster.CopySlots(board);
|
||||
int n = 0;
|
||||
for (int i = 0; i < board.Count; i++)
|
||||
{
|
||||
LifeSagaSlot s = board[i];
|
||||
if (s == null || s.UnitId == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Standing is overwritten on Tick; heat is the durable harness lever.
|
||||
LifeSagaRoster.HarnessSetScores(s.UnitId, standing: 1f, heat: heat);
|
||||
s.AdmittedAt = now;
|
||||
s.TouchedAt = now;
|
||||
s.LastChapterAt = now;
|
||||
n++;
|
||||
}
|
||||
|
||||
LifeSagaRoster.Dirty = true;
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"stiffened={n} heat={heat:0.##} roster={LifeSagaRoster.Count}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "saga_consider_hard_focus":
|
||||
{
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
long id = EventFeedUtil.SafeId(focus);
|
||||
bool ok = id != 0
|
||||
&& LifeSagaRoster.ConsiderAdmit(id, heat: 6f, hardArcAdmit: true);
|
||||
if (ok)
|
||||
float heat = 6f;
|
||||
if (!string.IsNullOrEmpty(cmd.value)
|
||||
&& float.TryParse(
|
||||
cmd.value.Trim(),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out float parsed)
|
||||
&& parsed > 0f)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
heat = parsed;
|
||||
}
|
||||
|
||||
Emit(cmd, ok,
|
||||
if (id == 0)
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "no focus");
|
||||
break;
|
||||
}
|
||||
|
||||
bool admitted = LifeSagaRoster.ConsiderAdmit(id, heat: heat, hardArcAdmit: true);
|
||||
_cmdOk++;
|
||||
Emit(
|
||||
cmd,
|
||||
ok: true,
|
||||
detail:
|
||||
$"hardAdmit id={id} on={LifeSagaRoster.IsMc(id)} roster={LifeSagaRoster.Count}");
|
||||
$"hardAdmit id={id} on={admitted} heat={heat:0.##} roster={LifeSagaRoster.Count}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -3847,9 +3949,19 @@ public static class AgentHarness
|
|||
break;
|
||||
|
||||
case "snapshot":
|
||||
if (WantsPreserveSaga(cmd))
|
||||
{
|
||||
_preserveSagaAfterBatch = true;
|
||||
}
|
||||
|
||||
DoSnapshot(cmd);
|
||||
break;
|
||||
|
||||
case "soak_probe":
|
||||
case "saga_soak_probe":
|
||||
DoSoakProbe(cmd);
|
||||
break;
|
||||
|
||||
case "scenario":
|
||||
ExpandScenario(cmd);
|
||||
return;
|
||||
|
|
@ -3881,12 +3993,19 @@ public static class AgentHarness
|
|||
{
|
||||
// Free AFK after harness must not inherit a leftover crisis closer tip.
|
||||
StoryPlanner.PurgeCloserLeftovers();
|
||||
LifeSagaRoster.Clear();
|
||||
LifeSagaMemory.Clear();
|
||||
bool preserve = _preserveSagaAfterBatch;
|
||||
_preserveSagaAfterBatch = false;
|
||||
if (!preserve)
|
||||
{
|
||||
LifeSagaRoster.Clear();
|
||||
LifeSagaMemory.Clear();
|
||||
}
|
||||
|
||||
string status = _assertFail == 0 && _cmdFail == 0 ? "PASS" : "FAIL";
|
||||
WriteLastResult(status, $"ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail}");
|
||||
string wipe = preserve ? "saga_preserved" : "saga_wiped";
|
||||
WriteLastResult(status, $"ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail} {wipe}");
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator][HARNESS] batch done status={status} ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail}");
|
||||
$"[IdleSpectator][HARNESS] batch done status={status} ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail} {wipe}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3903,6 +4022,8 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
_lastBatchId = string.IsNullOrEmpty(cmd.id) ? name : cmd.id;
|
||||
// Scenario batches always wipe at end - never inherit a prior soak preserve flag.
|
||||
_preserveSagaAfterBatch = false;
|
||||
bool keepStats = (cmd.expect ?? "").Trim().Equals("keep", System.StringComparison.OrdinalIgnoreCase);
|
||||
if (!keepStats)
|
||||
{
|
||||
|
|
@ -8555,8 +8676,11 @@ public static class AgentHarness
|
|||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
long id = EventFeedUtil.SafeId(focus);
|
||||
LifeSagaRoster.Tick(Time.unscaledTime);
|
||||
pass = id != 0 && LifeSagaRoster.IsMc(id);
|
||||
detail = $"saga focus={SafeName(focus)} id={id} on={pass} prefer={LifeSagaRoster.IsPrefer(id)}";
|
||||
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||
bool have = id != 0 && LifeSagaRoster.IsMc(id);
|
||||
pass = have == want;
|
||||
detail =
|
||||
$"saga focus={SafeName(focus)} id={id} on={have} want={want} prefer={LifeSagaRoster.IsPrefer(id)}";
|
||||
break;
|
||||
}
|
||||
case "saga_prefer_focus_on":
|
||||
|
|
@ -12329,6 +12453,16 @@ public static class AgentHarness
|
|||
$"[IdleSpectator][ASSERT] {(pass ? "PASS" : "FAIL")} id={cmd.id} expect={expect} {detail}");
|
||||
}
|
||||
|
||||
private static bool WantsPreserveSaga(HarnessCommand cmd)
|
||||
{
|
||||
string v = (cmd?.value ?? cmd?.expect ?? "").Trim().ToLowerInvariant();
|
||||
return v == "preserve"
|
||||
|| v == "soak"
|
||||
|| v == "keep"
|
||||
|| v == "keep_saga"
|
||||
|| v == "preserve_saga";
|
||||
}
|
||||
|
||||
private static void DoSnapshot(HarnessCommand cmd)
|
||||
{
|
||||
string drops = InterestDropLog.FormatRecent(6);
|
||||
|
|
@ -12344,6 +12478,116 @@ public static class AgentHarness
|
|||
LogService.LogInfo("[IdleSpectator][SNAP] " + snap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only AFK dump: roster vs camera, Active/Involved chrome, rival/crossover counts.
|
||||
/// Preserves saga roster+memory when the batch ends.
|
||||
/// </summary>
|
||||
private static void DoSoakProbe(HarnessCommand cmd)
|
||||
{
|
||||
_preserveSagaAfterBatch = true;
|
||||
float now = Time.unscaledTime;
|
||||
LifeSagaRoster.Tick(now);
|
||||
LifeSagaRail.Refresh();
|
||||
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
long focusId = EventFeedUtil.SafeId(focus);
|
||||
InterestCandidate tip = InterestDirector.CurrentCandidate;
|
||||
bool focusMc = focusId != 0 && LifeSagaRoster.IsMc(focusId);
|
||||
bool focusPrefer = focusId != 0 && LifeSagaRoster.IsPrefer(focusId);
|
||||
bool focusCast = focusId != 0 && LifeSagaRoster.IsMcCast(focusId);
|
||||
int softRank = tip != null ? LifeSagaRoster.SoftBiasRank(tip) : 0;
|
||||
int touchedMcs = tip != null ? LifeSagaRoster.CountTouchedMcs(tip) : 0;
|
||||
|
||||
bool rival = false;
|
||||
string rivalNote = "";
|
||||
if (LifeSagaMemory.TryGetEarnedRival(focusId, out LifeSagaFact rivalFact) && rivalFact != null)
|
||||
{
|
||||
rival = true;
|
||||
rivalNote = !string.IsNullOrEmpty(rivalFact.Note)
|
||||
? rivalFact.Note
|
||||
: (rivalFact.Provenance ?? rivalFact.Kind.ToString());
|
||||
}
|
||||
|
||||
int facts = 0;
|
||||
int crossovers = 0;
|
||||
if (focusId != 0)
|
||||
{
|
||||
var list = LifeSagaMemory.FactsFor(focusId);
|
||||
facts = list != null ? list.Count : 0;
|
||||
if (list != null)
|
||||
{
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i] != null && list[i].McCrossover)
|
||||
{
|
||||
crossovers++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var board = new List<LifeSagaSlot>(LifeSagaRoster.Cap);
|
||||
LifeSagaRoster.CopySlots(board);
|
||||
var names = new StringBuilder(160);
|
||||
for (int i = 0; i < board.Count; i++)
|
||||
{
|
||||
LifeSagaSlot s = board[i];
|
||||
if (s == null || s.UnitId == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (names.Length > 0)
|
||||
{
|
||||
names.Append(',');
|
||||
}
|
||||
|
||||
string n = string.IsNullOrEmpty(s.DisplayName) ? ("#" + s.UnitId) : s.DisplayName;
|
||||
if (s.UnitId == focusId)
|
||||
{
|
||||
n = "*" + n;
|
||||
}
|
||||
|
||||
if (s.Prefer || s.GameFavorite)
|
||||
{
|
||||
n += "^";
|
||||
}
|
||||
|
||||
names.Append(n);
|
||||
}
|
||||
|
||||
string tipLabel = tip?.Label ?? CameraDirector.LastWatchLabel ?? "";
|
||||
string detail =
|
||||
$"focus={SafeName(focus)} id={focusId} onRoster={focusMc} prefer={focusPrefer} cast={focusCast} "
|
||||
+ $"activeLit={LifeSagaRail.LastActiveHighlight} involvedLit={LifeSagaRail.LastInvolvedHighlightCount} "
|
||||
+ $"softRank={softRank} touchedMcs={touchedMcs} "
|
||||
+ $"rival={rival} crossoverFacts={crossovers} facts={facts} lives={LifeSagaMemory.LifeCount} "
|
||||
+ $"roster={LifeSagaRoster.Count} tip={tipLabel} "
|
||||
+ $"score={InterestDirector.CurrentScoreLabel} names=[{names}]";
|
||||
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: detail);
|
||||
LogService.LogInfo("[IdleSpectator][SOAK] " + detail);
|
||||
|
||||
try
|
||||
{
|
||||
EnsureDirs();
|
||||
string path = Path.Combine(HarnessDir(), "soak-probe.txt");
|
||||
File.WriteAllText(
|
||||
path,
|
||||
detail
|
||||
+ Environment.NewLine
|
||||
+ "rivalNote=" + rivalNote
|
||||
+ Environment.NewLine
|
||||
+ "preserved=true"
|
||||
+ Environment.NewLine);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore IO
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoDiscoveryNote(HarnessCommand cmd)
|
||||
{
|
||||
string assetId = ResolveAssetId(cmd);
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ internal static class HarnessScenarios
|
|||
return SagaRivalRematch();
|
||||
case "saga_prefer_cuts_ambient":
|
||||
return SagaPreferCutsAmbient();
|
||||
case "saga_hard_arc_escalates":
|
||||
return SagaHardArcEscalates();
|
||||
case "saga_overview_has_mc":
|
||||
return SagaOverviewHasMc();
|
||||
case "saga_snapshot_popover":
|
||||
|
|
@ -387,6 +389,7 @@ internal static class HarnessScenarios
|
|||
Nested("reg_saga_rival_kill", "saga_rival_not_single_kill"),
|
||||
Nested("reg_saga_rival_rematch", "saga_rival_rematch"),
|
||||
Nested("reg_saga_ambient_cut", "saga_prefer_cuts_ambient"),
|
||||
Nested("reg_saga_hard_arc_heat", "saga_hard_arc_escalates"),
|
||||
Nested("reg_saga_overview", "saga_overview_has_mc"),
|
||||
Nested("reg_saga_snapshot", "saga_adaptive_dossier"),
|
||||
Nested("reg_saga_memory", "saga_memory_survives"),
|
||||
|
|
@ -3213,6 +3216,40 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prolonged hard-arc heat must beat a full Cap of stiff slots; one-shot heat 1.5 must not.
|
||||
/// Mirrors soak: stranger duel stuck off-rail until admit heat escalates.
|
||||
/// </summary>
|
||||
private static List<HarnessCommand> SagaHardArcEscalates()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("she0", "dismiss_windows"),
|
||||
Step("she1", "wait_world"),
|
||||
Step("she2", "set_setting", expect: "enabled", value: "true"),
|
||||
Step("she3", "fast_timing", value: "true"),
|
||||
Step("she4", "spawn", asset: "human", count: 14),
|
||||
Step("she5", "spectator", value: "off"),
|
||||
Step("she6", "spectator", value: "on"),
|
||||
Step("she7", "interest_saga_clear"),
|
||||
Step("she8", "pick_unit", asset: "human"),
|
||||
Step("she9", "happiness_remember_partner"),
|
||||
Step("she10", "saga_fill_cap", asset: "human"),
|
||||
Step("she50", "assert", expect: "saga_roster_count", value: "10"),
|
||||
Step("she51", "saga_stiffen_roster", value: "8"),
|
||||
Step("she60", "focus_partner"),
|
||||
Step("she62", "assert", expect: "saga_has_focus", value: "false"),
|
||||
Step("she63", "saga_consider_hard_focus", value: "1.5"),
|
||||
Step("she64", "assert", expect: "saga_has_focus", value: "false"),
|
||||
Step("she65", "saga_consider_hard_focus", value: "12"),
|
||||
Step("she66", "assert", expect: "saga_has_focus"),
|
||||
Step("she67", "saga_rail_refresh"),
|
||||
Step("she68", "assert", expect: "saga_rail_active_highlight", value: "true"),
|
||||
Step("she90", "fast_timing", value: "false"),
|
||||
Step("she99", "snapshot"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compact saga snapshot: admission headline, collapsible layout, structured chapters,
|
||||
/// no planner jargon / trait descs / Chronicle dumps.
|
||||
|
|
|
|||
|
|
@ -683,6 +683,26 @@ public static class LifeSagaRoster
|
|||
trimmed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hard-arc admit heat grows with climax age so prolonged theater can challenge a full Cap.
|
||||
/// One-shot heat 1.5 loses to a roster of notables; ~2 minutes reaches <see cref="HeatCap"/>.
|
||||
/// </summary>
|
||||
public static float HardArcAdmitHeat(StoryArc arc, float now = -1f)
|
||||
{
|
||||
if (now < 0f)
|
||||
{
|
||||
now = Time.unscaledTime;
|
||||
}
|
||||
|
||||
if (arc == null || arc.StartedAt <= 0f)
|
||||
{
|
||||
return 1.5f;
|
||||
}
|
||||
|
||||
float age = Mathf.Max(0f, now - arc.StartedAt);
|
||||
return Mathf.Min(HeatCap, 1.5f + age / 12f);
|
||||
}
|
||||
|
||||
public static void StampChapterFromArc(StoryArc arc, InterestCandidate tip, float now)
|
||||
{
|
||||
if (arc == null || !arc.IsActive)
|
||||
|
|
@ -697,6 +717,8 @@ public static class LifeSagaRoster
|
|||
}
|
||||
|
||||
bool hard = IsHardStoryKind(arc.Kind);
|
||||
float subjectHeat = hard ? HardArcAdmitHeat(arc, now) : 1.5f;
|
||||
float relatedHeat = hard ? Mathf.Max(1.2f, subjectHeat * 0.85f) : 1.2f;
|
||||
if (arc.CastIds != null)
|
||||
{
|
||||
for (int i = 0; i < arc.CastIds.Count; i++)
|
||||
|
|
@ -704,7 +726,7 @@ public static class LifeSagaRoster
|
|||
long id = arc.CastIds[i];
|
||||
if (hard)
|
||||
{
|
||||
ConsiderAdmit(id, 1.5f, hardArcAdmit: true, now);
|
||||
ConsiderAdmit(id, subjectHeat, hardArcAdmit: true, now);
|
||||
MarkHardArcContext(id, arc.Kind, line);
|
||||
}
|
||||
|
||||
|
|
@ -720,13 +742,20 @@ public static class LifeSagaRoster
|
|||
{
|
||||
if (hard)
|
||||
{
|
||||
ConsiderAdmit(tip.SubjectId, 1.5f, hardArcAdmit: true, now);
|
||||
ConsiderAdmit(tip.SubjectId, subjectHeat, hardArcAdmit: true, now);
|
||||
MarkHardArcContext(tip.SubjectId, arc.Kind, line);
|
||||
if (tip.RelatedId != 0)
|
||||
{
|
||||
ConsiderAdmit(tip.RelatedId, 1.2f, hardArcAdmit: true, now);
|
||||
ConsiderAdmit(tip.RelatedId, relatedHeat, hardArcAdmit: true, now);
|
||||
MarkHardArcContext(tip.RelatedId, arc.Kind, line);
|
||||
}
|
||||
|
||||
long follow = EventFeedUtil.SafeId(tip.FollowUnit);
|
||||
if (follow != 0 && follow != tip.SubjectId && follow != tip.RelatedId)
|
||||
{
|
||||
ConsiderAdmit(follow, subjectHeat, hardArcAdmit: true, now);
|
||||
MarkHardArcContext(follow, arc.Kind, line);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsMc(tip.SubjectId))
|
||||
|
|
@ -738,6 +767,15 @@ public static class LifeSagaRoster
|
|||
{
|
||||
StampChapter(tip.RelatedId, ToChapterKind(arc.Kind), line, now);
|
||||
}
|
||||
|
||||
long followStamp = EventFeedUtil.SafeId(tip.FollowUnit);
|
||||
if (followStamp != 0
|
||||
&& followStamp != tip.SubjectId
|
||||
&& followStamp != tip.RelatedId
|
||||
&& IsMc(followStamp))
|
||||
{
|
||||
StampChapter(followStamp, ToChapterKind(arc.Kind), line, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,6 +210,12 @@ public static class StoryPlanner
|
|||
_active.HardHold = hardHold || _active.HardHold;
|
||||
NoteCastFrom(current, _active);
|
||||
StampHeadline(_active, current);
|
||||
// Re-challenge Cap while climax holds: admit heat escalates with arc age so a
|
||||
// prolonged duel/war/plot on a stranger can still join the rail (Active chrome).
|
||||
if (IsHardStoryKind(_active.Kind))
|
||||
{
|
||||
LifeSagaRoster.StampChapterFromArc(_active, current, Time.unscaledTime);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCombatKind(StoryArcKind kind) =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.29.57",
|
||||
"version": "0.29.59",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ Hard-arc admissions also retain the arc kind and chapter context that brought th
|
|||
- Prefer and WorldBox favorites are hard pins and fully count toward Cap.
|
||||
- Admission-role units always compete on the periodic scan.
|
||||
- Non-admission "nobodies" challenge Cap only via hard-arc chapter stamps (Combat / War / Plot / Love / Grief).
|
||||
- Hard-arc admit heat escalates with climax age (re-stamped while the arc holds) so a prolonged stranger duel can displace dull Cap slots and light Active chrome.
|
||||
- Soft `NoteFeatured` heats existing MCs only.
|
||||
- Newcomers need a small challenger margin over the weakest non-pin so the rail does not thrash.
|
||||
|
||||
|
|
@ -106,6 +107,7 @@ Quiet can refresh briefly when a soft crumb ends (bounded).
|
|||
- `StoryPlanner.Clear` does not wipe the roster or saga memory.
|
||||
- Lore browse / brief idle-off keeps the roster and memory.
|
||||
- World replacement, harness batch end, and `interest_saga_clear` wipe roster + memory.
|
||||
- Read-only soak dumps use `soak_probe` (or `snapshot` with `value=preserve`) and skip the batch-end wipe.
|
||||
|
||||
## Harness
|
||||
|
||||
|
|
@ -120,6 +122,7 @@ Quiet can refresh briefly when a soft crumb ends (bounded).
|
|||
- `saga_rival_not_single_kill`
|
||||
- `saga_rival_rematch`
|
||||
- `saga_prefer_cuts_ambient`
|
||||
- `saga_hard_arc_escalates`
|
||||
- `saga_overview_has_mc`
|
||||
- `saga_adaptive_dossier`
|
||||
- `saga_memory_survives`
|
||||
|
|
|
|||
Loading…
Reference in a new issue