fix: block Suika falling splash water unit targets
This commit is contained in:
parent
7d2b7ef9d5
commit
78c72adba4
@ -0,0 +1,59 @@
|
||||
# TH1-CI-2026-06-27-001 - Suika falling splash water unit targeting
|
||||
|
||||
- Status: Fixed in code; guardrail added; Unity validation pending
|
||||
- First recorded date: 2026-06-27
|
||||
- Severity: High
|
||||
|
||||
## Raw Symptom
|
||||
|
||||
伊吹萃香 Lv4 的“从天而降”已禁止直接选择水域空格,但仍可选择 3 格距离外位于海上的敌方单位,并执行天降攻击。
|
||||
|
||||
Affected paths:
|
||||
|
||||
- `Unity/Assets/Scripts/TH1_Logic/Unit/UnitLogic.cs` move/attack target classification
|
||||
- `Unity/Assets/Scripts/TH1_Logic/Action/ActionLogic.cs` `UnitAttackAction.CheckCan`
|
||||
- `Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/HakureiNorwayHeroSkill.cs`
|
||||
|
||||
## Why This Is Recurring
|
||||
|
||||
Suika falling splash has two target paths:
|
||||
|
||||
- empty-grid targeting via `IsCanAttackTargetGrid` / `CanSuikaFlyToGrid`
|
||||
- enemy-unit targeting via `CanSuikaFlyAttackTarget` and the normal `UnitAttack` action
|
||||
|
||||
The earlier water fix only guarded the empty-grid path by checking the landing grid. The unit-target path still checked level, attack point, enemy, distance, and sight, but did not apply the same water-target rule before presenting or accepting the unit attack.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The ability-specific target contract was split between grid targeting and unit targeting. `UnitAttackAction.CheckCan` relies on `UnitData.IsCanAttackTargetUnit`, but `SuikaFallingSplashSkill` did not provide a negative filter for invalid 3-range falling-splash unit attacks.
|
||||
|
||||
## Root-Cause Fix
|
||||
|
||||
- Added a shared `CanUseGridAsSuikaFallingSplashTarget` predicate that rejects `TerrainType.ShallowSea` and `TerrainType.DeepSea`.
|
||||
- Applied it to `CanSuikaFlyToGrid` so empty water targets stay blocked.
|
||||
- Applied it to `CanSuikaFlyAttackTarget` so units standing on water targets are not presented as attackable.
|
||||
- Added `SuikaFallingSplashSkill.IsCanAttackTargetUnit` so directly constructed `UnitAttack` params are rejected by authoritative `CheckCan` when they represent a 3-range falling-splash attack.
|
||||
|
||||
## Guardrail Added
|
||||
|
||||
Added `Tools/CheckSuikaFallingSplashTargeting.ps1`.
|
||||
|
||||
The script verifies that:
|
||||
|
||||
- both ground and unit falling-splash target paths reuse the water-target predicate
|
||||
- the predicate rejects shallow/deep sea
|
||||
- `SuikaFallingSplashSkill.IsCanAttackTargetUnit` gates `UnitAttackAction.CheckCan`
|
||||
- `UnitLogic` still uses `CanSuikaFlyAttackTarget` for UI/AI target classification
|
||||
|
||||
## Verification Performed
|
||||
|
||||
- `Tools/CheckSuikaFallingSplashTargeting.ps1`
|
||||
- `dotnet build Unity/TH1.Hotfix.csproj --no-restore`
|
||||
|
||||
## Remaining Validation Gaps
|
||||
|
||||
Unity Editor scenario still needs manual validation:
|
||||
|
||||
- Lv4 Suika cannot select an enemy naval unit exactly 3 grids away on shallow/deep sea.
|
||||
- Lv4 Suika can still select a valid enemy unit exactly 3 grids away on land.
|
||||
- Lv4 Suika cannot select shallow/deep sea empty targets.
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
| ID | 日期 | 状态 | 严重度 | 问题 | 根治方向 | 记录 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 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-26-002 | 2026-06-26 | Fixed in code; guardrail added; Unity validation pending | Critical | Aunn shared HP ignored `DamageBearer` substitute-damage paths | Treat shared HP as an invariant over the actual mutated unit, `DamageBearer ?? DamageTarget`, with entrypoint fallback and shared-death cleanup | [record](2026-06-26-aunn-shared-health-damage-bearer.md) |
|
||||
| TH1-CI-2026-06-26-001 | 2026-06-26 | Fixed in code; guardrail added; Unity validation pending | High | Suika Lv4 falling splash can move data without complete landing, projectile, and fog/sight presentation after a 3-grid jump | Use a dedicated Suika falling-splash Fragment that owns jump, splash, final landing, and newly opened fog refresh | [record](2026-06-26-suika-falling-splash-landing-animation.md) |
|
||||
| TH1-CI-2026-06-25-004 | 2026-06-25 | Fixed after correcting Aunn 331/332/342/344 source-buff map; Unity validation pending | Critical | Aunn hero source skills and same-name runtime buffs were conflated and edited without a confirmed skill source map | Enforce 331 as defense-only runtime buff, 332 as hidden approach state, 342 as Lv3+ Aunn Unique/source skill, and 344 as same-name Positive beneficiary damage-bearing buff | [record](2026-06-25-aunn-lv3-petrified-defense-aura-display.md) |
|
||||
|
||||
61
Tools/CheckSuikaFallingSplashTargeting.ps1
Normal file
61
Tools/CheckSuikaFallingSplashTargeting.ps1
Normal file
@ -0,0 +1,61 @@
|
||||
param(
|
||||
[string]$SkillFile = "Unity/Assets/Scripts/TH1_Logic/Skill/AllSkill/HakureiNorwayHeroSkill.cs",
|
||||
[string]$UnitLogicFile = "Unity/Assets/Scripts/TH1_Logic/Unit/UnitLogic.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 "Suika falling splash targeting guardrail failed: $label missing '$needle'."
|
||||
}
|
||||
}
|
||||
|
||||
$skillText = Read-RepoFile $SkillFile
|
||||
$unitLogicText = Read-RepoFile $UnitLogicFile
|
||||
|
||||
Assert-Contains $skillText 'private static bool CanUseGridAsSuikaFallingSplashTarget' 'shared target grid predicate'
|
||||
Assert-Contains $skillText 'grid.Terrain is TerrainType.ShallowSea or TerrainType.DeepSea' 'water target rejection'
|
||||
Assert-Contains $skillText 'public static bool IsSuikaFallingSplashAttackAttempt' 'falling splash attack attempt predicate'
|
||||
Assert-Contains $skillText 'public override bool IsCanAttackTargetUnit' 'authoritative UnitAttack CheckCan gate'
|
||||
Assert-Contains $unitLogicText 'HakureiNorwayHeroSkillUtil.CanSuikaFlyAttackTarget' 'move/attack UI and AI entrypoint'
|
||||
|
||||
$flyGridMethod = [regex]::Match($skillText, '(?ms)public static bool CanSuikaFlyToGrid\(MapData map, UnitData suika, GridData target\).*?^\s*public static bool CanSuikaFlyAttackTarget')
|
||||
if (-not $flyGridMethod.Success) {
|
||||
throw "Suika falling splash targeting guardrail failed: cannot locate CanSuikaFlyToGrid body."
|
||||
}
|
||||
if ($flyGridMethod.Value -notmatch 'CanUseGridAsSuikaFallingSplashTarget\(map, suika, target\)[\s\S]*?CanUnitLandOnGrid\(map, suika, target\)') {
|
||||
throw "Suika falling splash targeting guardrail failed: ground-target path must reject water before landing/occupancy checks."
|
||||
}
|
||||
|
||||
$flyAttackMethod = [regex]::Match($skillText, '(?ms)public static bool CanSuikaFlyAttackTarget\(MapData map, UnitData suika, UnitData target\).*?^\s*public static bool IsSuikaFallingSplashAttackAttempt')
|
||||
if (-not $flyAttackMethod.Success) {
|
||||
throw "Suika falling splash targeting guardrail failed: cannot locate CanSuikaFlyAttackTarget body."
|
||||
}
|
||||
if ($flyAttackMethod.Value -notmatch 'IsSuikaFallingSplashAttackAttempt\(map, suika, target, out var targetGrid\)[\s\S]*?CanUseGridAsSuikaFallingSplashTarget\(map, suika, targetGrid\)') {
|
||||
throw "Suika falling splash targeting guardrail failed: unit-target path must reuse the same water target predicate."
|
||||
}
|
||||
|
||||
$skillClassMethod = [regex]::Match($skillText, '(?ms)public partial class SuikaFallingSplashSkill.*?^\s*public partial class SuikaThrowReadySkill')
|
||||
if (-not $skillClassMethod.Success) {
|
||||
throw "Suika falling splash targeting guardrail failed: cannot locate SuikaFallingSplashSkill body."
|
||||
}
|
||||
if ($skillClassMethod.Value -notmatch 'public override bool IsCanAttackTargetUnit\(MapData map, UnitData self, UnitData target\)[\s\S]*?IsSuikaFallingSplashAttackAttempt\(map, self, target, out _\)[\s\S]*?CanSuikaFlyAttackTarget\(map, self, target\)') {
|
||||
throw "Suika falling splash targeting guardrail failed: SuikaFallingSplashSkill must block invalid falling-splash UnitAttack params in CheckCan."
|
||||
}
|
||||
|
||||
Write-Host "Suika falling splash targeting guardrail passed."
|
||||
@ -122,6 +122,12 @@ namespace Logic.Skill
|
||||
return Main.UnitLogic.CheckUnitAbleForGrid_RealTimeStatus(map, unit, grid);
|
||||
}
|
||||
|
||||
private static bool CanUseGridAsSuikaFallingSplashTarget(MapData map, UnitData suika, GridData grid)
|
||||
{
|
||||
if (grid == null || grid.Terrain is TerrainType.ShallowSea or TerrainType.DeepSea) return false;
|
||||
return IsUnitLandingTerrain(map, suika, grid);
|
||||
}
|
||||
|
||||
public static bool TryFindFirstEmptyAround(MapData map, GridData center, int minRange, int maxRange,
|
||||
out GridData emptyGrid)
|
||||
{
|
||||
@ -1784,6 +1790,7 @@ namespace Logic.Skill
|
||||
if (!IsSuikaGiantThrowForm(suika)) return false;
|
||||
if (suika.GetSkill(SkillType.SuikaThrowReady, out _)) return false;
|
||||
if (suika.GetActionPoint(ActionPointType.Attack) <= 0) return false;
|
||||
if (!CanUseGridAsSuikaFallingSplashTarget(map, suika, target)) return false;
|
||||
if (!CanUnitLandOnGrid(map, suika, target)) return false;
|
||||
if (target.RealUnit(map, out _)) return false;
|
||||
if (!suika.Grid(map, out var suikaGrid)) return false;
|
||||
@ -1794,15 +1801,24 @@ namespace Logic.Skill
|
||||
|
||||
public static bool CanSuikaFlyAttackTarget(MapData map, UnitData suika, UnitData target)
|
||||
{
|
||||
if (map == null || suika == null || target == null) return false;
|
||||
if (!IsSuikaFallingSplashAttackAttempt(map, suika, target, out var targetGrid)) return false;
|
||||
if (!CanUseGridAsSuikaFallingSplashTarget(map, suika, targetGrid)) return false;
|
||||
var player = suika.Player(map);
|
||||
return player?.Sight != null && player.Sight.CheckIsInSight(targetGrid.Id);
|
||||
}
|
||||
|
||||
public static bool IsSuikaFallingSplashAttackAttempt(MapData map, UnitData suika, UnitData target,
|
||||
out GridData targetGrid)
|
||||
{
|
||||
targetGrid = null;
|
||||
if (map == null || suika == null || target == null) return false;
|
||||
if (!IsSuikaGiantThrowForm(suika)) return false;
|
||||
if (suika.GetSkill(SkillType.SuikaThrowReady, out _)) return false;
|
||||
if (suika.GetActionPoint(ActionPointType.Attack) <= 0) return false;
|
||||
if (!IsEnemy(map, suika, target)) return false;
|
||||
if (!suika.Grid(map, out var suikaGrid) || !target.Grid(map, out var targetGrid)) return false;
|
||||
if (map.GridMap.CalcDistance(suikaGrid, targetGrid) != SuikaFallingSplashRange) return false;
|
||||
var player = suika.Player(map);
|
||||
return player?.Sight != null && player.Sight.CheckIsInSight(targetGrid.Id);
|
||||
if (!suika.Grid(map, out var suikaGrid) || !target.Grid(map, out targetGrid)) return false;
|
||||
return map.GridMap.CalcDistance(suikaGrid, targetGrid) == SuikaFallingSplashRange;
|
||||
}
|
||||
|
||||
public static bool TryExecuteSuikaFlyToGround(MapData map, UnitData suika, GridData target,
|
||||
@ -3669,6 +3685,13 @@ namespace Logic.Skill
|
||||
return HakureiNorwayHeroSkillUtil.TryExecuteSuikaFlyToGround(map, self, target, out animSkillType);
|
||||
}
|
||||
|
||||
public override bool IsCanAttackTargetUnit(MapData map, UnitData self, UnitData target)
|
||||
{
|
||||
if (!HakureiNorwayHeroSkillUtil.IsSuikaFallingSplashAttackAttempt(map, self, target, out _))
|
||||
return true;
|
||||
return HakureiNorwayHeroSkillUtil.CanSuikaFlyAttackTarget(map, self, target);
|
||||
}
|
||||
|
||||
public override void AfterActiveAttackOther(MapData mapData, AttackInfo attackInfo)
|
||||
{
|
||||
HakureiNorwayHeroSkillUtil.TryExecuteSuikaFlyAfterAttack(mapData, attackInfo);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user