Implement comprehensive happiness tracking features across multiple components. Introduce methods for enumerating happiness IDs, logging happiness events, and managing happiness-related actions in ActivityLog and AgentHarness. Enhance HUD icon handling for happiness effects and add new harness scenarios for testing happiness functionalities. Increment version in mod.json to reflect these changes.
This commit is contained in:
parent
7068ac44c5
commit
892e645872
16 changed files with 3211 additions and 18 deletions
|
|
@ -158,6 +158,51 @@ public static class ActivityAssetCatalog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<string> EnumerateLiveHappinessIds()
|
||||||
|
{
|
||||||
|
var ids = new List<string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HappinessLibrary lib = AssetManager.happiness_library;
|
||||||
|
if (lib?.list == null)
|
||||||
|
{
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < lib.list.Count; i++)
|
||||||
|
{
|
||||||
|
HappinessAsset asset = lib.list[i];
|
||||||
|
if (asset != null && !string.IsNullOrEmpty(asset.id))
|
||||||
|
{
|
||||||
|
ids.Add(asset.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Library can be unavailable during early boot.
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HappinessAsset TryGetHappinessAsset(string effectId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return AssetManager.happiness_library?.get(effectId.Trim());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static List<string> EnumerateLiveTaskIds()
|
public static List<string> EnumerateLiveTaskIds()
|
||||||
{
|
{
|
||||||
var ids = new List<string>();
|
var ids = new List<string>();
|
||||||
|
|
|
||||||
247
IdleSpectator/ActivityHappinessHarness.cs
Normal file
247
IdleSpectator/ActivityHappinessHarness.cs
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
public sealed class ActivityHappinessAuditResult
|
||||||
|
{
|
||||||
|
public bool Passed;
|
||||||
|
public string Detail = "";
|
||||||
|
public int Total;
|
||||||
|
public int MissingAuthored;
|
||||||
|
public int MissingIcon;
|
||||||
|
public int LocaleLeaks;
|
||||||
|
public int EmptyPhrase;
|
||||||
|
public int OrphanAuthored;
|
||||||
|
public int MissingPolicy;
|
||||||
|
public int RequiredContextFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exhaustive live-library audit for happiness Activity prose, icons, and policies.
|
||||||
|
/// </summary>
|
||||||
|
public static class ActivityHappinessHarness
|
||||||
|
{
|
||||||
|
public static ActivityHappinessAuditResult RunAudit(string outputDirectory)
|
||||||
|
{
|
||||||
|
List<string> liveIds = ActivityAssetCatalog.EnumerateLiveHappinessIds();
|
||||||
|
var authored = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (string id in HappinessEventCatalog.AuthoredIds)
|
||||||
|
{
|
||||||
|
authored.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int missingAuthored = 0;
|
||||||
|
int missingIcon = 0;
|
||||||
|
int localeLeaks = 0;
|
||||||
|
int emptyPhrase = 0;
|
||||||
|
int orphanAuthored = 0;
|
||||||
|
int missingPolicy = 0;
|
||||||
|
int requiredContextFailures = 0;
|
||||||
|
var tsv = new StringBuilder();
|
||||||
|
tsv.AppendLine(
|
||||||
|
"id\tvalue\tpsychopath\tpresentation\tlife\tcontext\toverlap\tclause\tauthored\ticon\tlocale_leak\tnotes");
|
||||||
|
|
||||||
|
var liveSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (int i = 0; i < liveIds.Count; i++)
|
||||||
|
{
|
||||||
|
string id = liveIds[i];
|
||||||
|
liveSet.Add(id);
|
||||||
|
HappinessAsset asset = ActivityAssetCatalog.TryGetHappinessAsset(id);
|
||||||
|
bool hasAuthored = HappinessEventCatalog.HasAuthored(id);
|
||||||
|
HappinessCatalogEntry entry = HappinessEventCatalog.GetOrFallback(id);
|
||||||
|
if (!hasAuthored || entry.IsFallback)
|
||||||
|
{
|
||||||
|
missingAuthored++;
|
||||||
|
}
|
||||||
|
|
||||||
|
string clauseWith = ActivityHappinessProse.Clause(entry, "Ava", out bool usedRelated, out _);
|
||||||
|
string clauseWithout = ActivityHappinessProse.Clause(entry, "", out _, out _);
|
||||||
|
if (string.IsNullOrEmpty(clauseWith) || string.IsNullOrEmpty(clauseWithout))
|
||||||
|
{
|
||||||
|
emptyPhrase++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Context == HappinessContextRequirement.RequiresRelatedActor)
|
||||||
|
{
|
||||||
|
if (!usedRelated || clauseWith.IndexOf("Ava", StringComparison.Ordinal) < 0)
|
||||||
|
{
|
||||||
|
requiredContextFailures++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clauseWith.IndexOf("{related}", StringComparison.Ordinal) >= 0
|
||||||
|
|| clauseWithout.IndexOf("{related}", StringComparison.Ordinal) >= 0)
|
||||||
|
{
|
||||||
|
requiredContextFailures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Presentation == 0 && entry.Life == 0 && string.IsNullOrEmpty(entry.InterestCategory))
|
||||||
|
{
|
||||||
|
missingPolicy++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(entry.InterestCategory)
|
||||||
|
|| entry.VariantsWithoutRelated == null
|
||||||
|
|| entry.VariantsWithoutRelated.Length == 0)
|
||||||
|
{
|
||||||
|
missingPolicy++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite icon = HudIcons.FromHappinessId(id)
|
||||||
|
?? HudIcons.FromUiIcon("iconHappy")
|
||||||
|
?? HudIcons.Task();
|
||||||
|
bool iconOk = icon != null;
|
||||||
|
if (!iconOk)
|
||||||
|
{
|
||||||
|
missingIcon++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool leak = LooksLikeLocaleLeak(clauseWith, id) || LooksLikeLocaleLeak(clauseWithout, id);
|
||||||
|
if (leak)
|
||||||
|
{
|
||||||
|
localeLeaks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = 0;
|
||||||
|
string psycho = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (asset != null)
|
||||||
|
{
|
||||||
|
value = asset.value;
|
||||||
|
psycho = asset.ignored_by_psychopaths ? "1" : "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
string notes = "ok";
|
||||||
|
if (!hasAuthored || entry.IsFallback)
|
||||||
|
{
|
||||||
|
notes = "missing_authored";
|
||||||
|
}
|
||||||
|
else if (!iconOk)
|
||||||
|
{
|
||||||
|
notes = "missing_icon";
|
||||||
|
}
|
||||||
|
else if (leak)
|
||||||
|
{
|
||||||
|
notes = "locale_leak";
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(clauseWith) || string.IsNullOrEmpty(clauseWithout))
|
||||||
|
{
|
||||||
|
notes = "empty_phrase";
|
||||||
|
}
|
||||||
|
else if (entry.Context == HappinessContextRequirement.RequiresRelatedActor
|
||||||
|
&& (!usedRelated || clauseWith.IndexOf("Ava", StringComparison.Ordinal) < 0))
|
||||||
|
{
|
||||||
|
notes = "required_context";
|
||||||
|
}
|
||||||
|
|
||||||
|
tsv.Append(id).Append('\t')
|
||||||
|
.Append(value).Append('\t')
|
||||||
|
.Append(psycho).Append('\t')
|
||||||
|
.Append(entry.Presentation).Append('\t')
|
||||||
|
.Append(entry.Life).Append('\t')
|
||||||
|
.Append(entry.Context).Append('\t')
|
||||||
|
.Append(entry.Overlap).Append('\t')
|
||||||
|
.Append(Escape(clauseWith)).Append('\t')
|
||||||
|
.Append(hasAuthored ? "1" : "0").Append('\t')
|
||||||
|
.Append(iconOk ? "1" : "0").Append('\t')
|
||||||
|
.Append(leak ? "1" : "0").Append('\t')
|
||||||
|
.Append(notes).AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string id in authored)
|
||||||
|
{
|
||||||
|
if (!liveSet.Contains(id))
|
||||||
|
{
|
||||||
|
orphanAuthored++;
|
||||||
|
tsv.Append(id).Append('\t')
|
||||||
|
.Append("\t\t\t\t\t\t\t1\t0\t0\torphan_authored").AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(outputDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(outputDirectory);
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(outputDirectory, "activity-happiness-audit.tsv"),
|
||||||
|
tsv.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Disk may be unavailable in some harness contexts.
|
||||||
|
}
|
||||||
|
|
||||||
|
bool passed = missingAuthored == 0
|
||||||
|
&& missingIcon == 0
|
||||||
|
&& localeLeaks == 0
|
||||||
|
&& emptyPhrase == 0
|
||||||
|
&& orphanAuthored == 0
|
||||||
|
&& missingPolicy == 0
|
||||||
|
&& requiredContextFailures == 0;
|
||||||
|
|
||||||
|
return new ActivityHappinessAuditResult
|
||||||
|
{
|
||||||
|
Passed = passed,
|
||||||
|
Total = liveIds.Count,
|
||||||
|
MissingAuthored = missingAuthored,
|
||||||
|
MissingIcon = missingIcon,
|
||||||
|
LocaleLeaks = localeLeaks,
|
||||||
|
EmptyPhrase = emptyPhrase,
|
||||||
|
OrphanAuthored = orphanAuthored,
|
||||||
|
MissingPolicy = missingPolicy,
|
||||||
|
RequiredContextFailures = requiredContextFailures,
|
||||||
|
Detail =
|
||||||
|
$"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} "
|
||||||
|
+ $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} "
|
||||||
|
+ $"missingPolicy={missingPolicy} requiredContextFailures={requiredContextFailures}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeLocaleLeak(string text, string id)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.IndexOf("happiness_", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(id)
|
||||||
|
&& text.Equals(id, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.IndexOf('{') >= 0 || text.IndexOf('}') >= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
96
IdleSpectator/ActivityHappinessProse.cs
Normal file
96
IdleSpectator/ActivityHappinessProse.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
/// <summary>Builds target-aware happiness clauses from the authored catalog.</summary>
|
||||||
|
public static class ActivityHappinessProse
|
||||||
|
{
|
||||||
|
public static string TaskKey(string effectId)
|
||||||
|
{
|
||||||
|
return "happiness:" + (effectId ?? "").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseTaskKey(string key, out string effectId)
|
||||||
|
{
|
||||||
|
effectId = "";
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string prefix = "happiness:";
|
||||||
|
if (key.Length <= prefix.Length
|
||||||
|
|| !key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
effectId = key.Substring(prefix.Length).Trim();
|
||||||
|
return !string.IsNullOrEmpty(effectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Clause(
|
||||||
|
HappinessCatalogEntry entry,
|
||||||
|
string relatedName,
|
||||||
|
out bool usedRelated,
|
||||||
|
out bool authored)
|
||||||
|
{
|
||||||
|
usedRelated = false;
|
||||||
|
authored = entry != null && !entry.IsFallback && HappinessEventCatalog.HasAuthored(entry.Id);
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
return "feels a change in happiness";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasRelated = !string.IsNullOrEmpty(relatedName);
|
||||||
|
string[] variants = hasRelated && entry.VariantsWithRelated != null && entry.VariantsWithRelated.Length > 0
|
||||||
|
? entry.VariantsWithRelated
|
||||||
|
: entry.VariantsWithoutRelated;
|
||||||
|
if (variants == null || variants.Length == 0)
|
||||||
|
{
|
||||||
|
variants = entry.VariantsWithoutRelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variants == null || variants.Length == 0)
|
||||||
|
{
|
||||||
|
return "feels a change in happiness";
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = Math.Abs((entry.Id + (relatedName ?? "")).GetHashCode()) % variants.Length;
|
||||||
|
string template = variants[idx] ?? "";
|
||||||
|
usedRelated = hasRelated && template.IndexOf("{related}", StringComparison.Ordinal) >= 0;
|
||||||
|
return ApplyTemplate(template, subjectName: "", relatedName: relatedName ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ApplyTemplate(string template, string subjectName, string relatedName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(template))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
string result = template;
|
||||||
|
if (!string.IsNullOrEmpty(subjectName))
|
||||||
|
{
|
||||||
|
result = result.Replace("{subject}", subjectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(relatedName))
|
||||||
|
{
|
||||||
|
result = result.Replace("{related}", relatedName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Strip unresolved related placeholders defensively.
|
||||||
|
result = result.Replace("{related}", "someone");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FallbackClause(string effectId)
|
||||||
|
{
|
||||||
|
string id = string.IsNullOrEmpty(effectId) ? "unknown" : effectId.Replace('_', ' ');
|
||||||
|
return "feels " + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,8 @@ public enum ActivityKind
|
||||||
TaskStart,
|
TaskStart,
|
||||||
ActionBeat,
|
ActionBeat,
|
||||||
StatusGain,
|
StatusGain,
|
||||||
StatusLoss
|
StatusLoss,
|
||||||
|
Happiness
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ActivityEntry
|
public sealed class ActivityEntry
|
||||||
|
|
@ -58,6 +59,8 @@ public static class ActivityLog
|
||||||
private static int _statusRefreshesSinceClear;
|
private static int _statusRefreshesSinceClear;
|
||||||
private static string _lastStatusGainId = "";
|
private static string _lastStatusGainId = "";
|
||||||
private static string _lastStatusLossId = "";
|
private static string _lastStatusLossId = "";
|
||||||
|
private static int _happinessSinceClear;
|
||||||
|
private static string _lastHappinessId = "";
|
||||||
/// <summary>When non-zero, status counters only track this subject (harness isolation).</summary>
|
/// <summary>When non-zero, status counters only track this subject (harness isolation).</summary>
|
||||||
private static long _statusCounterSubjectId;
|
private static long _statusCounterSubjectId;
|
||||||
private static float _clearedAt = -1f;
|
private static float _clearedAt = -1f;
|
||||||
|
|
@ -73,6 +76,8 @@ public static class ActivityLog
|
||||||
public static int StatusRefreshesSinceClear => _statusRefreshesSinceClear;
|
public static int StatusRefreshesSinceClear => _statusRefreshesSinceClear;
|
||||||
public static string LastStatusGainId => _lastStatusGainId ?? "";
|
public static string LastStatusGainId => _lastStatusGainId ?? "";
|
||||||
public static string LastStatusLossId => _lastStatusLossId ?? "";
|
public static string LastStatusLossId => _lastStatusLossId ?? "";
|
||||||
|
public static int HappinessSinceClear => _happinessSinceClear;
|
||||||
|
public static string LastHappinessId => _lastHappinessId ?? "";
|
||||||
public static int VerbSuppressedSinceClear => _verbSuppressedSinceClear;
|
public static int VerbSuppressedSinceClear => _verbSuppressedSinceClear;
|
||||||
|
|
||||||
public static void ClearSession()
|
public static void ClearSession()
|
||||||
|
|
@ -84,6 +89,7 @@ public static class ActivityLog
|
||||||
LastLoggedVerbAt.Clear();
|
LastLoggedVerbAt.Clear();
|
||||||
LineIndex.Clear();
|
LineIndex.Clear();
|
||||||
ResetSampleCounters();
|
ResetSampleCounters();
|
||||||
|
HappinessEventRouter.ClearSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Harness: reset rate counters without wiping the ring.</summary>
|
/// <summary>Harness: reset rate counters without wiping the ring.</summary>
|
||||||
|
|
@ -98,6 +104,8 @@ public static class ActivityLog
|
||||||
_verbSuppressedSinceClear = 0;
|
_verbSuppressedSinceClear = 0;
|
||||||
_lastStatusGainId = "";
|
_lastStatusGainId = "";
|
||||||
_lastStatusLossId = "";
|
_lastStatusLossId = "";
|
||||||
|
_happinessSinceClear = 0;
|
||||||
|
_lastHappinessId = "";
|
||||||
_clearedAt = Time.unscaledTime;
|
_clearedAt = Time.unscaledTime;
|
||||||
TaskHitsSinceClear.Clear();
|
TaskHitsSinceClear.Clear();
|
||||||
}
|
}
|
||||||
|
|
@ -568,6 +576,14 @@ public static class ActivityLog
|
||||||
}
|
}
|
||||||
|
|
||||||
ForceStatusNote(id, statusId.Trim(), gained, actorName);
|
ForceStatusNote(id, statusId.Trim(), gained, actorName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteStatusPhase(id, statusId.Trim());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Harness / forced path for status lines without a live transition.</summary>
|
/// <summary>Harness / forced path for status lines without a live transition.</summary>
|
||||||
|
|
@ -625,6 +641,80 @@ public static class ActivityLog
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Happiness / emotion Activity line (no beat cooldown). Presentation already decided by router.</summary>
|
||||||
|
public static void NoteHappiness(HappinessOccurrence occ)
|
||||||
|
{
|
||||||
|
if (occ == null || occ.SubjectId == 0 || string.IsNullOrEmpty(occ.EffectId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string name = string.IsNullOrEmpty(occ.SubjectName) ? "Someone" : occ.SubjectName;
|
||||||
|
string clause = occ.PlainClause ?? "";
|
||||||
|
string richClause = !string.IsNullOrEmpty(occ.RichClause) ? occ.RichClause : clause;
|
||||||
|
string display = string.IsNullOrEmpty(clause) ? name : name + " " + clause;
|
||||||
|
string displayRich = ActivityProse.ColorName(name)
|
||||||
|
+ (string.IsNullOrEmpty(richClause) ? "" : " " + richClause);
|
||||||
|
string key = ActivityHappinessProse.TaskKey(occ.EffectId);
|
||||||
|
string fact = !string.IsNullOrEmpty(occ.FactLine) ? occ.FactLine : ("Happiness " + occ.EffectId);
|
||||||
|
|
||||||
|
if (_statusCounterSubjectId == 0 || occ.SubjectId == _statusCounterSubjectId)
|
||||||
|
{
|
||||||
|
_happinessSinceClear++;
|
||||||
|
_lastHappinessId = occ.EffectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordSample(key, isBeat: true);
|
||||||
|
Append(occ.SubjectId, new ActivityEntry
|
||||||
|
{
|
||||||
|
CreatedAt = Time.unscaledTime,
|
||||||
|
SubjectId = occ.SubjectId,
|
||||||
|
Kind = ActivityKind.Happiness,
|
||||||
|
TaskId = key,
|
||||||
|
JobId = occ.Amount.ToString(),
|
||||||
|
TargetLabel = !string.IsNullOrEmpty(occ.RelatedName) ? occ.RelatedName : occ.EffectId,
|
||||||
|
Line = fact,
|
||||||
|
DisplayLine = display,
|
||||||
|
DisplayLineRich = displayRich
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Harness: inject a happiness ring line without a live game apply.</summary>
|
||||||
|
public static bool ForceHappinessNote(
|
||||||
|
long subjectId,
|
||||||
|
string effectId,
|
||||||
|
string actorName = "",
|
||||||
|
string relatedName = "",
|
||||||
|
int amount = 0)
|
||||||
|
{
|
||||||
|
if (subjectId == 0 || string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessCatalogEntry entry = HappinessEventCatalog.GetOrFallback(effectId);
|
||||||
|
string clause = ActivityHappinessProse.Clause(entry, relatedName, out bool usedRelated, out _);
|
||||||
|
string rich = usedRelated && !string.IsNullOrEmpty(relatedName)
|
||||||
|
? clause.Replace(relatedName, ActivityProse.ColorName(relatedName))
|
||||||
|
: clause;
|
||||||
|
var occ = new HappinessOccurrence
|
||||||
|
{
|
||||||
|
EffectId = effectId.Trim(),
|
||||||
|
Amount = amount != 0 ? amount : HappinessGameApi.ResolveAmount(effectId),
|
||||||
|
SubjectId = subjectId,
|
||||||
|
SubjectName = string.IsNullOrEmpty(actorName) ? "Someone" : actorName,
|
||||||
|
RelatedName = relatedName ?? "",
|
||||||
|
PlainClause = clause,
|
||||||
|
RichClause = rich,
|
||||||
|
FactLine = "Happiness " + effectId.Trim(),
|
||||||
|
Presentation = entry.Presentation,
|
||||||
|
Applied = true,
|
||||||
|
SourceHook = HappinessSourceHook.Harness
|
||||||
|
};
|
||||||
|
NoteHappiness(occ);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static string LowerFirst(string text)
|
private static string LowerFirst(string text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
|
|
|
||||||
143
IdleSpectator/ActorHappinessPatches.cs
Normal file
143
IdleSpectator/ActorHappinessPatches.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
using HarmonyLib;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authoritative happiness hooks. Universal changeHappiness covers every effect id;
|
||||||
|
/// death/birth patches supply related-unit context the history struct lacks.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPatch]
|
||||||
|
internal static class ActorHappinessPatches
|
||||||
|
{
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.changeHappiness))]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
public static void PostfixChangeHappiness(Actor __instance, string pID, int pValue, bool __result)
|
||||||
|
{
|
||||||
|
if (__instance == null || string.IsNullOrEmpty(pID))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!__result)
|
||||||
|
{
|
||||||
|
HappinessEventRouter.PublishSuppressed(
|
||||||
|
__instance, pID, HappinessSourceHook.ChangeHappiness);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Actor related = null;
|
||||||
|
HappinessRelationRole role = HappinessRelationRole.None;
|
||||||
|
HappinessSourceHook hook = HappinessSourceHook.ChangeHappiness;
|
||||||
|
|
||||||
|
if (HappinessContextBag.TryGetRelated(__instance, out Actor bagRelated, out HappinessRelationRole bagRole))
|
||||||
|
{
|
||||||
|
related = bagRelated;
|
||||||
|
role = bagRole != HappinessRelationRole.None
|
||||||
|
? bagRole
|
||||||
|
: HappinessContextBag.RoleForDeathEffect(pID);
|
||||||
|
if (pID != null && pID.StartsWith("death_"))
|
||||||
|
{
|
||||||
|
hook = HappinessSourceHook.DeathEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pID != null && pID.StartsWith("death_"))
|
||||||
|
{
|
||||||
|
// Death event without bag - still log survivor-centered subject-only if required fails.
|
||||||
|
role = HappinessContextBag.RoleForDeathEffect(pID);
|
||||||
|
hook = HappinessSourceHook.DeathEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
int amount = HappinessGameApi.ResolveAmount(pID, pValue);
|
||||||
|
HappinessEventRouter.PublishApplied(__instance, pID, amount, hook, related, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), "checkHappinessChangeFromDeathEvent")]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static void PrefixDeathHappiness(Actor __instance)
|
||||||
|
{
|
||||||
|
HappinessContextBag.BeginDeathEvent(__instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), "checkHappinessChangeFromDeathEvent")]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
public static void PostfixDeathHappiness()
|
||||||
|
{
|
||||||
|
HappinessContextBag.EndDeathEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.birthEvent))]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static void PrefixBirthEvent(Actor __instance)
|
||||||
|
{
|
||||||
|
// birthEvent runs on the parent; related newborn is not passed.
|
||||||
|
// Mark role so prose can stay subject-aware even without a name.
|
||||||
|
HappinessContextBag.SetRelated(null, HappinessRelationRole.Newborn, "just_had_child");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.birthEvent))]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
public static void PostfixBirthEvent()
|
||||||
|
{
|
||||||
|
HappinessContextBag.ClearRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.becomeLoversWith))]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static void PrefixLovers(Actor __instance, Actor pTarget)
|
||||||
|
{
|
||||||
|
HappinessContextBag.SetPair(__instance, pTarget, HappinessRelationRole.Lover, "fallen_in_love");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (__instance != null)
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(__instance.getID(), "milestone_lover");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pTarget != null)
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(pTarget.getID(), "milestone_lover");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.becomeLoversWith))]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
public static void PostfixLovers()
|
||||||
|
{
|
||||||
|
HappinessContextBag.ClearRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setBestFriend))]
|
||||||
|
[HarmonyPrefix]
|
||||||
|
public static void PrefixBestFriend(Actor __instance, Actor pActor, bool pNew)
|
||||||
|
{
|
||||||
|
if (!pNew)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessContextBag.SetRelated(pActor, HappinessRelationRole.BestFriend, "just_made_friend");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (__instance != null)
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(__instance.getID(), "milestone_friend");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setBestFriend))]
|
||||||
|
[HarmonyPostfix]
|
||||||
|
public static void PostfixBestFriend()
|
||||||
|
{
|
||||||
|
HappinessContextBag.ClearRelated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ public static class AgentHarness
|
||||||
private static readonly Queue<HarnessCommand> Queue = new Queue<HarnessCommand>();
|
private static readonly Queue<HarnessCommand> Queue = new Queue<HarnessCommand>();
|
||||||
private static Actor _lastSpawned;
|
private static Actor _lastSpawned;
|
||||||
private static string _lastSpawnedAssetId = "";
|
private static string _lastSpawnedAssetId = "";
|
||||||
|
private static Actor _happinessPartner;
|
||||||
private static bool _directorRunActive;
|
private static bool _directorRunActive;
|
||||||
private static string _rememberedFocusKey = "";
|
private static string _rememberedFocusKey = "";
|
||||||
private static string LastScreenshotPath = "";
|
private static string LastScreenshotPath = "";
|
||||||
|
|
@ -705,6 +706,358 @@ public static class AgentHarness
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "happiness_reset":
|
||||||
|
{
|
||||||
|
long id = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||||
|
{
|
||||||
|
id = MoveCamera._focus_unit.getID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
id = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id == 0)
|
||||||
|
{
|
||||||
|
id = WatchCaption.CurrentUnitId;
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessEventRouter.ResetCountersForSubject(id);
|
||||||
|
ActivityLog.ResetSampleCounters();
|
||||||
|
_cmdOk++;
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok: true,
|
||||||
|
detail:
|
||||||
|
$"subject={id} notes={HappinessEventRouter.NotesSinceClear} "
|
||||||
|
+ $"ring={HappinessEventRouter.RingWritesSinceClear}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_apply":
|
||||||
|
{
|
||||||
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate";
|
||||||
|
int bonus = 0;
|
||||||
|
if (!string.IsNullOrEmpty(cmd.label)
|
||||||
|
&& int.TryParse(cmd.label, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedBonus))
|
||||||
|
{
|
||||||
|
bonus = parsedBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool expectBlocked = (cmd.expect ?? "").IndexOf("block", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
bool alive = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
alive = focus != null && focus.isAlive();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applied = alive && HappinessGameApi.TryChangeHappiness(focus, effectId, bonus);
|
||||||
|
bool ok = expectBlocked ? !applied : applied;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchCaption.ForceRefreshHistory();
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"effect={effectId} applied={applied} expectBlocked={expectBlocked} "
|
||||||
|
+ $"notes={HappinessEventRouter.NotesSinceClear} ring={HappinessEventRouter.RingWritesSinceClear} "
|
||||||
|
+ $"last={HappinessEventRouter.LastEffectId}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_force_note":
|
||||||
|
{
|
||||||
|
long id = WatchCaption.CurrentUnitId;
|
||||||
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
if (id == 0 && focus != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
id = focus.getID();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate";
|
||||||
|
string related = cmd.label ?? "";
|
||||||
|
string actorName = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (focus != null)
|
||||||
|
{
|
||||||
|
actorName = focus.getName() ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
actorName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = ActivityLog.ForceHappinessNote(id, effectId, actorName, related);
|
||||||
|
WatchCaption.ForceRefreshHistory();
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(cmd, ok, detail: $"forced happiness={effectId} related={related} id={id}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_inject_all":
|
||||||
|
{
|
||||||
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
bool alive = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
alive = focus != null && focus.isAlive();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alive)
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
Emit(cmd, ok: false, detail: "no living focus");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> ids = ActivityAssetCatalog.EnumerateLiveHappinessIds();
|
||||||
|
int applied = 0;
|
||||||
|
int blocked = 0;
|
||||||
|
for (int i = 0; i < ids.Count; i++)
|
||||||
|
{
|
||||||
|
if (HappinessGameApi.TryChangeHappiness(focus, ids[i], 0))
|
||||||
|
{
|
||||||
|
applied++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
blocked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchCaption.ForceRefreshHistory();
|
||||||
|
bool ok = ids.Count > 0 && applied + blocked == ids.Count;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"live={ids.Count} applied={applied} blocked={blocked} "
|
||||||
|
+ $"notes={HappinessEventRouter.NotesSinceClear}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_remember_partner":
|
||||||
|
{
|
||||||
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
|
||||||
|
_happinessPartner = focus;
|
||||||
|
bool ok = _happinessPartner != null;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(cmd, ok, detail: ok ? $"partner={SafeName(_happinessPartner)}" : "no partner");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_bond_lovers":
|
||||||
|
{
|
||||||
|
Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
Actor b = _happinessPartner;
|
||||||
|
bool ok = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (a != null && b != null && a.isAlive() && b.isAlive() && a != b)
|
||||||
|
{
|
||||||
|
a.becomeLoversWith(b);
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchCaption.ForceRefreshHistory();
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"a={SafeName(a)} b={SafeName(b)} notes={HappinessEventRouter.NotesSinceClear}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_bond_friends":
|
||||||
|
{
|
||||||
|
Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
Actor b = _happinessPartner;
|
||||||
|
bool ok = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (a != null && b != null && a.isAlive() && b.isAlive() && a != b)
|
||||||
|
{
|
||||||
|
a.setBestFriend(b, true);
|
||||||
|
b.setBestFriend(a, true);
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchCaption.ForceRefreshHistory();
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"a={SafeName(a)} b={SafeName(b)} notes={HappinessEventRouter.NotesSinceClear}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_kill_partner":
|
||||||
|
{
|
||||||
|
Actor partner = _happinessPartner;
|
||||||
|
bool ok = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (partner != null && partner.isAlive())
|
||||||
|
{
|
||||||
|
partner.die(true, AttackType.Divine);
|
||||||
|
ok = !partner.isAlive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchCaption.ForceRefreshHistory();
|
||||||
|
Emit(cmd, ok, detail: $"killed={SafeName(partner)} ok={ok}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "happiness_burst":
|
||||||
|
{
|
||||||
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_started_war";
|
||||||
|
int n = Math.Max(1, cmd.count > 0 ? cmd.count : 64);
|
||||||
|
bool alive = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
alive = focus != null && focus.isAlive();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alive)
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
Emit(cmd, ok: false, detail: "no living focus");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
float t0 = Time.realtimeSinceStartup;
|
||||||
|
int applied = 0;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
if (HappinessGameApi.TryChangeHappiness(focus, effectId, 0))
|
||||||
|
{
|
||||||
|
applied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float ms = (Time.realtimeSinceStartup - t0) * 1000f;
|
||||||
|
HappinessEventRouter.FlushAggregatesNow();
|
||||||
|
bool ok = applied > 0 && ms < 250f;
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok,
|
||||||
|
detail:
|
||||||
|
$"effect={effectId} n={n} applied={applied} ms={ms:0.0} "
|
||||||
|
+ $"agg={HappinessEventRouter.AggregateSummariesSinceClear}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "kill_focus":
|
case "kill_focus":
|
||||||
{
|
{
|
||||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
|
|
@ -2202,7 +2555,7 @@ public static class AgentHarness
|
||||||
}
|
}
|
||||||
case "activity_log_contains":
|
case "activity_log_contains":
|
||||||
{
|
{
|
||||||
long id = WatchCaption.CurrentUnitId;
|
long id = ResolveActivitySubjectId();
|
||||||
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||||
IReadOnlyList<ActivityEntry> entries =
|
IReadOnlyList<ActivityEntry> entries =
|
||||||
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
|
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
|
||||||
|
|
@ -2232,12 +2585,12 @@ public static class AgentHarness
|
||||||
}
|
}
|
||||||
|
|
||||||
pass = hit;
|
pass = hit;
|
||||||
detail = $"hit={hit} needle='{needle}' sample='{sample}' count={entries.Count}";
|
detail = $"hit={hit} needle='{needle}' sample='{sample}' count={entries.Count} id={id}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "activity_log_not_contains":
|
case "activity_log_not_contains":
|
||||||
{
|
{
|
||||||
long id = WatchCaption.CurrentUnitId;
|
long id = ResolveActivitySubjectId();
|
||||||
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||||
IReadOnlyList<ActivityEntry> entries =
|
IReadOnlyList<ActivityEntry> entries =
|
||||||
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
|
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
|
||||||
|
|
@ -2718,6 +3071,66 @@ public static class AgentHarness
|
||||||
detail = audit.Detail;
|
detail = audit.Detail;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "activity_happiness_audit":
|
||||||
|
{
|
||||||
|
ActivityHappinessAuditResult audit = ActivityHappinessHarness.RunAudit(HarnessDir());
|
||||||
|
pass = audit.Passed;
|
||||||
|
detail = audit.Detail;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "activity_happiness_count":
|
||||||
|
{
|
||||||
|
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||||
|
int got = HappinessEventRouter.NotesSinceClear;
|
||||||
|
string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant();
|
||||||
|
pass = mode == "min" ? got >= want : got == want;
|
||||||
|
detail = $"notes={got} want={want} mode={mode} last={HappinessEventRouter.LastEffectId}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "activity_happiness_ring_count":
|
||||||
|
{
|
||||||
|
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||||
|
int got = HappinessEventRouter.RingWritesSinceClear;
|
||||||
|
string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant();
|
||||||
|
pass = mode == "min" ? got >= want : got == want;
|
||||||
|
detail = $"ring={got} want={want} mode={mode}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "activity_happiness_life_count":
|
||||||
|
{
|
||||||
|
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||||
|
int got = HappinessEventRouter.LifeWritesSinceClear;
|
||||||
|
string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant();
|
||||||
|
pass = mode == "min" ? got >= want : got == want;
|
||||||
|
detail = $"life={got} want={want} mode={mode}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "activity_happiness_task_id":
|
||||||
|
{
|
||||||
|
long id = ResolveActivitySubjectId();
|
||||||
|
|
||||||
|
string want = cmd.value ?? "";
|
||||||
|
string got = "";
|
||||||
|
IReadOnlyList<ActivityEntry> list = ActivityLog.LatestForSubject(id, 1);
|
||||||
|
if (list != null && list.Count > 0 && list[0] != null)
|
||||||
|
{
|
||||||
|
got = list[0].TaskId ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pass = !string.IsNullOrEmpty(want)
|
||||||
|
&& got.Equals(want, StringComparison.OrdinalIgnoreCase);
|
||||||
|
detail = $"got='{got}' want='{want}' id={id}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "activity_happiness_merged":
|
||||||
|
{
|
||||||
|
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||||
|
int got = HappinessEventRouter.MergedSinceClear;
|
||||||
|
string mode = (cmd.label ?? "min").Trim().ToLowerInvariant();
|
||||||
|
pass = mode == "exact" ? got == want : got >= want;
|
||||||
|
detail = $"merged={got} want={want} mode={mode}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "activity_status_gain_count":
|
case "activity_status_gain_count":
|
||||||
{
|
{
|
||||||
int want = ParseCountExpect(cmd, defaultValue: 1);
|
int want = ParseCountExpect(cmd, defaultValue: 1);
|
||||||
|
|
@ -4268,6 +4681,29 @@ public static class AgentHarness
|
||||||
return SafeName(MoveCamera._focus_unit);
|
return SafeName(MoveCamera._focus_unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long ResolveActivitySubjectId()
|
||||||
|
{
|
||||||
|
// Prefer the camera focus (harness apply/assert target) over watch caption,
|
||||||
|
// which can still point at a prior spectator subject during nested regression.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
|
||||||
|
{
|
||||||
|
long focusId = MoveCamera._focus_unit.getID();
|
||||||
|
if (focusId != 0)
|
||||||
|
{
|
||||||
|
return focusId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return WatchCaption.CurrentUnitId;
|
||||||
|
}
|
||||||
|
|
||||||
private static string SafeName(Actor actor)
|
private static string SafeName(Actor actor)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -1228,6 +1228,14 @@ public static class Chronicle
|
||||||
string victimName = SafeName(victim);
|
string victimName = SafeName(victim);
|
||||||
string line = $"Killed {victimName} ({SpeciesOf(victim)})";
|
string line = $"Killed {victimName} ({SpeciesOf(victim)})";
|
||||||
ActivityLog.NoteMilestone(killer, "milestone_kill", line, victimName);
|
ActivityLog.NoteMilestone(killer, "milestone_kill", line, victimName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(killer.getID(), "milestone_kill");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
if (ModSettings.ChronicleEnabled)
|
if (ModSettings.ChronicleEnabled)
|
||||||
{
|
{
|
||||||
AppendHistory(ChronicleKind.Kill, killer, victim, line);
|
AppendHistory(ChronicleKind.Kill, killer, victim, line);
|
||||||
|
|
@ -1287,6 +1295,15 @@ public static class Chronicle
|
||||||
string lineB = $"Became lovers with {nameA}";
|
string lineB = $"Became lovers with {nameA}";
|
||||||
ActivityLog.NoteMilestone(a, "milestone_lover", lineA, nameB);
|
ActivityLog.NoteMilestone(a, "milestone_lover", lineA, nameB);
|
||||||
ActivityLog.NoteMilestone(b, "milestone_lover", lineB, nameA);
|
ActivityLog.NoteMilestone(b, "milestone_lover", lineB, nameA);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(a.getID(), "milestone_lover");
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(b.getID(), "milestone_lover");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
if (ModSettings.ChronicleEnabled)
|
if (ModSettings.ChronicleEnabled)
|
||||||
{
|
{
|
||||||
AppendHistory(ChronicleKind.Lover, a, b, lineA);
|
AppendHistory(ChronicleKind.Lover, a, b, lineA);
|
||||||
|
|
@ -1315,6 +1332,15 @@ public static class Chronicle
|
||||||
string lineB = $"Befriended {nameA}";
|
string lineB = $"Befriended {nameA}";
|
||||||
ActivityLog.NoteMilestone(a, "milestone_friend", lineA, nameB);
|
ActivityLog.NoteMilestone(a, "milestone_friend", lineA, nameB);
|
||||||
ActivityLog.NoteMilestone(b, "milestone_friend", lineB, nameA);
|
ActivityLog.NoteMilestone(b, "milestone_friend", lineB, nameA);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(a.getID(), "milestone_friend");
|
||||||
|
HappinessEventRouter.NoteCanonicalMilestone(b.getID(), "milestone_friend");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
if (ModSettings.ChronicleEnabled)
|
if (ModSettings.ChronicleEnabled)
|
||||||
{
|
{
|
||||||
AppendHistory(ChronicleKind.Friend, a, b, lineA);
|
AppendHistory(ChronicleKind.Friend, a, b, lineA);
|
||||||
|
|
@ -1322,6 +1348,27 @@ public static class Chronicle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Durable happiness Life projection (birth, grief, roles, housing, book/plot/injury).
|
||||||
|
/// Canonical lover/friend/kill milestones stay on their own paths.
|
||||||
|
/// </summary>
|
||||||
|
public static bool NoteHappinessLife(HappinessOccurrence occ, Actor subject, Actor related = null)
|
||||||
|
{
|
||||||
|
if (occ == null || subject == null || string.IsNullOrEmpty(occ.PlainClause))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ModSettings.ChronicleEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string line = char.ToUpperInvariant(occ.PlainClause[0]) + occ.PlainClause.Substring(1);
|
||||||
|
AppendHistory(ChronicleKind.Other, subject, related, line);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static void NoteWorldLog(WorldLogMessage message, string label)
|
public static void NoteWorldLog(WorldLogMessage message, string label)
|
||||||
{
|
{
|
||||||
if (!ModSettings.ChronicleEnabled || message == null || string.IsNullOrEmpty(label))
|
if (!ModSettings.ChronicleEnabled || message == null || string.IsNullOrEmpty(label))
|
||||||
|
|
|
||||||
128
IdleSpectator/HappinessContextBag.cs
Normal file
128
IdleSpectator/HappinessContextBag.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Short-lived apply-site context for happiness hooks that do not retain related units in history.
|
||||||
|
/// </summary>
|
||||||
|
internal static class HappinessContextBag
|
||||||
|
{
|
||||||
|
private static Actor _deceased;
|
||||||
|
private static Actor _relatedA;
|
||||||
|
private static Actor _relatedB;
|
||||||
|
private static HappinessRelationRole _role;
|
||||||
|
private static string _effectHint;
|
||||||
|
|
||||||
|
public static void BeginDeathEvent(Actor deceased)
|
||||||
|
{
|
||||||
|
_deceased = deceased;
|
||||||
|
_relatedA = deceased;
|
||||||
|
_relatedB = null;
|
||||||
|
_role = HappinessRelationRole.None;
|
||||||
|
_effectHint = "death";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EndDeathEvent()
|
||||||
|
{
|
||||||
|
_deceased = null;
|
||||||
|
_relatedA = null;
|
||||||
|
_relatedB = null;
|
||||||
|
_role = HappinessRelationRole.None;
|
||||||
|
_effectHint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetRelated(Actor related, HappinessRelationRole role, string effectHint = null)
|
||||||
|
{
|
||||||
|
_relatedA = related;
|
||||||
|
_relatedB = null;
|
||||||
|
_role = role;
|
||||||
|
if (!string.IsNullOrEmpty(effectHint))
|
||||||
|
{
|
||||||
|
_effectHint = effectHint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetPair(Actor a, Actor b, HappinessRelationRole role, string effectHint = null)
|
||||||
|
{
|
||||||
|
_relatedA = a;
|
||||||
|
_relatedB = b;
|
||||||
|
_role = role;
|
||||||
|
if (!string.IsNullOrEmpty(effectHint))
|
||||||
|
{
|
||||||
|
_effectHint = effectHint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearRelated()
|
||||||
|
{
|
||||||
|
_relatedA = null;
|
||||||
|
_relatedB = null;
|
||||||
|
_role = HappinessRelationRole.None;
|
||||||
|
_effectHint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Actor CurrentDeceased => _deceased;
|
||||||
|
|
||||||
|
public static bool TryGetRelated(Actor subject, out Actor related, out HappinessRelationRole role)
|
||||||
|
{
|
||||||
|
related = null;
|
||||||
|
role = _role;
|
||||||
|
if (subject == null)
|
||||||
|
{
|
||||||
|
related = _relatedA ?? _deceased;
|
||||||
|
return related != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_deceased != null)
|
||||||
|
{
|
||||||
|
related = _deceased;
|
||||||
|
role = role != HappinessRelationRole.None
|
||||||
|
? role
|
||||||
|
: HappinessRelationRole.None;
|
||||||
|
return related != null && related != subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_relatedA != null && _relatedB != null)
|
||||||
|
{
|
||||||
|
if (subject == _relatedA)
|
||||||
|
{
|
||||||
|
related = _relatedB;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subject == _relatedB)
|
||||||
|
{
|
||||||
|
related = _relatedA;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_relatedA != null && _relatedA != subject)
|
||||||
|
{
|
||||||
|
related = _relatedA;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HappinessRelationRole RoleForDeathEffect(string effectId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return HappinessRelationRole.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (effectId)
|
||||||
|
{
|
||||||
|
case "death_child":
|
||||||
|
return HappinessRelationRole.DeceasedChild;
|
||||||
|
case "death_lover":
|
||||||
|
return HappinessRelationRole.DeceasedLover;
|
||||||
|
case "death_best_friend":
|
||||||
|
return HappinessRelationRole.DeceasedBestFriend;
|
||||||
|
case "death_family_member":
|
||||||
|
return HappinessRelationRole.DeceasedFamily;
|
||||||
|
default:
|
||||||
|
return HappinessRelationRole.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
879
IdleSpectator/HappinessEventCatalog.cs
Normal file
879
IdleSpectator/HappinessEventCatalog.cs
Normal file
|
|
@ -0,0 +1,879 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
/// <summary>Authored policy + prose for every live happiness effect.</summary>
|
||||||
|
public static class HappinessEventCatalog
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, HappinessCatalogEntry> Entries =
|
||||||
|
new Dictionary<string, HappinessCatalogEntry>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["death_family_member"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "death_family_member",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RequiresRelatedActor,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.DeceasedFamily,
|
||||||
|
InterestCategory = "Grief",
|
||||||
|
VariantsWithRelated = new[] { "mourns the death of family member {related}", "grieves after family member {related} dies" },
|
||||||
|
VariantsWithoutRelated = new[] { "mourns a family member's death", "grieves a family loss" },
|
||||||
|
},
|
||||||
|
["death_lover"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "death_lover",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RequiresRelatedActor,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.DeceasedLover,
|
||||||
|
InterestCategory = "Grief",
|
||||||
|
VariantsWithRelated = new[] { "falls into despair after their lover {related}'s death", "mourns their lover {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "mourns a lost lover", "grieves a lover's death" },
|
||||||
|
},
|
||||||
|
["death_child"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "death_child",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RequiresRelatedActor,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.DeceasedChild,
|
||||||
|
InterestCategory = "Grief",
|
||||||
|
VariantsWithRelated = new[] { "falls into despair after their child {related}'s death", "mourns their child {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "mourns a child's death", "grieves the loss of a child" },
|
||||||
|
},
|
||||||
|
["death_best_friend"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "death_best_friend",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RequiresRelatedActor,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.DeceasedBestFriend,
|
||||||
|
InterestCategory = "Grief",
|
||||||
|
VariantsWithRelated = new[] { "mourns their best friend {related}", "grieves after best friend {related} dies" },
|
||||||
|
VariantsWithoutRelated = new[] { "mourns a best friend's death", "grieves a lost best friend" },
|
||||||
|
},
|
||||||
|
["got_robbed"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "got_robbed",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.Robber,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
VariantsWithRelated = new[] { "is robbed by {related}", "loses belongings to {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "is robbed", "gets robbed" },
|
||||||
|
},
|
||||||
|
["got_poked"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "got_poked",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
VariantsWithRelated = new[] { "gets poked by the gods", "is poked" },
|
||||||
|
VariantsWithoutRelated = new[] { "gets poked by the gods", "is poked" },
|
||||||
|
},
|
||||||
|
["lost_fight"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "lost_fight",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
VariantsWithRelated = new[] { "loses a fight", "is beaten in a fight" },
|
||||||
|
VariantsWithoutRelated = new[] { "loses a fight", "is beaten in a fight" },
|
||||||
|
},
|
||||||
|
["got_caught"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "got_caught",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
VariantsWithRelated = new[] { "is caught doing something shady", "gets caught" },
|
||||||
|
VariantsWithoutRelated = new[] { "is caught doing something shady", "gets caught" },
|
||||||
|
},
|
||||||
|
["paid_tax"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "paid_tax",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
VariantsWithRelated = new[] { "pays taxes", "grumbles while paying taxes" },
|
||||||
|
VariantsWithoutRelated = new[] { "pays taxes", "grumbles while paying taxes" },
|
||||||
|
},
|
||||||
|
["just_ate"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_ate",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "just_ate",
|
||||||
|
VariantsWithRelated = new[] { "finishes a meal", "eats and feels better" },
|
||||||
|
VariantsWithoutRelated = new[] { "finishes a meal", "eats and feels better" },
|
||||||
|
},
|
||||||
|
["just_received_gift"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_received_gift",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.GiftPartner,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
VariantsWithRelated = new[] { "receives a gift from {related}", "is given a gift by {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "receives a gift", "is given a gift" },
|
||||||
|
},
|
||||||
|
["just_gave_gift"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_gave_gift",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.GiftPartner,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
VariantsWithRelated = new[] { "gives {related} a gift", "offers a gift to {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "gives a gift", "offers a gift" },
|
||||||
|
},
|
||||||
|
["just_pooped"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_pooped",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
VariantsWithRelated = new[] { "finds relief", "finishes relieving themselves" },
|
||||||
|
VariantsWithoutRelated = new[] { "finds relief", "finishes relieving themselves" },
|
||||||
|
},
|
||||||
|
["just_slept"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_slept",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
VariantsWithRelated = new[] { "wakes from a restful sleep", "finishes sleeping" },
|
||||||
|
VariantsWithoutRelated = new[] { "wakes from a restful sleep", "finishes sleeping" },
|
||||||
|
},
|
||||||
|
["had_bad_dream"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "had_bad_dream",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "had_bad_dream",
|
||||||
|
VariantsWithRelated = new[] { "wakes from a bad dream", "shakes off a bad dream" },
|
||||||
|
VariantsWithoutRelated = new[] { "wakes from a bad dream", "shakes off a bad dream" },
|
||||||
|
},
|
||||||
|
["had_good_dream"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "had_good_dream",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "had_good_dream",
|
||||||
|
VariantsWithRelated = new[] { "wakes from a good dream", "smiles about a good dream" },
|
||||||
|
VariantsWithoutRelated = new[] { "wakes from a good dream", "smiles about a good dream" },
|
||||||
|
},
|
||||||
|
["had_nightmare"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "had_nightmare",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "had_nightmare",
|
||||||
|
VariantsWithRelated = new[] { "wakes from a nightmare", "shakes off a nightmare" },
|
||||||
|
VariantsWithoutRelated = new[] { "wakes from a nightmare", "shakes off a nightmare" },
|
||||||
|
},
|
||||||
|
["slept_outside"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "slept_outside",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
VariantsWithRelated = new[] { "sleeps outside and feels worse for it", "spends a rough night outside" },
|
||||||
|
VariantsWithoutRelated = new[] { "sleeps outside and feels worse for it", "spends a rough night outside" },
|
||||||
|
},
|
||||||
|
["just_kissed"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_kissed",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.Lover,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
VariantsWithRelated = new[] { "shares a kiss with {related}", "kisses {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "shares a kiss", "steals a kiss" },
|
||||||
|
},
|
||||||
|
["just_killed"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_killed",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.CanonicalMilestone,
|
||||||
|
RelationRole = HappinessRelationRole.Victim,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
CanonicalMilestoneKey = "milestone_kill",
|
||||||
|
VariantsWithRelated = new[] { "kills {related} and feels a rush", "takes a life" },
|
||||||
|
VariantsWithoutRelated = new[] { "takes a life", "feels a rush after a kill" },
|
||||||
|
},
|
||||||
|
["become_king"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "become_king",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "is crowned king", "takes the throne" },
|
||||||
|
VariantsWithoutRelated = new[] { "is crowned king", "takes the throne" },
|
||||||
|
},
|
||||||
|
["become_leader"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "become_leader",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "becomes a leader", "rises to leadership" },
|
||||||
|
VariantsWithoutRelated = new[] { "becomes a leader", "rises to leadership" },
|
||||||
|
},
|
||||||
|
["just_won_war"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_won_war",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "rejoices at a won war", "celebrates victory in war" },
|
||||||
|
VariantsWithoutRelated = new[] { "rejoices at a won war", "celebrates victory in war" },
|
||||||
|
},
|
||||||
|
["just_made_peace"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_made_peace",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "feels relief as peace is made", "welcomes peace" },
|
||||||
|
VariantsWithoutRelated = new[] { "feels relief as peace is made", "welcomes peace" },
|
||||||
|
},
|
||||||
|
["just_lost_war"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_lost_war",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "despairs after a lost war", "mourns a lost war" },
|
||||||
|
VariantsWithoutRelated = new[] { "despairs after a lost war", "mourns a lost war" },
|
||||||
|
},
|
||||||
|
["was_conquered"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "was_conquered",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "suffers under conquest", "is crushed by conquest" },
|
||||||
|
VariantsWithoutRelated = new[] { "suffers under conquest", "is crushed by conquest" },
|
||||||
|
},
|
||||||
|
["kingdom_fell_apart"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "kingdom_fell_apart",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "watches the kingdom fall apart", "despairs as the kingdom fractures" },
|
||||||
|
VariantsWithoutRelated = new[] { "watches the kingdom fall apart", "despairs as the kingdom fractures" },
|
||||||
|
},
|
||||||
|
["just_started_war"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_started_war",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "cheers as war begins", "is stirred by the start of war" },
|
||||||
|
VariantsWithoutRelated = new[] { "cheers as war begins", "is stirred by the start of war" },
|
||||||
|
},
|
||||||
|
["just_rebelled"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_rebelled",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "joins a rebellion", "rises in rebellion" },
|
||||||
|
VariantsWithoutRelated = new[] { "joins a rebellion", "rises in rebellion" },
|
||||||
|
},
|
||||||
|
["fallen_in_love"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "fallen_in_love",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.CanonicalMilestone,
|
||||||
|
RelationRole = HappinessRelationRole.Lover,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
CanonicalMilestoneKey = "milestone_lover",
|
||||||
|
StatusOverlapId = "fell_in_love",
|
||||||
|
VariantsWithRelated = new[] { "falls in love with {related}", "is smitten with {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "falls in love", "is smitten" },
|
||||||
|
},
|
||||||
|
["just_had_child"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_had_child",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.Newborn,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "welcomes newborn {related}", "has a child named {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "welcomes a newborn child", "has a child" },
|
||||||
|
},
|
||||||
|
["just_read_book"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_read_book",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
VariantsWithRelated = new[] { "finishes reading a book", "puts down a book feeling wiser" },
|
||||||
|
VariantsWithoutRelated = new[] { "finishes reading a book", "puts down a book feeling wiser" },
|
||||||
|
},
|
||||||
|
["just_played"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_played",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
VariantsWithRelated = new[] { "finishes playing", "has fun playing" },
|
||||||
|
VariantsWithoutRelated = new[] { "finishes playing", "has fun playing" },
|
||||||
|
},
|
||||||
|
["just_talked"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_talked",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.ConversationPartner,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
VariantsWithRelated = new[] { "shares a talk with {related}", "finishes a conversation with {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "shares a quiet talk", "finishes a conversation" },
|
||||||
|
},
|
||||||
|
["just_laughed"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_laughed",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "laughing",
|
||||||
|
VariantsWithRelated = new[] { "bursts out laughing", "shares a laugh" },
|
||||||
|
VariantsWithoutRelated = new[] { "bursts out laughing", "shares a laugh" },
|
||||||
|
},
|
||||||
|
["just_sang"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_sang",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "singing",
|
||||||
|
VariantsWithRelated = new[] { "finishes singing", "sings with feeling" },
|
||||||
|
VariantsWithoutRelated = new[] { "finishes singing", "sings with feeling" },
|
||||||
|
},
|
||||||
|
["just_swore"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_swore",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "swearing",
|
||||||
|
VariantsWithRelated = new[] { "lets out a curse", "swears loudly" },
|
||||||
|
VariantsWithoutRelated = new[] { "lets out a curse", "swears loudly" },
|
||||||
|
},
|
||||||
|
["just_cried"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_cried",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "crying",
|
||||||
|
VariantsWithRelated = new[] { "finishes crying", "cries it out" },
|
||||||
|
VariantsWithoutRelated = new[] { "finishes crying", "cries it out" },
|
||||||
|
},
|
||||||
|
["just_talked_gossip"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_talked_gossip",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.ConversationPartner,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
VariantsWithRelated = new[] { "trades gossip with {related}", "shares juicy gossip with {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "trades gossip", "shares juicy gossip" },
|
||||||
|
},
|
||||||
|
["just_surprised"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_surprised",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "surprised",
|
||||||
|
VariantsWithRelated = new[] { "is startled", "jumps in surprise" },
|
||||||
|
VariantsWithoutRelated = new[] { "is startled", "jumps in surprise" },
|
||||||
|
},
|
||||||
|
["just_born"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_born",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "is born into the world", "takes a first breath" },
|
||||||
|
VariantsWithoutRelated = new[] { "is born into the world", "takes a first breath" },
|
||||||
|
},
|
||||||
|
["just_magnetised"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_magnetised",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "magnetized",
|
||||||
|
VariantsWithRelated = new[] { "is magnetized", "feels a magnetic pull" },
|
||||||
|
VariantsWithoutRelated = new[] { "is magnetized", "feels a magnetic pull" },
|
||||||
|
},
|
||||||
|
["just_forced_power"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_forced_power",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
VariantsWithRelated = new[] { "is forced by divine power", "suffers a forced power" },
|
||||||
|
VariantsWithoutRelated = new[] { "is forced by divine power", "suffers a forced power" },
|
||||||
|
},
|
||||||
|
["just_possessed"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_possessed",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "possessed",
|
||||||
|
VariantsWithRelated = new[] { "is possessed", "falls under possession" },
|
||||||
|
VariantsWithoutRelated = new[] { "is possessed", "falls under possession" },
|
||||||
|
},
|
||||||
|
["strange_urge"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "strange_urge",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "strange_urge",
|
||||||
|
VariantsWithRelated = new[] { "feels a strange urge", "is gripped by a strange urge" },
|
||||||
|
VariantsWithoutRelated = new[] { "feels a strange urge", "is gripped by a strange urge" },
|
||||||
|
},
|
||||||
|
["just_had_tantrum"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_had_tantrum",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "tantrum",
|
||||||
|
VariantsWithRelated = new[] { "finishes a tantrum", "calms after a tantrum" },
|
||||||
|
VariantsWithoutRelated = new[] { "finishes a tantrum", "calms after a tantrum" },
|
||||||
|
},
|
||||||
|
["just_felt_the_divine"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_felt_the_divine",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
VariantsWithRelated = new[] { "feels the divine touch", "is touched by the divine" },
|
||||||
|
VariantsWithoutRelated = new[] { "feels the divine touch", "is touched by the divine" },
|
||||||
|
},
|
||||||
|
["just_enchanted"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_enchanted",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "enchanted",
|
||||||
|
VariantsWithRelated = new[] { "is enchanted", "feels an enchantment take hold" },
|
||||||
|
VariantsWithoutRelated = new[] { "is enchanted", "feels an enchantment take hold" },
|
||||||
|
},
|
||||||
|
["just_inspired"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_inspired",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "inspired",
|
||||||
|
VariantsWithRelated = new[] { "feels inspired", "is struck by inspiration" },
|
||||||
|
VariantsWithoutRelated = new[] { "feels inspired", "is struck by inspiration" },
|
||||||
|
},
|
||||||
|
["wrote_book"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "wrote_book",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "writes a book", "finishes writing a book" },
|
||||||
|
VariantsWithoutRelated = new[] { "writes a book", "finishes writing a book" },
|
||||||
|
},
|
||||||
|
["just_became_adult"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_became_adult",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "comes of age", "becomes an adult" },
|
||||||
|
VariantsWithoutRelated = new[] { "comes of age", "becomes an adult" },
|
||||||
|
},
|
||||||
|
["just_got_out_of_egg"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_got_out_of_egg",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "hatches from an egg", "breaks free of an egg" },
|
||||||
|
VariantsWithoutRelated = new[] { "hatches from an egg", "breaks free of an egg" },
|
||||||
|
},
|
||||||
|
["just_finished_plot"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_finished_plot",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "completes a plot", "finishes a long plot" },
|
||||||
|
VariantsWithoutRelated = new[] { "completes a plot", "finishes a long plot" },
|
||||||
|
},
|
||||||
|
["just_found_house"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_found_house",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "finds a home", "settles into a house" },
|
||||||
|
VariantsWithoutRelated = new[] { "finds a home", "settles into a house" },
|
||||||
|
},
|
||||||
|
["just_lost_house"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_lost_house",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "loses a home", "is left homeless" },
|
||||||
|
VariantsWithoutRelated = new[] { "loses a home", "is left homeless" },
|
||||||
|
},
|
||||||
|
["just_made_friend"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_made_friend",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.RelatedOptional,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.CanonicalMilestone,
|
||||||
|
RelationRole = HappinessRelationRole.BestFriend,
|
||||||
|
InterestCategory = "Social",
|
||||||
|
CanonicalMilestoneKey = "milestone_friend",
|
||||||
|
VariantsWithRelated = new[] { "befriends {related}", "makes a best friend of {related}" },
|
||||||
|
VariantsWithoutRelated = new[] { "makes a best friend", "finds a best friend" },
|
||||||
|
},
|
||||||
|
["just_injured"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_injured",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "suffers a lasting injury", "is crippled by injury" },
|
||||||
|
VariantsWithoutRelated = new[] { "suffers a lasting injury", "is crippled by injury" },
|
||||||
|
},
|
||||||
|
["just_cursed"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "just_cursed",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Emotion",
|
||||||
|
StatusOverlapId = "cursed",
|
||||||
|
VariantsWithRelated = new[] { "is cursed", "falls under a curse" },
|
||||||
|
VariantsWithoutRelated = new[] { "is cursed", "falls under a curse" },
|
||||||
|
},
|
||||||
|
["starving"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "starving",
|
||||||
|
Presentation = HappinessPresentationTier.Ambient,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Continuation,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Daily",
|
||||||
|
StatusOverlapId = "starving",
|
||||||
|
VariantsWithRelated = new[] { "is starving", "goes hungry" },
|
||||||
|
VariantsWithoutRelated = new[] { "is starving", "goes hungry" },
|
||||||
|
},
|
||||||
|
["conquered_city"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "conquered_city",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "celebrates a conquered city", "rejoices at a city conquered" },
|
||||||
|
VariantsWithoutRelated = new[] { "celebrates a conquered city", "rejoices at a city conquered" },
|
||||||
|
},
|
||||||
|
["destroyed_city"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "destroyed_city",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "celebrates a destroyed city", "cheers a city's destruction" },
|
||||||
|
VariantsWithoutRelated = new[] { "celebrates a destroyed city", "cheers a city's destruction" },
|
||||||
|
},
|
||||||
|
["lost_crown"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "lost_crown",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "loses the crown", "is stripped of the crown" },
|
||||||
|
VariantsWithoutRelated = new[] { "loses the crown", "is stripped of the crown" },
|
||||||
|
},
|
||||||
|
["razed_capital"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "razed_capital",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "despairs as the capital is razed", "mourns a razed capital" },
|
||||||
|
VariantsWithoutRelated = new[] { "despairs as the capital is razed", "mourns a razed capital" },
|
||||||
|
},
|
||||||
|
["lost_capital"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "lost_capital",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "despairs at the lost capital", "mourns the lost capital" },
|
||||||
|
VariantsWithoutRelated = new[] { "despairs at the lost capital", "mourns the lost capital" },
|
||||||
|
},
|
||||||
|
["razed_city"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "razed_city",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "despairs as a city is razed", "mourns a razed city" },
|
||||||
|
VariantsWithoutRelated = new[] { "despairs as a city is razed", "mourns a razed city" },
|
||||||
|
},
|
||||||
|
["lost_city"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "lost_city",
|
||||||
|
Presentation = HappinessPresentationTier.Aggregate,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "Civic",
|
||||||
|
VariantsWithRelated = new[] { "despairs at a lost city", "mourns a lost city" },
|
||||||
|
VariantsWithoutRelated = new[] { "despairs at a lost city", "mourns a lost city" },
|
||||||
|
},
|
||||||
|
["become_alpha"] = new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = "become_alpha",
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ProjectToLife,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
RelationRole = HappinessRelationRole.None,
|
||||||
|
InterestCategory = "LifeChapter",
|
||||||
|
VariantsWithRelated = new[] { "becomes the alpha", "rises as alpha" },
|
||||||
|
VariantsWithoutRelated = new[] { "becomes the alpha", "rises as alpha" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||||
|
|
||||||
|
public static bool HasAuthored(string effectId)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(effectId) && Entries.ContainsKey(effectId.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGet(string effectId, out HappinessCatalogEntry entry)
|
||||||
|
{
|
||||||
|
entry = null;
|
||||||
|
if (string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Entries.TryGetValue(effectId.Trim(), out entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HappinessCatalogEntry GetOrFallback(string effectId)
|
||||||
|
{
|
||||||
|
if (TryGet(effectId, out HappinessCatalogEntry entry))
|
||||||
|
{
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
string id = string.IsNullOrEmpty(effectId) ? "unknown" : effectId.Trim();
|
||||||
|
return new HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Presentation = HappinessPresentationTier.Signal,
|
||||||
|
Context = HappinessContextRequirement.None,
|
||||||
|
Life = HappinessLifePolicy.ActivityOnly,
|
||||||
|
Overlap = HappinessOverlapPolicy.Independent,
|
||||||
|
InterestCategory = "Unknown",
|
||||||
|
VariantsWithRelated = new[] { "feels a change in happiness" },
|
||||||
|
VariantsWithoutRelated = new[] { "feels a change in happiness" },
|
||||||
|
IsFallback = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
738
IdleSpectator/HappinessEventRouter.cs
Normal file
738
IdleSpectator/HappinessEventRouter.cs
Normal file
|
|
@ -0,0 +1,738 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sole writer from happiness hooks into Activity / Chronicle Life.
|
||||||
|
/// Patches publish occurrences; presentation tiers and overlap live here.
|
||||||
|
/// </summary>
|
||||||
|
public static class HappinessEventRouter
|
||||||
|
{
|
||||||
|
public const float AmbientRingCooldown = 25f;
|
||||||
|
public const float CorrelationWindow = 3f;
|
||||||
|
public const float AggregateWindow = 0.75f;
|
||||||
|
public const int AggregateSampleCap = 8;
|
||||||
|
public const int MaxRecentOccurrences = 256;
|
||||||
|
|
||||||
|
private static readonly List<HappinessOccurrence> Recent =
|
||||||
|
new List<HappinessOccurrence>(MaxRecentOccurrences);
|
||||||
|
private static readonly Dictionary<string, float> AmbientRingAt =
|
||||||
|
new Dictionary<string, float>();
|
||||||
|
private static readonly Dictionary<string, float> CorrelationAt =
|
||||||
|
new Dictionary<string, float>();
|
||||||
|
private static readonly Dictionary<string, AggregateBucket> Aggregates =
|
||||||
|
new Dictionary<string, AggregateBucket>();
|
||||||
|
|
||||||
|
private static int _notesSinceClear;
|
||||||
|
private static int _ringWritesSinceClear;
|
||||||
|
private static int _lifeWritesSinceClear;
|
||||||
|
private static int _mergedSinceClear;
|
||||||
|
private static int _suppressedSinceClear;
|
||||||
|
private static int _aggregateSummariesSinceClear;
|
||||||
|
private static string _lastEffectId = "";
|
||||||
|
private static long _counterSubjectId;
|
||||||
|
|
||||||
|
private sealed class AggregateBucket
|
||||||
|
{
|
||||||
|
public string EffectId;
|
||||||
|
public float StartedAt;
|
||||||
|
public int Total;
|
||||||
|
public int Sampled;
|
||||||
|
public Actor FirstNotable;
|
||||||
|
public string CivKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int NotesSinceClear => _notesSinceClear;
|
||||||
|
public static int RingWritesSinceClear => _ringWritesSinceClear;
|
||||||
|
public static int LifeWritesSinceClear => _lifeWritesSinceClear;
|
||||||
|
public static int MergedSinceClear => _mergedSinceClear;
|
||||||
|
public static int SuppressedSinceClear => _suppressedSinceClear;
|
||||||
|
public static int AggregateSummariesSinceClear => _aggregateSummariesSinceClear;
|
||||||
|
public static string LastEffectId => _lastEffectId ?? "";
|
||||||
|
|
||||||
|
public static void ClearSession()
|
||||||
|
{
|
||||||
|
Recent.Clear();
|
||||||
|
AmbientRingAt.Clear();
|
||||||
|
CorrelationAt.Clear();
|
||||||
|
Aggregates.Clear();
|
||||||
|
_notesSinceClear = 0;
|
||||||
|
_ringWritesSinceClear = 0;
|
||||||
|
_lifeWritesSinceClear = 0;
|
||||||
|
_mergedSinceClear = 0;
|
||||||
|
_suppressedSinceClear = 0;
|
||||||
|
_aggregateSummariesSinceClear = 0;
|
||||||
|
_lastEffectId = "";
|
||||||
|
_counterSubjectId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ResetCountersForSubject(long subjectId)
|
||||||
|
{
|
||||||
|
_counterSubjectId = subjectId;
|
||||||
|
_notesSinceClear = 0;
|
||||||
|
_ringWritesSinceClear = 0;
|
||||||
|
_lifeWritesSinceClear = 0;
|
||||||
|
_mergedSinceClear = 0;
|
||||||
|
_suppressedSinceClear = 0;
|
||||||
|
_aggregateSummariesSinceClear = 0;
|
||||||
|
_lastEffectId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<HappinessOccurrence> Latest(int max = 32)
|
||||||
|
{
|
||||||
|
if (max <= 0 || Recent.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<HappinessOccurrence>();
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = Math.Max(0, Recent.Count - max);
|
||||||
|
var list = new List<HappinessOccurrence>(Recent.Count - start);
|
||||||
|
for (int i = start; i < Recent.Count; i++)
|
||||||
|
{
|
||||||
|
list.Add(Recent[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Publish a happiness effect that the game actually applied (or harness force).</summary>
|
||||||
|
public static HappinessOccurrence PublishApplied(
|
||||||
|
Actor subject,
|
||||||
|
string effectId,
|
||||||
|
int amount,
|
||||||
|
HappinessSourceHook hook,
|
||||||
|
Actor related = null,
|
||||||
|
HappinessRelationRole role = HappinessRelationRole.None,
|
||||||
|
bool forceRing = false)
|
||||||
|
{
|
||||||
|
return PublishCore(
|
||||||
|
subject,
|
||||||
|
effectId,
|
||||||
|
amount,
|
||||||
|
hook,
|
||||||
|
related,
|
||||||
|
role,
|
||||||
|
applied: true,
|
||||||
|
suppressedByPsychopath: false,
|
||||||
|
forceRing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Record that the game refused to apply happiness (psychopath/egg/no emotions).</summary>
|
||||||
|
public static void PublishSuppressed(Actor subject, string effectId, HappinessSourceHook hook)
|
||||||
|
{
|
||||||
|
if (_counterSubjectId == 0 || (subject != null && subject.getID() == _counterSubjectId))
|
||||||
|
{
|
||||||
|
_suppressedSinceClear++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var occ = BuildOccurrence(
|
||||||
|
subject,
|
||||||
|
effectId,
|
||||||
|
amount: 0,
|
||||||
|
hook,
|
||||||
|
related: null,
|
||||||
|
role: HappinessRelationRole.None,
|
||||||
|
applied: false,
|
||||||
|
suppressedByPsychopath: true);
|
||||||
|
occ.Merged = true;
|
||||||
|
Remember(occ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HappinessOccurrence PublishCore(
|
||||||
|
Actor subject,
|
||||||
|
string effectId,
|
||||||
|
int amount,
|
||||||
|
HappinessSourceHook hook,
|
||||||
|
Actor related,
|
||||||
|
HappinessRelationRole role,
|
||||||
|
bool applied,
|
||||||
|
bool suppressedByPsychopath,
|
||||||
|
bool forceRing)
|
||||||
|
{
|
||||||
|
if (subject == null || string.IsNullOrEmpty(effectId) || !applied)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushStaleAggregates();
|
||||||
|
|
||||||
|
HappinessCatalogEntry entry = HappinessEventCatalog.GetOrFallback(effectId);
|
||||||
|
if (related == null
|
||||||
|
&& HappinessContextBag.TryGetRelated(subject, out Actor bagRelated, out HappinessRelationRole bagRole))
|
||||||
|
{
|
||||||
|
related = bagRelated;
|
||||||
|
if (role == HappinessRelationRole.None)
|
||||||
|
{
|
||||||
|
role = bagRole;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == HappinessRelationRole.None)
|
||||||
|
{
|
||||||
|
role = entry.RelationRole != HappinessRelationRole.None
|
||||||
|
? entry.RelationRole
|
||||||
|
: HappinessContextBag.RoleForDeathEffect(effectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Presentation == HappinessPresentationTier.Aggregate && !forceRing)
|
||||||
|
{
|
||||||
|
return PublishAggregateMember(subject, effectId, amount, hook, related, role, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessOccurrence occ = BuildOccurrence(
|
||||||
|
subject, effectId, amount, hook, related, role, applied, suppressedByPsychopath);
|
||||||
|
FillProse(occ, entry);
|
||||||
|
|
||||||
|
if (ShouldMerge(occ, entry))
|
||||||
|
{
|
||||||
|
occ.Merged = true;
|
||||||
|
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_mergedSinceClear++;
|
||||||
|
_notesSinceClear++;
|
||||||
|
_lastEffectId = effectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Remember(occ);
|
||||||
|
return occ;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeRing = forceRing || ShouldWriteRing(occ, entry);
|
||||||
|
if (writeRing)
|
||||||
|
{
|
||||||
|
ActivityLog.NoteHappiness(occ);
|
||||||
|
occ.WroteActivityRing = true;
|
||||||
|
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_ringWritesSinceClear++;
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkAmbient(occ);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Life == HappinessLifePolicy.ProjectToLife
|
||||||
|
&& entry.Overlap != HappinessOverlapPolicy.CanonicalMilestone)
|
||||||
|
{
|
||||||
|
if (Chronicle.NoteHappinessLife(occ, subject, related))
|
||||||
|
{
|
||||||
|
occ.WroteLife = true;
|
||||||
|
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_lifeWritesSinceClear++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkCorrelation(occ, entry);
|
||||||
|
if (_counterSubjectId == 0 || occ.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_notesSinceClear++;
|
||||||
|
_lastEffectId = effectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Remember(occ);
|
||||||
|
return occ;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HappinessOccurrence PublishAggregateMember(
|
||||||
|
Actor subject,
|
||||||
|
string effectId,
|
||||||
|
int amount,
|
||||||
|
HappinessSourceHook hook,
|
||||||
|
Actor related,
|
||||||
|
HappinessRelationRole role,
|
||||||
|
HappinessCatalogEntry entry)
|
||||||
|
{
|
||||||
|
string civKey = CivKey(subject, effectId);
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (!Aggregates.TryGetValue(civKey, out AggregateBucket bucket)
|
||||||
|
|| now - bucket.StartedAt > AggregateWindow)
|
||||||
|
{
|
||||||
|
bucket = new AggregateBucket
|
||||||
|
{
|
||||||
|
EffectId = effectId,
|
||||||
|
StartedAt = now,
|
||||||
|
CivKey = civKey
|
||||||
|
};
|
||||||
|
Aggregates[civKey] = bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket.Total++;
|
||||||
|
bool notable = IsNotable(subject);
|
||||||
|
bool takeSample = bucket.Sampled < AggregateSampleCap && (notable ? bucket.FirstNotable == null : true);
|
||||||
|
if (notable && bucket.FirstNotable == null)
|
||||||
|
{
|
||||||
|
bucket.FirstNotable = subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (takeSample)
|
||||||
|
{
|
||||||
|
bucket.Sampled++;
|
||||||
|
|
||||||
|
// Notables and early samples get individual ring lines (one notable max).
|
||||||
|
HappinessOccurrence sample = BuildOccurrence(
|
||||||
|
subject, effectId, amount, hook, related, role, applied: true, suppressedByPsychopath: false);
|
||||||
|
sample.Presentation = HappinessPresentationTier.Aggregate;
|
||||||
|
sample.CivEventKey = civKey;
|
||||||
|
sample.SampledCount = 1;
|
||||||
|
sample.TotalAffectedCount = bucket.Total;
|
||||||
|
FillProse(sample, entry);
|
||||||
|
|
||||||
|
bool writeRing = notable || bucket.Sampled <= 3;
|
||||||
|
if (writeRing)
|
||||||
|
{
|
||||||
|
ActivityLog.NoteHappiness(sample);
|
||||||
|
sample.WroteActivityRing = true;
|
||||||
|
if (_counterSubjectId == 0 || sample.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_ringWritesSinceClear++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_counterSubjectId == 0 || sample.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_notesSinceClear++;
|
||||||
|
_lastEffectId = effectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Remember(sample);
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overflow: count only; summary flushed later.
|
||||||
|
HappinessOccurrence skipped = BuildOccurrence(
|
||||||
|
subject, effectId, amount, hook, related, role, applied: true, suppressedByPsychopath: false);
|
||||||
|
skipped.Presentation = HappinessPresentationTier.Aggregate;
|
||||||
|
skipped.CivEventKey = civKey;
|
||||||
|
skipped.TotalAffectedCount = bucket.Total;
|
||||||
|
skipped.Merged = true;
|
||||||
|
FillProse(skipped, entry);
|
||||||
|
Remember(skipped);
|
||||||
|
return skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void FlushAggregatesNow()
|
||||||
|
{
|
||||||
|
FlushStaleAggregates(force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FlushStaleAggregates(bool force = false)
|
||||||
|
{
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (Aggregates.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var done = new List<string>();
|
||||||
|
foreach (KeyValuePair<string, AggregateBucket> kv in Aggregates)
|
||||||
|
{
|
||||||
|
if (force || now - kv.Value.StartedAt >= AggregateWindow)
|
||||||
|
{
|
||||||
|
done.Add(kv.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < done.Count; i++)
|
||||||
|
{
|
||||||
|
string key = done[i];
|
||||||
|
AggregateBucket bucket = Aggregates[key];
|
||||||
|
Aggregates.Remove(key);
|
||||||
|
if (bucket.Total <= bucket.Sampled)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Actor subject = bucket.FirstNotable;
|
||||||
|
if (subject == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
HappinessCatalogEntry entry = HappinessEventCatalog.GetOrFallback(bucket.EffectId);
|
||||||
|
HappinessOccurrence summary = BuildOccurrence(
|
||||||
|
subject,
|
||||||
|
bucket.EffectId,
|
||||||
|
amount: HappinessGameApi.ResolveAmount(bucket.EffectId),
|
||||||
|
HappinessSourceHook.AggregateSummary,
|
||||||
|
related: null,
|
||||||
|
role: HappinessRelationRole.None,
|
||||||
|
applied: true,
|
||||||
|
suppressedByPsychopath: false);
|
||||||
|
summary.Presentation = HappinessPresentationTier.Aggregate;
|
||||||
|
summary.CivEventKey = bucket.CivKey;
|
||||||
|
summary.SampledCount = bucket.Sampled;
|
||||||
|
summary.TotalAffectedCount = bucket.Total;
|
||||||
|
int others = Math.Max(0, bucket.Total - bucket.Sampled);
|
||||||
|
string baseClause = ActivityHappinessProse.Clause(entry, "", out _, out _);
|
||||||
|
summary.PlainClause = baseClause + " (and " + others + " others)";
|
||||||
|
summary.RichClause = summary.PlainClause;
|
||||||
|
summary.FactLine = "Happiness " + bucket.EffectId + " x" + bucket.Total;
|
||||||
|
ActivityLog.NoteHappiness(summary);
|
||||||
|
summary.WroteActivityRing = true;
|
||||||
|
_aggregateSummariesSinceClear++;
|
||||||
|
if (_counterSubjectId == 0 || summary.SubjectId == _counterSubjectId)
|
||||||
|
{
|
||||||
|
_ringWritesSinceClear++;
|
||||||
|
_notesSinceClear++;
|
||||||
|
_lastEffectId = bucket.EffectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Remember(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HappinessOccurrence BuildOccurrence(
|
||||||
|
Actor subject,
|
||||||
|
string effectId,
|
||||||
|
int amount,
|
||||||
|
HappinessSourceHook hook,
|
||||||
|
Actor related,
|
||||||
|
HappinessRelationRole role,
|
||||||
|
bool applied,
|
||||||
|
bool suppressedByPsychopath)
|
||||||
|
{
|
||||||
|
HappinessCatalogEntry entry = HappinessEventCatalog.GetOrFallback(effectId);
|
||||||
|
long subjectId = 0;
|
||||||
|
string subjectName = "Someone";
|
||||||
|
string species = "";
|
||||||
|
Vector3 pos = Vector3.zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subjectId = subject.getID();
|
||||||
|
subjectName = subject.getName();
|
||||||
|
if (string.IsNullOrEmpty(subjectName))
|
||||||
|
{
|
||||||
|
subjectName = subject.asset != null ? subject.asset.id : "creature";
|
||||||
|
}
|
||||||
|
|
||||||
|
species = subject.asset != null ? subject.asset.id : "";
|
||||||
|
pos = subject.current_position;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// keep defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
long relatedId = 0;
|
||||||
|
string relatedName = "";
|
||||||
|
string relatedSpecies = "";
|
||||||
|
if (related != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
relatedId = related.getID();
|
||||||
|
relatedName = related.getName();
|
||||||
|
if (string.IsNullOrEmpty(relatedName))
|
||||||
|
{
|
||||||
|
relatedName = related.asset != null ? related.asset.id : "creature";
|
||||||
|
}
|
||||||
|
|
||||||
|
relatedSpecies = related.asset != null ? related.asset.id : "";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StampWorldDate(out double worldTime, out string dateLabel);
|
||||||
|
string corr = subjectId + "|" + effectId + "|" + relatedId + "|" + Math.Floor(worldTime);
|
||||||
|
|
||||||
|
return new HappinessOccurrence
|
||||||
|
{
|
||||||
|
EffectId = effectId.Trim(),
|
||||||
|
Amount = amount,
|
||||||
|
SubjectId = subjectId,
|
||||||
|
SubjectName = subjectName,
|
||||||
|
SubjectSpecies = species,
|
||||||
|
RelatedId = relatedId,
|
||||||
|
RelatedName = relatedName,
|
||||||
|
RelatedSpecies = relatedSpecies,
|
||||||
|
RelationRole = role,
|
||||||
|
Position = pos,
|
||||||
|
WorldTime = worldTime,
|
||||||
|
DateLabel = dateLabel,
|
||||||
|
SourceHook = hook,
|
||||||
|
CorrelationKey = corr,
|
||||||
|
SuppressedByPsychopath = suppressedByPsychopath,
|
||||||
|
Applied = applied,
|
||||||
|
Presentation = entry.Presentation,
|
||||||
|
Life = entry.Life,
|
||||||
|
Overlap = entry.Overlap,
|
||||||
|
Context = entry.Context,
|
||||||
|
InterestCategory = entry.InterestCategory,
|
||||||
|
CanonicalMilestoneKey = entry.CanonicalMilestoneKey,
|
||||||
|
StatusOverlapId = entry.StatusOverlapId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FillProse(HappinessOccurrence occ, HappinessCatalogEntry entry)
|
||||||
|
{
|
||||||
|
string clause = ActivityHappinessProse.Clause(
|
||||||
|
entry, occ.RelatedName, out bool usedRelated, out _);
|
||||||
|
if (entry.Context == HappinessContextRequirement.RequiresRelatedActor
|
||||||
|
&& string.IsNullOrEmpty(occ.RelatedName))
|
||||||
|
{
|
||||||
|
// Still author a subject-only line at runtime for safety; audit fails separately.
|
||||||
|
clause = ActivityHappinessProse.Clause(entry, "", out _, out _);
|
||||||
|
usedRelated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
occ.PlainClause = clause;
|
||||||
|
if (usedRelated && !string.IsNullOrEmpty(occ.RelatedName))
|
||||||
|
{
|
||||||
|
occ.RichClause = clause.Replace(
|
||||||
|
occ.RelatedName,
|
||||||
|
ActivityProse.ColorName(occ.RelatedName));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
occ.RichClause = clause;
|
||||||
|
}
|
||||||
|
|
||||||
|
occ.FactLine = "Happiness " + occ.EffectId
|
||||||
|
+ (occ.Amount != 0 ? " " + (occ.Amount > 0 ? "+" : "") + occ.Amount : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldMerge(HappinessOccurrence occ, HappinessCatalogEntry entry)
|
||||||
|
{
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (entry.Overlap == HappinessOverlapPolicy.CanonicalMilestone
|
||||||
|
&& !string.IsNullOrEmpty(entry.CanonicalMilestoneKey))
|
||||||
|
{
|
||||||
|
string key = "canon|" + occ.SubjectId + "|" + entry.CanonicalMilestoneKey;
|
||||||
|
if (CorrelationAt.TryGetValue(key, out float at) && now - at <= CorrelationWindow)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Overlap == HappinessOverlapPolicy.Continuation
|
||||||
|
&& !string.IsNullOrEmpty(entry.StatusOverlapId))
|
||||||
|
{
|
||||||
|
string key = "status|" + occ.SubjectId + "|" + entry.StatusOverlapId;
|
||||||
|
if (CorrelationAt.TryGetValue(key, out float at) && now - at <= CorrelationWindow)
|
||||||
|
{
|
||||||
|
// Same-second status+happiness: keep status as the visible phase; merge happiness ring.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string selfKey = "happ|" + occ.SubjectId + "|" + occ.EffectId + "|" + occ.RelatedId;
|
||||||
|
if (CorrelationAt.TryGetValue(selfKey, out float selfAt) && now - selfAt <= 0.35f)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldWriteRing(HappinessOccurrence occ, HappinessCatalogEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Presentation == HappinessPresentationTier.Signal)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Presentation == HappinessPresentationTier.Ambient)
|
||||||
|
{
|
||||||
|
string key = occ.SubjectId + "|" + occ.EffectId;
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
if (AmbientRingAt.TryGetValue(key, out float at) && now - at < AmbientRingCooldown)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MarkAmbient(HappinessOccurrence occ)
|
||||||
|
{
|
||||||
|
if (occ.Presentation != HappinessPresentationTier.Ambient)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AmbientRingAt[occ.SubjectId + "|" + occ.EffectId] = Time.unscaledTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MarkCorrelation(HappinessOccurrence occ, HappinessCatalogEntry entry)
|
||||||
|
{
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
CorrelationAt["happ|" + occ.SubjectId + "|" + occ.EffectId + "|" + occ.RelatedId] = now;
|
||||||
|
if (!string.IsNullOrEmpty(entry.CanonicalMilestoneKey))
|
||||||
|
{
|
||||||
|
CorrelationAt["canon|" + occ.SubjectId + "|" + entry.CanonicalMilestoneKey] = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(entry.StatusOverlapId))
|
||||||
|
{
|
||||||
|
CorrelationAt["status|" + occ.SubjectId + "|" + entry.StatusOverlapId] = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
PruneDict(CorrelationAt, now, CorrelationWindow * 4f);
|
||||||
|
PruneDict(AmbientRingAt, now, AmbientRingCooldown * 4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Called when Chronicle logs a canonical lover/friend/kill milestone.</summary>
|
||||||
|
public static void NoteCanonicalMilestone(long subjectId, string milestoneKey)
|
||||||
|
{
|
||||||
|
if (subjectId == 0 || string.IsNullOrEmpty(milestoneKey))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CorrelationAt["canon|" + subjectId + "|" + milestoneKey] = Time.unscaledTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Called when status gain/loss is logged so Continuation overlaps can merge.</summary>
|
||||||
|
public static void NoteStatusPhase(long subjectId, string statusId)
|
||||||
|
{
|
||||||
|
if (subjectId == 0 || string.IsNullOrEmpty(statusId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CorrelationAt["status|" + subjectId + "|" + statusId] = Time.unscaledTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Remember(HappinessOccurrence occ)
|
||||||
|
{
|
||||||
|
if (occ == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Recent.Add(occ);
|
||||||
|
while (Recent.Count > MaxRecentOccurrences)
|
||||||
|
{
|
||||||
|
Recent.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNotable(Actor actor)
|
||||||
|
{
|
||||||
|
if (actor == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null
|
||||||
|
&& MoveCamera._focus_unit.getID() == actor.getID())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (actor.isFavorite())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (actor.isKing() || actor.isCityLeader())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CivKey(Actor subject, string effectId)
|
||||||
|
{
|
||||||
|
string kingdom = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (subject.kingdom != null)
|
||||||
|
{
|
||||||
|
kingdom = subject.kingdom.name ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
kingdom = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return effectId + "|" + kingdom + "|" + Math.Floor(Time.unscaledTime / AggregateWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void StampWorldDate(out double worldTime, out string dateLabel)
|
||||||
|
{
|
||||||
|
worldTime = 0;
|
||||||
|
dateLabel = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (World.world == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldTime = World.world.getCurWorldTime();
|
||||||
|
int[] raw = Date.getRawDate(worldTime);
|
||||||
|
if (raw == null || raw.Length < 3)
|
||||||
|
{
|
||||||
|
dateLabel = "y" + Date.getCurrentYear() + " m" + Date.getCurrentMonth();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string monthName = Date.formatMonth(raw[1]);
|
||||||
|
if (string.IsNullOrEmpty(monthName))
|
||||||
|
{
|
||||||
|
monthName = "m" + raw[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
dateLabel = monthName + " " + raw[2];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
dateLabel = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PruneDict(Dictionary<string, float> dict, float now, float maxAge)
|
||||||
|
{
|
||||||
|
if (dict.Count < 256)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remove = new List<string>();
|
||||||
|
foreach (KeyValuePair<string, float> kv in dict)
|
||||||
|
{
|
||||||
|
if (now - kv.Value > maxAge)
|
||||||
|
{
|
||||||
|
remove.Add(kv.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < remove.Count; i++)
|
||||||
|
{
|
||||||
|
dict.Remove(remove[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
IdleSpectator/HappinessGameApi.cs
Normal file
73
IdleSpectator/HappinessGameApi.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
/// <summary>Reflection helpers for happiness library / changeHappiness harness paths.</summary>
|
||||||
|
internal static class HappinessGameApi
|
||||||
|
{
|
||||||
|
private static MethodInfo _changeHappiness;
|
||||||
|
|
||||||
|
public static bool TryChangeHappiness(Actor actor, string effectId, int bonus = 0)
|
||||||
|
{
|
||||||
|
if (actor == null || string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_changeHappiness == null)
|
||||||
|
{
|
||||||
|
_changeHappiness = AccessTools.Method(
|
||||||
|
typeof(Actor),
|
||||||
|
"changeHappiness",
|
||||||
|
new[] { typeof(string), typeof(int) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_changeHappiness == null)
|
||||||
|
{
|
||||||
|
// Public method should be callable directly.
|
||||||
|
return actor.changeHappiness(effectId.Trim(), bonus);
|
||||||
|
}
|
||||||
|
|
||||||
|
object result = _changeHappiness.Invoke(actor, new object[] { effectId.Trim(), bonus });
|
||||||
|
return result is bool ok && ok;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return actor.changeHappiness(effectId.Trim(), bonus);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HappinessAsset TryGetAsset(string effectId)
|
||||||
|
{
|
||||||
|
return ActivityAssetCatalog.TryGetHappinessAsset(effectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ResolveAmount(string effectId, int bonus = 0)
|
||||||
|
{
|
||||||
|
HappinessAsset asset = TryGetAsset(effectId);
|
||||||
|
if (asset == null)
|
||||||
|
{
|
||||||
|
return bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return bonus + asset.value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return bonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
IdleSpectator/HappinessModels.cs
Normal file
111
IdleSpectator/HappinessModels.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace IdleSpectator;
|
||||||
|
|
||||||
|
public enum HappinessPresentationTier
|
||||||
|
{
|
||||||
|
Signal,
|
||||||
|
Ambient,
|
||||||
|
Aggregate
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HappinessContextRequirement
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
RequiresRelatedActor,
|
||||||
|
RelatedOptional
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HappinessLifePolicy
|
||||||
|
{
|
||||||
|
ActivityOnly,
|
||||||
|
ProjectToLife
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HappinessOverlapPolicy
|
||||||
|
{
|
||||||
|
Independent,
|
||||||
|
CanonicalMilestone,
|
||||||
|
MergeSameOccurrence,
|
||||||
|
Continuation
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HappinessRelationRole
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
DeceasedChild,
|
||||||
|
DeceasedLover,
|
||||||
|
DeceasedBestFriend,
|
||||||
|
DeceasedFamily,
|
||||||
|
Newborn,
|
||||||
|
Lover,
|
||||||
|
BestFriend,
|
||||||
|
GiftPartner,
|
||||||
|
ConversationPartner,
|
||||||
|
Robber,
|
||||||
|
Victim
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HappinessSourceHook
|
||||||
|
{
|
||||||
|
ChangeHappiness,
|
||||||
|
DeathEvent,
|
||||||
|
BirthEvent,
|
||||||
|
Harness,
|
||||||
|
AggregateSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Authored catalog row for one happiness effect id.</summary>
|
||||||
|
public sealed class HappinessCatalogEntry
|
||||||
|
{
|
||||||
|
public string Id = "";
|
||||||
|
public HappinessPresentationTier Presentation = HappinessPresentationTier.Signal;
|
||||||
|
public HappinessContextRequirement Context = HappinessContextRequirement.None;
|
||||||
|
public HappinessLifePolicy Life = HappinessLifePolicy.ActivityOnly;
|
||||||
|
public HappinessOverlapPolicy Overlap = HappinessOverlapPolicy.Independent;
|
||||||
|
public HappinessRelationRole RelationRole = HappinessRelationRole.None;
|
||||||
|
public string InterestCategory = "";
|
||||||
|
public string CanonicalMilestoneKey = "";
|
||||||
|
public string StatusOverlapId = "";
|
||||||
|
public string[] VariantsWithRelated = Array.Empty<string>();
|
||||||
|
public string[] VariantsWithoutRelated = Array.Empty<string>();
|
||||||
|
public bool IsFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Normalized happiness/emotion occurrence for Activity, Life, and future spectator interest.</summary>
|
||||||
|
public sealed class HappinessOccurrence
|
||||||
|
{
|
||||||
|
public string EffectId = "";
|
||||||
|
public int Amount;
|
||||||
|
public long SubjectId;
|
||||||
|
public string SubjectName = "";
|
||||||
|
public string SubjectSpecies = "";
|
||||||
|
public long RelatedId;
|
||||||
|
public string RelatedName = "";
|
||||||
|
public string RelatedSpecies = "";
|
||||||
|
public HappinessRelationRole RelationRole = HappinessRelationRole.None;
|
||||||
|
public Vector3 Position;
|
||||||
|
public double WorldTime;
|
||||||
|
public string DateLabel = "";
|
||||||
|
public HappinessSourceHook SourceHook = HappinessSourceHook.ChangeHappiness;
|
||||||
|
public string CorrelationKey = "";
|
||||||
|
public bool SuppressedByPsychopath;
|
||||||
|
public bool Applied;
|
||||||
|
public HappinessPresentationTier Presentation = HappinessPresentationTier.Signal;
|
||||||
|
public HappinessLifePolicy Life = HappinessLifePolicy.ActivityOnly;
|
||||||
|
public HappinessOverlapPolicy Overlap = HappinessOverlapPolicy.Independent;
|
||||||
|
public HappinessContextRequirement Context = HappinessContextRequirement.None;
|
||||||
|
public string InterestCategory = "";
|
||||||
|
public string CanonicalMilestoneKey = "";
|
||||||
|
public string StatusOverlapId = "";
|
||||||
|
public int SampledCount = 1;
|
||||||
|
public int TotalAffectedCount = 1;
|
||||||
|
public string CivEventKey = "";
|
||||||
|
public string PlainClause = "";
|
||||||
|
public string RichClause = "";
|
||||||
|
public string FactLine = "";
|
||||||
|
public bool WroteActivityRing;
|
||||||
|
public bool WroteLife;
|
||||||
|
public bool Merged;
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,10 @@ internal static class HarnessScenarios
|
||||||
return ActivityIdleSmoke();
|
return ActivityIdleSmoke();
|
||||||
case "activity_status_smoke":
|
case "activity_status_smoke":
|
||||||
return ActivityStatusSmoke();
|
return ActivityStatusSmoke();
|
||||||
|
case "activity_happiness_smoke":
|
||||||
|
return ActivityHappinessSmoke();
|
||||||
|
case "activity_happiness_audit":
|
||||||
|
return ActivityHappinessAudit();
|
||||||
case "activity_prose_showcase":
|
case "activity_prose_showcase":
|
||||||
return ActivityProseShowcase();
|
return ActivityProseShowcase();
|
||||||
case "activity_taxonomy_audit":
|
case "activity_taxonomy_audit":
|
||||||
|
|
@ -125,11 +129,95 @@ internal static class HarnessScenarios
|
||||||
Nested("reg_chronicle", "chronicle_smoke"),
|
Nested("reg_chronicle", "chronicle_smoke"),
|
||||||
Nested("reg_activity", "activity_idle_smoke"),
|
Nested("reg_activity", "activity_idle_smoke"),
|
||||||
Nested("reg_activity_status", "activity_status_smoke"),
|
Nested("reg_activity_status", "activity_status_smoke"),
|
||||||
|
Nested("reg_activity_happiness", "activity_happiness_smoke"),
|
||||||
Step("reg_end", "fast_timing", value: "false"),
|
Step("reg_end", "fast_timing", value: "false"),
|
||||||
Step("reg_snap", "snapshot"),
|
Step("reg_snap", "snapshot"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<HarnessCommand> ActivityHappinessAudit()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("aha0", "wait_world"),
|
||||||
|
Step("aha1", "pick_unit", asset: "auto"),
|
||||||
|
Step("aha2", "focus", asset: "auto"),
|
||||||
|
Step("aha3", "assert", expect: "activity_happiness_audit"),
|
||||||
|
Step("aha4", "snapshot")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HarnessCommand> ActivityHappinessSmoke()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("hp0", "wait_world"),
|
||||||
|
Step("hp1", "dismiss_windows"),
|
||||||
|
Step("hp1b", "spectator", value: "off"),
|
||||||
|
Step("hp2", "spawn", asset: "human"),
|
||||||
|
Step("hp3", "focus", asset: "auto"),
|
||||||
|
Step("hp4", "activity_clear"),
|
||||||
|
Step("hp5", "happiness_reset"),
|
||||||
|
Step("hp6", "assert", expect: "activity_happiness_audit"),
|
||||||
|
|
||||||
|
// Signal + Life: crown
|
||||||
|
Step("hp10", "happiness_apply", value: "become_king"),
|
||||||
|
Step("hp11", "assert", expect: "activity_happiness_task_id", value: "happiness:become_king"),
|
||||||
|
Step("hp12", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"),
|
||||||
|
Step("hp13", "assert", expect: "activity_happiness_life_count", value: "1", label: "min"),
|
||||||
|
Step("hp14", "assert", expect: "activity_log_contains", value: "throne"),
|
||||||
|
|
||||||
|
// Ambient cooldown: second meal within window should not add another ring write
|
||||||
|
Step("hp20", "happiness_reset"),
|
||||||
|
Step("hp21", "happiness_apply", value: "just_ate"),
|
||||||
|
Step("hp22", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"),
|
||||||
|
Step("hp23", "happiness_apply", value: "just_ate"),
|
||||||
|
Step("hp24", "assert", expect: "activity_happiness_count", value: "2", label: "exact"),
|
||||||
|
Step("hp25", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"),
|
||||||
|
|
||||||
|
// Force related grief prose
|
||||||
|
Step("hp30", "happiness_reset"),
|
||||||
|
Step("hp31", "happiness_force_note", value: "death_child", label: "Ava"),
|
||||||
|
Step("hp32", "assert", expect: "activity_log_contains", value: "Ava"),
|
||||||
|
Step("hp33", "assert", expect: "activity_happiness_task_id", value: "happiness:death_child"),
|
||||||
|
|
||||||
|
// Real lover grief path
|
||||||
|
Step("hp40", "spawn", asset: "human"),
|
||||||
|
Step("hp41", "happiness_remember_partner"),
|
||||||
|
Step("hp42", "spawn", asset: "human"),
|
||||||
|
Step("hp43", "focus", asset: "auto"),
|
||||||
|
Step("hp44", "activity_clear"),
|
||||||
|
Step("hp45", "happiness_reset"),
|
||||||
|
Step("hp46", "happiness_bond_lovers"),
|
||||||
|
Step("hp47", "wait", value: "0.5"),
|
||||||
|
Step("hp48", "happiness_kill_partner"),
|
||||||
|
Step("hp49", "wait", value: "0.5"),
|
||||||
|
Step("hp50", "assert", expect: "activity_log_contains", value: "lover"),
|
||||||
|
Step("hp51", "assert", expect: "activity_happiness_life_count", value: "1", label: "min"),
|
||||||
|
|
||||||
|
// Friend bond + canonical merge path
|
||||||
|
Step("hp60", "spawn", asset: "human"),
|
||||||
|
Step("hp61", "happiness_remember_partner"),
|
||||||
|
Step("hp62", "spawn", asset: "human"),
|
||||||
|
Step("hp63", "focus", asset: "auto"),
|
||||||
|
Step("hp64", "activity_clear"),
|
||||||
|
Step("hp65", "happiness_reset"),
|
||||||
|
Step("hp66", "happiness_bond_friends"),
|
||||||
|
Step("hp67", "assert", expect: "activity_happiness_count", value: "1", label: "min"),
|
||||||
|
|
||||||
|
// Full catalog inject through real API
|
||||||
|
Step("hp70", "happiness_reset"),
|
||||||
|
Step("hp71", "happiness_inject_all"),
|
||||||
|
Step("hp72", "assert", expect: "activity_happiness_count", value: "1", label: "min"),
|
||||||
|
|
||||||
|
// Aggregate burst budget
|
||||||
|
Step("hp80", "happiness_reset"),
|
||||||
|
Step("hp81", "happiness_burst", value: "just_started_war", count: 48),
|
||||||
|
|
||||||
|
Step("hp90", "snapshot")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static List<HarnessCommand> ActivityStatusAudit()
|
private static List<HarnessCommand> ActivityStatusAudit()
|
||||||
{
|
{
|
||||||
return new List<HarnessCommand>
|
return new List<HarnessCommand>
|
||||||
|
|
@ -469,14 +557,15 @@ internal static class HarnessScenarios
|
||||||
Step("dt1", "wait_world"),
|
Step("dt1", "wait_world"),
|
||||||
Step("dt2", "set_setting", expect: "enabled", value: "true"),
|
Step("dt2", "set_setting", expect: "enabled", value: "true"),
|
||||||
Step("dt3", "fast_timing", value: "true"),
|
Step("dt3", "fast_timing", value: "true"),
|
||||||
Step("dt4", "pick_unit", asset: "auto"),
|
// Calm unit + spectator restart clears collector so world Story tips cannot steal CurioA.
|
||||||
|
Step("dt4", "spawn", asset: "sheep"),
|
||||||
Step("dt5", "spectator", value: "off"),
|
Step("dt5", "spectator", value: "off"),
|
||||||
Step("dt6", "spectator", value: "on"),
|
Step("dt6", "spectator", value: "on"),
|
||||||
Step("dt7", "focus", asset: "auto"),
|
Step("dt7", "focus", asset: "sheep"),
|
||||||
Step("dt8", "reset_counters"),
|
Step("dt8", "reset_counters"),
|
||||||
|
|
||||||
// Curiosity can replace ambient after rotate window.
|
// Curiosity can replace ambient after rotate window.
|
||||||
Step("dt10", "trigger_interest", asset: "auto", label: "CurioA", tier: "Curiosity"),
|
Step("dt10", "trigger_interest", asset: "sheep", label: "CurioA", tier: "Curiosity"),
|
||||||
Step("dt11", "age_current", wait: 2f),
|
Step("dt11", "age_current", wait: 2f),
|
||||||
Step("dt12", "director_run", wait: 1.2f),
|
Step("dt12", "director_run", wait: 1.2f),
|
||||||
Step("dt13", "assert", expect: "current_tier", value: "Curiosity"),
|
Step("dt13", "assert", expect: "current_tier", value: "Curiosity"),
|
||||||
|
|
@ -512,28 +601,29 @@ internal static class HarnessScenarios
|
||||||
Step("d2", "set_setting", expect: "enabled", value: "true"),
|
Step("d2", "set_setting", expect: "enabled", value: "true"),
|
||||||
Step("d3", "fast_timing", value: "true"),
|
Step("d3", "fast_timing", value: "true"),
|
||||||
Step("d4", "discovery_reset"),
|
Step("d4", "discovery_reset"),
|
||||||
Step("d5", "pick_unit", asset: "auto"),
|
// Calm animal focus so Action-tier combat does not block curiosity drain.
|
||||||
|
Step("d5", "spawn", asset: "sheep"),
|
||||||
Step("d6", "spectator", value: "off"),
|
Step("d6", "spectator", value: "off"),
|
||||||
Step("d7", "spectator", value: "on"),
|
Step("d7", "spectator", value: "on"),
|
||||||
Step("d8", "focus", asset: "auto"),
|
Step("d8", "focus", asset: "sheep"),
|
||||||
Step("d9", "reset_counters"),
|
Step("d9", "reset_counters"),
|
||||||
|
|
||||||
// Live-path dedupe: mark presented drops buffered copy.
|
// Live-path dedupe: mark presented drops buffered copy.
|
||||||
Step("d10", "discovery_note", asset: "auto"),
|
Step("d10", "discovery_note", asset: "sheep"),
|
||||||
Step("d11", "assert", expect: "pending_discovery", value: "1"),
|
Step("d11", "assert", expect: "pending_discovery", value: "1"),
|
||||||
Step("d12", "discovery_mark", asset: "auto"),
|
Step("d12", "discovery_mark", asset: "sheep"),
|
||||||
Step("d13", "assert", expect: "pending_discovery", value: "0"),
|
Step("d13", "assert", expect: "pending_discovery", value: "0"),
|
||||||
Step("d14", "assert", expect: "presented", asset: "auto", value: "true"),
|
Step("d14", "assert", expect: "presented", asset: "sheep", value: "true"),
|
||||||
|
|
||||||
// Force drain path once curiosity is acceptable.
|
// Force drain path once curiosity is acceptable.
|
||||||
Step("d20", "discovery_reset"),
|
Step("d20", "discovery_reset"),
|
||||||
Step("d21", "discovery_note", asset: "auto"),
|
Step("d21", "discovery_note", asset: "sheep"),
|
||||||
Step("d22", "assert", expect: "pending_discovery", value: "1"),
|
Step("d22", "assert", expect: "pending_discovery", value: "1"),
|
||||||
Step("d23", "age_current", wait: 2f),
|
Step("d23", "age_current", wait: 2f),
|
||||||
Step("d24", "assert", expect: "would_accept_curiosity", value: "true"),
|
Step("d24", "assert", expect: "would_accept_curiosity", value: "true"),
|
||||||
Step("d25", "discovery_drain"),
|
Step("d25", "discovery_drain"),
|
||||||
Step("d26", "assert", expect: "pending_discovery_asset", value: "false"),
|
Step("d26", "assert", expect: "pending_discovery_asset", value: "false"),
|
||||||
Step("d27", "assert", expect: "presented", asset: "auto", value: "true"),
|
Step("d27", "assert", expect: "presented", asset: "sheep", value: "true"),
|
||||||
Step("d28", "director_run", wait: 1.2f),
|
Step("d28", "director_run", wait: 1.2f),
|
||||||
Step("d29", "assert", expect: "tip_matches_unit"),
|
Step("d29", "assert", expect: "tip_matches_unit"),
|
||||||
Step("d30", "assert", expect: "no_bad"),
|
Step("d30", "assert", expect: "no_bad"),
|
||||||
|
|
@ -804,6 +894,8 @@ internal static class HarnessScenarios
|
||||||
Step("ch18m", "lore_open_focus"),
|
Step("ch18m", "lore_open_focus"),
|
||||||
Step("ch18n", "wait", wait: 0.3f),
|
Step("ch18n", "wait", wait: 0.3f),
|
||||||
Step("ch18o", "assert", expect: "idle", value: "false"),
|
Step("ch18o", "assert", expect: "idle", value: "false"),
|
||||||
|
Step("ch18o1", "lore_tab", value: "living"),
|
||||||
|
Step("ch18o1b", "wait", wait: 0.15f),
|
||||||
Step("ch18o2", "assert", expect: "lore_tab", value: "living"),
|
Step("ch18o2", "assert", expect: "lore_tab", value: "living"),
|
||||||
Step("ch18o3", "assert", expect: "lore_detail", value: "true"),
|
Step("ch18o3", "assert", expect: "lore_detail", value: "true"),
|
||||||
Step("ch18o3b", "assert", expect: "lore_follow", value: "true"),
|
Step("ch18o3b", "assert", expect: "lore_follow", value: "true"),
|
||||||
|
|
@ -843,8 +935,9 @@ internal static class HarnessScenarios
|
||||||
Step("ch18p6", "wait", wait: 0.25f),
|
Step("ch18p6", "wait", wait: 0.25f),
|
||||||
Step("ch18p7", "assert", expect: "idle", value: "false"),
|
Step("ch18p7", "assert", expect: "idle", value: "false"),
|
||||||
Step("ch18p8", "assert", expect: "lore_detail", value: "true"),
|
Step("ch18p8", "assert", expect: "lore_detail", value: "true"),
|
||||||
Step("ch18p9", "assert", expect: "dossier_history_shown", value: "0", label: "exact", tier: "life"),
|
// Fresh spawn still gets just_born Life; empty History UI is gone - expect birth biography.
|
||||||
Step("ch18p10", "assert", expect: "lore_history_shown", value: "0", label: "exact"),
|
Step("ch18p9", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "life"),
|
||||||
|
Step("ch18p10", "assert", expect: "lore_history_shown", value: "1", label: "min"),
|
||||||
Step("ch18p11", "assert", expect: "dossier_lore_history_match"),
|
Step("ch18p11", "assert", expect: "dossier_lore_history_match"),
|
||||||
Step("ch18p12", "screenshot", value: "hud-lore-empty-history.png"),
|
Step("ch18p12", "screenshot", value: "hud-lore-empty-history.png"),
|
||||||
Step("ch18p13", "wait", wait: 0.55f),
|
Step("ch18p13", "wait", wait: 0.55f),
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,14 @@ public static class HudIcons
|
||||||
?? FromUiIcon("iconClock");
|
?? FromUiIcon("iconClock");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ActivityHappinessProse.TryParseTaskKey(key, out string happinessId))
|
||||||
|
{
|
||||||
|
return FromHappinessId(happinessId)
|
||||||
|
?? FromUiIcon("iconHappy")
|
||||||
|
?? Task()
|
||||||
|
?? FromUiIcon("iconClock");
|
||||||
|
}
|
||||||
|
|
||||||
string verb = ActivityVerbMap.Resolve(key);
|
string verb = ActivityVerbMap.Resolve(key);
|
||||||
if (string.IsNullOrEmpty(verb))
|
if (string.IsNullOrEmpty(verb))
|
||||||
{
|
{
|
||||||
|
|
@ -415,6 +423,64 @@ public static class HudIcons
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Sprite FromHappinessId(string effectId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(effectId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HappinessAsset asset = AssetManager.happiness_library?.get(effectId.Trim());
|
||||||
|
if (asset == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromHappiness(asset);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Sprite FromHappiness(HappinessAsset asset)
|
||||||
|
{
|
||||||
|
if (asset == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Sprite sprite = asset.getSprite();
|
||||||
|
if (sprite != null)
|
||||||
|
{
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(asset.path_icon))
|
||||||
|
{
|
||||||
|
return SpriteTextureLoader.getSprite(asset.path_icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static void Apply(UnityEngine.UI.Image image, Sprite sprite, bool preserveAspect = true)
|
public static void Apply(UnityEngine.UI.Image image, Sprite sprite, bool preserveAspect = true)
|
||||||
{
|
{
|
||||||
if (image == null)
|
if (image == null)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.12.103",
|
"version": "0.13.8",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Detailed living activity prose by default.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Exhaustive happiness/emotion Activity + Life logging.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ REGRESSION_SCENARIOS=(
|
||||||
chronicle_smoke
|
chronicle_smoke
|
||||||
activity_idle_smoke
|
activity_idle_smoke
|
||||||
activity_status_smoke
|
activity_status_smoke
|
||||||
|
activity_happiness_smoke
|
||||||
)
|
)
|
||||||
|
|
||||||
scenario=""
|
scenario=""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue