Fix displacement sight refresh
This commit is contained in:
parent
c3ff43e14f
commit
dba3113d88
@ -0,0 +1,56 @@
|
||||
# TH1-CI-2026-06-29-001 Movement Displacement Sight Refresh
|
||||
|
||||
- Status: Fixed in code; guardrail added; Unity validation pending
|
||||
- First recorded date: 2026-06-29
|
||||
- Severity: High
|
||||
|
||||
## Raw Symptom
|
||||
|
||||
Suika Lv4 falling splash attacks a unit exactly 3 grids away. When the attack does not kill the target, Suika lands on an adjacent side grid, but the fog/sight around that final landing position does not open correctly.
|
||||
|
||||
Affected entrypoints:
|
||||
|
||||
- `SuikaFallingSplashSkill.AfterActiveAttackOther`
|
||||
- `HakureiNorwayHeroSkillUtil.TryExecuteSuikaFlyAfterAttack`
|
||||
- `MapSightData.UpdateSightByPath`
|
||||
- `FragmentSuikaFallingSplash.RefreshLandingSight`
|
||||
|
||||
## Why This Is Recurring
|
||||
|
||||
TH1 has several movement-like paths that do not go through the standard `UnitMoveAction` presentation sequence: forced landing, push, throw, teleport, swap, and hero-specific jumps. Each path must update data, unit visuals, grid/city visuals, highlights, and sight.
|
||||
|
||||
The shared `UpdateSightByPath` helper was named like a movement sight refresh but used `GetAroundGridIdList(range, grid)` with the default `remainCenter: false`. That means the unit's final standing grid was not guaranteed to enter `SightGidSet`. Standard movement often hides this because the path or destination was already visible, but displacement to a side landing grid can expose the missing center grid.
|
||||
|
||||
Suika falling splash also maintains a separate list of newly opened grids for its delayed landing fragment. That list used the same default center-excluding radius query, so the fragment could also skip manually refreshing the final landing grid.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The movement sight contract was ambiguous. "Update sight by path / unit standing at pos" should reveal the standing grid plus surrounding sight radius, but the implementation delegated to a radius helper whose default excludes the center grid.
|
||||
|
||||
The problem is systematic because hero/skill displacement code repeatedly hand-writes sight refresh and renderer refresh instead of using one fully documented movement-displacement postcondition.
|
||||
|
||||
## Root-Cause Fix
|
||||
|
||||
- Changed `MapSightData.UpdateSightByPath` to call `GetAroundGridIdList(range, grid, remainCenter: true)`.
|
||||
- Changed `HakureiNorwayHeroSkillUtil.CollectSuikaFallingSplashNewSightGrids` to include `landingGrid` by using `remainCenter: true`.
|
||||
- Kept Suika's attack landing flow caching newly opened grids before the sight mutation, then passing those grids into `FragmentSuikaFallingSplash` for delayed fog/grid refresh.
|
||||
|
||||
## Guardrail Added
|
||||
|
||||
- Added `Tools/CheckMovementSightRefresh.ps1`.
|
||||
- Expanded `Tools/CheckSuikaFallingSplashAnimation.ps1`.
|
||||
|
||||
The guardrails now check that:
|
||||
|
||||
- `UpdateSightByPath` includes the standing grid.
|
||||
- Suika falling splash landing sight cache includes the final landing grid.
|
||||
- Suika attack landing caches sight before data reposition, updates sight at `landingGrid`, and hands cached grids to the landing fragment.
|
||||
|
||||
## Verification Performed
|
||||
|
||||
- `Tools/CheckMovementSightRefresh.ps1`
|
||||
- `Tools/CheckSuikaFallingSplashAnimation.ps1`
|
||||
|
||||
## Remaining Validation Gaps
|
||||
|
||||
- Unity Editor validation is still needed for the exact animation/timing case: Suika Lv4 attacks a 3-grid target, target survives, Suika lands on an adjacent side grid that was previously fogged, and the final standing grid plus surrounding sight open after the landing.
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
| ID | 日期 | 状态 | 严重度 | 问题 | 根治方向 | 记录 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| TH1-CI-2026-06-29-001 | 2026-06-29 | Fixed in code; guardrail added; Unity validation pending | High | Movement/skill displacement sight refresh could fail to reveal the unit's final standing grid, exposed by Suika Lv4 falling splash side landing after a non-lethal 3-grid attack | Make the shared movement sight path include the standing grid and guard Suika landing sight handoff | [record](2026-06-29-movement-displacement-sight-refresh.md) |
|
||||
| TH1-CI-2026-06-28-002 | 2026-06-28 | Fixed in code; guardrail added; Unity validation pending | High | Momiji hunter movement could lose 2 movement when another rule replaced the movement range | Split normal movement from prey-adjacent hunter target movement and ban the old global add-then-payback formula | [record](2026-06-28-momiji-hunter-movement-payback.md) |
|
||||
| TH1-CI-2026-06-28-001 | 2026-06-28 | Fixed in code; guardrail added; Unity validation pending | High | Aunn twin levels could diverge when one body was on a ship and the other upgraded or later landed | Treat Aunn level as a player-hero invariant over all same-player land and ship bodies, synchronized after upgrade and landing | [record](2026-06-28-aunn-ship-level-sync.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) |
|
||||
|
||||
65
Tools/CheckMovementSightRefresh.ps1
Normal file
65
Tools/CheckMovementSightRefresh.ps1
Normal file
@ -0,0 +1,65 @@
|
||||
param(
|
||||
[string]$PlayerDataFile = "Unity/Assets/Scripts/TH1_Data/PlayerData.cs",
|
||||
[string]$SkillFile = "Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/HakureiNorwayHeroSkill.cs",
|
||||
[string]$ActionFile = "Unity/Assets/Scripts/TH1_Logic/Action/ActionLogic.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())
|
||||
|
||||
function Read-RepoFile([string]$relativePath) {
|
||||
$fullPath = Join-Path $repoRoot $relativePath
|
||||
if (-not (Test-Path -LiteralPath $fullPath)) {
|
||||
throw "Required file not found: $fullPath"
|
||||
}
|
||||
return Get-Content -LiteralPath $fullPath -Raw -Encoding UTF8
|
||||
}
|
||||
|
||||
function Assert-Contains([string]$text, [string]$needle, [string]$label) {
|
||||
if (-not $text.Contains($needle)) {
|
||||
throw "Movement sight refresh guardrail failed: $label missing '$needle'."
|
||||
}
|
||||
}
|
||||
|
||||
$playerDataText = Read-RepoFile $PlayerDataFile
|
||||
$skillText = Read-RepoFile $SkillFile
|
||||
$actionText = Read-RepoFile $ActionFile
|
||||
|
||||
$updateSightByPath = [regex]::Match($playerDataText, '(?ms)public bool UpdateSightByPath\(UnitData unit,PlayerData player,Vector2Int pos,MapData map\)\s*\{[\s\S]*?return true;\s*\}')
|
||||
if (-not $updateSightByPath.Success) {
|
||||
throw "Movement sight refresh guardrail failed: cannot locate UpdateSightByPath."
|
||||
}
|
||||
|
||||
Assert-Contains $updateSightByPath.Value 'GetAroundGridIdList(range, grid, remainCenter: true)' 'UpdateSightByPath must reveal the unit standing grid'
|
||||
Assert-Contains $updateSightByPath.Value 'unit.HeroTask(map)?.OnExploredGrids(map, unit, count);' 'UpdateSightByPath must keep hero exploration task accounting'
|
||||
Assert-Contains $updateSightByPath.Value 'OnAnyExploredGrids(map, unit, count)' 'UpdateSightByPath must keep global hero exploration task accounting'
|
||||
|
||||
$suikaLandingSight = [regex]::Match($skillText, '(?ms)public static List<GridData> CollectSuikaFallingSplashNewSightGrids\(MapData map, UnitData suika,\s*PlayerData player, GridData landingGrid\).*?^\s*private static void ExecuteSuikaFallingSplashDamage', [System.Text.RegularExpressions.RegexOptions]::Multiline)
|
||||
if (-not $suikaLandingSight.Success) {
|
||||
throw "Movement sight refresh guardrail failed: cannot locate CollectSuikaFallingSplashNewSightGrids."
|
||||
}
|
||||
|
||||
Assert-Contains $suikaLandingSight.Value 'GetAroundGridIdList(range, landingGrid, remainCenter: true)' 'Suika landing sight refresh must include final landing grid'
|
||||
|
||||
$suikaFlyAfterAttack = [regex]::Match($skillText, '(?ms)public static bool TryExecuteSuikaFlyAfterAttack\(MapData map, AttackInfo attackInfo\).*?^\s*public static List<GridData> CollectSuikaFallingSplashNewSightGrids', [System.Text.RegularExpressions.RegexOptions]::Multiline)
|
||||
if (-not $suikaFlyAfterAttack.Success) {
|
||||
throw "Movement sight refresh guardrail failed: cannot locate TryExecuteSuikaFlyAfterAttack."
|
||||
}
|
||||
|
||||
if ($suikaFlyAfterAttack.Value -notmatch 'CollectSuikaFallingSplashNewSightGrids\(map, suika, attackInfo\.OriginPlayer,\s*landingGrid\)[\s\S]*?TryRepositionUnitWithoutMoveSideEffects\(map, suika, landingGrid\)[\s\S]*?UpdateSightByPath\(suika, attackInfo\.OriginPlayer, landingGrid\.Pos\.V2\(\), map\)') {
|
||||
throw "Movement sight refresh guardrail failed: Suika attack landing must cache new sight before data reposition and update sight at final landing grid."
|
||||
}
|
||||
|
||||
$unitAttackAction = [regex]::Match($actionText, '(?ms)private static bool TryCreateSuikaFallingSplashFragment\(.*?^\s*public override bool CheckCan', [System.Text.RegularExpressions.RegexOptions]::Multiline)
|
||||
if (-not $unitAttackAction.Success) {
|
||||
throw "Movement sight refresh guardrail failed: cannot locate Suika falling splash fragment handoff in UnitAttackAction."
|
||||
}
|
||||
|
||||
Assert-Contains $unitAttackAction.Value 'sightRefreshGrids: attackInfo.SuikaFallingSplashSightRefreshGrids' 'UnitAttackAction must hand cached landing sight grids to Suika fragment'
|
||||
|
||||
Write-Host "Movement sight refresh guardrail passed."
|
||||
@ -4,6 +4,7 @@ param(
|
||||
[string]$FragmentFile = "Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentSuikaFallingSplash.cs",
|
||||
[string]$FragmentDataFile = "Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentData.cs",
|
||||
[string]$AttackGroundFragmentFile = "Unity/Assets/Scripts/TH1_Anim/Fragments/FragmentAttackGround.cs",
|
||||
[string]$PlayerDataFile = "Unity/Assets/Scripts/TH1_Data/PlayerData.cs",
|
||||
[string]$UnitLogicFile = "Unity/Assets/Scripts/TH1_Logic/Unit/UnitLogic.cs",
|
||||
[string]$FragmentManagerFile = "Unity/Assets/Scripts/TH1_Anim/FragmentManager.cs",
|
||||
[string]$AtomFile = "Unity/Assets/Scripts/TH1_Anim/UnitAtomAnim/UnitAtomAnim.cs",
|
||||
@ -31,6 +32,7 @@ $actionText = Read-RepoFile $ActionFile
|
||||
$fragmentText = Read-RepoFile $FragmentFile
|
||||
$fragmentDataText = Read-RepoFile $FragmentDataFile
|
||||
$attackGroundFragmentText = Read-RepoFile $AttackGroundFragmentFile
|
||||
$playerDataText = Read-RepoFile $PlayerDataFile
|
||||
$unitLogicText = Read-RepoFile $UnitLogicFile
|
||||
$fragmentManagerText = Read-RepoFile $FragmentManagerFile
|
||||
$atomText = Read-RepoFile $AtomFile
|
||||
@ -61,6 +63,7 @@ Assert-Contains $fragmentText 'RefreshFinalState' 'final refresh'
|
||||
Assert-Contains $fragmentText 'RefreshLandingSight' 'landing sight refresh'
|
||||
Assert-Contains $fragmentText 'foreach (var grid in Data.SightRefreshGrids)' 'new sight grid iteration'
|
||||
Assert-Contains $fragmentText 'gridRenderer?.InstantUpdateGrid(true)' 'new sight grid force refresh'
|
||||
Assert-Contains $playerDataText 'GetAroundGridIdList(range, grid, remainCenter: true)' 'movement sight includes standing grid'
|
||||
Assert-Contains $fragmentDataText 'public List<GridData> SightRefreshGrids;' 'fragment sight refresh data'
|
||||
Assert-Contains $unitLogicText 'public List<GridData> SuikaFallingSplashSightRefreshGrids;' 'attack info sight refresh data'
|
||||
Assert-Contains $actionText 'sightRefreshGrids: attackInfo.SuikaFallingSplashSightRefreshGrids' 'attack fragment sight refresh handoff'
|
||||
@ -72,6 +75,7 @@ Assert-Contains $actionText 'FragmentType.SuikaFallingSplash' 'attack action fra
|
||||
Assert-Contains $actionText 'visualCollector?.FlushTo(suikaFallingSplashFragment)' 'splash damage visual collection'
|
||||
Assert-Contains $skillText 'TryRepositionUnitWithoutMoveSideEffects(map, suika, target)' 'ground self-jump data-only reposition'
|
||||
Assert-Contains $skillText 'CollectSuikaFallingSplashNewSightGrids' 'Suika sight refresh collector'
|
||||
Assert-Contains $skillText 'GetAroundGridIdList(range, landingGrid, remainCenter: true)' 'Suika landing sight includes final grid'
|
||||
Assert-Contains $skillText 'attackInfo.IsSuikaFallingSplash = true;' 'skill attack marker'
|
||||
Assert-Contains $skillText 'attackInfo.SuikaFallingSplashFinalGrid = landingGrid;' 'skill final grid marker'
|
||||
Assert-Contains $skillText 'ExecuteSuikaFallingSplashDamage(map, suika, targetGrid, AnimPhase.AttackImpact + 10)' 'post-impact splash visual phase'
|
||||
|
||||
@ -1576,7 +1576,7 @@ namespace RuntimeData
|
||||
return false;
|
||||
}
|
||||
var range = unit?.GetSightRange(map,grid) ?? 1;
|
||||
var list = map.GridMap.GetAroundGridIdList(range,grid);
|
||||
var list = map.GridMap.GetAroundGridIdList(range, grid, remainCenter: true);
|
||||
var count = Main.PlayerLogic.UpdateSight_LogicView(map, player, list);
|
||||
unit.HeroTask(map)?.OnExploredGrids(map, unit, count);
|
||||
foreach (var kv in player.PlayerHeroData.HeroTaskDict) kv.Value.OnAnyExploredGrids(map, unit, count);
|
||||
|
||||
@ -1991,7 +1991,7 @@ namespace Logic.Skill
|
||||
{
|
||||
if (map == null || suika == null || player?.Sight == null || landingGrid == null) return null;
|
||||
var range = suika.GetSightRange(map, landingGrid);
|
||||
var gridIds = map.GridMap.GetAroundGridIdList(range, landingGrid);
|
||||
var gridIds = map.GridMap.GetAroundGridIdList(range, landingGrid, remainCenter: true);
|
||||
var result = new List<GridData>();
|
||||
foreach (var gid in gridIds)
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user