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:
parent
097333d39a
commit
0bd93369a5
18
README.md
18
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 = {}) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue