Use dedicated combat SFX voices for cannon, hit, and enemy laser

- Split the shared `fire` event into `cannon_fire`, `cannon_hit`, and `fire` so each weapon gets its own distinct voice
- Add three new sound assets (combat-mass-cannon, combat-mass-cannon-hit, combat-laser) to data/sfx.json and load them in GameScene
- Remove the 0.45 volume multiplier on enemy fire; the laser now plays at full volume since it has its own voice
- Update CombatSfx event map, both combat contexts (CombatScene, CombatSystem), dev tests, and README to reflect the ten-event SFX set
This commit is contained in:
Brian Fertig 2026-09-19 19:07:50 -06:00
parent 097333d39a
commit 0bd93369a5
8 changed files with 107 additions and 42 deletions

View File

@ -73,14 +73,16 @@ respawns, the bar — lives in **`data/combat.json`**:
**player goes down**, and a REFORMED bloom on **respawn**. Shield **player goes down**, and a REFORMED bloom on **respawn**. Shield
reads cyan and hull reads orange in both the flash and the number, so reads cyan and hull reads orange in both the flash and the number, so
a glance tells you which layer took the hit. a glance tells you which layer took the hit.
- **Feedback — SFX** — the same eight events each get a *distinct* voice - **Feedback — SFX** — the same ten events each get a *distinct* voice,
from the existing `data/sfx.json` set (no new sound files), through through one shared event→voice map + playback path
one shared event→voice map + playback path (`js/combat/CombatSfx.js`) (`js/combat/CombatSfx.js`) so the two fronts can't drift: the player's
so the two fronts can't drift: firing = `scan`, impact = `ui_click`, cannon = `mass_cannon` (assets/fx/combat-mass-cannon.mp3), a hostile
shield = `solar_buzz`, hull = `mining_split`, kill = `thunder_01..04` taking the player's shot = `mass_cannon_hit`
(assets/fx/combat-mass-cannon-hit.mp3), the enemy's laser = `laser`
(assets/fx/combat-laser.mp3), impact = `ui_click`, shield =
`solar_buzz`, hull = `mining_split`, kill = `thunder_01..04`
(one at random), down = `deconstruct`, respawn = `discovery`, (one at random), down = `deconstruct`, respawn = `discovery`,
level-up = `solar_flare_01`. Enemy fire reuses the `scan` voice, level-up = `solar_flare_01`.
quieter.
- **Readability layer** — a shared HUD (`js/combat/CombatHud.js`, pure - **Readability layer** — a shared HUD (`js/combat/CombatHud.js`, pure
rules in `js/combat/Combat.js`) makes the fight *readable* in both rules in `js/combat/Combat.js`) makes the fight *readable* in both
fronts: a rotating **lock reticle** on the current target (the same fronts: a rotating **lock reticle** on the current target (the same
@ -111,7 +113,7 @@ respawns, the bar — lives in **`data/combat.json`**:
wiring is verifiable headless in both fronts. CDP checkers drive the wiring is verifiable headless in both fronts. CDP checkers drive the
real game and read state back: `node dev/combat-flycheck.mjs` real game and read state back: `node dev/combat-flycheck.mjs`
(flight feel — t60/t30, spin, stop distance, top speed), (flight feel — t60/t30, spin, stop distance, top speed),
`node dev/combat.sfxcheck.mjs` (all eight SFX events fire through the `node dev/combat.sfxcheck.mjs` (all the SFX events fire through the
shared path, in both the range and the main game), and shared path, in both the range and the main game), and
`node dev/combat-shot.mjs [outDir]` (captures a labeled feedback frame `node dev/combat-shot.mjs [outDir]` (captures a labeled feedback frame
per context through the production paths into `dev/shots/` — the per context through the production paths into `dev/shots/` — the

View File

@ -1,10 +1,13 @@
{ {
"_comment": "Sound effects. enabled = master switch (also skips the load). volume is 0..1. construct = text typing in (the dossier, menus decoding in); deconstruct = text UNDECODING (the dossier collapsing) — reserved for the text only, never for closing UI; discovery = a new object coming into view; mining = the mining arm powering up (the 'Extending Mining Arm...' reach); mining_loop = the hum that LOOPS while the mining beam is live (starts when the arm finishes extending, stops when the sequence ends — stopping mining itself plays no sound); scan = the DEEP SCAN pulse going out (the command deck's SCAN button); ui_hover = the tick on HOVER of a clickable item (menu/deck buttons, compass name tags, panel buttons, save slots); ui_click = the tick on CLICK of a clickable item (the SCAN slot plays its own sonar ping instead of ui_click); ui_window = the whoosh when a window OPENS (the mining pop-up, the comms/landing panel); ui_close = the tick when a UI element is CANCELLED/CLOSED (the menu sub-bar, the save vault, the confirm dialog). Convention: the key here IS the play name — playSfx('<key>') plays the asset queued as 'sfx_<key>'. mining_split = the asteroid CRACKING when it breaks (shatters into pieces, or splits in half) — once per break. thunder_01…thunder_04 = the HABITABLE system's storm STRIKE (the lightning going off) — one of the four at random, once per strike, with a short throttle so near-simultaneous clusters make one clap, not four; solar_buzz = the BINARY system's two-light trade passing its MIDPOINT (the star light's color change half-way between the two stars' hues — both crossings, A→B and B→A); solar_flare_01/02 = the RED DWARF's flare FLARING UP — one of the two at random, once per flare onset.", "_comment": "Sound effects. enabled = master switch (also skips the load). volume is 0..1. construct = text typing in (the dossier, menus decoding in); deconstruct = text UNDECODING (the dossier collapsing) — reserved for the text only, never for closing UI; discovery = a new object coming into view; mining = the mining arm powering up (the 'Extending Mining Arm...' reach); mining_loop = the hum that LOOPS while the mining beam is live (starts when the arm finishes extending, stops when the sequence ends — stopping mining itself plays no sound); scan = the DEEP SCAN pulse going out (the command deck's SCAN button); ui_hover = the tick on HOVER of a clickable item (menu/deck buttons, compass name tags, panel buttons, save slots); ui_click = the tick on CLICK of a clickable item (the SCAN slot plays its own sonar ping instead of ui_click); ui_window = the whoosh when a window OPENS (the mining pop-up, the comms/landing panel); ui_close = the tick when a UI element is CANCELLED/CLOSED (the menu sub-bar, the save vault, the confirm dialog). Convention: the key here IS the play name — playSfx('<key>') plays the asset queued as 'sfx_<key>'. mining_split = the asteroid CRACKING when it breaks (shatters into pieces, or splits in half) — once per break. thunder_01…thunder_04 = the HABITABLE system's storm STRIKE (the lightning going off) — one of the four at random, once per strike, with a short throttle so near-simultaneous clusters make one clap, not four; solar_buzz = the BINARY system's two-light trade passing its MIDPOINT (the star light's color change half-way between the two stars' hues — both crossings, A→B and B→A); solar_flare_01/02 = the RED DWARF's flare FLARING UP — one of the two at random, once per flare onset; mass_cannon = the player's CANNON going off (combat — the new assets/fx/combat-mass-cannon.mp3); mass_cannon_hit = the player's tracer STRIKING a hostile (combat — the new assets/fx/combat-mass-cannon-hit.mp3); laser = the ENEMY's laser going off (combat — the new assets/fx/combat-laser.mp3).",
"enabled": true, "enabled": true,
"volume": 1, "volume": 1,
"construct": "assets/fx/type-construct.mp3", "construct": "assets/fx/type-construct.mp3",
"deconstruct": "assets/fx/type-deconstruct.mp3", "deconstruct": "assets/fx/type-deconstruct.mp3",
"discovery": "assets/fx/discovery.mp3", "discovery": "assets/fx/discovery.mp3",
"laser": "assets/fx/combat-laser.mp3",
"mass_cannon": "assets/fx/combat-mass-cannon.mp3",
"mass_cannon_hit": "assets/fx/combat-mass-cannon-hit.mp3",
"mining": "assets/fx/system-scan.mp3", "mining": "assets/fx/system-scan.mp3",
"mining_loop": "assets/fx/mining-01.mp3", "mining_loop": "assets/fx/mining-01.mp3",
"mining_split": "assets/fx/mining-02.mp3", "mining_split": "assets/fx/mining-02.mp3",

View File

@ -6,10 +6,10 @@
* Drives REAL game events in BOTH combat contexts — the range * Drives REAL game events in BOTH combat contexts — the range
* (http://127.0.0.1:8080/combat) and the main game * (http://127.0.0.1:8080/combat) and the main game
* (http://127.0.0.1:8080/index.html?combat) — and asserts that each of * (http://127.0.0.1:8080/index.html?combat) — and asserts that each of
* the seven combat events (fire / impact / shield / hull / kill / * the nine combat events (cannon_fire / cannon_hit / fire / impact /
* down / respawn) went through the SHARED SFX path * shield / hull / kill / down / respawn) went through the SHARED SFX path
* (js/combat/CombatSfx.js → window.__COMBAT_SFX__ dev log) — and, in * (js/combat/CombatSfx.js → window.__COMBAT_SFX__ dev log) — and, in
* the main game, the EIGHTH event, levelup (the combat level landing — * the main game, the TENTH event, levelup (the combat level landing —
* the run's XP bank, fed through the dev hook). * the run's XP bank, fed through the dev hook).
* *
* Why a log: a headless browser has no audio manager, so the shared * Why a log: a headless browser has no audio manager, so the shared
@ -22,7 +22,7 @@
*/ */
const CDP = process.env.CDP_PORT ? `127.0.0.1:${process.env.CDP_PORT}` : '127.0.0.1:9333'; const CDP = process.env.CDP_PORT ? `127.0.0.1:${process.env.CDP_PORT}` : '127.0.0.1:9333';
const BASE = process.env.BASE_URL ?? 'http://127.0.0.1:8080'; const BASE = process.env.BASE_URL ?? 'http://127.0.0.1:8080';
const EVENTS_RANGE = ['fire', 'impact', 'shield', 'hull', 'kill', 'down', 'respawn']; const EVENTS_RANGE = ['cannon_fire', 'cannon_hit', 'fire', 'impact', 'shield', 'hull', 'kill', 'down', 'respawn'];
const EVENTS_MAIN = [...EVENTS_RANGE, 'levelup']; // the main game's XP bank const EVENTS_MAIN = [...EVENTS_RANGE, 'levelup']; // the main game's XP bank
function connect(target) { function connect(target) {
@ -78,9 +78,15 @@ function driveScript(scopeExpr, withLevelUp = false) {
const driveLevelUp = ${Boolean(withLevelUp)}; // injected at build time const driveLevelUp = ${Boolean(withLevelUp)}; // injected at build time
const logBefore = (window.__COMBAT_SFX__ && window.__COMBAT_SFX__.log || []).length; const logBefore = (window.__COMBAT_SFX__ && window.__COMBAT_SFX__.log || []).length;
// fire — the cannon goes off (player side, full voice). // cannon_fire — the player's cannon goes off.
scope.fire(); scope.fire();
// cannon_hit — the player's tracer strikes a hostile.
scope.hitEnemy();
// fire — the enemy's laser goes off (full volume).
scope.fireEnemy();
// impact + shield — a hit absorbed by the shields. // impact + shield — a hit absorbed by the shields.
scope.armShielded(); scope.armShielded();
scope.hitPlayer(5); scope.hitPlayer(5);
@ -161,6 +167,19 @@ const scopeRange = `(() => {
const cfg = window.config; const cfg = window.config;
return { return {
fire: () => scene.firePlayer(0, cfg.section('combat.player.weapon', {})), fire: () => scene.firePlayer(0, cfg.section('combat.player.weapon', {})),
hitEnemy: () => {
const e = scene.enemies.find((x) => !x.dead);
if (!e) return;
const wpn = cfg.section('combat.player.weapon', {});
// A tracer parked 18px off the hostile's flank, aimed at it — the
// real collide() pass connects it (cannon_hit + the layer voices).
scene.spawnBullet(e.x - 18, e.y, 0, wpn, 'player');
scene.collide(16);
},
fireEnemy: () => {
const e = scene.enemies.find((x) => !x.dead);
if (e) scene.fireEnemy(e); // a hostile's lance at the player (the laser voice)
},
armShielded: () => { scene.shields = scene.maxShields; scene.playerDead = false; scene.ship.setVisible(true); }, armShielded: () => { scene.shields = scene.maxShields; scene.playerDead = false; scene.ship.setVisible(true); },
armBare: () => { scene.shields = 0; scene.hull = 50; }, armBare: () => { scene.shields = 0; scene.hull = 50; },
hitPlayer: (n) => scene.damagePlayer(n), hitPlayer: (n) => scene.damagePlayer(n),
@ -185,6 +204,23 @@ const scopeMain = `(() => {
const t = sys.nearestEnemy(sys.ship.x, sys.ship.y); const t = sys.nearestEnemy(sys.ship.x, sys.ship.y);
if (t) sys.firePlayerAt(sys.ship, t, sys._now()); if (t) sys.firePlayerAt(sys.ship, t, sys._now());
}, },
hitEnemy: () => {
const e = sys.enemies.find((x) => !x.dead);
if (!e) return;
const v = sys.visibleView();
const wpn = window.config.section('combat.player.weapon', {});
// Park the hostile at the camera view's CENTER (the on-screen rule
// gates hits — the pack streams in from off-screen), then a tracer
// 18px off its flank, aimed at it — the real collide() pass
// connects it (cannon_hit + the layer voices).
if (v) { e.x = v.x + v.width / 2; e.y = v.y + v.height / 2; }
sys.spawnBullet(e.x - 18, e.y, 0, wpn, 'player');
sys.collide(16, sys.ship);
},
fireEnemy: () => {
const e = sys.enemies.find((x) => !x.dead);
if (e) sys.fireEnemy(e, sys.ship, sys._now()); // a hostile's lance (the laser voice)
},
armShielded: () => { sys.shields = sys.maxShields; sys.playerDown = false; sys.ship.setVisible(true); }, armShielded: () => { sys.shields = sys.maxShields; sys.playerDown = false; sys.ship.setVisible(true); },
armBare: () => { sys.shields = 0; sys.hull = 50; }, armBare: () => { sys.shields = 0; sys.hull = 50; },
hitPlayer: (n) => sys.damagePlayer(n), hitPlayer: (n) => sys.damagePlayer(n),
@ -226,7 +262,7 @@ results.push(await checkContext(
)); ));
if (results.every(Boolean)) { if (results.every(Boolean)) {
console.log('combat SFX wiring: PASS (both contexts — the range\'s seven, the main game\'s eight)'); console.log('combat SFX wiring: PASS (both contexts — the range\'s nine, the main game\'s ten)');
process.exit(0); process.exit(0);
} else { } else {
console.log('combat SFX wiring: FAIL'); console.log('combat SFX wiring: FAIL');

View File

@ -610,9 +610,9 @@ const E = (k, d) => config.get(`combat.enemy.${k}`, d);
}; };
const sfxNames = config.get('sfx') ?? {}; const sfxNames = config.get('sfx') ?? {};
check('sfx: the eight combat events, each mapped', check('sfx: the ten combat events, each mapped',
Object.keys(COMBAT_SFX).sort().join(',') === Object.keys(COMBAT_SFX).sort().join(',') ===
'down,fire,hull,impact,kill,levelup,respawn,shield'); 'cannon_fire,cannon_hit,down,fire,hull,impact,kill,levelup,respawn,shield');
check('sfx: every mapped voice exists in data/sfx.json', check('sfx: every mapped voice exists in data/sfx.json',
Object.values(COMBAT_SFX).flat().every((n) => typeof sfxNames[n] === 'string')); Object.values(COMBAT_SFX).flat().every((n) => typeof sfxNames[n] === 'string'));
@ -622,14 +622,23 @@ const E = (k, d) => config.get(`combat.enemy.${k}`, d);
sets.every((a, i) => sets.every((b, j) => i === j || ![...a].some((n) => b.has(n))))); sets.every((a, i) => sets.every((b, j) => i === j || ![...a].some((n) => b.has(n)))));
const s1 = makeSfxScene(); const s1 = makeSfxScene();
check('sfx: fire → the scan ping, base volume', check('sfx: fire → the laser voice, base volume',
combatSfx(s1, 'fire') === 'fire' && combatSfx(s1, 'fire') === 'fire' &&
s1.played.length === 1 && s1.played.length === 1 &&
s1.played[0].key === 'sfx_scan' && s1.played[0].key === 'sfx_laser' &&
s1.played[0].volume === Number(config.get('sfx.volume', 0.55))); s1.played[0].volume === Number(config.get('sfx.volume', 0.55)));
const sC = makeSfxScene();
check('sfx: cannon_fire → the mass-cannon voice',
combatSfx(sC, 'cannon_fire') === 'cannon_fire' &&
sC.played.length === 1 && sC.played[0].key === 'sfx_mass_cannon');
const sCh = makeSfxScene();
check('sfx: cannon_hit → the mass-cannon hit voice',
combatSfx(sCh, 'cannon_hit') === 'cannon_hit' &&
sCh.played.length === 1 && sCh.played[0].key === 'sfx_mass_cannon_hit');
const s2 = makeSfxScene(); const s2 = makeSfxScene();
combatSfx(s2, 'fire', { vol: 0.45 }); // enemy lances play quieter combatSfx(s2, 'fire', { vol: 0.45 }); // the vol knob scales the volume
check('sfx: vol scales the volume', check('sfx: vol scales the volume',
Math.abs(s2.played[0].volume - Number(config.get('sfx.volume', 0.55)) * 0.45) < 1e-9); Math.abs(s2.played[0].volume - Number(config.get('sfx.volume', 0.55)) * 0.45) < 1e-9);

View File

@ -348,7 +348,7 @@ export class CombatScene extends Phaser.Scene {
const y = this.ship.y + Math.sin(heading) * nose; const y = this.ship.y + Math.sin(heading) * nose;
this.spawnBullet(x, y, angle, wpn, 'player'); this.spawnBullet(x, y, angle, wpn, 'player');
this.fx.muzzle(x, y, angle, wpn.color ?? 0x00e5ff, Number(wpn.muzzle) || 46); this.fx.muzzle(x, y, angle, wpn.color ?? 0x00e5ff, Number(wpn.muzzle) || 46);
combatSfx(this, 'fire'); combatSfx(this, 'cannon_fire'); // the mass-cannon voice (the enemy lances keep the blaster ping)
} }
updateThrustGlow(ship, heading, thrusting, dt) { updateThrustGlow(ship, heading, thrusting, dt) {
@ -413,7 +413,7 @@ export class CombatScene extends Phaser.Scene {
const my = e.y + Math.sin(toPlayer) * nose; const my = e.y + Math.sin(toPlayer) * nose;
this.spawnBullet(mx, my, angle, wpn, 'enemy'); this.spawnBullet(mx, my, angle, wpn, 'enemy');
this.fx.muzzle(mx, my, angle, wpn.color ?? 0xff2d6f, Number(wpn.muzzle) || 50); this.fx.muzzle(mx, my, angle, wpn.color ?? 0xff2d6f, Number(wpn.muzzle) || 50);
combatSfx(this, 'fire', { vol: 0.45 }); // the same blaster voice, quieter combatSfx(this, 'fire'); // the laser voice, at full volume (the player's cannon has its own voice)
} }
killEnemy(e) { killEnemy(e) {
@ -533,7 +533,7 @@ export class CombatScene extends Phaser.Scene {
// and the layer's cue (a cyan shield arc / orange shards). // and the layer's cue (a cyan shield arc / orange shards).
this.fx.enemyHit(e, b.x, b.y, b.angle0, kind, b.damage); this.fx.enemyHit(e, b.x, b.y, b.angle0, kind, b.damage);
this.fx.damage(b.x, b.y, b.damage, kind); this.fx.damage(b.x, b.y, b.damage, kind);
combatSfx(this, 'impact'); combatSfx(this, 'cannon_hit'); // the mass-cannon hit voice (the shield/hull cues ride on)
combatSfxEnemyHit(this, preShields, b.damage); combatSfxEnemyHit(this, preShields, b.damage);
if (killed) this.killEnemy(e); if (killed) this.killEnemy(e);
if (b.consume()) this.bullets.splice(i, 1); if (b.consume()) this.bullets.splice(i, 1);

View File

@ -7,28 +7,39 @@ import { playSfxOn, playSfxOneOfOn } from '../utils/Sfx.js';
* The contexts call `combatSfx(scene, event)` at their event sites; the * The contexts call `combatSfx(scene, event)` at their event sites; the
* map + the playback path live HERE, so the two contexts can never drift. * map + the playback path live HERE, so the two contexts can never drift.
* *
* Every event gets a DISTINCT voice from the EXISTING data/sfx.json set * Every event gets a DISTINCT voice from the data/sfx.json set — the two
* (no new sound files): * cannon voices ride the new assets/fx/combat-mass-cannon*.mp3 files, the
* rest the existing set:
* *
* fire scan the blaster ping (player cannon; enemy lances * cannon_fire mass_cannon the player's cannon going off (the new
* play the same voice, quieter via `vol`) * combat-mass-cannon.mp3)
* impact ui_click the light tick of a tracer striking * cannon_hit mass_cannon_hit the player's tracer striking a hostile
* shield solar_buzz the energy shimmer of a field absorbing * (the new combat-mass-cannon-hit.mp3)
* hull mining_split the heavy crack of armor taking the hit * fire laser the ENEMY's laser going off (the new
* kill thunder_01..04 one at random — the boom of a ship dying * combat-laser.mp3 — the player's cannon
* down deconstruct the player's death (the collapse voice) * plays cannon_fire; the laser plays at
* respawn discovery the player reforming (the "new object in * the same volume)
* view" voice) * impact ui_click the light tick of a tracer striking (the
* levelup solar_flare_01 the combat level landing (the charge-rise * player being hit, wall ticks — a hostile
* voice — js/combat/XpManager.js's 'levelup' * taking the player's shot plays cannon_hit)
* event, subscribed by CombatSystem) * shield solar_buzz the energy shimmer of a field absorbing
* hull mining_split the heavy crack of armor taking the hit
* kill thunder_01..04 one at random — the boom of a ship dying
* down deconstruct the player's death (the collapse voice)
* respawn discovery the player reforming (the "new object in
* view" voice)
* levelup solar_flare_01 the combat level landing (the charge-rise
* voice — js/combat/XpManager.js's 'levelup'
* event, subscribed by CombatSystem)
* *
* Playback goes through the shared Sfx.js guards (master switch, audio * Playback goes through the shared Sfx.js guards (master switch, audio
* manager, cache) — a headless scene with no manager simply doesn't * manager, cache) — a headless scene with no manager simply doesn't
* sound, but the dev log below still records the call. * sound, but the dev log below still records the call.
*/ */
export const COMBAT_SFX = { export const COMBAT_SFX = {
fire: ['scan'], cannon_fire: ['mass_cannon'],
cannon_hit: ['mass_cannon_hit'],
fire: ['laser'],
impact: ['ui_click'], impact: ['ui_click'],
shield: ['solar_buzz'], shield: ['solar_buzz'],
hull: ['mining_split'], hull: ['mining_split'],
@ -63,7 +74,8 @@ function remember(entry) {
* *
* @param {object} scene a Phaser scene (any of the combat contexts') * @param {object} scene a Phaser scene (any of the combat contexts')
* @param {string} event a COMBAT_SFX key * @param {string} event a COMBAT_SFX key
* @param {object} [o] `vol` = 0..1 multiplier (enemy fire plays quieter) * @param {object} [o] `vol` = 0..1 volume multiplier (default 1 — the
* enemy laser and the player's cannon both play at full)
* @returns {string|null} the event name (null for an unknown event) * @returns {string|null} the event name (null for an unknown event)
*/ */
export function combatSfx(scene, event, o = {}) { export function combatSfx(scene, event, o = {}) {

View File

@ -619,7 +619,7 @@ export class CombatSystem {
const y = player.y + Math.sin(a) * nose; const y = player.y + Math.sin(a) * nose;
this.spawnBullet(x, y, a, wpn, 'player'); this.spawnBullet(x, y, a, wpn, 'player');
this.fx.muzzle(x, y, a, wpn.color ?? '#00e5ff', Number(wpn.muzzle) || 46); this.fx.muzzle(x, y, a, wpn.color ?? '#00e5ff', Number(wpn.muzzle) || 46);
combatSfx(this.scene, 'fire'); combatSfx(this.scene, 'cannon_fire'); // the mass-cannon voice (the enemy lances keep the blaster ping)
} }
/** A hostile's lance at the player (aimed at the ship, spread-jittered). */ /** A hostile's lance at the player (aimed at the ship, spread-jittered). */
@ -632,7 +632,7 @@ export class CombatSystem {
const my = e.y + Math.sin(a) * nose; const my = e.y + Math.sin(a) * nose;
this.spawnBullet(mx, my, a, wpn, 'enemy'); this.spawnBullet(mx, my, a, wpn, 'enemy');
this.fx.muzzle(mx, my, a, wpn.color ?? '#ff2d6f', Number(wpn.muzzle) || 50); this.fx.muzzle(mx, my, a, wpn.color ?? '#ff2d6f', Number(wpn.muzzle) || 50);
combatSfx(this.scene, 'fire', { vol: 0.45 }); // the same blaster voice, quieter combatSfx(this.scene, 'fire'); // the laser voice, at full volume (the player's cannon has its own voice)
} }
spawnBullet(x, y, angle, wpn, owner) { spawnBullet(x, y, angle, wpn, owner) {
@ -695,7 +695,7 @@ export class CombatSystem {
// and the layer's cue (a cyan shield arc / orange shards). // and the layer's cue (a cyan shield arc / orange shards).
this.fx.enemyHit(e, b.x, b.y, b.angle0, kind, b.damage); this.fx.enemyHit(e, b.x, b.y, b.angle0, kind, b.damage);
this.fx.damage(b.x, b.y, b.damage, kind); this.fx.damage(b.x, b.y, b.damage, kind);
combatSfx(this.scene, 'impact'); combatSfx(this.scene, 'cannon_hit'); // the mass-cannon hit voice (the shield/hull cues ride on)
combatSfxEnemyHit(this.scene, preShields, b.damage); combatSfxEnemyHit(this.scene, preShields, b.damage);
if (killed) this.killEnemy(e, this._now()); if (killed) this.killEnemy(e, this._now());
if (b.consume()) this.bullets.splice(i, 1); if (b.consume()) this.bullets.splice(i, 1);

View File

@ -339,6 +339,9 @@ export class GameScene extends Phaser.Scene {
this.load.audio('sfx_construct', config.get('sfx.construct', 'assets/fx/type-construct.mp3')); this.load.audio('sfx_construct', config.get('sfx.construct', 'assets/fx/type-construct.mp3'));
this.load.audio('sfx_deconstruct', config.get('sfx.deconstruct', 'assets/fx/type-deconstruct.mp3')); this.load.audio('sfx_deconstruct', config.get('sfx.deconstruct', 'assets/fx/type-deconstruct.mp3'));
this.load.audio('sfx_discovery', config.get('sfx.discovery', 'assets/fx/discovery.mp3')); this.load.audio('sfx_discovery', config.get('sfx.discovery', 'assets/fx/discovery.mp3'));
this.load.audio('sfx_laser', config.get('sfx.laser', 'assets/fx/combat-laser.mp3'));
this.load.audio('sfx_mass_cannon', config.get('sfx.mass_cannon', 'assets/fx/combat-mass-cannon.mp3'));
this.load.audio('sfx_mass_cannon_hit', config.get('sfx.mass_cannon_hit', 'assets/fx/combat-mass-cannon-hit.mp3'));
this.load.audio('sfx_mining', config.get('sfx.mining', 'assets/fx/system-scan.mp3')); this.load.audio('sfx_mining', config.get('sfx.mining', 'assets/fx/system-scan.mp3'));
this.load.audio('sfx_mining_loop', config.get('sfx.mining_loop', 'assets/fx/mining-01.mp3')); this.load.audio('sfx_mining_loop', config.get('sfx.mining_loop', 'assets/fx/mining-01.mp3'));
this.load.audio('sfx_mining_split', config.get('sfx.mining_split', 'assets/fx/mining-02.mp3')); this.load.audio('sfx_mining_split', config.get('sfx.mining_split', 'assets/fx/mining-02.mp3'));