Fix move transform visual refresh

This commit is contained in:
daixiawu 2026-06-30 23:23:08 +08:00
parent c72daf8c46
commit bbfed5bf97
5 changed files with 114 additions and 1 deletions

View File

@ -0,0 +1,53 @@
# TH1-CI-2026-06-30-002 Move Transform Visual Refresh
- Status: Fixed in code; guardrail added; Unity validation pending
- First recorded date: 2026-06-30
- Severity: High
## Raw Symptom
Player report: a unit passively displaced by a giant into a port-water tile looked like it was still a ship after later moving ashore.
Existing local evidence:
- `DOC/bugs.json` has an open report: "船移动上岸但是没有立刻变成陆地单位".
- CrashSight summaries include repeated `BoatUnitOnLandState` / `BoatUnitOnLandBeforeAction` diagnostics around move fragments and landing-related paths.
## Why This Recurs
TH1 movement mutates authoritative data before the move Fragment plays. `UnitLogic.MoveToLogic` correctly handles:
- `LandAndPort` unit entering `ResourceType.Port` through `LandToBoat`.
- `WaterAndAshore` unit entering `TerrainType.Land` through `BoatToLand`.
However, `UnitTypeTransform` only changes data and skill state. Renderer sprite refresh normally happens at the end of `FragmentMove`, so during the move animation the visible unit can still use the pre-transform sprite. This is especially visible after passive displacement into a port followed by landing.
## Root Cause
`FragmentMove` started the move animation before refreshing the `UnitRenderer` image from already-mutated `UnitData`. When the action had already converted a unit between land and ship forms, the renderer could animate with the stale sprite until the fragment settle step.
## Root-Cause Fix
- Added `UnitRenderer.InstantUpdateUnitImageOnly()` for image/status refresh without changing visibility or position.
- `FragmentMove` now calls that method before enqueuing the move atom animation, so movement involving `LandToBoat` / `BoatToLand` starts with the current data form while preserving the existing final settle refresh.
## Guardrail Added
- `Tools/CheckMoveTransformVisualRefresh.ps1` checks that:
- `UnitRenderer` exposes the image-only refresh method.
- `FragmentMove` refreshes transformed unit image before starting `UnitAtomAnimType.Move`.
- `FragmentMove` still performs its final `InstantUpdateUnit(true)` settle refresh.
## Verification Performed
- Pending in this work item:
- `Tools/CheckMoveTransformVisualRefresh.ps1`
- `dotnet build Unity/TH1.Hotfix.csproj --no-restore`
## Remaining Validation Gaps
- Unity Editor visual validation is still required for the exact scenario:
1. A giant spawns or moves onto an occupied tile.
2. The displaced land unit passively moves into an allied port-water tile.
3. The resulting ship moves ashore.
4. During and after the landing animation, the unit sprite matches its land form.

View File

@ -6,6 +6,7 @@
| ID | 日期 | 状态 | 严重度 | 问题 | 根治方向 | 记录 | | ID | 日期 | 状态 | 严重度 | 问题 | 根治方向 | 记录 |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| TH1-CI-2026-06-30-002 | 2026-06-30 | Fixed in code; guardrail added; Unity validation pending | High | 上下船移动的数据形态已变化,但 `FragmentMove` 开始动画时仍可能显示旧船/旧陆地外观 | 在移动 Fragment 起步前用当前 `UnitData` 刷新单位图片,同时保留终点 settle 刷新 | [record](2026-06-30-move-transform-visual-refresh.md) |
| TH1-CI-2026-06-30-001 | 2026-06-30 | Fixed in code; guardrail added; Unity validation pending | High | `UnitAttackGround` support/place skills can reset Peace wonder progress because the entrypoint name is treated as active attack | Centralize ground-target active-attack semantics and require every `AttackGroundExecute` skill to declare whether it counts as an attack | [record](2026-06-30-attackground-peace-wonder-semantics.md) | | TH1-CI-2026-06-30-001 | 2026-06-30 | Fixed in code; guardrail added; Unity validation pending | High | `UnitAttackGround` support/place skills can reset Peace wonder progress because the entrypoint name is treated as active attack | Centralize ground-target active-attack semantics and require every `AttackGroundExecute` skill to declare whether it counts as an attack | [record](2026-06-30-attackground-peace-wonder-semantics.md) |
| TH1-CI-2026-06-29-001 | 2026-06-29 | Fixed in code; 17-player 10-turn batch passed | Critical | AI Director fallback can repeat zero-effect actions until the AI loop guard fires | Filter no-progress fallback actions, align BuildWonder CheckCan with Execute prerequisites, and keep batch diagnostics compact | [record](2026-06-29-ai-director-zero-effect-action-loop.md) | | TH1-CI-2026-06-29-001 | 2026-06-29 | Fixed in code; 17-player 10-turn batch passed | Critical | AI Director fallback can repeat zero-effect actions until the AI loop guard fires | Filter no-progress fallback actions, align BuildWonder CheckCan with Execute prerequisites, and keep batch diagnostics compact | [record](2026-06-29-ai-director-zero-effect-action-loop.md) |
| TH1-CI-2026-06-27-001 | 2026-06-27 | Fixed in code; guardrail added; Unity validation pending | High | Suika Lv4 falling splash water restriction applied to empty grids but not unit targets on water | Share falling-splash water target validation across ground targeting, unit targeting, and `UnitAttack` CheckCan filtering | [record](2026-06-27-suika-falling-splash-water-targeting.md) | | TH1-CI-2026-06-27-001 | 2026-06-27 | Fixed in code; guardrail added; Unity validation pending | High | Suika Lv4 falling splash water restriction applied to empty grids but not unit targets on water | Share falling-splash water target validation across ground targeting, unit targeting, and `UnitAttack` CheckCan filtering | [record](2026-06-27-suika-falling-splash-water-targeting.md) |

View File

@ -0,0 +1,52 @@
param(
[string]$FragmentMoveFile = "Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentMove.cs",
[string]$UnitRendererFile = "Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs"
)
$ErrorActionPreference = "Stop"
$repoRoot = git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) {
throw "Not inside a git repository."
}
$repoRoot = [System.IO.Path]::GetFullPath($repoRoot.Trim())
$fragmentMovePath = Join-Path $repoRoot $FragmentMoveFile
$unitRendererPath = Join-Path $repoRoot $UnitRendererFile
if (-not (Test-Path -LiteralPath $fragmentMovePath)) {
throw "FragmentMove file not found: $fragmentMovePath"
}
if (-not (Test-Path -LiteralPath $unitRendererPath)) {
throw "UnitRenderer file not found: $unitRendererPath"
}
$fragmentMoveText = Get-Content -LiteralPath $fragmentMovePath -Raw -Encoding UTF8
$unitRendererText = Get-Content -LiteralPath $unitRendererPath -Raw -Encoding UTF8
foreach ($needle in @(
'public void InstantUpdateUnitImageOnly()',
'RenderUpdateUnitImage();'
)) {
if (-not $unitRendererText.Contains($needle)) {
throw "Move transform visual-refresh guardrail failed: UnitRenderer missing '$needle'."
}
}
foreach ($needle in @(
'Data.UnitRenderer?.InstantUpdateUnitImageOnly();',
'Data.UnitRenderer.AnimManager.EnqueueAnim(UnitAtomAnimType.Move,animData);',
'Data.UnitRenderer?.InstantUpdateUnit(true);'
)) {
if (-not $fragmentMoveText.Contains($needle)) {
throw "Move transform visual-refresh guardrail failed: FragmentMove missing '$needle'."
}
}
$startRefresh = $fragmentMoveText.IndexOf('Data.UnitRenderer?.InstantUpdateUnitImageOnly();')
$moveAnim = $fragmentMoveText.IndexOf('Data.UnitRenderer.AnimManager.EnqueueAnim(UnitAtomAnimType.Move,animData);')
if ($startRefresh -lt 0 -or $moveAnim -lt 0 -or $startRefresh -gt $moveAnim) {
throw "Move transform visual-refresh guardrail failed: FragmentMove must refresh transformed unit image before starting the move animation."
}
Write-Host "Move transform visual-refresh guardrail passed."

View File

@ -71,6 +71,7 @@ namespace TH1_Anim.Fragments
{ {
_step1_move = true; _step1_move = true;
if (Data.OriginGrid == null || Data.TargetGrid == null) return; if (Data.OriginGrid == null || Data.TargetGrid == null) return;
Data.UnitRenderer?.InstantUpdateUnitImageOnly();
var animData = UnitAtomAnimDataFactory.Create(UnitAtomAnimType.Move,Data.OriginGrid.Pos.V2(), Data.TargetGrid.Pos.V2(),Data.Path); var animData = UnitAtomAnimDataFactory.Create(UnitAtomAnimType.Move,Data.OriginGrid.Pos.V2(), Data.TargetGrid.Pos.V2(),Data.Path);
Data.UnitRenderer.AnimManager.EnqueueAnim(UnitAtomAnimType.Move,animData); Data.UnitRenderer.AnimManager.EnqueueAnim(UnitAtomAnimType.Move,animData);
//刷新目标城市的状态(比如离开了着火的占领城市) //刷新目标城市的状态(比如离开了着火的占领城市)
@ -184,4 +185,4 @@ namespace TH1_Anim.Fragments
} }
} }
} }

View File

@ -418,6 +418,12 @@ namespace TH1_Renderer
return _unitData != null && _unitData.InMainSight(); return _unitData != null && _unitData.InMainSight();
} }
public void InstantUpdateUnitImageOnly()
{
if (_unitMono == null || !TryRefreshUnitRefs()) return;
RenderUpdateUnitImage();
}
//瞬间更新unit的 die的情况 //瞬间更新unit的 die的情况
public bool InstantUpdateTryDie() public bool InstantUpdateTryDie()