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,
|
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 airshipSheetUrl from './assets/airships.svg?url';
|
||||||
import explosionSheetUrl from './assets/explosions.svg?url';
|
import explosionSheetUrl from './assets/explosions.svg?url';
|
||||||
import { airshipLog, registerDebugSnapshot } from './diagnostics.js';
|
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';
|
import { pirateClassForWave, separationVector } from './pirate-ai.js';
|
||||||
|
|
||||||
const WORLD_SIZE = 2400;
|
const WORLD_SIZE = 2400;
|
||||||
@ -536,7 +536,8 @@ class GameScene extends Phaser.Scene {
|
|||||||
shot.body.setAllowGravity(false);
|
shot.body.setAllowGravity(false);
|
||||||
shot.setData('bornAt', this.time.now);
|
shot.setData('bornAt', this.time.now);
|
||||||
shot.setData('damage', damage);
|
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(
|
shot.setVelocity(
|
||||||
Math.cos(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.x * 0.25,
|
Math.cos(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.x * 0.25,
|
||||||
Math.sin(angle) * SHOT_SPEED * velocityScale + ship.body.velocity.y * 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) {
|
hitPirate(first, second) {
|
||||||
if (!shot?.active || !pirate?.active || this.gameState !== 'playing') return;
|
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 damage = shot.getData('damage') || 10;
|
||||||
const { x, y } = shot;
|
const { x, y } = shot;
|
||||||
shot.destroy();
|
shot.destroy();
|
||||||
@ -580,15 +586,27 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.flashShip(pirate, 0xffcccc);
|
this.flashShip(pirate, 0xffcccc);
|
||||||
}
|
}
|
||||||
|
|
||||||
hitPlayer(shot) {
|
hitPlayer(first, second) {
|
||||||
if (!shot?.active || this.gameState !== 'playing') return;
|
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;
|
if (shot.getData('playerHitQueued')) return;
|
||||||
|
|
||||||
const damage = normalizePlayerDamage(shot.getData('damage'), PIRATE_SHOT_DAMAGE);
|
const damage = normalizePlayerDamage(shot.getData('damage'), PIRATE_SHOT_DAMAGE);
|
||||||
|
|
||||||
// The Arcade overlap callback only records the collision. Hull mutation
|
// The overlap callback only records a verified pirate projectile.
|
||||||
// happens from the scene update after the physics step; projectile cleanup
|
// Hull mutation and projectile destruction happen on the next scene update,
|
||||||
// remains on the normal lifetime path to avoid collision-time lifecycle changes.
|
// outside the Arcade Physics callback.
|
||||||
shot.setData('playerHitQueued', true);
|
shot.setData('playerHitQueued', true);
|
||||||
this.pendingPlayerHits.push({
|
this.pendingPlayerHits.push({
|
||||||
shot,
|
shot,
|
||||||
@ -632,7 +650,14 @@ class GameScene extends Phaser.Scene {
|
|||||||
const { totalDamage, appliedHits, gameOver } = result;
|
const { totalDamage, appliedHits, gameOver } = result;
|
||||||
|
|
||||||
for (const impact of hits) {
|
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) {
|
if (appliedHits > 0) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
reducePlayerHitQueue,
|
reducePlayerHitQueue,
|
||||||
reducePlayerProjectileHits,
|
reducePlayerProjectileHits,
|
||||||
reducePlayerVolleyHits,
|
reducePlayerVolleyHits,
|
||||||
|
resolveTaggedProjectile,
|
||||||
} from '../src/game-logic.js';
|
} from '../src/game-logic.js';
|
||||||
|
|
||||||
test('first pirate hit removes exactly the default damage and does not end the game', () => {
|
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.totalDamage, 18);
|
||||||
assert.equal(result.appliedHits, 3);
|
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\.alpha !== 1\) this\.player\.setAlpha\(1\)/);
|
||||||
assert.match(source, /if \(this\.player\.body && !this\.player\.body\.enable\)/);
|
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