Resolve projectile collisions by explicit owner tag
This commit is contained in:
parent
f39b13dcac
commit
618a881268
@ -132,3 +132,12 @@ export const reducePlayerProjectileHits = ({
|
||||
gameOver: nextHull <= 0,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const resolveTaggedProjectile = (first, second, owner) => {
|
||||
for (const candidate of [first, second]) {
|
||||
if (!candidate || typeof candidate.getData !== 'function') continue;
|
||||
if (candidate.getData('projectileOwner') === owner) return candidate;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import './style.css';
|
||||
import airshipSheetUrl from './assets/airships.svg?url';
|
||||
import explosionSheetUrl from './assets/explosions.svg?url';
|
||||
import { airshipLog, registerDebugSnapshot } from './diagnostics.js';
|
||||
import { normalizePlayerDamage, reducePlayerProjectileHits } from './game-logic.js';
|
||||
import { normalizePlayerDamage, reducePlayerProjectileHits, resolveTaggedProjectile } from './game-logic.js';
|
||||
import { pirateClassForWave, separationVector } from './pirate-ai.js';
|
||||
|
||||
const WORLD_SIZE = 2400;
|
||||
@ -536,7 +536,8 @@ class GameScene extends Phaser.Scene {
|
||||
shot.body.setAllowGravity(false);
|
||||
shot.setData('bornAt', this.time.now);
|
||||
shot.setData('damage', damage);
|
||||
if (group === this.pirateShots) shot.setData('playerHitQueued', false);
|
||||
shot.setData('projectileOwner', actor);
|
||||
if (actor === 'pirate') shot.setData('playerHitQueued', false);
|
||||
shot.setVelocity(
|
||||
Math.cos(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.x * 0.25,
|
||||
Math.sin(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.y * 0.25,
|
||||
@ -544,8 +545,13 @@ class GameScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
hitPirate(shot, pirate) {
|
||||
if (!shot?.active || !pirate?.active || this.gameState !== 'playing') return;
|
||||
hitPirate(first, second) {
|
||||
if (this.gameState !== 'playing') return;
|
||||
|
||||
const shot = resolveTaggedProjectile(first, second, 'player');
|
||||
const pirate = shot === first ? second : first;
|
||||
if (!shot?.active || !pirate?.active || pirate === this.player) return;
|
||||
|
||||
const damage = shot.getData('damage') || 10;
|
||||
const { x, y } = shot;
|
||||
shot.destroy();
|
||||
@ -580,15 +586,27 @@ class GameScene extends Phaser.Scene {
|
||||
this.flashShip(pirate, 0xffcccc);
|
||||
}
|
||||
|
||||
hitPlayer(shot) {
|
||||
if (!shot?.active || this.gameState !== 'playing') return;
|
||||
hitPlayer(first, second) {
|
||||
if (this.gameState !== 'playing') return;
|
||||
|
||||
const shot = resolveTaggedProjectile(first, second, 'pirate');
|
||||
if (!shot?.active || shot === this.player) {
|
||||
airshipLog('combat:player-hit-rejected', {
|
||||
firstOwner: first?.getData?.('projectileOwner') ?? null,
|
||||
secondOwner: second?.getData?.('projectileOwner') ?? null,
|
||||
firstIsPlayer: first === this.player,
|
||||
secondIsPlayer: second === this.player,
|
||||
}, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
if (shot.getData('playerHitQueued')) return;
|
||||
|
||||
const damage = normalizePlayerDamage(shot.getData('damage'), PIRATE_SHOT_DAMAGE);
|
||||
|
||||
// The Arcade overlap callback only records the collision. Hull mutation
|
||||
// happens from the scene update after the physics step; projectile cleanup
|
||||
// remains on the normal lifetime path to avoid collision-time lifecycle changes.
|
||||
// The overlap callback only records a verified pirate projectile.
|
||||
// Hull mutation and projectile destruction happen on the next scene update,
|
||||
// outside the Arcade Physics callback.
|
||||
shot.setData('playerHitQueued', true);
|
||||
this.pendingPlayerHits.push({
|
||||
shot,
|
||||
@ -632,7 +650,14 @@ class GameScene extends Phaser.Scene {
|
||||
const { totalDamage, appliedHits, gameOver } = result;
|
||||
|
||||
for (const impact of hits) {
|
||||
if (impact.shot?.active) impact.shot.destroy();
|
||||
const shot = impact.shot;
|
||||
if (
|
||||
shot !== this.player
|
||||
&& shot?.active
|
||||
&& shot?.getData?.('projectileOwner') === 'pirate'
|
||||
) {
|
||||
shot.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedHits > 0) {
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
reducePlayerHitQueue,
|
||||
reducePlayerProjectileHits,
|
||||
reducePlayerVolleyHits,
|
||||
resolveTaggedProjectile,
|
||||
} from '../src/game-logic.js';
|
||||
|
||||
test('first pirate hit removes exactly the default damage and does not end the game', () => {
|
||||
@ -240,3 +241,29 @@ test('multiple distinct pirate projectiles in the same frame all apply damage',
|
||||
assert.equal(result.totalDamage, 18);
|
||||
assert.equal(result.appliedHits, 3);
|
||||
});
|
||||
|
||||
|
||||
test('projectile resolver is independent of Phaser overlap argument order', () => {
|
||||
const player = {
|
||||
getData() { return undefined; },
|
||||
};
|
||||
const pirateShot = {
|
||||
getData(key) {
|
||||
return key === 'projectileOwner' ? 'pirate' : undefined;
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(resolveTaggedProjectile(pirateShot, player, 'pirate'), pirateShot);
|
||||
assert.equal(resolveTaggedProjectile(player, pirateShot, 'pirate'), pirateShot);
|
||||
});
|
||||
|
||||
test('projectile resolver never mistakes the player for an untagged shot', () => {
|
||||
const player = {
|
||||
getData() { return undefined; },
|
||||
};
|
||||
const untagged = {
|
||||
getData() { return undefined; },
|
||||
};
|
||||
|
||||
assert.equal(resolveTaggedProjectile(player, untagged, 'pirate'), null);
|
||||
});
|
||||
|
||||
@ -236,3 +236,14 @@ test('non-lethal damage preserves the live player sprite and body', async () =>
|
||||
assert.match(source, /if \(this\.player\.alpha !== 1\) this\.player\.setAlpha\(1\)/);
|
||||
assert.match(source, /if \(this\.player\.body && !this\.player\.body\.enable\)/);
|
||||
});
|
||||
|
||||
|
||||
test('player overlap resolves an explicitly tagged pirate projectile before queueing or destroying it', async () => {
|
||||
const source = await read('src/game-v4.js');
|
||||
|
||||
assert.match(source, /shot\.setData\('projectileOwner', actor\)/);
|
||||
assert.match(source, /hitPlayer\(first, second\)/);
|
||||
assert.match(source, /resolveTaggedProjectile\(first, second, 'pirate'\)/);
|
||||
assert.match(source, /if \(!shot\?\.active \|\| shot === this\.player\)/);
|
||||
assert.match(source, /shot !== this\.player[\s\S]*?projectileOwner'\) === 'pirate'[\s\S]*?shot\.destroy\(\)/);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user