diff --git a/assets/fx/vega-zoomout.mp3 b/assets/fx/vega-zoomout.mp3 deleted file mode 100644 index a96750e..0000000 Binary files a/assets/fx/vega-zoomout.mp3 and /dev/null differ diff --git a/assets/fx/vega-build.mp3 b/assets/fx/vega/vega-build.mp3 similarity index 100% rename from assets/fx/vega-build.mp3 rename to assets/fx/vega/vega-build.mp3 diff --git a/assets/fx/vega-close.mp3 b/assets/fx/vega/vega-close.mp3 similarity index 100% rename from assets/fx/vega-close.mp3 rename to assets/fx/vega/vega-close.mp3 diff --git a/assets/fx/vega/vega-endturn.mp3 b/assets/fx/vega/vega-endturn.mp3 new file mode 100644 index 0000000..aeabf95 Binary files /dev/null and b/assets/fx/vega/vega-endturn.mp3 differ diff --git a/assets/fx/vega/vega-missle-hit-12345.mp3 b/assets/fx/vega/vega-missle-hit-12345.mp3 new file mode 100644 index 0000000..9451109 Binary files /dev/null and b/assets/fx/vega/vega-missle-hit-12345.mp3 differ diff --git a/assets/fx/vega/vega-missle-hit-67.mp3 b/assets/fx/vega/vega-missle-hit-67.mp3 new file mode 100644 index 0000000..5c6a858 Binary files /dev/null and b/assets/fx/vega/vega-missle-hit-67.mp3 differ diff --git a/assets/fx/vega/vega-missle-launch-12345.mp3 b/assets/fx/vega/vega-missle-launch-12345.mp3 new file mode 100644 index 0000000..a704323 Binary files /dev/null and b/assets/fx/vega/vega-missle-launch-12345.mp3 differ diff --git a/assets/fx/vega/vega-missle-launch-67.mp3 b/assets/fx/vega/vega-missle-launch-67.mp3 new file mode 100644 index 0000000..0cec2e8 Binary files /dev/null and b/assets/fx/vega/vega-missle-launch-67.mp3 differ diff --git a/assets/fx/vega-newturn.mp3 b/assets/fx/vega/vega-newturn.mp3 similarity index 100% rename from assets/fx/vega-newturn.mp3 rename to assets/fx/vega/vega-newturn.mp3 diff --git a/assets/fx/vega-select.mp3 b/assets/fx/vega/vega-select.mp3 similarity index 100% rename from assets/fx/vega-select.mp3 rename to assets/fx/vega/vega-select.mp3 diff --git a/assets/fx/vega-star.mp3 b/assets/fx/vega/vega-star.mp3 similarity index 100% rename from assets/fx/vega-star.mp3 rename to assets/fx/vega/vega-star.mp3 diff --git a/assets/fx/vega-unit.mp3 b/assets/fx/vega/vega-unit.mp3 similarity index 100% rename from assets/fx/vega-unit.mp3 rename to assets/fx/vega/vega-unit.mp3 diff --git a/assets/fx/vega-view.mp3 b/assets/fx/vega/vega-view.mp3 similarity index 100% rename from assets/fx/vega-view.mp3 rename to assets/fx/vega/vega-view.mp3 diff --git a/assets/fx/vega-viewcolony.mp3 b/assets/fx/vega/vega-viewcolony.mp3 similarity index 100% rename from assets/fx/vega-viewcolony.mp3 rename to assets/fx/vega/vega-viewcolony.mp3 diff --git a/assets/fx/vega-warp.mp3 b/assets/fx/vega/vega-warp.mp3 similarity index 100% rename from assets/fx/vega-warp.mp3 rename to assets/fx/vega/vega-warp.mp3 diff --git a/assets/fx/vega/vega-weapon-123.mp3 b/assets/fx/vega/vega-weapon-123.mp3 new file mode 100644 index 0000000..98402cf Binary files /dev/null and b/assets/fx/vega/vega-weapon-123.mp3 differ diff --git a/assets/fx/vega/vega-weapon-45.mp3 b/assets/fx/vega/vega-weapon-45.mp3 new file mode 100644 index 0000000..f986c8c Binary files /dev/null and b/assets/fx/vega/vega-weapon-45.mp3 differ diff --git a/assets/fx/vega/vega-weapon-67.mp3 b/assets/fx/vega/vega-weapon-67.mp3 new file mode 100644 index 0000000..cac0267 Binary files /dev/null and b/assets/fx/vega/vega-weapon-67.mp3 differ diff --git a/assets/fx/vega-zoomin.mp3 b/assets/fx/vega/vega-zoomin.mp3 similarity index 100% rename from assets/fx/vega-zoomin.mp3 rename to assets/fx/vega/vega-zoomin.mp3 diff --git a/assets/fx/vega/vega-zoomout.mp3 b/assets/fx/vega/vega-zoomout.mp3 new file mode 100644 index 0000000..81aa603 Binary files /dev/null and b/assets/fx/vega/vega-zoomout.mp3 differ diff --git a/data/masterofvega-music.json b/data/masterofvega-music.json index 5e5bdb3..8cb40e7 100644 --- a/data/masterofvega-music.json +++ b/data/masterofvega-music.json @@ -97,11 +97,7 @@ "artist": "Ssakar", "title": "Meet N Greet" }] }, - "lithox": { "tracks": [{ - "file": "vega/audience-lithox.mp3", - "artist": "Lithox", - "title": "Meet N Greet" - }] } + "lithox": { "tracks": [] } } } } diff --git a/data/mastervega-rules.json b/data/mastervega-rules.json index 1741891..3e8f386 100644 --- a/data/mastervega-rules.json +++ b/data/mastervega-rules.json @@ -498,7 +498,9 @@ "disengageRound": 25, "planetDefenseBase": 20, "bombardPopKill": 0.22, - "groundOddsScale": 0.01 + "groundOddsScale": 0.01, + "cloakEvasion": 0.02, + "singularityShieldPierce": 0.5 }, "victory": { diff --git a/docs/mastervega-build-plan.md b/docs/mastervega-build-plan.md index 407e0ed..d39a8e4 100644 --- a/docs/mastervega-build-plan.md +++ b/docs/mastervega-build-plan.md @@ -282,6 +282,33 @@ Each of these was a real bug that produced a plausible-looking but broken game. two states apart, so the placeholder's actual value never matters. Section 2 asserts both branches. +24. **`bestComponents()` computing a flag is not the same as `designFor()` + returning it.** Wiring the `cloaked`/`singularity` tech effects into combat + (2026-08-09) found `singularity` tallied correctly into `comps.singularity` + in `VegaShips.js` — same as `cloaked` — but the object literal `designFor()` + returns only ever copied `cloaked: comps.cloaked` onto the design; there was + no `singularity:` line at all, so every design's `design.singularity` was + `undefined` forever, silently. A combat check that read `shooter.design?. + singularity` would never have fired and nothing would have said why. Caught + only because the new verifier check builds a design with **both** flags + stripped as a sanity assertion (`baseDesign.cloaked && baseDesign. + singularity`) *before* running the A/B battle that depends on them — + without that assertion the A/B test would have "passed" by measuring + nothing. Lesson: any per-tech boolean/stat added to `bestComponents()` + needs a matching line in `designFor()`'s return object, and there is no + compiler to catch the omission — assert the field is actually present on a + fully-teched design, not just that the battle outcome looks plausible. + Also: attrition combat **compounds** small per-shot edges hard. An isolated + A/B fleet fight (same design, one flag flipped) at `cloakEvasion: 0.15` + produced a 99% win rate — more decisive than the suite's own ">0.8" bar for + a full two-tech-tier lead across every field at once. Tuned down empirically + (not from the single-hit-chance formula) against an isolated same-fleet A/B + harness until it landed near what a mild species combat-trait is worth in + that same harness (~62%): `cloakEvasion: 0.02`, `singularityShieldPierce: + 0.5`. Both are checked in section 5 with a `> 0.52 && < 0.8` band, loose + enough to absorb RNG noise but tight enough to catch another 0.15-style + overshoot. + ### Ship rows carry video, so they have to be pooled Every place a ship is listed — the side panel's task force, its in-transit, diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 7f09e05..42a3023 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -171,6 +171,29 @@ export const MANIFEST = { { type: 'audio', key: 'scifi-explode', path: 'assets/fx/scifi-explode.mp3' }, { type: 'audio', key: 'ta-rocket-1', path: 'assets/fx/ta-rocket-1.mp3' }, { type: 'audio', key: 'ta-rocket-2', path: 'assets/fx/ta-rocket-2.mp3' }, + // UI sound effects (src/ui/Sounds.js's SFX.VEGA_*). Used to be loaded + // eagerly by PreloadScene for every game; moved here 2026-08-09 alongside + // Brian's own move of the files into assets/fx/vega/. + { type: 'audio', key: 'sfx-vega-select', path: 'assets/fx/vega/vega-select.mp3' }, + { type: 'audio', key: 'sfx-vega-build', path: 'assets/fx/vega/vega-build.mp3' }, + { type: 'audio', key: 'sfx-vega-close', path: 'assets/fx/vega/vega-close.mp3' }, + { type: 'audio', key: 'sfx-vega-newturn', path: 'assets/fx/vega/vega-newturn.mp3' }, + { type: 'audio', key: 'sfx-vega-unit', path: 'assets/fx/vega/vega-unit.mp3' }, + { type: 'audio', key: 'sfx-vega-view', path: 'assets/fx/vega/vega-view.mp3' }, + { type: 'audio', key: 'sfx-vega-warp', path: 'assets/fx/vega/vega-warp.mp3' }, + { type: 'audio', key: 'sfx-vega-star', path: 'assets/fx/vega/vega-star.mp3' }, + { type: 'audio', key: 'sfx-vega-zoomin', path: 'assets/fx/vega/vega-zoomin.mp3' }, + { type: 'audio', key: 'sfx-vega-zoomout', path: 'assets/fx/vega/vega-zoomout.mp3' }, + { type: 'audio', key: 'sfx-vega-viewcolony', path: 'assets/fx/vega/vega-viewcolony.mp3' }, + { type: 'audio', key: 'sfx-vega-endturn', path: 'assets/fx/vega/vega-endturn.mp3' }, + // Combat weapon cues, Mark-banded (see VegaCombat.js's weaponSfxKey()). + { type: 'audio', key: 'sfx-vega-weapon-123', path: 'assets/fx/vega/vega-weapon-123.mp3' }, + { type: 'audio', key: 'sfx-vega-weapon-45', path: 'assets/fx/vega/vega-weapon-45.mp3' }, + { type: 'audio', key: 'sfx-vega-weapon-67', path: 'assets/fx/vega/vega-weapon-67.mp3' }, + { type: 'audio', key: 'sfx-vega-missle-launch-12345', path: 'assets/fx/vega/vega-missle-launch-12345.mp3' }, + { type: 'audio', key: 'sfx-vega-missle-launch-67', path: 'assets/fx/vega/vega-missle-launch-67.mp3' }, + { type: 'audio', key: 'sfx-vega-missle-hit-12345', path: 'assets/fx/vega/vega-missle-hit-12345.mp3' }, + { type: 'audio', key: 'sfx-vega-missle-hit-67', path: 'assets/fx/vega/vega-missle-hit-67.mp3' }, (scene) => vegaMusicFrom(scene, 'masterofvega-music'), ], forbiddenisland: [ diff --git a/src/games/mastervega/MasterOfVegaGame.js b/src/games/mastervega/MasterOfVegaGame.js index a5bcc61..7be608c 100644 --- a/src/games/mastervega/MasterOfVegaGame.js +++ b/src/games/mastervega/MasterOfVegaGame.js @@ -662,7 +662,10 @@ export default class MasterOfVegaGame extends Phaser.Scene { openLeaderScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done, () => this.refreshHud()))); - this.endTurnBtn = new Button(this, GAME_WIDTH - 130, 31, 'End turn', this.uiClick(() => this.onEndTurn()), + // Not this.uiClick — onEndTurn plays its own vega-endturn cue (shared with + // combat's Next round button) rather than the generic click, and a no-op + // press while busy/modal should stay silent. + this.endTurnBtn = new Button(this, GAME_WIDTH - 130, 31, 'End turn', () => this.onEndTurn(), { width: 190, height: 46, fontSize: 20 }); hud.add(this.endTurnBtn); @@ -1001,6 +1004,7 @@ export default class MasterOfVegaGame extends Phaser.Scene { onEndTurn() { if (this.busy || this.modalOpen) return; + playSound(this, SFX.VEGA_ENDTURN); this.busy = true; this.endTurnBtn.setEnabled(false); this.writeSave(); diff --git a/src/games/mastervega/VegaCombat.js b/src/games/mastervega/VegaCombat.js index 7029eee..53987e3 100644 --- a/src/games/mastervega/VegaCombat.js +++ b/src/games/mastervega/VegaCombat.js @@ -124,10 +124,20 @@ function hitChance(C, shooter, target, shooterTraits, targetTraits) { const targeting = shooter.design?.targeting ?? 0; const attack = (shooter.design?.attack ?? 0); const defense = (target.design?.defense ?? 0); + // Stealth Field: a simplified always-on version of MOO1's "evasive unless + // it attacks" cloak — flat accuracy malus against the target. A single hit- + // chance percentage point sounds trivial but a fleet fight is thousands of + // aggregate shots over many rounds (see sampleHits), so even a small, + // *sustained* per-shot edge compounds round over round. Calibrated against + // an isolated same-fleet A/B (tools/verifyMasterOfVega.js section 5): 0.02 + // lands around a mild species combat-trait's worth of advantage, well + // short of the >0.8 win rate the suite treats as "decisive". + const cloak = target.design?.cloaked ? C.cloakEvasion : 0; const chance = C.baseHitChance + C.hitPerTargeting * targeting + C.hitPerAttack * attack - - C.hitPerDefense * defense; + - C.hitPerDefense * defense + - cloak; return Math.max(0.05, Math.min(0.95, chance)); } @@ -168,7 +178,15 @@ function fire(b, shooter, targets, events) { const hits = sampleHits(b.rnd, totalShots, chance); if (hits <= 0) continue; - const effShield = Math.max(0, (target.shield ?? 0) - (w.shieldPierce ?? 0)); + // Black Hole Generator: the whole ship's fire punches through deflectors a + // little further, on top of whatever the mounted weapon already pierces. + // Unlike a weapon's own shieldPierce, this rides for free on every mount + // with no hull-space trade-off, so it is kept well under an integer + // shield point (see the cloak comment above for how the calibration was + // done) — otherwise a passive fleet-wide buff would outclass equivalent + // per-weapon pierce tech like Disruptor Cannon or Disintegrator Beam. + const singularityPierce = shooter.design?.singularity ? (C.singularityShieldPierce ?? 0) : 0; + const effShield = Math.max(0, (target.shield ?? 0) - (w.shieldPierce ?? 0) - singularityPierce); const perHit = Math.max(0, avgDmg(w) - effShield); if (perHit <= 0) { events.push({ kind: 'bounce', from: shooter.uid, to: target.uid, weapon: w.name }); @@ -191,6 +209,11 @@ function applyDamage(b, target, damage, events, shooter, weapon = null) { from: shooter.uid, to: target.uid, weapon: weapon?.name ?? 'Planetary Defences', + // Plain data for the render tier to key sound/FX off of (beam vs missile, + // and the firing ship's Mark) — deliberately not a resolved asset key, + // since this file stays headless (no ui/Sounds.js import). + weaponKind: weapon?.kind ?? null, + mark: shooter.mark ?? null, damage: Math.round(damage), }); } diff --git a/src/games/mastervega/VegaCombatView.js b/src/games/mastervega/VegaCombatView.js index b7f1795..a90f0e9 100644 --- a/src/games/mastervega/VegaCombatView.js +++ b/src/games/mastervega/VegaCombatView.js @@ -8,6 +8,7 @@ import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js'; import { Button } from '../../ui/Button.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; import { FONT, D, uiClick } from './VegaScreens.js'; import VegaFx from './VegaFx.js'; import { stepRound, runBattle, battleResult } from './VegaCombat.js'; @@ -16,6 +17,20 @@ import { shipFrame } from './VegaArt.js'; const LANE_LEFT = 260; const LANE_RIGHT = GAME_WIDTH - 260; +// Weapon cues are banded by the firing ship's Mark (I-VII) — one file per +// band rather than one per Mark, matching the clips Brian recorded. +function weaponSfxKey(mark) { + if (mark <= 3) return SFX.VEGA_WEAPON_123; + if (mark <= 5) return SFX.VEGA_WEAPON_45; + return SFX.VEGA_WEAPON_67; +} +function missileLaunchSfxKey(mark) { + return mark <= 5 ? SFX.VEGA_MISSILE_LAUNCH_12345 : SFX.VEGA_MISSILE_LAUNCH_67; +} +function missileHitSfxKey(mark) { + return mark <= 5 ? SFX.VEGA_MISSILE_HIT_12345 : SFX.VEGA_MISSILE_HIT_67; +} + export function openCombatView(scene, rules, battle, art, opts = {}) { const { attackerSpecies = 'human', defenderSpecies = 'human', onDone = null, playerSide = null } = opts; @@ -97,16 +112,41 @@ export function openCombatView(scene, rules, battle, art, opts = {}) { function animate(step) { if (!step) return; roundText.setText(`Round ${step.round}`); + // A round can bank many 'fire' events (one per stack per mount), and they + // are all processed in this one synchronous loop — playing every cue the + // instant its event is seen would fire them all on the exact same frame + // and clip. Stagger each weapon/missile cue by a growing offset instead: + // still close enough to overlap (a barrage should sound like one), just + // never literally simultaneous. Visual FX are untouched — only sound + // timing is offset — and a missile's hit cue inherits its launch's slot + // plus travel time, so it stays offset from other shots' hits too. + let soundDelay = 0; + const SOUND_STAGGER_MS = 65; for (const ev of step.events) { const from = markers.get(ev.from); const to = markers.get(ev.to); if (ev.kind === 'fire' && from && to) { - const missile = /rocket|missile/i.test(ev.weapon); + // weaponKind comes straight off the fired weapon's own `kind`, unlike + // the old name-sniffing regex here, which missed both Torpedo weapons + // (matches neither /rocket/ nor /missile/) and so drew and sounded + // them as beams. + const missile = ev.weaponKind === 'missile'; + const mark = ev.mark ?? 1; fx.beam(from.container.x, from.container.y, to.container.x, to.container.y, missile ? 0xffb060 : 0x9fd8ff, missile); - scene.time.delayedCall(missile ? 240 : 60, () => { + const hitDelay = missile ? 240 : 60; + scene.time.delayedCall(hitDelay, () => { if (to.container.active) fx.hit(to.container.x, to.container.y, missile ? 0xffb060 : 0xffd28a); }); + if (ev.weaponKind === 'beam' || missile) { + const thisDelay = soundDelay; + soundDelay += SOUND_STAGGER_MS; + const key = missile ? missileLaunchSfxKey(mark) : weaponSfxKey(mark); + scene.time.delayedCall(thisDelay, () => playSound(scene, key)); + if (missile) { + scene.time.delayedCall(thisDelay + hitDelay, () => playSound(scene, missileHitSfxKey(mark))); + } + } } else if (ev.kind === 'losses') { const m = markers.get(ev.uid); if (m) scene.time.delayedCall(180, () => { @@ -138,11 +178,15 @@ export function openCombatView(scene, rules, battle, art, opts = {}) { } // --- controls - const next = new Button(scene, GAME_WIDTH / 2 - 230, GAME_HEIGHT - 70, 'Next round', uiClick(scene, () => { + // Not uiClick — this plays its own vega-endturn cue (same one the main + // game's End Turn button uses) rather than the generic click, and a no-op + // press on a finished battle should stay silent. + const next = new Button(scene, GAME_WIDTH / 2 - 230, GAME_HEIGHT - 70, 'Next round', () => { if (battle.done) return; + playSound(scene, SFX.VEGA_ENDTURN); animate(stepRound(battle, {})); if (battle.done) scene.time.delayedCall(700, finish); - }), { width: 220, height: 52 }); + }, { width: 220, height: 52 }); layer.add(next); const auto = new Button(scene, GAME_WIDTH / 2, GAME_HEIGHT - 70, 'Auto-resolve', uiClick(scene, () => { diff --git a/src/games/mastervega/VegaShipDetail.js b/src/games/mastervega/VegaShipDetail.js index 8bb9df6..54d77d0 100644 --- a/src/games/mastervega/VegaShipDetail.js +++ b/src/games/mastervega/VegaShipDetail.js @@ -164,6 +164,7 @@ export function openShipDetail(scene, rules, state, art, opts, onClose) { const flags = []; if (design.cloaked) flags.push('Cloaked'); + if (design.singularity) flags.push('Singularity core'); if (design.planetCracker) flags.push('Planet cracker'); if (design.immobile) flags.push('Never leaves its system'); if (flags.length) text(flags.join(' · '), 15, '#ffd88a', 4); diff --git a/src/games/mastervega/VegaShips.js b/src/games/mastervega/VegaShips.js index 92bf990..3a815c3 100644 --- a/src/games/mastervega/VegaShips.js +++ b/src/games/mastervega/VegaShips.js @@ -264,6 +264,7 @@ export function designFor(rules, known, hullId, traits = {}, skills = {}) { troops: hull.troops ?? 0, planetCracker: comps.planetCracker && mounts.some((m) => m.weapon.planetCracker), cloaked: comps.cloaked, + singularity: comps.singularity, }; } diff --git a/src/games/mastervega/VegaTechEffects.js b/src/games/mastervega/VegaTechEffects.js index 7ea98fc..f51348c 100644 --- a/src/games/mastervega/VegaTechEffects.js +++ b/src/games/mastervega/VegaTechEffects.js @@ -5,10 +5,9 @@ // // Every formatter is hand-matched to how the effect is actually consumed // elsewhere in the engine (VegaCombat.js/VegaLogic.js/VegaShips.js) — not a -// generic key/value dump. A few effects (cloaked, scanRange, singularity) -// are currently inert stats with no downstream mechanic wired up yet; their -// wording stays modest/flavor rather than promising a numeric benefit that -// doesn't exist in the sim. +// generic key/value dump. `scanRange` is currently an inert stat with no +// downstream mechanic wired up yet; its wording stays modest/flavor rather +// than promising a numeric benefit that doesn't exist in the sim. const pct = (mult) => `${Math.round(Math.abs(mult - 1) * 100)}%`; @@ -29,7 +28,7 @@ function hostilityWorlds(rules, tier) { const FORMATTERS = { armor: (rules, v) => `Hull armor upgraded to ${v.name} plating — hull hit points ×${v.hpMult} (+${pct(v.hpMult)}).`, - cloaked: () => 'Ships are fitted with a stealth field, cloaking them from enemy sensors.', + cloaked: (rules) => `Ships are fitted with a stealth field — enemy weapons are ${Math.round((rules.combat.cloakEvasion ?? 0) * 100)} percentage points less likely to hit.`, colonizeHostility: (rules, v) => `Colonists can now settle ${hostilityWorlds(rules, v)} worlds.`, counterEspionage: (rules, v) => `+${v} counter-espionage — makes it harder for enemy agents to steal your technology.`, engine: (rules, v) => `Engines upgraded to ${v.name} Drive (speed ${v.speed}).`, @@ -49,7 +48,7 @@ const FORMATTERS = { researchMult: (rules, v) => `Research output increased by ${pct(v)} empire-wide.`, scanRange: (rules, v) => `+${v} scanner range for detecting distant fleets and systems.`, shield: (rules, v) => `Deflector shields absorb ${v} point${v === 1 ? '' : 's'} of damage from every incoming hit, on ships and planetary defenses alike.`, - singularity: () => "A breakthrough in exotic physics — the foundation for the galaxy's ultimate weapon.", + singularity: (rules) => `A breakthrough in exotic physics — every weapon on this ship warps enemy deflectors, trimming ${rules.combat.singularityShieldPierce ?? 0} point of shielding off every hit it lands.`, targeting: (rules, v) => `Combat accuracy increased by ${Math.round(v * 6)}% (better targeting computers).`, wasteMult: (rules, v) => `Industrial waste reduced by ${pct(v)}.`, weapon: (rules, v) => describeWeapon(v), diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index 112ae68..4a4b6cf 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -182,18 +182,6 @@ export default class PreloadScene extends Phaser.Scene { this.load.audio('sfx-monopoly-expense', 'assets/fx/monopoly-expense.mp3'); this.load.audio('sfx-monopoly-pay', 'assets/fx/monopoly-pay.mp3'); this.load.audio('sfx-monopoly-paid', 'assets/fx/monopoly-paid.mp3'); - - this.load.audio('sfx-vega-select', 'assets/fx/vega-select.mp3'); - this.load.audio('sfx-vega-build', 'assets/fx/vega-build.mp3'); - this.load.audio('sfx-vega-close', 'assets/fx/vega-close.mp3'); - this.load.audio('sfx-vega-newturn', 'assets/fx/vega-newturn.mp3'); - this.load.audio('sfx-vega-unit', 'assets/fx/vega-unit.mp3'); - this.load.audio('sfx-vega-view', 'assets/fx/vega-view.mp3'); - this.load.audio('sfx-vega-warp', 'assets/fx/vega-warp.mp3'); - this.load.audio('sfx-vega-star', 'assets/fx/vega-star.mp3'); - this.load.audio('sfx-vega-zoomin', 'assets/fx/vega-zoomin.mp3'); - this.load.audio('sfx-vega-zoomout', 'assets/fx/vega-zoomout.mp3'); - this.load.audio('sfx-vega-viewcolony', 'assets/fx/vega-viewcolony.mp3'); } async create() { diff --git a/src/ui/Sounds.js b/src/ui/Sounds.js index 314402b..3c001a9 100644 --- a/src/ui/Sounds.js +++ b/src/ui/Sounds.js @@ -110,6 +110,18 @@ export const SFX = { VEGA_ZOOMIN: 'sfx-vega-zoomin', VEGA_ZOOMOUT: 'sfx-vega-zoomout', VEGA_VIEWCOLONY: 'sfx-vega-viewcolony', + VEGA_ENDTURN: 'sfx-vega-endturn', + // Beam cues split by the firing ship's Mark (I-VII); missile cues split the + // same way but only two ways, and further split into launch vs. impact. + // See VegaCombatView.js's weaponSfxKey()/missileLaunchSfxKey()/ + // missileHitSfxKey() for the Mark bands. + VEGA_WEAPON_123: 'sfx-vega-weapon-123', + VEGA_WEAPON_45: 'sfx-vega-weapon-45', + VEGA_WEAPON_67: 'sfx-vega-weapon-67', + VEGA_MISSILE_LAUNCH_12345: 'sfx-vega-missle-launch-12345', + VEGA_MISSILE_LAUNCH_67: 'sfx-vega-missle-launch-67', + VEGA_MISSILE_HIT_12345: 'sfx-vega-missle-hit-12345', + VEGA_MISSILE_HIT_67: 'sfx-vega-missle-hit-67', }; export function playSound(scene, key) { diff --git a/tools/verifyMasterOfVega.js b/tools/verifyMasterOfVega.js index a55b1b0..79d397b 100644 --- a/tools/verifyMasterOfVega.js +++ b/tools/verifyMasterOfVega.js @@ -1380,6 +1380,32 @@ section('5. Combat'); check('a large invasion force takes a lightly held world', inv.captured); const inv2 = Combat.resolveInvasion(RULES, mulberry32(7), 4, 0, { groundDefense: 200 }, 200, 300); check('a token force fails against a fortress', !inv2.captured); + + // Cloaked (Stealth Field) and singularity (Black Hole Generator) used to be + // computed onto the design and never read anywhere in combat. Isolate each + // flag by hand-cloning an otherwise-identical fully-teched design — passing + // `design` directly on a ship entry skips the known-tech derivation, so the + // A/B pair differs in exactly one field. + const baseDesign = Ships.designFor(RULES, techsUpTo(9), 'cruiser', RULES.species.human.traits); + check('a fully-teched design has both flags available to strip', baseDesign.cloaked && baseDesign.singularity); + const withoutCloak = { ...baseDesign, cloaked: false }; + const withoutSingularity = { ...baseDesign, singularity: false }; + + const abBattle = (aDesign, dDesign, seed) => Combat.runBattle(Combat.createBattle(RULES, { + attacker: { empireIdx: 0, name: 'a', empire: mkEmp('human', 9), ships: [{ hullId: 'cruiser', count: 5, design: aDesign }] }, + defender: { empireIdx: 1, name: 'd', empire: mkEmp('human', 9), ships: [{ hullId: 'cruiser', count: 5, design: dDesign }] }, + rnd: mulberry32(seed), + })); + // Both effects are deliberately modest — calibrated (see VegaCombat.js + // comments) to land near what a mild species combat-trait bonus is worth in + // this exact harness (~62%), nowhere near the >0.8 "decisive" bar used + // above for a two-tier tech lead or a numbers advantage. + const cloakRate = rate((s) => abBattle(baseDesign, withoutCloak, s)); + check('cloak makes an otherwise-identical fleet harder to beat, but not decisively', + cloakRate > 0.52 && cloakRate < 0.8, `attacker (cloaked) won ${(cloakRate * 100).toFixed(1)}%`); + const singularityRate = rate((s) => abBattle(baseDesign, withoutSingularity, s)); + check('singularity shield-pierce makes an otherwise-identical fleet harder to beat, but not decisively', + singularityRate > 0.52 && singularityRate < 0.8, `attacker (singularity) won ${(singularityRate * 100).toFixed(1)}%`); } // ---------------------------------------------------------------------------