Fix enemy ships reviving with stuck white hit-flash tint
- Add Fx.releaseCraft() to settle a craft's pending white tintFlash immediately on kill/destroy, so sprites never die mid-flash - Add exported settleCraftTint() helper that resets tint mode and tint to identity; call it on revive (EnemyShip), respawn, and reuse seams - Guard restore callbacks against destroyed sprites with try/catch so a half-restored craft can't leak white state - Update combat screenshots
This commit is contained in:
parent
716f9270ae
commit
db75222ca2
Binary file not shown.
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 47 KiB |
|
|
@ -10,7 +10,7 @@ import { Ship } from '../entities/Ship.js';
|
||||||
import { EnemyShip } from './EnemyShip.js';
|
import { EnemyShip } from './EnemyShip.js';
|
||||||
import { enemySheetList } from './EnemyTypes.js';
|
import { enemySheetList } from './EnemyTypes.js';
|
||||||
import { Bullet } from './Bullet.js';
|
import { Bullet } from './Bullet.js';
|
||||||
import { Fx, FX_COLORS } from './Fx.js';
|
import { Fx, FX_COLORS, settleCraftTint } from './Fx.js';
|
||||||
import { StarDrift } from './StarDrift.js';
|
import { StarDrift } from './StarDrift.js';
|
||||||
import { applyDamage, handleFeel, pickTarget } from './Combat.js';
|
import { applyDamage, handleFeel, pickTarget } from './Combat.js';
|
||||||
import { combatSfx, combatSfxEnemyHit, loadCombatSfx } from './CombatSfx.js';
|
import { combatSfx, combatSfxEnemyHit, loadCombatSfx } from './CombatSfx.js';
|
||||||
|
|
@ -414,6 +414,9 @@ export class CombatScene extends Phaser.Scene {
|
||||||
killEnemy(e) {
|
killEnemy(e) {
|
||||||
if (e._killFx) return; // idempotent — damage() sets e.dead first, so
|
if (e._killFx) return; // idempotent — damage() sets e.dead first, so
|
||||||
e._killFx = true; // the dead flag can't be the "already counted" guard
|
e._killFx = true; // the dead flag can't be the "already counted" guard
|
||||||
|
// Settle any pending white hit-flash NOW — the craft dies in its
|
||||||
|
// normal colors, never mid-flash (the reuse seam's edge case).
|
||||||
|
this.fx.releaseCraft(e);
|
||||||
this.kills += 1;
|
this.kills += 1;
|
||||||
this.fx.explode(e.x, e.y, 0xff5d6f);
|
this.fx.explode(e.x, e.y, 0xff5d6f);
|
||||||
combatShake(this, 'kill'); // the world kick (the bar stays still)
|
combatShake(this, 'kill'); // the world kick (the bar stays still)
|
||||||
|
|
@ -681,6 +684,7 @@ export class CombatScene extends Phaser.Scene {
|
||||||
killPlayer() {
|
killPlayer() {
|
||||||
this.playerDead = true;
|
this.playerDead = true;
|
||||||
this.hull = 0;
|
this.hull = 0;
|
||||||
|
this.fx.releaseCraft(this.ship); // out in normal colors, never mid-flash
|
||||||
this.fx.down(this.ship.x, this.ship.y); // the DOWN cue (burst + hold)
|
this.fx.down(this.ship.x, this.ship.y); // the DOWN cue (burst + hold)
|
||||||
combatSfx(this, 'down');
|
combatSfx(this, 'down');
|
||||||
this.ship.stop();
|
this.ship.stop();
|
||||||
|
|
@ -699,6 +703,7 @@ export class CombatScene extends Phaser.Scene {
|
||||||
this.ship.y = this.h / 2;
|
this.ship.y = this.h / 2;
|
||||||
this.ship.rotation = 0;
|
this.ship.rotation = 0;
|
||||||
this.ship.stop();
|
this.ship.stop();
|
||||||
|
settleCraftTint(this.ship); // never reappear holding a white hit-flash
|
||||||
this.ship.setVisible(true);
|
this.ship.setVisible(true);
|
||||||
this.playerDead = false;
|
this.playerDead = false;
|
||||||
this.clearBullets();
|
this.clearBullets();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { config } from '../config/Config.js';
|
||||||
import { EnemyShip } from './EnemyShip.js';
|
import { EnemyShip } from './EnemyShip.js';
|
||||||
import { enemyTypeConfig } from './EnemyTypes.js';
|
import { enemyTypeConfig } from './EnemyTypes.js';
|
||||||
import { Bullet } from './Bullet.js';
|
import { Bullet } from './Bullet.js';
|
||||||
import { Fx, FX_COLORS } from './Fx.js';
|
import { Fx, FX_COLORS, settleCraftTint } from './Fx.js';
|
||||||
import { applyDamage, pickTarget } from './Combat.js';
|
import { applyDamage, pickTarget } from './Combat.js';
|
||||||
import { combatSfx, combatSfxEnemyHit } from './CombatSfx.js';
|
import { combatSfx, combatSfxEnemyHit } from './CombatSfx.js';
|
||||||
import { combatShake } from './CamShake.js';
|
import { combatShake } from './CamShake.js';
|
||||||
|
|
@ -256,7 +256,10 @@ export class CombatSystem {
|
||||||
}
|
}
|
||||||
if (destroy) {
|
if (destroy) {
|
||||||
for (const e of this.enemies) {
|
for (const e of this.enemies) {
|
||||||
if (e.wave && !e.destroyed) e.destroy();
|
if (e.wave && !e.destroyed) {
|
||||||
|
this.fx.releaseCraft(e); // out in normal colors, never mid-flash
|
||||||
|
e.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.enemies = this.enemies.filter((e) => !e.wave);
|
this.enemies = this.enemies.filter((e) => !e.wave);
|
||||||
}
|
}
|
||||||
|
|
@ -699,6 +702,7 @@ export class CombatSystem {
|
||||||
this.playerDown = true;
|
this.playerDown = true;
|
||||||
this.hull = 0;
|
this.hull = 0;
|
||||||
if (this.ship) {
|
if (this.ship) {
|
||||||
|
this.fx.releaseCraft(this.ship); // out in normal colors, never mid-flash
|
||||||
this.fx.down(this.ship.x, this.ship.y); // the DOWN cue (burst + hold)
|
this.fx.down(this.ship.x, this.ship.y); // the DOWN cue (burst + hold)
|
||||||
this.ship.stop();
|
this.ship.stop();
|
||||||
this.ship.setVisible(false);
|
this.ship.setVisible(false);
|
||||||
|
|
@ -717,7 +721,10 @@ export class CombatSystem {
|
||||||
this.shields = this.maxShields;
|
this.shields = this.maxShields;
|
||||||
this.lastHitAt = 0;
|
this.lastHitAt = 0;
|
||||||
this.respawnAt = 0;
|
this.respawnAt = 0;
|
||||||
if (this.ship) this.ship.setVisible(true);
|
if (this.ship) {
|
||||||
|
settleCraftTint(this.ship); // never reappear holding a white hit-flash
|
||||||
|
this.ship.setVisible(true);
|
||||||
|
}
|
||||||
if (this.ship) this.fx.respawn(this.ship.x, this.ship.y); // the REFORMED cue
|
if (this.ship) this.fx.respawn(this.ship.x, this.ship.y); // the REFORMED cue
|
||||||
combatSfx(this.scene, 'respawn');
|
combatSfx(this.scene, 'respawn');
|
||||||
if (this.onPlayerUp) this.onPlayerUp();
|
if (this.onPlayerUp) this.onPlayerUp();
|
||||||
|
|
@ -733,6 +740,9 @@ export class CombatSystem {
|
||||||
killEnemy(e, time) {
|
killEnemy(e, time) {
|
||||||
if (e._killFx) return;
|
if (e._killFx) return;
|
||||||
e._killFx = true;
|
e._killFx = true;
|
||||||
|
// Settle any pending white hit-flash NOW — the craft dies in its
|
||||||
|
// normal colors, never mid-flash (the reuse seam's edge case).
|
||||||
|
this.fx.releaseCraft(e);
|
||||||
this.kills += 1;
|
this.kills += 1;
|
||||||
this.fx.explode(e.x, e.y, '#ff5d6f');
|
this.fx.explode(e.x, e.y, '#ff5d6f');
|
||||||
combatShake(this.scene, 'kill'); // the world kick (the bar stays still)
|
combatShake(this.scene, 'kill'); // the world kick (the bar stays still)
|
||||||
|
|
@ -802,7 +812,10 @@ export class CombatSystem {
|
||||||
/** Tear the combat down (scene destroy). */
|
/** Tear the combat down (scene destroy). */
|
||||||
destroy() {
|
destroy() {
|
||||||
this.waveRun = null;
|
this.waveRun = null;
|
||||||
for (const e of this.enemies) e.destroy();
|
for (const e of this.enemies) {
|
||||||
|
this.fx.releaseCraft(e); // settle flashes before the craft goes
|
||||||
|
e.destroy();
|
||||||
|
}
|
||||||
for (const b of this.bullets) if (!b.dead) b.destroy();
|
for (const b of this.bullets) if (!b.dead) b.destroy();
|
||||||
this.enemies = [];
|
this.enemies = [];
|
||||||
this.bullets = [];
|
this.bullets = [];
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import Phaser from '../vendor/phaser.js';
|
||||||
import { toColor } from '../utils/Color.js';
|
import { toColor } from '../utils/Color.js';
|
||||||
import { assignWorld } from '../visuals/UiCameras.js';
|
import { assignWorld } from '../visuals/UiCameras.js';
|
||||||
import { enemyTypeConfig } from './EnemyTypes.js';
|
import { enemyTypeConfig } from './EnemyTypes.js';
|
||||||
|
import { settleCraftTint } from './Fx.js';
|
||||||
import { applyDamage, headingOf, orbitStep } from './Combat.js';
|
import { applyDamage, headingOf, orbitStep } from './Combat.js';
|
||||||
|
|
||||||
const FALLBACK_W = 76; // the procedural wedge (it faces east)
|
const FALLBACK_W = 76; // the procedural wedge (it faces east)
|
||||||
|
|
@ -239,6 +240,11 @@ export class EnemyShip extends Phaser.Physics.Arcade.Sprite {
|
||||||
this.hull = this.maxHull;
|
this.hull = this.maxHull;
|
||||||
this.shields = this.maxShields;
|
this.shields = this.maxShields;
|
||||||
this.dead = false;
|
this.dead = false;
|
||||||
|
// THE REUSE SEAM: this is the SAME sprite that died (the range and
|
||||||
|
// the main game both revive in place). If a white hit-flash was
|
||||||
|
// still pending when it went down, settle the tint here too — a
|
||||||
|
// revived craft must never come back hot white.
|
||||||
|
settleCraftTint(this);
|
||||||
if (this.body) {
|
if (this.body) {
|
||||||
this.body.velocity.x = 0;
|
this.body.velocity.x = 0;
|
||||||
this.body.velocity.y = 0;
|
this.body.velocity.y = 0;
|
||||||
|
|
|
||||||
|
|
@ -432,9 +432,11 @@ export class Fx {
|
||||||
t0: 0,
|
t0: 0,
|
||||||
life,
|
life,
|
||||||
kind: 'tintFlash',
|
kind: 'tintFlash',
|
||||||
|
// Each write is guarded on its own — a throw in one must not
|
||||||
|
// skip the other (a half-restored craft is a white craft).
|
||||||
restore: () => {
|
restore: () => {
|
||||||
ship.setTintMode(mode);
|
try { ship.setTintMode(mode); } catch { /* gone */ }
|
||||||
ship.setTint(tint);
|
try { ship.setTint(tint); } catch { /* gone */ }
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return ship;
|
return ship;
|
||||||
|
|
@ -446,6 +448,27 @@ export class Fx {
|
||||||
return this.flash(ship.x, ship.y, 0xffffff, r * 2.6, life);
|
return this.flash(ship.x, ship.y, 0xffffff, r * 2.6, life);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settle a craft's pending white flash RIGHT NOW — the kill/destroy
|
||||||
|
* seam. A craft must never die, or be hidden and reused, while still
|
||||||
|
* holding the white-fill state: the hit is over (the kill burst or the
|
||||||
|
* reformed craft supersedes it), and the craft goes out in its normal
|
||||||
|
* colors (a sprite that dies mid-tint can leak the white state into
|
||||||
|
* its reuse — the "revived enemies are hot white and stay that way"
|
||||||
|
* edge case). Idempotent; safe on a destroyed craft.
|
||||||
|
*/
|
||||||
|
releaseCraft(ship) {
|
||||||
|
if (!ship) return;
|
||||||
|
for (let i = this.items.length - 1; i >= 0; i--) {
|
||||||
|
const it = this.items[i];
|
||||||
|
if (it.kind !== 'tintFlash' || it.img !== ship) continue;
|
||||||
|
if (it.restore) {
|
||||||
|
try { it.restore(); } catch { /* the craft may already be gone */ }
|
||||||
|
}
|
||||||
|
this.items.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------ the player's shield
|
// ------------------------------------------------------ the player's shield
|
||||||
|
|
||||||
/** The shield-sphere tuning (data/combat.json → combat.fx.shield). */
|
/** The shield-sphere tuning (data/combat.json → combat.fx.shield). */
|
||||||
|
|
@ -663,3 +686,18 @@ export class Fx {
|
||||||
this.items.length = 0;
|
this.items.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a craft's tint to identity (white corners + multiply mode).
|
||||||
|
* The reuse seams (revive/respawn) call this so a craft can NEVER
|
||||||
|
* reappear while still holding the white hit-flash state — the last
|
||||||
|
* line of defense for the "revived enemies are hot white" edge case.
|
||||||
|
* Safe on stubs and destroyed sprites. (Neither craft class tints for
|
||||||
|
* coloring — their colors are baked into the art — so identity is the
|
||||||
|
* always-correct default.)
|
||||||
|
*/
|
||||||
|
export function settleCraftTint(ship) {
|
||||||
|
if (!ship) return;
|
||||||
|
try { if (typeof ship.setTintMode === 'function') ship.setTintMode(0); } catch { /* stub */ }
|
||||||
|
try { if (typeof ship.setTint === 'function') ship.setTint(0xffffff); } catch { /* stub */ }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue