From bbfed5bf97d1c67d3ebe3f8afee1975f5a9fa346 Mon Sep 17 00:00:00 2001 From: daixiawu Date: Tue, 30 Jun 2026 23:23:08 +0800 Subject: [PATCH] Fix move transform visual refresh --- ...026-06-30-move-transform-visual-refresh.md | 53 +++++++++++++++++++ MD/ChronicIssueList/index.md | 1 + Tools/CheckMoveTransformVisualRefresh.ps1 | 52 ++++++++++++++++++ .../TH1_Anim/Fragments/FragmentMove.cs | 3 +- .../Scripts/TH1_Renderer/UnitRenderer.cs | 6 +++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 MD/ChronicIssueList/2026-06-30-move-transform-visual-refresh.md create mode 100644 Tools/CheckMoveTransformVisualRefresh.ps1 diff --git a/MD/ChronicIssueList/2026-06-30-move-transform-visual-refresh.md b/MD/ChronicIssueList/2026-06-30-move-transform-visual-refresh.md new file mode 100644 index 000000000..f3aad8f8e --- /dev/null +++ b/MD/ChronicIssueList/2026-06-30-move-transform-visual-refresh.md @@ -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. diff --git a/MD/ChronicIssueList/index.md b/MD/ChronicIssueList/index.md index dda274d24..08d972fae 100644 --- a/MD/ChronicIssueList/index.md +++ b/MD/ChronicIssueList/index.md @@ -6,6 +6,7 @@ | 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-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) | diff --git a/Tools/CheckMoveTransformVisualRefresh.ps1 b/Tools/CheckMoveTransformVisualRefresh.ps1 new file mode 100644 index 000000000..72075f3b3 --- /dev/null +++ b/Tools/CheckMoveTransformVisualRefresh.ps1 @@ -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." diff --git a/Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentMove.cs b/Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentMove.cs index 665d73d56..6cb8ea67b 100644 --- a/Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentMove.cs +++ b/Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentMove.cs @@ -71,6 +71,7 @@ namespace TH1_Anim.Fragments { _step1_move = true; 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); Data.UnitRenderer.AnimManager.EnqueueAnim(UnitAtomAnimType.Move,animData); //刷新目标城市的状态(比如离开了着火的占领城市) @@ -184,4 +185,4 @@ namespace TH1_Anim.Fragments } } -} \ No newline at end of file +} diff --git a/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs b/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs index 2944794a5..aa003f0e2 100644 --- a/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs +++ b/Unity/Assets/Scripts/TH1_Renderer/UnitRenderer.cs @@ -418,6 +418,12 @@ namespace TH1_Renderer return _unitData != null && _unitData.InMainSight(); } + + public void InstantUpdateUnitImageOnly() + { + if (_unitMono == null || !TryRefreshUnitRefs()) return; + RenderUpdateUnitImage(); + } //瞬间更新unit的 die的情况 public bool InstantUpdateTryDie()