Fix focus handoff
This commit is contained in:
parent
e4c00b4bbb
commit
3ed85d01d1
7 changed files with 114 additions and 40 deletions
|
|
@ -39,48 +39,67 @@ public static class CameraDirector
|
|||
LastWatchLabel = interest.Label ?? "";
|
||||
LastWatchAssetId = interest.AssetId ?? "";
|
||||
LogService.LogInfo($"[IdleSpectator] {tip}");
|
||||
WatchCaption.SetFromInterest(interest);
|
||||
|
||||
Actor follow = interest.HasFollowUnit
|
||||
? interest.FollowUnit
|
||||
: null;
|
||||
|
||||
// Species discovery: only attach a species-matched unit, never a random stranger.
|
||||
if (follow == null && !string.IsNullOrEmpty(interest.AssetId) && interest.AssetId != "live_battle"
|
||||
&& interest.AssetId != "scored_unit" && interest.Label != null && interest.Label.StartsWith("New species:"))
|
||||
{
|
||||
follow = WorldActivityScanner.FindNearestAliveUnit(interest.Position, 24f, interest.AssetId);
|
||||
}
|
||||
// Location-only civic: pan only. Do not invent a stranger FollowUnit.
|
||||
|
||||
Actor follow = ResolveFollowUnit(interest);
|
||||
if (follow != null && follow.isAlive())
|
||||
{
|
||||
// If this is a species event, refuse mismatched units.
|
||||
if (interest.Label != null && interest.Label.StartsWith("New species:")
|
||||
&& !string.IsNullOrEmpty(interest.AssetId)
|
||||
&& follow.asset != null && follow.asset.id != interest.AssetId)
|
||||
{
|
||||
Actor matched = WorldActivityScanner.FindNearestAliveUnit(interest.Position, 24f, interest.AssetId);
|
||||
if (matched == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
follow = matched;
|
||||
}
|
||||
|
||||
// Focus first, then nametag - same order as FocusUnit, so tip/focus/dossier
|
||||
// never diverge across a handoff frame.
|
||||
RetargetFollow(follow);
|
||||
WatchCaption.SetFromActor(follow);
|
||||
return;
|
||||
}
|
||||
|
||||
// No owned unit: pan only if we are not already following someone.
|
||||
// Never clear an existing focus unit here - that flashes the power bar.
|
||||
// No accepted living follow: tip already logged. Do not bind a rejected FollowUnit
|
||||
// (dead / species mismatch) as the dossier subject.
|
||||
if (!interest.HasFollowUnit)
|
||||
{
|
||||
WatchCaption.SetFromInterest(interest);
|
||||
}
|
||||
|
||||
if (!MoveCamera.hasFocusUnit())
|
||||
{
|
||||
PanCamera(interest.Position);
|
||||
}
|
||||
}
|
||||
|
||||
private static Actor ResolveFollowUnit(InterestEvent interest)
|
||||
{
|
||||
if (interest == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Actor follow = interest.HasFollowUnit ? interest.FollowUnit : null;
|
||||
|
||||
// Species discovery: only attach a species-matched unit, never a random stranger.
|
||||
bool speciesTip = interest.Label != null
|
||||
&& interest.Label.StartsWith("New species:");
|
||||
if (follow == null
|
||||
&& speciesTip
|
||||
&& !string.IsNullOrEmpty(interest.AssetId)
|
||||
&& interest.AssetId != "live_battle"
|
||||
&& interest.AssetId != "scored_unit")
|
||||
{
|
||||
follow = WorldActivityScanner.FindNearestAliveUnit(interest.Position, 24f, interest.AssetId);
|
||||
}
|
||||
|
||||
if (follow == null || !follow.isAlive())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (speciesTip
|
||||
&& !string.IsNullOrEmpty(interest.AssetId)
|
||||
&& follow.asset != null
|
||||
&& follow.asset.id != interest.AssetId)
|
||||
{
|
||||
return WorldActivityScanner.FindNearestAliveUnit(interest.Position, 24f, interest.AssetId);
|
||||
}
|
||||
|
||||
return follow;
|
||||
}
|
||||
|
||||
public static string FormatWatchTip(InterestEvent interest)
|
||||
{
|
||||
if (interest == null || string.IsNullOrEmpty(interest.Label))
|
||||
|
|
|
|||
|
|
@ -1062,6 +1062,7 @@ internal static class HarnessScenarios
|
|||
Step("cf23", "combat_maintain_focus"),
|
||||
Step("cf24", "assert", expect: "has_focus", value: "true"),
|
||||
Step("cf25", "assert", expect: "tip_matches_focus"),
|
||||
Step("cf25b", "assert", expect: "dossier_matches_focus"),
|
||||
Step("cf26", "assert", expect: "unit_asset", asset: "wolf"),
|
||||
|
||||
// Cold combat must not keep "is fighting".
|
||||
|
|
|
|||
|
|
@ -561,12 +561,60 @@ public static class WatchCaption
|
|||
SetVisible(true);
|
||||
}
|
||||
|
||||
// Safety net: any focus change that skipped SetFromActor (or rematched follow)
|
||||
// must not leave the nametag on the previous person for a frame.
|
||||
ReconcileDossierToFocus();
|
||||
RefreshLivePortrait();
|
||||
RefreshLiveTask();
|
||||
RefreshOwnedReason();
|
||||
RefreshHistoryIfChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// While Idle Spectator is live, dossier nametag must track the camera focus unit.
|
||||
/// Skips pause/fallen pins where the archive subject intentionally differs from focus.
|
||||
/// </summary>
|
||||
private static void ReconcileDossierToFocus()
|
||||
{
|
||||
if (_pinnedWhilePaused || !SpectatorMode.Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor focus = MoveCamera._focus_unit;
|
||||
if (!focus.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long focusId = 0;
|
||||
try
|
||||
{
|
||||
focusId = focus.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_current != null && _current.UnitId == focusId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetFromActor(focus);
|
||||
}
|
||||
|
||||
private static bool HasStatusBanner()
|
||||
{
|
||||
return !string.IsNullOrEmpty(_statusBanner) && Time.unscaledTime < _statusBannerUntil;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.25.51",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Combat focus + tip alignment.",
|
||||
"version": "0.25.52",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Dossier nametag tracks focus on handoff.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ Done (see [camera-presentability-plan.md](camera-presentability-plan.md) and `co
|
|||
- Combat tip/focus alignment (highest-scored participant; tip subject matches focus;
|
||||
clears "is fighting" when combat goes cold)
|
||||
|
||||
**Polish next:** dossier caption/nametag sync if tip+focus match but the nametag briefly shows someone else.
|
||||
Done (0.25.52): dossier nametag tracks focus on handoff (`CameraDirector.Watch` focus-then-caption;
|
||||
`WatchCaption.ReconcileDossierToFocus`; `combat_focus` asserts `dossier_matches_focus`).
|
||||
|
||||
Soak audit helper: `./scripts/soak-audit-player-log.sh [seconds]` (or pass `--since-bytes N`).
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ See [`camera-presentability-plan.md`](camera-presentability-plan.md).
|
|||
| Ambient building falls are B-only | live patch + pipelines `id_drop` |
|
||||
| Status-overlap onset without live status | `status_overlap_camera` / `status_overlap_absent` |
|
||||
| Combat tip subject matches focus | `combat_focus` / `tip_matches_focus` |
|
||||
| Combat dossier nametag matches focus | `combat_focus` / `dossier_matches_focus` |
|
||||
| Cold combat clears fight tip | `combat_focus` / `tip_not_contains` fighting |
|
||||
|
||||
Live idle soak helper (Player.log window):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
# ./scripts/soak-audit-player-log.sh # last 120s wall wait, then audit new bytes
|
||||
# ./scripts/soak-audit-player-log.sh 60 # wait 60s then audit
|
||||
# ./scripts/soak-audit-player-log.sh --since-bytes N # audit from byte offset (no wait)
|
||||
# ./scripts/soak-audit-player-log.sh --no-wait # audit from current EOF backward? no - from mark file
|
||||
# ./scripts/soak-audit-player-log.sh --since-bytes N 60 # wait 60s, then audit from N
|
||||
# ./scripts/soak-audit-player-log.sh --no-wait # audit empty window from current EOF
|
||||
#
|
||||
# Env:
|
||||
# PLAYER_LOG - override Player.log path
|
||||
|
|
@ -46,15 +47,18 @@ if [[ ! -f "$LOG" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
MARKED_FROM_EOF=0
|
||||
if [[ -z "$SINCE_BYTES" ]]; then
|
||||
SINCE_BYTES=$(wc -c < "$LOG" | tr -d ' ')
|
||||
if [[ "$SECONDS_WAIT" -gt 0 ]]; then
|
||||
echo "Marked offset $SINCE_BYTES; waiting ${SECONDS_WAIT}s (leave Idle Spectator running) ..."
|
||||
sleep "$SECONDS_WAIT"
|
||||
else
|
||||
echo "No wait and no --since-bytes: auditing from current EOF (empty window)."
|
||||
echo "Tip: ./scripts/soak-audit-player-log.sh 60"
|
||||
fi
|
||||
MARKED_FROM_EOF=1
|
||||
fi
|
||||
|
||||
if [[ "$SECONDS_WAIT" -gt 0 ]]; then
|
||||
echo "Marked offset $SINCE_BYTES; waiting ${SECONDS_WAIT}s (leave Idle Spectator running) ..."
|
||||
sleep "$SECONDS_WAIT"
|
||||
elif [[ "$MARKED_FROM_EOF" -eq 1 ]]; then
|
||||
echo "No wait and no --since-bytes: auditing from current EOF (empty window)."
|
||||
echo "Tip: ./scripts/soak-audit-player-log.sh 60"
|
||||
fi
|
||||
|
||||
python3 - "$LOG" "$SINCE_BYTES" <<'PY'
|
||||
|
|
|
|||
Loading…
Reference in a new issue