Stabilize battle unit hitboxes

This commit is contained in:
2026-07-03 17:56:33 +09:00
parent 43dab94ba6
commit 3c65e71c67
8 changed files with 68 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -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`

View File

@@ -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');