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`
|
- `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
|
- `dossier_click_history_other`, `dossier_force_named_reason` - dossier row → Lore other; gold names in orange reason
|
||||||
- `snapshot`, `reset_counters`, `scenario`
|
- `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
|
## Process safety
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ public static class AgentHarness
|
||||||
private static Actor _happinessPartner;
|
private static Actor _happinessPartner;
|
||||||
private static long _activitySubjectId;
|
private static long _activitySubjectId;
|
||||||
private static bool _directorRunActive;
|
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 _rememberedFocusKey = "";
|
||||||
private static string _rememberedTip = "";
|
private static string _rememberedTip = "";
|
||||||
private static long _rememberedRelatedId;
|
private static long _rememberedRelatedId;
|
||||||
|
|
@ -2972,6 +2977,50 @@ public static class AgentHarness
|
||||||
break;
|
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":
|
case "saga_force_admit_partner":
|
||||||
{
|
{
|
||||||
Actor partner = _happinessPartner;
|
Actor partner = _happinessPartner;
|
||||||
|
|
@ -3133,24 +3182,77 @@ public static class AgentHarness
|
||||||
break;
|
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":
|
case "saga_consider_hard_focus":
|
||||||
{
|
{
|
||||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
long id = EventFeedUtil.SafeId(focus);
|
long id = EventFeedUtil.SafeId(focus);
|
||||||
bool ok = id != 0
|
float heat = 6f;
|
||||||
&& LifeSagaRoster.ConsiderAdmit(id, heat: 6f, hardArcAdmit: true);
|
if (!string.IsNullOrEmpty(cmd.value)
|
||||||
if (ok)
|
&& float.TryParse(
|
||||||
|
cmd.value.Trim(),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out float parsed)
|
||||||
|
&& parsed > 0f)
|
||||||
{
|
{
|
||||||
_cmdOk++;
|
heat = parsed;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_cmdFail++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
detail:
|
||||||
$"hardAdmit id={id} on={LifeSagaRoster.IsMc(id)} roster={LifeSagaRoster.Count}");
|
$"hardAdmit id={id} on={admitted} heat={heat:0.##} roster={LifeSagaRoster.Count}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3847,9 +3949,19 @@ public static class AgentHarness
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "snapshot":
|
case "snapshot":
|
||||||
|
if (WantsPreserveSaga(cmd))
|
||||||
|
{
|
||||||
|
_preserveSagaAfterBatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
DoSnapshot(cmd);
|
DoSnapshot(cmd);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "soak_probe":
|
||||||
|
case "saga_soak_probe":
|
||||||
|
DoSoakProbe(cmd);
|
||||||
|
break;
|
||||||
|
|
||||||
case "scenario":
|
case "scenario":
|
||||||
ExpandScenario(cmd);
|
ExpandScenario(cmd);
|
||||||
return;
|
return;
|
||||||
|
|
@ -3881,12 +3993,19 @@ public static class AgentHarness
|
||||||
{
|
{
|
||||||
// Free AFK after harness must not inherit a leftover crisis closer tip.
|
// Free AFK after harness must not inherit a leftover crisis closer tip.
|
||||||
StoryPlanner.PurgeCloserLeftovers();
|
StoryPlanner.PurgeCloserLeftovers();
|
||||||
|
bool preserve = _preserveSagaAfterBatch;
|
||||||
|
_preserveSagaAfterBatch = false;
|
||||||
|
if (!preserve)
|
||||||
|
{
|
||||||
LifeSagaRoster.Clear();
|
LifeSagaRoster.Clear();
|
||||||
LifeSagaMemory.Clear();
|
LifeSagaMemory.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
string status = _assertFail == 0 && _cmdFail == 0 ? "PASS" : "FAIL";
|
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(
|
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;
|
_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);
|
bool keepStats = (cmd.expect ?? "").Trim().Equals("keep", System.StringComparison.OrdinalIgnoreCase);
|
||||||
if (!keepStats)
|
if (!keepStats)
|
||||||
{
|
{
|
||||||
|
|
@ -8555,8 +8676,11 @@ public static class AgentHarness
|
||||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
long id = EventFeedUtil.SafeId(focus);
|
long id = EventFeedUtil.SafeId(focus);
|
||||||
LifeSagaRoster.Tick(Time.unscaledTime);
|
LifeSagaRoster.Tick(Time.unscaledTime);
|
||||||
pass = id != 0 && LifeSagaRoster.IsMc(id);
|
bool want = ParseBool(cmd.value, defaultValue: true);
|
||||||
detail = $"saga focus={SafeName(focus)} id={id} on={pass} prefer={LifeSagaRoster.IsPrefer(id)}";
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "saga_prefer_focus_on":
|
case "saga_prefer_focus_on":
|
||||||
|
|
@ -12329,6 +12453,16 @@ public static class AgentHarness
|
||||||
$"[IdleSpectator][ASSERT] {(pass ? "PASS" : "FAIL")} id={cmd.id} expect={expect} {detail}");
|
$"[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)
|
private static void DoSnapshot(HarnessCommand cmd)
|
||||||
{
|
{
|
||||||
string drops = InterestDropLog.FormatRecent(6);
|
string drops = InterestDropLog.FormatRecent(6);
|
||||||
|
|
@ -12344,6 +12478,116 @@ public static class AgentHarness
|
||||||
LogService.LogInfo("[IdleSpectator][SNAP] " + snap);
|
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)
|
private static void DoDiscoveryNote(HarnessCommand cmd)
|
||||||
{
|
{
|
||||||
string assetId = ResolveAssetId(cmd);
|
string assetId = ResolveAssetId(cmd);
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@ internal static class HarnessScenarios
|
||||||
return SagaRivalRematch();
|
return SagaRivalRematch();
|
||||||
case "saga_prefer_cuts_ambient":
|
case "saga_prefer_cuts_ambient":
|
||||||
return SagaPreferCutsAmbient();
|
return SagaPreferCutsAmbient();
|
||||||
|
case "saga_hard_arc_escalates":
|
||||||
|
return SagaHardArcEscalates();
|
||||||
case "saga_overview_has_mc":
|
case "saga_overview_has_mc":
|
||||||
return SagaOverviewHasMc();
|
return SagaOverviewHasMc();
|
||||||
case "saga_snapshot_popover":
|
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_kill", "saga_rival_not_single_kill"),
|
||||||
Nested("reg_saga_rival_rematch", "saga_rival_rematch"),
|
Nested("reg_saga_rival_rematch", "saga_rival_rematch"),
|
||||||
Nested("reg_saga_ambient_cut", "saga_prefer_cuts_ambient"),
|
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_overview", "saga_overview_has_mc"),
|
||||||
Nested("reg_saga_snapshot", "saga_adaptive_dossier"),
|
Nested("reg_saga_snapshot", "saga_adaptive_dossier"),
|
||||||
Nested("reg_saga_memory", "saga_memory_survives"),
|
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>
|
/// <summary>
|
||||||
/// Compact saga snapshot: admission headline, collapsible layout, structured chapters,
|
/// Compact saga snapshot: admission headline, collapsible layout, structured chapters,
|
||||||
/// no planner jargon / trait descs / Chronicle dumps.
|
/// no planner jargon / trait descs / Chronicle dumps.
|
||||||
|
|
|
||||||
|
|
@ -683,6 +683,26 @@ public static class LifeSagaRoster
|
||||||
trimmed);
|
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)
|
public static void StampChapterFromArc(StoryArc arc, InterestCandidate tip, float now)
|
||||||
{
|
{
|
||||||
if (arc == null || !arc.IsActive)
|
if (arc == null || !arc.IsActive)
|
||||||
|
|
@ -697,6 +717,8 @@ public static class LifeSagaRoster
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hard = IsHardStoryKind(arc.Kind);
|
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)
|
if (arc.CastIds != null)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < arc.CastIds.Count; i++)
|
for (int i = 0; i < arc.CastIds.Count; i++)
|
||||||
|
|
@ -704,7 +726,7 @@ public static class LifeSagaRoster
|
||||||
long id = arc.CastIds[i];
|
long id = arc.CastIds[i];
|
||||||
if (hard)
|
if (hard)
|
||||||
{
|
{
|
||||||
ConsiderAdmit(id, 1.5f, hardArcAdmit: true, now);
|
ConsiderAdmit(id, subjectHeat, hardArcAdmit: true, now);
|
||||||
MarkHardArcContext(id, arc.Kind, line);
|
MarkHardArcContext(id, arc.Kind, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,13 +742,20 @@ public static class LifeSagaRoster
|
||||||
{
|
{
|
||||||
if (hard)
|
if (hard)
|
||||||
{
|
{
|
||||||
ConsiderAdmit(tip.SubjectId, 1.5f, hardArcAdmit: true, now);
|
ConsiderAdmit(tip.SubjectId, subjectHeat, hardArcAdmit: true, now);
|
||||||
MarkHardArcContext(tip.SubjectId, arc.Kind, line);
|
MarkHardArcContext(tip.SubjectId, arc.Kind, line);
|
||||||
if (tip.RelatedId != 0)
|
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);
|
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))
|
if (IsMc(tip.SubjectId))
|
||||||
|
|
@ -738,6 +767,15 @@ public static class LifeSagaRoster
|
||||||
{
|
{
|
||||||
StampChapter(tip.RelatedId, ToChapterKind(arc.Kind), line, now);
|
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;
|
_active.HardHold = hardHold || _active.HardHold;
|
||||||
NoteCastFrom(current, _active);
|
NoteCastFrom(current, _active);
|
||||||
StampHeadline(_active, current);
|
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) =>
|
private static bool IsCombatKind(StoryArcKind kind) =>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.29.57",
|
"version": "0.29.59",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"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.
|
- Prefer and WorldBox favorites are hard pins and fully count toward Cap.
|
||||||
- Admission-role units always compete on the periodic scan.
|
- 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).
|
- 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.
|
- Soft `NoteFeatured` heats existing MCs only.
|
||||||
- Newcomers need a small challenger margin over the weakest non-pin so the rail does not thrash.
|
- 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.
|
- `StoryPlanner.Clear` does not wipe the roster or saga memory.
|
||||||
- Lore browse / brief idle-off keeps the roster and memory.
|
- Lore browse / brief idle-off keeps the roster and memory.
|
||||||
- World replacement, harness batch end, and `interest_saga_clear` wipe roster + 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
|
## Harness
|
||||||
|
|
||||||
|
|
@ -120,6 +122,7 @@ Quiet can refresh briefly when a soft crumb ends (bounded).
|
||||||
- `saga_rival_not_single_kill`
|
- `saga_rival_not_single_kill`
|
||||||
- `saga_rival_rematch`
|
- `saga_rival_rematch`
|
||||||
- `saga_prefer_cuts_ambient`
|
- `saga_prefer_cuts_ambient`
|
||||||
|
- `saga_hard_arc_escalates`
|
||||||
- `saga_overview_has_mc`
|
- `saga_overview_has_mc`
|
||||||
- `saga_adaptive_dossier`
|
- `saga_adaptive_dossier`
|
||||||
- `saga_memory_survives`
|
- `saga_memory_survives`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue