Enhance interest management in IdleSpectator by refining event handling and scoring logic. Update various event catalogs to adjust event strengths and ownership attributes, ensuring more accurate representation of combat and relationship events. Revise InterestDirector and InterestCompletion to improve camera dwell management and combat activity detection. Increment version in mod.json to 0.20.12 to reflect these changes.
This commit is contained in:
parent
c253b5dd9c
commit
ff824c76a1
11 changed files with 239 additions and 59 deletions
|
|
@ -327,7 +327,8 @@ public static class PhenotypeInterestCatalog
|
||||||
"{a} shows {id}",
|
"{a} shows {id}",
|
||||||
ambientStrength: 28f,
|
ambientStrength: 28f,
|
||||||
signalIds: null,
|
signalIds: null,
|
||||||
signalStrength: 60f);
|
signalStrength: 60f,
|
||||||
|
ambientCreatesInterest: false);
|
||||||
|
|
||||||
public static IEnumerable<string> AuthoredIds
|
public static IEnumerable<string> AuthoredIds
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ public static class InterestCompletion
|
||||||
return age < candidate.MaxWatch;
|
return age < candidate.MaxWatch;
|
||||||
|
|
||||||
case InterestCompletionKind.FixedDwell:
|
case InterestCompletionKind.FixedDwell:
|
||||||
return age < Mathf.Max(candidate.MinWatch, candidate.MaxWatch * 0.35f);
|
// Hold the authored MaxWatch window (director floors this to minCameraDwell).
|
||||||
|
return age < Mathf.Max(candidate.MinWatch, candidate.MaxWatch);
|
||||||
|
|
||||||
case InterestCompletionKind.CombatActive:
|
case InterestCompletionKind.CombatActive:
|
||||||
return CombatStillActive(candidate);
|
return CombatStillActive(candidate);
|
||||||
|
|
@ -64,17 +65,22 @@ public static class InterestCompletion
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float now = Time.unscaledTime;
|
||||||
|
bool hot = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (unit.has_attack_target)
|
if (unit.has_attack_target)
|
||||||
{
|
{
|
||||||
return true;
|
hot = true;
|
||||||
}
|
}
|
||||||
|
else if (unit.hasTask() && unit.ai?.task != null
|
||||||
if (unit.hasTask() && unit.ai?.task != null
|
&& (unit.ai.task.in_combat || unit.ai.task.is_fireman))
|
||||||
&& (unit.ai.task.in_combat || unit.ai.task.is_fireman))
|
|
||||||
{
|
{
|
||||||
return true;
|
hot = true;
|
||||||
|
}
|
||||||
|
else if (RelatedStillFightingUs(c, unit))
|
||||||
|
{
|
||||||
|
hot = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -82,14 +88,50 @@ public static class InterestCompletion
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battle asset without a live fighter: keep briefly via position only if battle still hot.
|
if (!hot && c.AssetId == "live_battle")
|
||||||
if (c.AssetId == "live_battle")
|
|
||||||
{
|
{
|
||||||
InterestEvent battle = WorldActivityScanner.FindHottestBattle();
|
InterestEvent battle = WorldActivityScanner.FindHottestBattle();
|
||||||
return battle != null;
|
hot = battle != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
if (hot)
|
||||||
|
{
|
||||||
|
// Refresh so brief attack_target gaps do not clear reason / open quiet grace.
|
||||||
|
c.LastSeenAt = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hysteresis: WorldBox clears attack_target between swings.
|
||||||
|
const float combatHysteresisSeconds = 8f;
|
||||||
|
return now - c.LastSeenAt < combatHysteresisSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RelatedStillFightingUs(InterestCandidate c, Actor unit)
|
||||||
|
{
|
||||||
|
Actor foe = c.RelatedUnit;
|
||||||
|
if (foe == null || !foe.isAlive())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!foe.has_attack_target || foe.attack_target == null || !foe.attack_target.isAlive())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foe.attack_target.isActor())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return foe.attack_target.a == unit;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ActivityStillActive(InterestCandidate c)
|
private static bool ActivityStillActive(InterestCandidate c)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ namespace IdleSpectator;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InterestDirector
|
public static class InterestDirector
|
||||||
{
|
{
|
||||||
public const float MinDwellSeconds = 10f;
|
public const float MinDwellSeconds = 15f;
|
||||||
public const float CuriosityDwellSeconds = 6f;
|
public const float CuriosityDwellSeconds = 6f;
|
||||||
public const float CuriosityRotateSeconds = 10f;
|
public const float CuriosityRotateSeconds = 10f;
|
||||||
public const float SwitchCooldownSeconds = 5f;
|
public const float SwitchCooldownSeconds = 5f;
|
||||||
|
|
@ -94,11 +94,12 @@ public static class InterestDirector
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Candidate that may drive the orange dossier reason: owns <paramref name="unit"/>
|
/// Candidate that may drive the orange dossier reason: owns <paramref name="unit"/>
|
||||||
/// and the director session is still in its active dwell (not quiet_grace).
|
/// while the director still holds that scene (not quiet_grace).
|
||||||
|
/// Sticky combat may briefly report !IsActive between swings - keep the reason up.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static InterestCandidate TryGetOwnedReasonCandidate(Actor unit)
|
public static InterestCandidate TryGetOwnedReasonCandidate(Actor unit)
|
||||||
{
|
{
|
||||||
if (unit == null || _current == null || !CurrentIsActive || InQuietGrace)
|
if (unit == null || _current == null || InQuietGrace)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +235,8 @@ public static class InterestDirector
|
||||||
}
|
}
|
||||||
|
|
||||||
float now = Time.unscaledTime;
|
float now = Time.unscaledTime;
|
||||||
float pastActive = Mathf.Max(_current.MinWatch, _current.MaxWatch * 0.35f) + 0.5f;
|
// Age past FixedDwell / Manual MaxWatch so IsActive is false.
|
||||||
|
float pastActive = Mathf.Max(_current.MinWatch, _current.MaxWatch) + 0.5f;
|
||||||
_currentStartedAt = now - pastActive;
|
_currentStartedAt = now - pastActive;
|
||||||
_lastSwitchAt = _currentStartedAt;
|
_lastSwitchAt = _currentStartedAt;
|
||||||
_inactiveSince = now - Mathf.Max(0f, inactiveSeconds);
|
_inactiveSince = now - Mathf.Max(0f, inactiveSeconds);
|
||||||
|
|
@ -411,6 +413,29 @@ public static class InterestDirector
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ambient: end promptly when the vignette goes cold - do not hold for minCameraDwell.
|
||||||
|
if (IsAmbientShot(_current))
|
||||||
|
{
|
||||||
|
if (_inactiveSince < 0f)
|
||||||
|
{
|
||||||
|
_inactiveSince = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - _inactiveSince >= _quietGrace)
|
||||||
|
{
|
||||||
|
EndCurrent(now, reason: "quiet_grace");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition ended early: still hold the camera until min dwell, then grace.
|
||||||
|
if (onCurrent < MinDwellFor(_current))
|
||||||
|
{
|
||||||
|
_inactiveSince = -999f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_inactiveSince < 0f)
|
if (_inactiveSince < 0f)
|
||||||
{
|
{
|
||||||
_inactiveSince = now;
|
_inactiveSince = now;
|
||||||
|
|
@ -649,29 +674,64 @@ public static class InterestDirector
|
||||||
|
|
||||||
private static float MinDwellFor(InterestCandidate current)
|
private static float MinDwellFor(InterestCandidate current)
|
||||||
{
|
{
|
||||||
|
ScoringWeights w = InterestScoringConfig.W;
|
||||||
|
float floor = w.minCameraDwell > 0f ? w.minCameraDwell : MinDwellSeconds;
|
||||||
if (current == null)
|
if (current == null)
|
||||||
{
|
{
|
||||||
return _minDwell;
|
return _minDwell < MinDwellSeconds * 0.5f ? _minDwell : floor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambient / Character fill: no event floor - real events may take the camera anytime.
|
||||||
|
if (IsAmbientShot(current))
|
||||||
|
{
|
||||||
|
if (_minDwell < MinDwellSeconds * 0.5f)
|
||||||
|
{
|
||||||
|
return InterestScoring.IsFillScore(current.TotalScore) ? _curiosityDwell : _minDwell;
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.dwellFill > 0f ? w.dwellFill : 6f;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScoringWeights w = InterestScoringConfig.W;
|
|
||||||
float dwell = InterestScoring.IsHotScore(current.TotalScore)
|
float dwell = InterestScoring.IsHotScore(current.TotalScore)
|
||||||
? w.dwellHot
|
? w.dwellHot
|
||||||
: (InterestScoring.IsFillScore(current.TotalScore) ? w.dwellFill : w.dwellNormal);
|
: w.dwellNormal;
|
||||||
if (_minDwell < MinDwellSeconds * 0.5f)
|
if (_minDwell < MinDwellSeconds * 0.5f)
|
||||||
{
|
{
|
||||||
// Harness fast timing: keep compressed dwells.
|
// Harness fast timing: keep compressed dwells.
|
||||||
dwell = InterestScoring.IsFillScore(current.TotalScore) ? _curiosityDwell : _minDwell;
|
dwell = _minDwell;
|
||||||
|
return current.MinWatch > 0f ? Mathf.Max(current.MinWatch, dwell) : dwell;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.MinWatch > 0f)
|
// Live EventLed: never peer-rotate / soft-cut before the camera floor.
|
||||||
|
// Score-margin interrupts bypass this via CanSwitchTo.
|
||||||
|
float minWatch = current.MinWatch > 0f ? current.MinWatch : 0f;
|
||||||
|
return Mathf.Max(floor, dwell, minWatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Character fill / low-score stroll - events should cut in freely.</summary>
|
||||||
|
private static bool IsAmbientShot(InterestCandidate c)
|
||||||
|
{
|
||||||
|
if (c == null)
|
||||||
{
|
{
|
||||||
return InterestScoring.IsFillScore(current.TotalScore)
|
return true;
|
||||||
? Mathf.Min(current.MinWatch, dwell)
|
|
||||||
: Mathf.Max(current.MinWatch, dwell * 0.15f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dwell;
|
if (c.LeadKind == InterestLeadKind.CharacterLed
|
||||||
|
|| c.Completion == InterestCompletionKind.CharacterVignette)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticky event holds are never ambient even if score is temporarily fill-band.
|
||||||
|
if (c.Completion == InterestCompletionKind.CombatActive
|
||||||
|
|| c.Completion == InterestCompletionKind.StatusPhase
|
||||||
|
|| c.Completion == InterestCompletionKind.HappinessGrief
|
||||||
|
|| c.Completion == InterestCompletionKind.ActivityActive)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InterestScoring.IsFillScore(c.TotalScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float MaxWatchFor(InterestCandidate current)
|
private static float MaxWatchFor(InterestCandidate current)
|
||||||
|
|
@ -709,7 +769,44 @@ public static class InterestDirector
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
float cap = current.MaxWatch > 0f ? Mathf.Min(current.MaxWatch, classCap) : classCap;
|
float cap = classCap;
|
||||||
|
if (current.MaxWatch > 0f)
|
||||||
|
{
|
||||||
|
// Sticky live holds use the class MaxWatch (e.g. combat 60s) - do not let a
|
||||||
|
// short candidate MaxWatch max_cap the fight while it is still active.
|
||||||
|
if (current.Completion == InterestCompletionKind.CombatActive
|
||||||
|
|| current.Completion == InterestCompletionKind.StatusPhase
|
||||||
|
|| current.Completion == InterestCompletionKind.HappinessGrief
|
||||||
|
|| current.Completion == InterestCompletionKind.ActivityActive)
|
||||||
|
{
|
||||||
|
cap = Mathf.Max(current.MaxWatch, classCap);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cap = Mathf.Min(current.MaxWatch, classCap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moment / FixedDwell shots must last at least the camera floor unless interrupted.
|
||||||
|
if (current.Completion == InterestCompletionKind.FixedDwell
|
||||||
|
|| current.Completion == InterestCompletionKind.Manual)
|
||||||
|
{
|
||||||
|
float floor = w.minCameraDwell > 0f ? w.minCameraDwell : MinDwellSeconds;
|
||||||
|
cap = Mathf.Max(cap, floor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambient fill: no minCameraDwell floor - short vignettes are fine.
|
||||||
|
if (IsAmbientShot(current))
|
||||||
|
{
|
||||||
|
float fillCap = w.dwellFill > 0f ? w.dwellFill * 2f : 20f;
|
||||||
|
if (current.MaxWatch > 0f)
|
||||||
|
{
|
||||||
|
fillCap = Mathf.Min(fillCap, Mathf.Max(current.MaxWatch, 8f));
|
||||||
|
}
|
||||||
|
|
||||||
|
cap = fillCap;
|
||||||
|
}
|
||||||
|
|
||||||
// Harness fast timing compresses caps.
|
// Harness fast timing compresses caps.
|
||||||
if (_minDwell < MinDwellSeconds * 0.5f)
|
if (_minDwell < MinDwellSeconds * 0.5f)
|
||||||
{
|
{
|
||||||
|
|
@ -735,6 +832,12 @@ public static class InterestDirector
|
||||||
bool active = InterestCompletion.IsActive(_current, _currentStartedAt, now);
|
bool active = InterestCompletion.IsActive(_current, _currentStartedAt, now);
|
||||||
if (!active)
|
if (!active)
|
||||||
{
|
{
|
||||||
|
// Ambient: no minCameraDwell hold after the vignette goes cold.
|
||||||
|
if (!IsAmbientShot(_current) && onCurrent < MinDwellFor(_current))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (_inactiveSince >= 0f && now - _inactiveSince >= _quietGrace)
|
if (_inactiveSince >= 0f && now - _inactiveSince >= _quietGrace)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -763,6 +866,19 @@ public static class InterestDirector
|
||||||
bool inGrace = InQuietGrace;
|
bool inGrace = InQuietGrace;
|
||||||
if (inGrace)
|
if (inGrace)
|
||||||
{
|
{
|
||||||
|
// Quiet grace after a sticky hold: only margin-worthy events may cut in.
|
||||||
|
// Prevents family-join / love status from stealing a fight between swings.
|
||||||
|
if (_current != null
|
||||||
|
&& (_current.Completion == InterestCompletionKind.CombatActive
|
||||||
|
|| _current.Completion == InterestCompletionKind.StatusPhase
|
||||||
|
|| _current.Completion == InterestCompletionKind.HappinessGrief))
|
||||||
|
{
|
||||||
|
float graceCur = _current.TotalScore;
|
||||||
|
float graceNext = candidate.TotalScore;
|
||||||
|
return graceNext >= graceCur + InterestScoringConfig.W.cutInMargin
|
||||||
|
&& IsWorthWatchingNow(candidate, now, selectingDuringGrace: true);
|
||||||
|
}
|
||||||
|
|
||||||
return IsWorthWatchingNow(candidate, now, selectingDuringGrace: true);
|
return IsWorthWatchingNow(candidate, now, selectingDuringGrace: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -776,7 +892,14 @@ public static class InterestDirector
|
||||||
float curScore = _current.TotalScore;
|
float curScore = _current.TotalScore;
|
||||||
float nextScore = candidate.TotalScore;
|
float nextScore = candidate.TotalScore;
|
||||||
bool nextFill = InterestScoring.IsFillScore(nextScore);
|
bool nextFill = InterestScoring.IsFillScore(nextScore);
|
||||||
bool curFill = InterestScoring.IsFillScore(curScore);
|
bool curAmbient = IsAmbientShot(_current);
|
||||||
|
bool curFill = curAmbient || InterestScoring.IsFillScore(curScore);
|
||||||
|
|
||||||
|
// Ambient fill: any real event may take the camera immediately (no min dwell).
|
||||||
|
if (curAmbient && !nextFill && !IsAmbientShot(candidate))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Instant score-margin cut - no settle grace, no MinDwell block.
|
// Instant score-margin cut - no settle grace, no MinDwell block.
|
||||||
if (nextScore >= curScore + w.cutInMargin)
|
if (nextScore >= curScore + w.cutInMargin)
|
||||||
|
|
@ -797,7 +920,7 @@ public static class InterestDirector
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protected EventLed hold: only margin cut-in (handled above).
|
// Protected EventLed hold: only margin cut-in (handled above).
|
||||||
if (protectedScene && !curFill)
|
if (protectedScene && !curAmbient)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -325,14 +325,15 @@ public static class InterestFeeds
|
||||||
}
|
}
|
||||||
|
|
||||||
string verb = taskOrBeat ?? "fighting";
|
string verb = taskOrBeat ?? "fighting";
|
||||||
string key = "activity:" + id + ":" + verb;
|
// One combat key per fighter so activity + scanner merge instead of thrashing.
|
||||||
|
string key = "combat:" + id;
|
||||||
var candidate = new InterestCandidate
|
var candidate = new InterestCandidate
|
||||||
{
|
{
|
||||||
Key = key,
|
Key = key,
|
||||||
LeadKind = InterestLeadKind.EventLed,
|
LeadKind = InterestLeadKind.EventLed,
|
||||||
Category = "Combat",
|
Category = "Combat",
|
||||||
Source = "activity",
|
Source = "activity",
|
||||||
EventStrength = 75f,
|
EventStrength = 88f,
|
||||||
VisualConfidence = 0.8f,
|
VisualConfidence = 0.8f,
|
||||||
Position = actor.current_position,
|
Position = actor.current_position,
|
||||||
FollowUnit = actor,
|
FollowUnit = actor,
|
||||||
|
|
@ -348,8 +349,8 @@ public static class InterestFeeds
|
||||||
CreatedAt = Time.unscaledTime,
|
CreatedAt = Time.unscaledTime,
|
||||||
LastSeenAt = Time.unscaledTime,
|
LastSeenAt = Time.unscaledTime,
|
||||||
ExpiresAt = Time.unscaledTime + 20f,
|
ExpiresAt = Time.unscaledTime + 20f,
|
||||||
MinWatch = 3f,
|
MinWatch = 4f,
|
||||||
MaxWatch = 30f,
|
MaxWatch = 60f,
|
||||||
Completion = InterestCompletionKind.CombatActive
|
Completion = InterestCompletionKind.CombatActive
|
||||||
};
|
};
|
||||||
candidate.ParticipantIds.Add(id);
|
candidate.ParticipantIds.Add(id);
|
||||||
|
|
@ -540,10 +541,10 @@ public static class InterestFeeds
|
||||||
float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId);
|
float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId);
|
||||||
bool birthMoment = IsFreshLifeMoment(occ.EffectId);
|
bool birthMoment = IsFreshLifeMoment(occ.EffectId);
|
||||||
|
|
||||||
// Birth/hatch only matter while still fresh - do not queue behind a long drama reel.
|
// Birth/hatch: short queue TTL, but on-camera floor comes from minCameraDwell.
|
||||||
float ttl = birthMoment ? 7f : 35f;
|
float ttl = birthMoment ? 7f : 35f;
|
||||||
float minWatch = birthMoment ? 2f : 4f;
|
float minWatch = birthMoment ? 4f : 8f;
|
||||||
float maxWatch = birthMoment ? 5.5f : 28f;
|
float maxWatch = birthMoment ? 15f : 28f;
|
||||||
if (birthMoment)
|
if (birthMoment)
|
||||||
{
|
{
|
||||||
strength = Mathf.Max(strength, 88f);
|
strength = Mathf.Max(strength, 88f);
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,15 @@ public class ScoringWeights
|
||||||
public float fillScoreMax = 55f;
|
public float fillScoreMax = 55f;
|
||||||
public float noticeScoreMin = 70f;
|
public float noticeScoreMin = 70f;
|
||||||
public float hotScoreMin = 110f;
|
public float hotScoreMin = 110f;
|
||||||
public float dwellFill = 6f;
|
public float dwellFill = 8f;
|
||||||
public float dwellNormal = 10f;
|
public float dwellNormal = 15f;
|
||||||
public float dwellHot = 14f;
|
public float dwellHot = 15f;
|
||||||
public float fillRotateSeconds = 10f;
|
public float fillRotateSeconds = 8f;
|
||||||
|
/// <summary>Minimum seconds on an EventLed shot before peer rotate / soft cut (interrupts bypass). Ambient exempt.</summary>
|
||||||
|
public float minCameraDwell = 15f;
|
||||||
|
|
||||||
// MaxWatch caps by completion class (director clamps candidate.MaxWatch).
|
// MaxWatch caps by completion class (director clamps candidate.MaxWatch).
|
||||||
public float maxWatchMoment = 10f;
|
public float maxWatchMoment = 15f;
|
||||||
public float maxWatchStatus = 30f;
|
public float maxWatchStatus = 30f;
|
||||||
public float maxWatchGrief = 45f;
|
public float maxWatchGrief = 45f;
|
||||||
public float maxWatchCombat = 60f;
|
public float maxWatchCombat = 60f;
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,24 @@ public static class RelationshipEventCatalog
|
||||||
Add("family_removed", 50f, "Relationship", "{a}'s family ends");
|
Add("family_removed", 50f, "Relationship", "{a}'s family ends");
|
||||||
Add("find_lover", 45f, "Relationship", "{a} is seeking a lover");
|
Add("find_lover", 45f, "Relationship", "{a} is seeking a lover");
|
||||||
Add("family_group_new", 60f, "Relationship", "{a} forms a family pack");
|
Add("family_group_new", 60f, "Relationship", "{a} forms a family pack");
|
||||||
Add("family_group_join", 48f, "Relationship", "{a} joins a family pack");
|
Add("family_group_join", 36f, "Relationship", "{a} joins a family pack", createsInterest: false);
|
||||||
Add("family_group_leave", 48f, "Relationship", "{a} leaves a family pack");
|
Add("family_group_leave", 36f, "Relationship", "{a} leaves a family pack", createsInterest: false);
|
||||||
Add("baby_created", 70f, "Relationship", "{a} welcomes a baby");
|
Add("baby_created", 70f, "Relationship", "{a} welcomes a baby");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Add(string id, float strength, string category, string label)
|
private static void Add(
|
||||||
|
string id,
|
||||||
|
float strength,
|
||||||
|
string category,
|
||||||
|
string label,
|
||||||
|
bool createsInterest = true)
|
||||||
{
|
{
|
||||||
Entries[id] = new DiscreteEventEntry
|
Entries[id] = new DiscreteEventEntry
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
EventStrength = strength,
|
EventStrength = strength,
|
||||||
Category = category,
|
Category = category,
|
||||||
CreatesInterest = true,
|
CreatesInterest = createsInterest,
|
||||||
LabelTemplate = label
|
LabelTemplate = label
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public static class StatusInterestCatalog
|
||||||
Add("tantrum", 52f, "StatusTransformation", true);
|
Add("tantrum", 52f, "StatusTransformation", true);
|
||||||
Add("egg", 40f, "StatusTransformation", false);
|
Add("egg", 40f, "StatusTransformation", false);
|
||||||
Add("magnetized", 55f, "StatusTransformation", true);
|
Add("magnetized", 55f, "StatusTransformation", true);
|
||||||
Add("invincible", 56f, "StatusTransformation", true);
|
Add("invincible", 40f, "StatusTransformation", false);
|
||||||
Add("powerup", 54f, "StatusTransformation", true);
|
Add("powerup", 54f, "StatusTransformation", true);
|
||||||
Add("enchanted", 52f, "StatusTransformation", true);
|
Add("enchanted", 52f, "StatusTransformation", true);
|
||||||
Add("ash_fever", 50f, "StatusTransformation", true);
|
Add("ash_fever", 50f, "StatusTransformation", true);
|
||||||
|
|
@ -49,7 +49,7 @@ public static class StatusInterestCatalog
|
||||||
Add("strange_urge", 45f, "StatusTransformation", true);
|
Add("strange_urge", 45f, "StatusTransformation", true);
|
||||||
Add("inspired", 42f, "Emotion", true);
|
Add("inspired", 42f, "Emotion", true);
|
||||||
Add("motivated", 40f, "Emotion", true);
|
Add("motivated", 40f, "Emotion", true);
|
||||||
Add("fell_in_love", 55f, "Relationship", true);
|
Add("fell_in_love", 42f, "Relationship", false);
|
||||||
Add("pregnant", 58f, "LifeChapter", true);
|
Add("pregnant", 58f, "LifeChapter", true);
|
||||||
Add("pregnant_parthenogenesis", 58f, "LifeChapter", true);
|
Add("pregnant_parthenogenesis", 58f, "LifeChapter", true);
|
||||||
Add("crying", 50f, "Grief", true, extendsGrief: true);
|
Add("crying", 50f, "Grief", true, extendsGrief: true);
|
||||||
|
|
|
||||||
|
|
@ -126,11 +126,11 @@ public static class WorldActivityScanner
|
||||||
string label = EventReason.Fight(actor, foe);
|
string label = EventReason.Fight(actor, foe);
|
||||||
var candidate = new InterestCandidate
|
var candidate = new InterestCandidate
|
||||||
{
|
{
|
||||||
Key = "scan:evt:" + id,
|
Key = "combat:" + id,
|
||||||
LeadKind = InterestLeadKind.EventLed,
|
LeadKind = InterestLeadKind.EventLed,
|
||||||
Category = "Combat",
|
Category = "Combat",
|
||||||
Source = "scanner",
|
Source = "scanner",
|
||||||
EventStrength = actionScore,
|
EventStrength = Mathf.Max(actionScore, 80f),
|
||||||
CharacterSignificance = charScore,
|
CharacterSignificance = charScore,
|
||||||
VisualConfidence = 0.7f,
|
VisualConfidence = 0.7f,
|
||||||
Position = actor.current_position,
|
Position = actor.current_position,
|
||||||
|
|
@ -149,8 +149,8 @@ public static class WorldActivityScanner
|
||||||
CreatedAt = Time.unscaledTime,
|
CreatedAt = Time.unscaledTime,
|
||||||
LastSeenAt = Time.unscaledTime,
|
LastSeenAt = Time.unscaledTime,
|
||||||
ExpiresAt = Time.unscaledTime + 18f,
|
ExpiresAt = Time.unscaledTime + 18f,
|
||||||
MinWatch = 2f,
|
MinWatch = 4f,
|
||||||
MaxWatch = 22f,
|
MaxWatch = 60f,
|
||||||
Completion = InterestCompletionKind.CombatActive
|
Completion = InterestCompletionKind.CombatActive
|
||||||
};
|
};
|
||||||
InterestScoring.ScoreCheap(candidate);
|
InterestScoring.ScoreCheap(candidate);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.20.9",
|
"version": "0.20.12",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,13 @@
|
||||||
"fillScoreMax": 55,
|
"fillScoreMax": 55,
|
||||||
"noticeScoreMin": 70,
|
"noticeScoreMin": 70,
|
||||||
"hotScoreMin": 110,
|
"hotScoreMin": 110,
|
||||||
"dwellFill": 6,
|
"dwellFill": 8,
|
||||||
"dwellNormal": 10,
|
"dwellNormal": 15,
|
||||||
"dwellHot": 14,
|
"dwellHot": 15,
|
||||||
"fillRotateSeconds": 10,
|
"fillRotateSeconds": 8,
|
||||||
|
"minCameraDwell": 15,
|
||||||
|
|
||||||
"maxWatchMoment": 10,
|
"maxWatchMoment": 15,
|
||||||
"maxWatchStatus": 30,
|
"maxWatchStatus": 30,
|
||||||
"maxWatchGrief": 45,
|
"maxWatchGrief": 45,
|
||||||
"maxWatchCombat": 60,
|
"maxWatchCombat": 60,
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,15 @@ Camera interrupt is score-margin only (`cutInMargin`); no Signal/Ambient ownersh
|
||||||
|
|
||||||
| Class | Seconds | JSON field |
|
| Class | Seconds | JSON field |
|
||||||
|-------|--------:|------------|
|
|-------|--------:|------------|
|
||||||
| Moment / FixedDwell | 10 | `maxWatchMoment` |
|
| Moment / FixedDwell | 15 | `maxWatchMoment` |
|
||||||
| Status | 30 | `maxWatchStatus` |
|
| Status | 30 | `maxWatchStatus` |
|
||||||
| Grief | 45 | `maxWatchGrief` |
|
| Grief | 45 | `maxWatchGrief` |
|
||||||
| Combat | 60 | `maxWatchCombat` |
|
| Combat | 60 | `maxWatchCombat` |
|
||||||
| Epic world | 50 | `maxWatchEpic` |
|
| Epic world | 50 | `maxWatchEpic` |
|
||||||
|
|
||||||
|
Peer rotate / soft cut on EventLed floors at `minCameraDwell` (15s). Score-margin interrupts bypass that floor.
|
||||||
|
Ambient / Character fill is exempt - events may take the camera immediately.
|
||||||
|
|
||||||
## Sources (wired)
|
## Sources (wired)
|
||||||
|
|
||||||
| Source | Primary owner | Reason factory | Notes |
|
| Source | Primary owner | Reason factory | Notes |
|
||||||
|
|
@ -61,9 +64,11 @@ Registry Upsert merges same key and refreshes `LastSeenAt` for sticky combat/sta
|
||||||
|
|
||||||
## Interrupt / hold / grace
|
## Interrupt / hold / grace
|
||||||
|
|
||||||
1. Instant cut when `next.TotalScore >= current + cutInMargin` (no settle).
|
1. Instant cut when `next.TotalScore >= current + cutInMargin` (no settle; ignores min dwell).
|
||||||
2. Else hold while `InterestCompletion.IsActive` and under MaxWatch.
|
2. Ambient fill: any EventLed may take the camera immediately (no `minCameraDwell`).
|
||||||
3. Quiet grace 6s: still-worth-watching filter, then Character fill with empty reason.
|
3. Else hold while `InterestCompletion.IsActive` and under MaxWatch (moments floor to `minCameraDwell`; combat uses class 60s).
|
||||||
|
4. Soft peer rotates on EventLed only after `minCameraDwell` (15s).
|
||||||
|
5. Quiet grace 6s: still-worth-watching filter, then Character fill with empty reason.
|
||||||
|
|
||||||
## Mutation gap triage
|
## Mutation gap triage
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue