Healthy Director
This commit is contained in:
parent
57d2e70476
commit
4491423236
8 changed files with 350 additions and 26 deletions
|
|
@ -191,6 +191,23 @@ public static class AgentHarness
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "force_live_feeds":
|
||||||
|
{
|
||||||
|
bool on = ParseBool(cmd.value, defaultValue: true);
|
||||||
|
ForceLiveEventFeeds = on;
|
||||||
|
_cmdOk++;
|
||||||
|
Emit(cmd, ok: true, detail: $"force_live_feeds={on}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "drop_clear":
|
||||||
|
{
|
||||||
|
InterestDropLog.Clear();
|
||||||
|
_cmdOk++;
|
||||||
|
Emit(cmd, ok: true, detail: "drops_cleared");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "age_current":
|
case "age_current":
|
||||||
{
|
{
|
||||||
float seconds = cmd.wait > 0f ? cmd.wait : ParseFloat(cmd.value, 2f);
|
float seconds = cmd.wait > 0f ? cmd.wait : ParseFloat(cmd.value, 2f);
|
||||||
|
|
@ -2261,6 +2278,7 @@ public static class AgentHarness
|
||||||
bool ok = published != null;
|
bool ok = published != null;
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
|
InterestFeeds.HarnessEnsureCursorBefore(published.Sequence);
|
||||||
_cmdOk++;
|
_cmdOk++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,7 @@ public static class EventCatalogConfig
|
||||||
InterestCategory = ExtractJsonString(obj, "category") ?? "",
|
InterestCategory = ExtractJsonString(obj, "category") ?? "",
|
||||||
CanonicalMilestoneKey = ExtractJsonString(obj, "canonicalMilestoneKey") ?? "",
|
CanonicalMilestoneKey = ExtractJsonString(obj, "canonicalMilestoneKey") ?? "",
|
||||||
StatusOverlapId = ExtractJsonString(obj, "statusOverlapId") ?? "",
|
StatusOverlapId = ExtractJsonString(obj, "statusOverlapId") ?? "",
|
||||||
|
StatusOverlapKind = ParseStatusOverlapKind(obj, ExtractJsonString(obj, "statusOverlapId")),
|
||||||
VariantsWithRelated = ExtractStringArray(obj, "variantsWithRelated"),
|
VariantsWithRelated = ExtractStringArray(obj, "variantsWithRelated"),
|
||||||
VariantsWithoutRelated = ExtractStringArray(obj, "variantsWithoutRelated")
|
VariantsWithoutRelated = ExtractStringArray(obj, "variantsWithoutRelated")
|
||||||
};
|
};
|
||||||
|
|
@ -468,6 +469,51 @@ public static class EventCatalogConfig
|
||||||
return Enum.TryParse(raw, true, out TEnum value) ? value : fallback;
|
return Enum.TryParse(raw, true, out TEnum value) ? value : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HappinessStatusOverlapKind ParseStatusOverlapKind(string json, string statusOverlapId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(statusOverlapId))
|
||||||
|
{
|
||||||
|
return HappinessStatusOverlapKind.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessStatusOverlapKind parsed = ParseEnum(
|
||||||
|
json, "statusOverlapKind", HappinessStatusOverlapKind.None);
|
||||||
|
if (parsed != HappinessStatusOverlapKind.None)
|
||||||
|
{
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy rows without statusOverlapKind: offset end-pings vs onset holds.
|
||||||
|
string id = ExtractJsonString(json, "id") ?? "";
|
||||||
|
if (IsLegacyOffsetOverlap(id))
|
||||||
|
{
|
||||||
|
return HappinessStatusOverlapKind.Offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HappinessStatusOverlapKind.Hold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLegacyOffsetOverlap(string effectId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (effectId.Trim().ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "just_laughed":
|
||||||
|
case "just_sang":
|
||||||
|
case "just_swore":
|
||||||
|
case "just_cried":
|
||||||
|
case "just_had_tantrum":
|
||||||
|
case "just_inspired":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string[] ExtractStringArray(string json, string key)
|
private static string[] ExtractStringArray(string json, string key)
|
||||||
{
|
{
|
||||||
if (!TryExtractArray(json, key, out string arr) || string.IsNullOrEmpty(arr))
|
if (!TryExtractArray(json, key, out string arr) || string.IsNullOrEmpty(arr))
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,30 @@ public static class InterestFeeds
|
||||||
InterestDropLog.Clear();
|
InterestDropLog.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Keep drain cursor coherent when the happiness router sequence resets.</summary>
|
||||||
|
public static void OnHappinessRouterCleared()
|
||||||
|
{
|
||||||
|
_happinessCursor = HappinessEventRouter.CurrentSequence;
|
||||||
|
HappinessDrain.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Harness: if the router was cleared without syncing the feed cursor, rewind so
|
||||||
|
/// <see cref="HarnessDrainOnce"/> can see the new sequence.
|
||||||
|
/// </summary>
|
||||||
|
public static void HarnessEnsureCursorBefore(ulong sequence)
|
||||||
|
{
|
||||||
|
if (sequence == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_happinessCursor >= sequence)
|
||||||
|
{
|
||||||
|
_happinessCursor = sequence - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static ulong HappinessCursor => _happinessCursor;
|
public static ulong HappinessCursor => _happinessCursor;
|
||||||
|
|
||||||
public static int CivicBoostCount
|
public static int CivicBoostCount
|
||||||
|
|
@ -728,6 +752,27 @@ public static class InterestFeeds
|
||||||
label = occ.SubjectName + " · " + occ.EffectId;
|
label = occ.SubjectName + " · " + occ.EffectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InterestCompletionKind completion = grief
|
||||||
|
? InterestCompletionKind.HappinessGrief
|
||||||
|
: InterestCompletionKind.FixedDwell;
|
||||||
|
string statusId = grief ? "crying" : "";
|
||||||
|
|
||||||
|
if (!grief
|
||||||
|
&& !hatchMoment
|
||||||
|
&& !alphaMoment
|
||||||
|
&& !TryBindStatusOverlapCamera(
|
||||||
|
occ,
|
||||||
|
subject,
|
||||||
|
ref key,
|
||||||
|
ref minWatch,
|
||||||
|
ref maxWatch,
|
||||||
|
ref strength,
|
||||||
|
ref completion,
|
||||||
|
ref statusId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var candidate = new InterestCandidate
|
var candidate = new InterestCandidate
|
||||||
{
|
{
|
||||||
Key = key,
|
Key = key,
|
||||||
|
|
@ -747,16 +792,14 @@ public static class InterestFeeds
|
||||||
AssetId = occ.SubjectSpecies,
|
AssetId = occ.SubjectSpecies,
|
||||||
SpeciesId = occ.SubjectSpecies,
|
SpeciesId = occ.SubjectSpecies,
|
||||||
HappinessEffectId = occ.EffectId,
|
HappinessEffectId = occ.EffectId,
|
||||||
StatusId = grief ? "crying" : "",
|
StatusId = statusId,
|
||||||
CorrelationKey = occ.CorrelationKey,
|
CorrelationKey = occ.CorrelationKey,
|
||||||
CreatedAt = Time.unscaledTime,
|
CreatedAt = Time.unscaledTime,
|
||||||
LastSeenAt = Time.unscaledTime,
|
LastSeenAt = Time.unscaledTime,
|
||||||
ExpiresAt = Time.unscaledTime + ttl,
|
ExpiresAt = Time.unscaledTime + ttl,
|
||||||
MinWatch = minWatch,
|
MinWatch = minWatch,
|
||||||
MaxWatch = maxWatch,
|
MaxWatch = maxWatch,
|
||||||
Completion = grief
|
Completion = completion
|
||||||
? InterestCompletionKind.HappinessGrief
|
|
||||||
: InterestCompletionKind.FixedDwell
|
|
||||||
};
|
};
|
||||||
candidate.ParticipantIds.Add(occ.SubjectId);
|
candidate.ParticipantIds.Add(occ.SubjectId);
|
||||||
if (occ.RelatedId != 0)
|
if (occ.RelatedId != 0)
|
||||||
|
|
@ -778,6 +821,117 @@ public static class InterestFeeds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Camera-worthy status-overlap happiness must not FixedDwell-claim a status the unit lacks.
|
||||||
|
/// Hold (onset): StatusPhase while live, else drop. Offset (end-ping): FixedDwell without status.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryBindStatusOverlapCamera(
|
||||||
|
HappinessOccurrence occ,
|
||||||
|
Actor subject,
|
||||||
|
ref string key,
|
||||||
|
ref float minWatch,
|
||||||
|
ref float maxWatch,
|
||||||
|
ref float strength,
|
||||||
|
ref InterestCompletionKind completion,
|
||||||
|
ref string statusId)
|
||||||
|
{
|
||||||
|
if (occ == null || string.IsNullOrEmpty(occ.StatusOverlapId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessStatusOverlapKind kind = ResolveStatusOverlapKind(occ);
|
||||||
|
if (kind == HappinessStatusOverlapKind.None)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string overlapId = occ.StatusOverlapId.Trim();
|
||||||
|
StatusInterestEntry statusEntry = EventCatalog.Status.GetOrFallback(overlapId);
|
||||||
|
if (!EventCatalog.IsCameraWorthy(statusEntry))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasStatus = StatusGameApi.HasStatus(subject, overlapId)
|
||||||
|
|| InterestCompletionHasStatus(subject, overlapId);
|
||||||
|
if (hasStatus)
|
||||||
|
{
|
||||||
|
key = "status:" + overlapId + ":" + occ.SubjectId;
|
||||||
|
statusId = overlapId;
|
||||||
|
completion = InterestCompletionKind.StatusPhase;
|
||||||
|
minWatch = Mathf.Max(minWatch, 2f);
|
||||||
|
maxWatch = Mathf.Max(maxWatch, InterestScoringConfig.W.maxWatchStatus);
|
||||||
|
strength = Mathf.Max(strength, statusEntry.EventStrength);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == HappinessStatusOverlapKind.Offset)
|
||||||
|
{
|
||||||
|
// End-ping after the status cleared - short FixedDwell, keep happiness key.
|
||||||
|
maxWatch = Mathf.Min(maxWatch, 12f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
InterestDropLog.Record("status_overlap_absent", occ.EffectId ?? overlapId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HappinessStatusOverlapKind ResolveStatusOverlapKind(HappinessOccurrence occ)
|
||||||
|
{
|
||||||
|
if (occ.StatusOverlapKind != HappinessStatusOverlapKind.None)
|
||||||
|
{
|
||||||
|
return occ.StatusOverlapKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(occ.StatusOverlapId))
|
||||||
|
{
|
||||||
|
return HappinessStatusOverlapKind.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = (occ.EffectId ?? "").Trim().ToLowerInvariant();
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
case "just_laughed":
|
||||||
|
case "just_sang":
|
||||||
|
case "just_swore":
|
||||||
|
case "just_cried":
|
||||||
|
case "just_had_tantrum":
|
||||||
|
case "just_inspired":
|
||||||
|
return HappinessStatusOverlapKind.Offset;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string clause = (occ.PlainClause ?? "").Trim();
|
||||||
|
if (clause.StartsWith("finishes", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| clause.StartsWith("comes down", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| clause.StartsWith("calms after", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| clause.StartsWith("lets inspiration", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return HappinessStatusOverlapKind.Offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HappinessStatusOverlapKind.Hold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InterestCompletionHasStatus(Actor actor, string statusId)
|
||||||
|
{
|
||||||
|
if (actor == null || string.IsNullOrEmpty(statusId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return actor.hasStatus(statusId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Direct registry insert for harness happiness signals (skips drain filters).</summary>
|
/// <summary>Direct registry insert for harness happiness signals (skips drain filters).</summary>
|
||||||
public static InterestCandidate RegisterHappinessSignal(
|
public static InterestCandidate RegisterHappinessSignal(
|
||||||
Actor subject,
|
Actor subject,
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ public static class HappinessEventRouter
|
||||||
_lastEffectId = "";
|
_lastEffectId = "";
|
||||||
_counterSubjectId = 0;
|
_counterSubjectId = 0;
|
||||||
_nextSequence = 1;
|
_nextSequence = 1;
|
||||||
|
InterestFeeds.OnHappinessRouterCleared();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Push listener for spectator interest intake (in addition to <see cref="DrainSince"/>).</summary>
|
/// <summary>Push listener for spectator interest intake (in addition to <see cref="DrainSince"/>).</summary>
|
||||||
|
|
@ -539,7 +540,8 @@ public static class HappinessEventRouter
|
||||||
Context = entry.Context,
|
Context = entry.Context,
|
||||||
InterestCategory = entry.InterestCategory,
|
InterestCategory = entry.InterestCategory,
|
||||||
CanonicalMilestoneKey = entry.CanonicalMilestoneKey,
|
CanonicalMilestoneKey = entry.CanonicalMilestoneKey,
|
||||||
StatusOverlapId = entry.StatusOverlapId
|
StatusOverlapId = entry.StatusOverlapId,
|
||||||
|
StatusOverlapKind = entry.StatusOverlapKind
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@ public enum HappinessOverlapPolicy
|
||||||
Continuation
|
Continuation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How a happiness <see cref="HappinessCatalogEntry.StatusOverlapId"/> relates to camera hold.
|
||||||
|
/// Hold = onset claim (require live status); Offset = end-ping FixedDwell when status is gone.
|
||||||
|
/// </summary>
|
||||||
|
public enum HappinessStatusOverlapKind
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Hold,
|
||||||
|
Offset
|
||||||
|
}
|
||||||
|
|
||||||
public enum HappinessRelationRole
|
public enum HappinessRelationRole
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
|
|
@ -69,6 +80,7 @@ public sealed class HappinessCatalogEntry
|
||||||
public float EventStrength = 40f;
|
public float EventStrength = 40f;
|
||||||
public string CanonicalMilestoneKey = "";
|
public string CanonicalMilestoneKey = "";
|
||||||
public string StatusOverlapId = "";
|
public string StatusOverlapId = "";
|
||||||
|
public HappinessStatusOverlapKind StatusOverlapKind = HappinessStatusOverlapKind.None;
|
||||||
public string[] VariantsWithRelated = Array.Empty<string>();
|
public string[] VariantsWithRelated = Array.Empty<string>();
|
||||||
public string[] VariantsWithoutRelated = Array.Empty<string>();
|
public string[] VariantsWithoutRelated = Array.Empty<string>();
|
||||||
public bool IsFallback;
|
public bool IsFallback;
|
||||||
|
|
@ -102,6 +114,7 @@ public sealed class HappinessOccurrence
|
||||||
public string InterestCategory = "";
|
public string InterestCategory = "";
|
||||||
public string CanonicalMilestoneKey = "";
|
public string CanonicalMilestoneKey = "";
|
||||||
public string StatusOverlapId = "";
|
public string StatusOverlapId = "";
|
||||||
|
public HappinessStatusOverlapKind StatusOverlapKind = HappinessStatusOverlapKind.None;
|
||||||
public int SampledCount = 1;
|
public int SampledCount = 1;
|
||||||
public int TotalAffectedCount = 1;
|
public int TotalAffectedCount = 1;
|
||||||
public string CivEventKey = "";
|
public string CivEventKey = "";
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ internal static class HarnessScenarios
|
||||||
case "camera_presentability":
|
case "camera_presentability":
|
||||||
case "presentability":
|
case "presentability":
|
||||||
return CameraPresentability();
|
return CameraPresentability();
|
||||||
|
case "status_overlap_camera":
|
||||||
|
case "status_overlap":
|
||||||
|
return StatusOverlapCamera();
|
||||||
case "director_action_priority":
|
case "director_action_priority":
|
||||||
case "action_priority":
|
case "action_priority":
|
||||||
return DirectorActionPriority();
|
return DirectorActionPriority();
|
||||||
|
|
@ -1025,6 +1028,76 @@ internal static class HarnessScenarios
|
||||||
|
|
||||||
Step("cp90", "fast_timing", value: "false"),
|
Step("cp90", "fast_timing", value: "false"),
|
||||||
Step("cp99", "snapshot"),
|
Step("cp99", "snapshot"),
|
||||||
|
Nested("cp_status_overlap", "status_overlap_camera"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status-overlap happiness must not FixedDwell-claim a camera-worthy status the unit lacks.
|
||||||
|
/// Hold while status is live; offset end-pings still register briefly without it.
|
||||||
|
/// </summary>
|
||||||
|
private static List<HarnessCommand> StatusOverlapCamera()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("so0", "dismiss_windows"),
|
||||||
|
Step("so1", "wait_world"),
|
||||||
|
Step("so2", "set_setting", expect: "enabled", value: "true"),
|
||||||
|
Step("so3", "fast_timing", value: "true"),
|
||||||
|
Step("so4", "spawn", asset: "human", count: 1),
|
||||||
|
Step("so5", "pick_unit", asset: "human"),
|
||||||
|
Step("so6", "spectator", value: "off"),
|
||||||
|
Step("so7", "spectator", value: "on"),
|
||||||
|
Step("so8", "focus", asset: "auto"),
|
||||||
|
Step("so9", "interest_end_session"),
|
||||||
|
Step("so9b", "interest_expire_pending", value: ""),
|
||||||
|
Step("so9c", "force_live_feeds", value: "true"),
|
||||||
|
|
||||||
|
// Orphan onset: just_cursed without cursed status must not own the camera.
|
||||||
|
Step("so10", "happiness_reset"),
|
||||||
|
Step("so11", "drop_clear"),
|
||||||
|
Step("so12", "happiness_remember_only", value: "just_cursed"),
|
||||||
|
Step("so13", "interest_feeds_tick"),
|
||||||
|
Step("so14", "assert", expect: "drop_contains", value: "status_overlap_absent"),
|
||||||
|
Step("so15", "assert", expect: "interest_no_key", value: "just_cursed"),
|
||||||
|
Step("so16", "assert", expect: "interest_no_key", value: "status:cursed"),
|
||||||
|
|
||||||
|
// Same class: possessed onset without status.
|
||||||
|
Step("so20", "drop_clear"),
|
||||||
|
Step("so21", "happiness_remember_only", value: "just_possessed"),
|
||||||
|
Step("so22", "interest_feeds_tick"),
|
||||||
|
Step("so23", "assert", expect: "drop_contains", value: "status_overlap_absent"),
|
||||||
|
Step("so24", "assert", expect: "interest_no_key", value: "just_possessed"),
|
||||||
|
|
||||||
|
// Offset end-ping still registers without the status.
|
||||||
|
Step("so30", "drop_clear"),
|
||||||
|
Step("so31", "happiness_remember_only", value: "just_inspired"),
|
||||||
|
Step("so32", "interest_feeds_tick"),
|
||||||
|
Step("so33", "assert", expect: "interest_has_key", value: "just_inspired"),
|
||||||
|
Step("so34", "interest_expire_pending", value: "just_inspired"),
|
||||||
|
|
||||||
|
// Live cursed status owns StatusPhase; happiness Continuation merges.
|
||||||
|
// Short timer: cursed may not clear via finishStatusEffect (re-applies / sticky).
|
||||||
|
Step("so40", "interest_end_session"),
|
||||||
|
Step("so41", "interest_expire_pending", value: ""),
|
||||||
|
Step("so42", "drop_clear"),
|
||||||
|
Step("so43", "status_apply", value: "cursed", label: "0.4"),
|
||||||
|
Step("so44", "assert", expect: "interest_has_key", value: "status:cursed"),
|
||||||
|
Step("so45", "happiness_remember_only", value: "just_cursed"),
|
||||||
|
Step("so46", "interest_feeds_tick"),
|
||||||
|
Step("so47", "assert", expect: "interest_has_key", value: "status:cursed"),
|
||||||
|
Step("so48", "assert", expect: "interest_no_key", value: "happiness:just_cursed"),
|
||||||
|
Step("so49", "wait", wait: 1.1f),
|
||||||
|
Step("so50", "assert", expect: "activity_status_active", value: "cursed", label: "false"),
|
||||||
|
Step("so50b", "age_current", wait: 3f),
|
||||||
|
Step("so50c", "director_run", wait: 1.2f),
|
||||||
|
Step("so51", "interest_end_session"),
|
||||||
|
Step("so52", "interest_expire_pending", value: "status:cursed"),
|
||||||
|
Step("so53", "assert", expect: "interest_no_key", value: "status:cursed"),
|
||||||
|
|
||||||
|
Step("so90", "force_live_feeds", value: "false"),
|
||||||
|
Step("so91", "fast_timing", value: "false"),
|
||||||
|
Step("so99", "snapshot"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"modVersion": "0.25.1",
|
"modVersion": "0.25.1",
|
||||||
"title": "IdleSpectator event catalog",
|
"title": "IdleSpectator event catalog",
|
||||||
"updated": "2026-07-16",
|
"updated": "2026-07-16",
|
||||||
"note": "Authored IdleSpectator event catalog. Score ladder (EventStrength): 95-100 epic world (war/kingdom fall/major disaster); 88-94 major spectacle; 78-87 kingdom politics / peak grief; 70-77 life milestones (notice band); 55-69 notable beats; 40-54 warm social; 28-39 soft daily Signal; ≤25 B-only FX. decisions.action = infinitive after \"decides to\"; other domains use label/variants. scoring-model.json remains global formula (margins/multipliers).",
|
"note": "Authored IdleSpectator event catalog. Score ladder (EventStrength): 95-100 epic world (war/kingdom fall/major disaster); 88-94 major spectacle; 78-87 kingdom politics / peak grief; 70-77 life milestones (notice band); 55-69 notable beats; 40-54 warm social; 28-39 soft daily Signal; \u226425 B-only FX. decisions.action = infinitive after \"decides to\"; other domains use label/variants. scoring-model.json remains global formula (margins/multipliers).",
|
||||||
"decisions": [
|
"decisions": [
|
||||||
{
|
{
|
||||||
"id": "find_lover",
|
"id": "find_lover",
|
||||||
|
|
@ -299,7 +299,8 @@
|
||||||
"eats and feels better",
|
"eats and feels better",
|
||||||
"enjoys a meal"
|
"enjoys a meal"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "just_ate"
|
"statusOverlapId": "just_ate",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_received_gift",
|
"id": "just_received_gift",
|
||||||
|
|
@ -390,7 +391,8 @@
|
||||||
"has a bad dream",
|
"has a bad dream",
|
||||||
"is troubled by a bad dream"
|
"is troubled by a bad dream"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "had_bad_dream"
|
"statusOverlapId": "had_bad_dream",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "had_good_dream",
|
"id": "had_good_dream",
|
||||||
|
|
@ -409,7 +411,8 @@
|
||||||
"has a good dream",
|
"has a good dream",
|
||||||
"dreams pleasantly"
|
"dreams pleasantly"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "had_good_dream"
|
"statusOverlapId": "had_good_dream",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "had_nightmare",
|
"id": "had_nightmare",
|
||||||
|
|
@ -428,7 +431,8 @@
|
||||||
"has a nightmare",
|
"has a nightmare",
|
||||||
"is haunted by a nightmare"
|
"is haunted by a nightmare"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "had_nightmare"
|
"statusOverlapId": "had_nightmare",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "slept_outside",
|
"id": "slept_outside",
|
||||||
|
|
@ -665,7 +669,8 @@
|
||||||
"is smitten"
|
"is smitten"
|
||||||
],
|
],
|
||||||
"canonicalMilestoneKey": "milestone_lover",
|
"canonicalMilestoneKey": "milestone_lover",
|
||||||
"statusOverlapId": "fell_in_love"
|
"statusOverlapId": "fell_in_love",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_had_child",
|
"id": "just_had_child",
|
||||||
|
|
@ -756,7 +761,8 @@
|
||||||
"finishes laughing",
|
"finishes laughing",
|
||||||
"had a good laugh"
|
"had a good laugh"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "laughing"
|
"statusOverlapId": "laughing",
|
||||||
|
"statusOverlapKind": "offset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_sang",
|
"id": "just_sang",
|
||||||
|
|
@ -775,7 +781,8 @@
|
||||||
"finishes singing",
|
"finishes singing",
|
||||||
"sings with feeling"
|
"sings with feeling"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "singing"
|
"statusOverlapId": "singing",
|
||||||
|
"statusOverlapKind": "offset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_swore",
|
"id": "just_swore",
|
||||||
|
|
@ -794,7 +801,8 @@
|
||||||
"finishes swearing",
|
"finishes swearing",
|
||||||
"lets out a string of curses"
|
"lets out a string of curses"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "swearing"
|
"statusOverlapId": "swearing",
|
||||||
|
"statusOverlapKind": "offset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_cried",
|
"id": "just_cried",
|
||||||
|
|
@ -813,7 +821,8 @@
|
||||||
"finishes crying",
|
"finishes crying",
|
||||||
"cries it out"
|
"cries it out"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "crying"
|
"statusOverlapId": "crying",
|
||||||
|
"statusOverlapKind": "offset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_talked_gossip",
|
"id": "just_talked_gossip",
|
||||||
|
|
@ -850,7 +859,8 @@
|
||||||
"is startled",
|
"is startled",
|
||||||
"jumps in surprise"
|
"jumps in surprise"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "surprised"
|
"statusOverlapId": "surprised",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_born",
|
"id": "just_born",
|
||||||
|
|
@ -887,7 +897,8 @@
|
||||||
"is magnetized",
|
"is magnetized",
|
||||||
"feels a magnetic pull"
|
"feels a magnetic pull"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "magnetized"
|
"statusOverlapId": "magnetized",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_forced_power",
|
"id": "just_forced_power",
|
||||||
|
|
@ -924,7 +935,8 @@
|
||||||
"is possessed",
|
"is possessed",
|
||||||
"falls under possession"
|
"falls under possession"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "possessed"
|
"statusOverlapId": "possessed",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "strange_urge",
|
"id": "strange_urge",
|
||||||
|
|
@ -943,7 +955,8 @@
|
||||||
"feels a strange urge",
|
"feels a strange urge",
|
||||||
"is gripped by a strange urge"
|
"is gripped by a strange urge"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "strange_urge"
|
"statusOverlapId": "strange_urge",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_had_tantrum",
|
"id": "just_had_tantrum",
|
||||||
|
|
@ -962,7 +975,8 @@
|
||||||
"finishes a tantrum",
|
"finishes a tantrum",
|
||||||
"calms after a tantrum"
|
"calms after a tantrum"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "tantrum"
|
"statusOverlapId": "tantrum",
|
||||||
|
"statusOverlapKind": "offset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_felt_the_divine",
|
"id": "just_felt_the_divine",
|
||||||
|
|
@ -999,7 +1013,8 @@
|
||||||
"is enchanted",
|
"is enchanted",
|
||||||
"feels an enchantment take hold"
|
"feels an enchantment take hold"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "enchanted"
|
"statusOverlapId": "enchanted",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "just_inspired",
|
"id": "just_inspired",
|
||||||
|
|
@ -1018,7 +1033,8 @@
|
||||||
"comes down from inspiration",
|
"comes down from inspiration",
|
||||||
"lets inspiration settle into contentment"
|
"lets inspiration settle into contentment"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "inspired"
|
"statusOverlapId": "inspired",
|
||||||
|
"statusOverlapKind": "offset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "wrote_book",
|
"id": "wrote_book",
|
||||||
|
|
@ -1182,7 +1198,8 @@
|
||||||
"is cursed",
|
"is cursed",
|
||||||
"falls under a curse"
|
"falls under a curse"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "cursed"
|
"statusOverlapId": "cursed",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "starving",
|
"id": "starving",
|
||||||
|
|
@ -1201,7 +1218,8 @@
|
||||||
"is starving",
|
"is starving",
|
||||||
"goes hungry"
|
"goes hungry"
|
||||||
],
|
],
|
||||||
"statusOverlapId": "starving"
|
"statusOverlapId": "starving",
|
||||||
|
"statusOverlapKind": "hold"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "conquered_city",
|
"id": "conquered_city",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.25.47",
|
"version": "0.25.50",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Focus hold repair mid-idle.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Status-overlap camera hold gate.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue