diff --git a/docs/battle-unit-hitbox-local-command-menu.png b/docs/battle-unit-hitbox-local-command-menu.png new file mode 100644 index 0000000..610920e Binary files /dev/null and b/docs/battle-unit-hitbox-local-command-menu.png differ diff --git a/docs/battle-unit-hitbox-local-selection.png b/docs/battle-unit-hitbox-local-selection.png new file mode 100644 index 0000000..e0b25ce Binary files /dev/null and b/docs/battle-unit-hitbox-local-selection.png differ diff --git a/docs/battle-unit-hitbox-local-target-chosen.png b/docs/battle-unit-hitbox-local-target-chosen.png new file mode 100644 index 0000000..1d26d55 Binary files /dev/null and b/docs/battle-unit-hitbox-local-target-chosen.png differ diff --git a/docs/battle-unit-hitbox-prod-command-menu.png b/docs/battle-unit-hitbox-prod-command-menu.png new file mode 100644 index 0000000..3d7aa8d Binary files /dev/null and b/docs/battle-unit-hitbox-prod-command-menu.png differ diff --git a/docs/battle-unit-hitbox-prod-selection.png b/docs/battle-unit-hitbox-prod-selection.png new file mode 100644 index 0000000..aff4ce0 Binary files /dev/null and b/docs/battle-unit-hitbox-prod-selection.png differ diff --git a/docs/battle-unit-hitbox-prod-target-chosen.png b/docs/battle-unit-hitbox-prod-target-chosen.png new file mode 100644 index 0000000..22b7688 Binary files /dev/null and b/docs/battle-unit-hitbox-prod-target-chosen.png differ diff --git a/docs/battle-unit-hitbox-report.md b/docs/battle-unit-hitbox-report.md new file mode 100644 index 0000000..8193c71 --- /dev/null +++ b/docs/battle-unit-hitbox-report.md @@ -0,0 +1,40 @@ +# Battle Unit Hitbox Verification + +Date: 2026-07-03 + +## Change + +- Unit sprites no longer receive pointer input from the full enlarged sprite rectangle. +- Each unit now owns an invisible tile-sized hit zone centered on its tactical tile. +- The hit zone follows unit movement, visibility, retreat state, and map visibility. +- Attack/support target markers stay above unit hit zones, so target selection keeps priority during targeting. + +## Local Verification + +- Command: `pnpm build` +- URL: `http://127.0.0.1:4193/heros_web/?debug&debugBattleSetup=attack-preview&v=unit-hitbox-local` +- Adjacent unit selection: clicked Liu Bei, Guan Yu, and Zhang Fei by tile center; final selection was Zhang Fei. +- Command menu: clicked Zhang Fei tile center again; Zhang Fei command menu opened. +- Targeting: selected Guan Yu attack, clicked the adjacent Yellow Turban target, and opened combat prediction. +- Console errors: 0 + +Screenshots: + +- `docs/battle-unit-hitbox-local-selection.png` +- `docs/battle-unit-hitbox-local-command-menu.png` +- `docs/battle-unit-hitbox-local-target-chosen.png` + +## Deployed Verification + +- Command: `pnpm deploy:nas` +- URL: `https://comtropy.synology.me/heros_web/?debug&debugBattleSetup=attack-preview&v=unit-hitbox-20260703` +- Adjacent unit selection: clicked Liu Bei, Guan Yu, and Zhang Fei by tile center; final selection was Zhang Fei. +- Command menu: clicked Zhang Fei tile center again; Zhang Fei command menu opened. +- Targeting: selected Guan Yu attack, clicked the adjacent Yellow Turban target, and opened combat prediction. +- Console errors: 0 + +Screenshots: + +- `docs/battle-unit-hitbox-prod-selection.png` +- `docs/battle-unit-hitbox-prod-command-menu.png` +- `docs/battle-unit-hitbox-prod-target-chosen.png` diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 4723455..4f793ea 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -1085,6 +1085,7 @@ type MiniMapLayout = { type UnitView = { sprite: Phaser.GameObjects.Sprite; + hitZone: Phaser.GameObjects.Zone; label: Phaser.GameObjects.Text; textureBase: string; direction: UnitDirection; @@ -3446,8 +3447,16 @@ export class BattleScene extends Phaser.Scene { if (this.mapMask) { sprite.setMask(this.mapMask); } - sprite.setInteractive({ useHandCursor: unit.faction === 'ally' }); - sprite.on('pointerdown', () => this.selectUnit(unit)); + + const hitZone = this.add.zone(centerX, centerY, this.layout.tileSize, this.layout.tileSize); + hitZone.setName(`unit-hit-${unit.id}`); + hitZone.setDepth(10.8); + hitZone.setInteractive({ useHandCursor: true }); + hitZone.on('pointerdown', (pointer: Phaser.Input.Pointer) => { + if (pointer.leftButtonDown()) { + this.selectUnit(unit); + } + }); const label = this.add.text(sprite.x, sprite.y + this.layout.tileSize * 0.58, unit.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', @@ -3464,6 +3473,7 @@ export class BattleScene extends Phaser.Scene { const view = { sprite, + hitZone, label, textureBase, direction: 'south' as UnitDirection, @@ -3897,6 +3907,7 @@ export class BattleScene extends Phaser.Scene { view.label.setPosition(view.baseX, view.baseY + this.layout.tileSize * 0.52); const visible = unit.hp > 0 && this.isTileVisible(unit.x, unit.y); view.sprite.setVisible(visible); + this.setUnitHitZoneEnabled(view, visible); view.label.setVisible(visible); this.syncUnitMotion(unit, view); this.applyUnitLegibilityStyle(unit, view); @@ -3921,6 +3932,7 @@ export class BattleScene extends Phaser.Scene { view.sprite.clearTint(); view.sprite.setAlpha(1); view.sprite.setVisible(visible); + this.setUnitHitZoneEnabled(view, visible); view.label.setVisible(visible); view.label.setAlpha(1); @@ -9495,9 +9507,6 @@ export class BattleScene extends Phaser.Scene { this.setUnitBasePosition(view, this.tileCenterX(unit.x), this.tileCenterY(unit.y)); this.resetUnitSpritePose(view); view.label.setPosition(view.baseX, view.baseY + this.layout.tileSize * 0.52); - if (unit.hp > 0) { - view.sprite.setInteractive({ useHandCursor: true }); - } this.applyUnitLegibilityStyle(unit, view); } @@ -11990,14 +11999,14 @@ export class BattleScene extends Phaser.Scene { const direction = this.directionFromDelta(x - fromX, y - fromY); const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y); const movementDuration = this.movementDuration(duration, movementDistance); - this.tweens.killTweensOf([view.sprite, view.label]); + this.tweens.killTweensOf([view.sprite, view.hitZone, view.label]); this.setUnitBasePosition(view, startX, startY); this.resetUnitSpritePose(view); view.label.setPosition(startX, startY + this.layout.tileSize * 0.52); this.playMovementSound(unit, movementDuration, movementDistance); this.playUnitWalk(view, direction); this.tweens.add({ - targets: view.sprite, + targets: [view.sprite, view.hitZone], x: targetX, y: targetY, duration: movementDuration, @@ -12616,14 +12625,14 @@ export class BattleScene extends Phaser.Scene { const direction = this.directionFromDelta(x - fromX, y - fromY); const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y); const movementDuration = this.movementDuration(duration, movementDistance); - this.tweens.killTweensOf([view.sprite, view.label]); + this.tweens.killTweensOf([view.sprite, view.hitZone, view.label]); this.setUnitBasePosition(view, startX, startY); this.resetUnitSpritePose(view); view.label.setPosition(startX, startY + this.layout.tileSize * 0.52); this.playMovementSound(unit, movementDuration, movementDistance); this.playUnitWalk(view, direction); this.tweens.add({ - targets: view.sprite, + targets: [view.sprite, view.hitZone], x: targetX, y: targetY, duration: movementDuration, @@ -12826,6 +12835,15 @@ export class BattleScene extends Phaser.Scene { view.baseX = x; view.baseY = y; view.sprite.setPosition(x, y); + view.hitZone.setPosition(x, y); + view.hitZone.setSize(this.layout.tileSize, this.layout.tileSize); + } + + private setUnitHitZoneEnabled(view: UnitView, enabled: boolean) { + view.hitZone.setVisible(enabled); + if (view.hitZone.input) { + view.hitZone.input.enabled = enabled; + } } private resetUnitSpritePose(view: UnitView) { @@ -13101,6 +13119,7 @@ export class BattleScene extends Phaser.Scene { view.sprite.clearTint(); view.sprite.setAlpha(0); view.sprite.setVisible(false); + this.setUnitHitZoneEnabled(view, false); view.label.setAlpha(0); view.label.setVisible(false); view.label.setColor('#b6b6b6');