Moar
This commit is contained in:
parent
4491423236
commit
c575c57190
7 changed files with 507 additions and 33 deletions
|
|
@ -1996,6 +1996,23 @@ public static class AgentHarness
|
|||
DoInterestForceSession(cmd);
|
||||
break;
|
||||
|
||||
case "interest_combat_session":
|
||||
DoInterestCombatSession(cmd);
|
||||
break;
|
||||
|
||||
case "combat_maintain_focus":
|
||||
{
|
||||
InterestDirector.HarnessMaintainCombatFocus();
|
||||
string tip = CameraDirector.LastWatchLabel ?? "";
|
||||
string focus = FocusLabel();
|
||||
_cmdOk++;
|
||||
Emit(
|
||||
cmd,
|
||||
ok: true,
|
||||
detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}");
|
||||
break;
|
||||
}
|
||||
|
||||
case "interest_variety_clear":
|
||||
InterestVariety.Clear();
|
||||
_cmdOk++;
|
||||
|
|
@ -3475,6 +3492,100 @@ public static class AgentHarness
|
|||
Emit(cmd, ok, detail: $"key={InterestDirector.CurrentKey} score={InterestDirector.CurrentScoreLabel} active={InterestDirector.CurrentIsActive}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force a CombatActive session with follow + related so handoff/tip rewrite can be asserted.
|
||||
/// asset = follow, value = related asset (optional), label overrides Fight prose.
|
||||
/// </summary>
|
||||
private static void DoInterestCombatSession(HarnessCommand cmd)
|
||||
{
|
||||
if (!WorldReady())
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "world_not_ready");
|
||||
return;
|
||||
}
|
||||
|
||||
Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset);
|
||||
if (follow == null)
|
||||
{
|
||||
follow = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
}
|
||||
|
||||
if (follow == null)
|
||||
{
|
||||
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
|
||||
}
|
||||
|
||||
Actor related = null;
|
||||
if (!string.IsNullOrEmpty(cmd.value) && cmd.value != "auto")
|
||||
{
|
||||
related = ResolveUnit(cmd.value);
|
||||
}
|
||||
|
||||
if (related == null || related == follow)
|
||||
{
|
||||
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
|
||||
{
|
||||
if (actor != null && actor != follow && actor.isAlive())
|
||||
{
|
||||
related = actor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (follow == null || !follow.isAlive())
|
||||
{
|
||||
_cmdFail++;
|
||||
Emit(cmd, ok: false, detail: "no follow unit for interest_combat_session");
|
||||
return;
|
||||
}
|
||||
|
||||
string label = string.IsNullOrEmpty(cmd.label)
|
||||
? EventReason.Fight(follow, related)
|
||||
: cmd.label.Replace("{a}", SafeName(follow)).Replace("{b}", SafeName(related));
|
||||
string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "combat_focus" : cmd.expect;
|
||||
InterestCandidate c = InterestFeeds.RegisterHarness(
|
||||
follow,
|
||||
label,
|
||||
keySuffix,
|
||||
lead: InterestLeadKind.EventLed,
|
||||
completion: InterestCompletionKind.CombatActive,
|
||||
forceActive: true,
|
||||
eventStrength: 120f,
|
||||
characterSignificance: 20f,
|
||||
maxWatch: 60f,
|
||||
ttlSeconds: 60f,
|
||||
related: related,
|
||||
resumable: true);
|
||||
if (c != null)
|
||||
{
|
||||
c.Category = "Combat";
|
||||
c.Verb = "fighting";
|
||||
if (related != null)
|
||||
{
|
||||
c.ParticipantCount = 2;
|
||||
}
|
||||
|
||||
InterestScoring.ScoreCheap(c);
|
||||
}
|
||||
|
||||
bool ok = InterestDirector.HarnessForceSession(c);
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} related={SafeName(related)} tip='{CameraDirector.LastWatchLabel}'");
|
||||
}
|
||||
|
||||
private static void DoSetSetting(HarnessCommand cmd)
|
||||
{
|
||||
string key = cmd.expect;
|
||||
|
|
@ -3581,6 +3692,41 @@ public static class AgentHarness
|
|||
$"tipAsset={tipAsset} unitAsset={unitAsset} tip={tip}";
|
||||
break;
|
||||
}
|
||||
case "tip_matches_focus":
|
||||
{
|
||||
Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
string tip = CameraDirector.LastWatchLabel ?? "";
|
||||
string name = SafeName(unit);
|
||||
if (string.IsNullOrEmpty(tip))
|
||||
{
|
||||
// Cleared fight tip during combat cold is OK.
|
||||
pass = true;
|
||||
detail = "tip_empty focus=" + name;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
pass = false;
|
||||
detail = "no_focus tip='" + tip + "'";
|
||||
}
|
||||
else
|
||||
{
|
||||
pass = tip.StartsWith(name, StringComparison.OrdinalIgnoreCase)
|
||||
|| tip.IndexOf(name, StringComparison.OrdinalIgnoreCase) == 0;
|
||||
// Also accept Battle-style where name leads.
|
||||
detail = $"tip='{tip}' focus='{name}' pass={pass}";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "tip_not_contains":
|
||||
{
|
||||
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
|
||||
string tip = CameraDirector.LastWatchLabel ?? "";
|
||||
pass = string.IsNullOrEmpty(needle)
|
||||
|| tip.IndexOf(needle, StringComparison.OrdinalIgnoreCase) < 0;
|
||||
detail = $"tip='{tip}' not_contains='{needle}'";
|
||||
break;
|
||||
}
|
||||
case "unit_asset":
|
||||
{
|
||||
string wantAsset = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
|
||||
|
|
|
|||
|
|
@ -338,7 +338,6 @@ public static class InterestFeeds
|
|||
return;
|
||||
}
|
||||
|
||||
long id = SafeId(actor);
|
||||
Actor foe = null;
|
||||
try
|
||||
{
|
||||
|
|
@ -353,7 +352,9 @@ public static class InterestFeeds
|
|||
}
|
||||
|
||||
string verb = taskOrBeat ?? "fighting";
|
||||
string key = EventCatalog.Combat.Key(id);
|
||||
WorldActivityScanner.PreferCombatFollow(actor, foe, out Actor follow, out Actor related);
|
||||
long followId = SafeId(follow ?? actor);
|
||||
string key = EventCatalog.Combat.Key(followId);
|
||||
var candidate = new InterestCandidate
|
||||
{
|
||||
Key = key,
|
||||
|
|
@ -362,17 +363,17 @@ public static class InterestFeeds
|
|||
Source = "activity",
|
||||
EventStrength = EventCatalog.Combat.ActivityStrength,
|
||||
VisualConfidence = 0.8f,
|
||||
Position = actor.current_position,
|
||||
FollowUnit = actor,
|
||||
RelatedUnit = foe,
|
||||
SubjectId = id,
|
||||
RelatedId = foe != null ? SafeId(foe) : 0,
|
||||
Label = EventReason.Fight(actor, foe),
|
||||
AssetId = actor.asset != null ? actor.asset.id : "",
|
||||
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
||||
Position = follow != null ? follow.current_position : actor.current_position,
|
||||
FollowUnit = follow ?? actor,
|
||||
RelatedUnit = related,
|
||||
SubjectId = followId,
|
||||
RelatedId = related != null ? SafeId(related) : 0,
|
||||
Label = EventReason.Fight(follow ?? actor, related),
|
||||
AssetId = (follow ?? actor).asset != null ? (follow ?? actor).asset.id : "",
|
||||
SpeciesId = (follow ?? actor).asset != null ? (follow ?? actor).asset.id : "",
|
||||
Verb = verb,
|
||||
CityKey = actor.city != null ? (actor.city.name ?? "") : "",
|
||||
KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
|
||||
CityKey = (follow ?? actor).city != null ? ((follow ?? actor).city.name ?? "") : "",
|
||||
KingdomKey = (follow ?? actor).kingdom != null ? ((follow ?? actor).kingdom.name ?? "") : "",
|
||||
CreatedAt = Time.unscaledTime,
|
||||
LastSeenAt = Time.unscaledTime,
|
||||
ExpiresAt = Time.unscaledTime + EventCatalog.Combat.ActivityTtl,
|
||||
|
|
@ -380,7 +381,15 @@ public static class InterestFeeds
|
|||
MaxWatch = EventCatalog.Combat.MaxWatch,
|
||||
Completion = InterestCompletionKind.CombatActive
|
||||
};
|
||||
candidate.ParticipantIds.Add(id);
|
||||
candidate.ParticipantIds.Add(followId);
|
||||
if (related != null)
|
||||
{
|
||||
long rid = SafeId(related);
|
||||
if (rid != 0)
|
||||
{
|
||||
candidate.ParticipantIds.Add(rid);
|
||||
}
|
||||
}
|
||||
InterestScoring.ScoreCheap(candidate);
|
||||
EventFeedUtil.RegisterCandidate(candidate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ internal static class HarnessScenarios
|
|||
case "status_overlap_camera":
|
||||
case "status_overlap":
|
||||
return StatusOverlapCamera();
|
||||
case "combat_focus":
|
||||
case "combat_tip_align":
|
||||
return CombatFocusAlign();
|
||||
case "director_action_priority":
|
||||
case "action_priority":
|
||||
return DirectorActionPriority();
|
||||
|
|
@ -1029,6 +1032,48 @@ internal static class HarnessScenarios
|
|||
Step("cp90", "fast_timing", value: "false"),
|
||||
Step("cp99", "snapshot"),
|
||||
Nested("cp_status_overlap", "status_overlap_camera"),
|
||||
Nested("cp_combat_focus", "combat_focus"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combat tip subject must match focus; death handoff rewrites tip; cold combat clears fight tip.
|
||||
/// </summary>
|
||||
private static List<HarnessCommand> CombatFocusAlign()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("cf0", "dismiss_windows"),
|
||||
Step("cf1", "wait_world"),
|
||||
Step("cf2", "set_setting", expect: "enabled", value: "true"),
|
||||
Step("cf3", "fast_timing", value: "true"),
|
||||
Step("cf4", "spawn", asset: "human", count: 1),
|
||||
Step("cf5", "spawn", asset: "wolf", count: 1),
|
||||
Step("cf6", "spectator", value: "off"),
|
||||
Step("cf7", "spectator", value: "on"),
|
||||
Step("cf8", "pick_unit", asset: "human"),
|
||||
Step("cf9", "focus", asset: "human"),
|
||||
Step("cf10", "interest_end_session"),
|
||||
|
||||
// Combat scene: human fighting wolf. Clear follow → related (wolf) must own tip+focus.
|
||||
Step("cf20", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_fight"),
|
||||
Step("cf21", "assert", expect: "tip_contains", value: "fighting"),
|
||||
Step("cf22", "interest_clear_follow", asset: "wolf"),
|
||||
Step("cf23", "combat_maintain_focus"),
|
||||
Step("cf24", "assert", expect: "has_focus", value: "true"),
|
||||
Step("cf25", "assert", expect: "tip_matches_focus"),
|
||||
Step("cf26", "assert", expect: "unit_asset", asset: "wolf"),
|
||||
|
||||
// Cold combat must not keep "is fighting".
|
||||
Step("cf30", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_cold"),
|
||||
Step("cf31", "assert", expect: "tip_contains", value: "fighting"),
|
||||
Step("cf32", "interest_release_force"),
|
||||
Step("cf33", "interest_mark_inactive", value: "1"),
|
||||
Step("cf34", "combat_maintain_focus"),
|
||||
Step("cf35", "assert", expect: "tip_not_contains", value: "fighting"),
|
||||
|
||||
Step("cf90", "fast_timing", value: "false"),
|
||||
Step("cf99", "snapshot"),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ public static class InterestDirector
|
|||
private static float _lastFeedsAt = -999f;
|
||||
private static float _enabledAt = -999f;
|
||||
private static float _inactiveSince = -999f;
|
||||
private static float _lastCombatFocusAt = -999f;
|
||||
private const float CombatFocusThrottleSeconds = 0.4f;
|
||||
private static readonly List<InterestCandidate> PendingScratch = new List<InterestCandidate>(96);
|
||||
|
||||
public static string CurrentTierName => CurrentScoreLabel;
|
||||
|
|
@ -357,6 +359,7 @@ public static class InterestDirector
|
|||
float sinceSwitch = now - _lastSwitchAt;
|
||||
|
||||
UpdateSessionLiveness(now, onCurrent);
|
||||
MaintainCombatFocus(now, force: false);
|
||||
|
||||
InterestCandidate next = SelectNext(now);
|
||||
if (next != null && CanSwitchTo(next, onCurrent, sinceSwitch))
|
||||
|
|
@ -466,6 +469,18 @@ public static class InterestDirector
|
|||
return;
|
||||
}
|
||||
|
||||
if (_current.Completion == InterestCompletionKind.CombatActive)
|
||||
{
|
||||
MaintainCombatFocus(Time.unscaledTime, force: true);
|
||||
if (!_current.HasFollowUnit
|
||||
&& (_current.RelatedUnit == null || !_current.RelatedUnit.isAlive()))
|
||||
{
|
||||
EndCurrent(Time.unscaledTime, reason: "follow_lost");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_current.HasFollowUnit)
|
||||
{
|
||||
// Living subject still owned - reattach if vanilla cleared focus mid-scene.
|
||||
|
|
@ -490,6 +505,92 @@ public static class InterestDirector
|
|||
EndCurrent(Time.unscaledTime, reason: "follow_lost");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combat scenes: keep camera on the highest-scored living participant and rewrite the tip
|
||||
/// so the subject always matches focus. Clears fight Labels once combat goes cold.
|
||||
/// </summary>
|
||||
private static void MaintainCombatFocus(float now, bool force)
|
||||
{
|
||||
if (_current == null || _current.Completion != InterestCompletionKind.CombatActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool combatHot = InterestCompletion.IsActive(_current, _currentStartedAt, now);
|
||||
if (!combatHot)
|
||||
{
|
||||
ClearStaleCombatLabel(now);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && now - _lastCombatFocusAt < CombatFocusThrottleSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastCombatFocusAt = now;
|
||||
if (!WorldActivityScanner.TryPickBestCombatFocus(
|
||||
_current, out Actor best, out Actor foe, out int fighters))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool mass = string.Equals(_current.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase)
|
||||
|| fighters > 2
|
||||
|| _current.ParticipantCount > 2;
|
||||
string label = mass
|
||||
? EventReason.Battle(best, fighters)
|
||||
: EventReason.Fight(best, foe);
|
||||
|
||||
bool followChanged = _current.FollowUnit != best;
|
||||
bool labelChanged = !string.Equals(_current.Label ?? "", label ?? "", StringComparison.Ordinal);
|
||||
_current.FollowUnit = best;
|
||||
_current.SubjectId = EventFeedUtil.SafeId(best);
|
||||
_current.RelatedUnit = foe;
|
||||
_current.RelatedId = foe != null ? EventFeedUtil.SafeId(foe) : 0;
|
||||
_current.Label = label;
|
||||
try
|
||||
{
|
||||
_current.Position = best.current_position;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// keep prior position
|
||||
}
|
||||
|
||||
if (followChanged
|
||||
|| labelChanged
|
||||
|| !HasLivingCameraFocus()
|
||||
|| MoveCamera._focus_unit != best)
|
||||
{
|
||||
CameraDirector.Watch(_current.ToInterestEvent());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearStaleCombatLabel(float now)
|
||||
{
|
||||
if (_current == null || string.IsNullOrEmpty(_current.Label))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_current.Label.IndexOf("fighting", StringComparison.OrdinalIgnoreCase) < 0
|
||||
&& _current.Label.IndexOf("fight of", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_current.Label = "";
|
||||
_lastCombatFocusAt = now;
|
||||
CameraDirector.Watch(_current.ToInterestEvent());
|
||||
}
|
||||
|
||||
/// <summary>Harness: force one combat focus maintenance pass.</summary>
|
||||
public static void HarnessMaintainCombatFocus()
|
||||
{
|
||||
MaintainCombatFocus(Time.unscaledTime, force: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keep a living focus unit whenever Idle Spectator is on.
|
||||
/// Covers death mid-combat and quiet_grace windows where vanilla cleared follow.
|
||||
|
|
@ -508,6 +609,15 @@ public static class InterestDirector
|
|||
|
||||
if (_current != null)
|
||||
{
|
||||
if (_current.Completion == InterestCompletionKind.CombatActive)
|
||||
{
|
||||
MaintainCombatFocus(now, force: true);
|
||||
if (HasLivingCameraFocus())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_current.HasFollowUnit)
|
||||
{
|
||||
CameraDirector.Watch(_current.ToInterestEvent());
|
||||
|
|
@ -519,12 +629,16 @@ public static class InterestDirector
|
|||
|
||||
if (_current.RelatedUnit != null && _current.RelatedUnit.isAlive())
|
||||
{
|
||||
_current.FollowUnit = _current.RelatedUnit;
|
||||
_current.SubjectId = EventFeedUtil.SafeId(_current.RelatedUnit);
|
||||
CameraDirector.Watch(_current.ToInterestEvent());
|
||||
if (HasLivingCameraFocus())
|
||||
// Non-combat ownership handoff; combat already tried picker above.
|
||||
if (_current.Completion != InterestCompletionKind.CombatActive)
|
||||
{
|
||||
return;
|
||||
_current.FollowUnit = _current.RelatedUnit;
|
||||
_current.SubjectId = EventFeedUtil.SafeId(_current.RelatedUnit);
|
||||
CameraDirector.Watch(_current.ToInterestEvent());
|
||||
if (HasLivingCameraFocus())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NeoModLoader.services;
|
||||
using UnityEngine;
|
||||
|
|
@ -123,7 +124,8 @@ public static class WorldActivityScanner
|
|||
CountFightCluster(actor.current_position, 10f, out int fighters, out int notables, out _);
|
||||
fighters = Mathf.Max(1, fighters);
|
||||
|
||||
string label = EventReason.Fight(actor, foe);
|
||||
PreferCombatFollow(actor, foe, out Actor follow, out Actor related);
|
||||
string label = EventReason.Fight(follow, related);
|
||||
var candidate = new InterestCandidate
|
||||
{
|
||||
Key = EventCatalog.Combat.Key(id),
|
||||
|
|
@ -133,17 +135,17 @@ public static class WorldActivityScanner
|
|||
EventStrength = Mathf.Max(actionScore, EventCatalog.Combat.ScannerStrengthFloor),
|
||||
CharacterSignificance = charScore,
|
||||
VisualConfidence = 0.7f,
|
||||
Position = actor.current_position,
|
||||
FollowUnit = actor,
|
||||
RelatedUnit = foe,
|
||||
SubjectId = id,
|
||||
RelatedId = foe != null ? EventFeedUtil.SafeId(foe) : 0,
|
||||
Position = follow != null ? follow.current_position : actor.current_position,
|
||||
FollowUnit = follow,
|
||||
RelatedUnit = related,
|
||||
SubjectId = follow != null ? EventFeedUtil.SafeId(follow) : id,
|
||||
RelatedId = related != null ? EventFeedUtil.SafeId(related) : 0,
|
||||
Label = label,
|
||||
Verb = "fighting",
|
||||
AssetId = actor.asset != null ? actor.asset.id : "scored_unit",
|
||||
SpeciesId = actor.asset != null ? actor.asset.id : "",
|
||||
CityKey = actor.city != null ? (actor.city.name ?? "") : "",
|
||||
KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
|
||||
AssetId = follow?.asset != null ? follow.asset.id : (actor.asset != null ? actor.asset.id : "scored_unit"),
|
||||
SpeciesId = follow?.asset != null ? follow.asset.id : (actor.asset != null ? actor.asset.id : ""),
|
||||
CityKey = follow?.city != null ? (follow.city.name ?? "") : (actor.city != null ? (actor.city.name ?? "") : ""),
|
||||
KingdomKey = follow?.kingdom != null ? (follow.kingdom.name ?? "") : (actor.kingdom != null ? (actor.kingdom.name ?? "") : ""),
|
||||
ParticipantCount = fighters,
|
||||
NotableParticipantCount = notables,
|
||||
CreatedAt = Time.unscaledTime,
|
||||
|
|
@ -318,6 +320,160 @@ public static class WorldActivityScanner
|
|||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Character weight for picking who to watch in a fight (notable / renown / role).
|
||||
/// Matches <see cref="CountFightCluster"/> bestFollow ranking.
|
||||
/// </summary>
|
||||
public static float CombatFocusWeight(Actor actor)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return -1f;
|
||||
}
|
||||
|
||||
SplitActorScore(actor, null, out _, out float charScore);
|
||||
float weight = charScore;
|
||||
if (InterestScoring.IsExtremelyNotable(actor))
|
||||
{
|
||||
weight += 30f;
|
||||
}
|
||||
|
||||
return weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefer the higher-scored of two combatants as follow; the other becomes related.
|
||||
/// </summary>
|
||||
public static void PreferCombatFollow(Actor a, Actor b, out Actor follow, out Actor related)
|
||||
{
|
||||
follow = a;
|
||||
related = b;
|
||||
if (a == null || !a.isAlive())
|
||||
{
|
||||
follow = b != null && b.isAlive() ? b : null;
|
||||
related = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (b == null || !b.isAlive())
|
||||
{
|
||||
related = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CombatFocusWeight(b) > CombatFocusWeight(a))
|
||||
{
|
||||
follow = b;
|
||||
related = a;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highest-scored living participant for an active combat scene.
|
||||
/// Considers Follow, Related, and (for mass fights) the nearby fight cluster.
|
||||
/// </summary>
|
||||
public static bool TryPickBestCombatFocus(
|
||||
InterestCandidate scene,
|
||||
out Actor best,
|
||||
out Actor foe,
|
||||
out int fighters)
|
||||
{
|
||||
best = null;
|
||||
foe = null;
|
||||
fighters = 1;
|
||||
if (scene == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fighters = Mathf.Max(1, scene.ParticipantCount);
|
||||
float bestWeight = -1f;
|
||||
Actor bestLocal = null;
|
||||
|
||||
void Consider(Actor unit)
|
||||
{
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float w = CombatFocusWeight(unit);
|
||||
if (w > bestWeight)
|
||||
{
|
||||
bestWeight = w;
|
||||
bestLocal = unit;
|
||||
}
|
||||
}
|
||||
|
||||
Consider(scene.FollowUnit);
|
||||
Consider(scene.RelatedUnit);
|
||||
|
||||
bool mass = string.Equals(scene.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase)
|
||||
|| scene.ParticipantCount > 2;
|
||||
Vector3 pos = scene.Position;
|
||||
if (bestLocal != null)
|
||||
{
|
||||
pos = bestLocal.current_position;
|
||||
}
|
||||
else if (scene.FollowUnit != null)
|
||||
{
|
||||
pos = scene.FollowUnit.current_position;
|
||||
}
|
||||
else if (scene.RelatedUnit != null)
|
||||
{
|
||||
pos = scene.RelatedUnit.current_position;
|
||||
}
|
||||
|
||||
if (mass || bestLocal == null)
|
||||
{
|
||||
float radius = mass ? 14f : 10f;
|
||||
CountFightCluster(pos, radius, out int clusterFighters, out _, out Actor clusterBest);
|
||||
fighters = Mathf.Max(fighters, clusterFighters);
|
||||
Consider(clusterBest);
|
||||
}
|
||||
|
||||
best = bestLocal;
|
||||
if (best == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (best.has_attack_target
|
||||
&& best.attack_target != null
|
||||
&& best.attack_target.isAlive()
|
||||
&& best.attack_target.isActor())
|
||||
{
|
||||
foe = best.attack_target.a;
|
||||
if (foe != null && !foe.isAlive())
|
||||
{
|
||||
foe = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
foe = null;
|
||||
}
|
||||
|
||||
if (foe == null)
|
||||
{
|
||||
Actor related = scene.RelatedUnit;
|
||||
Actor follow = scene.FollowUnit;
|
||||
if (related != null && related.isAlive() && related != best)
|
||||
{
|
||||
foe = related;
|
||||
}
|
||||
else if (follow != null && follow.isAlive() && follow != best)
|
||||
{
|
||||
foe = follow;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static float ScoreFightCluster(int deaths, int fighters, int notables)
|
||||
{
|
||||
ScoringWeights w = InterestScoringConfig.W;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.25.50",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Status-overlap camera hold gate.",
|
||||
"version": "0.25.51",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Combat focus + tip alignment.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,11 @@ See [world-event-inventory.md](world-event-inventory.md) for live library counts
|
|||
`mutation_discovery` refreshes `.harness/mutation_gaps.tsv`.
|
||||
Open rows are unexplained Wire candidates only.
|
||||
|
||||
## Idle quality (next)
|
||||
## Idle quality
|
||||
|
||||
Fire accuracy is covered by [event-live-verification-plan.md](event-live-verification-plan.md).
|
||||
Idle watch quality (presentability gate, ambient fall demotion, focus hold, tip placeholder) is tracked in [camera-presentability-plan.md](camera-presentability-plan.md).
|
||||
Presentability gate, ambient fall demotion, focus hold, and tip placeholder bans are done
|
||||
(see [camera-presentability-plan.md](camera-presentability-plan.md)).
|
||||
|
||||
**Polish next:** combat tip/focus alignment - camera follows the highest-scored living
|
||||
participant and rewrites the Watching tip so the subject matches focus; clears "is fighting"
|
||||
once combat goes cold.
|
||||
|
|
|
|||
Loading…
Reference in a new issue