492 lines
20 KiB
JavaScript
492 lines
20 KiB
JavaScript
// Master of Vega — combat simulator. Secret entrance: index.html?movsim
|
||
// (see PreloadScene). A dev tool for tuning VegaCombat: configure a fleet (or
|
||
// a fleet vs. a bare planet) on each side, then either watch one battle play
|
||
// out on the real tactical screen (openCombatView, unmodified) or fire a
|
||
// batch of headless trials through runBattle for win-rate stats. Both paths
|
||
// call the exact same VegaCombat stepper the live game uses, so a balance
|
||
// change this tool disagrees with is a change the real game disagrees with
|
||
// too.
|
||
//
|
||
// A "Live / V2 Prototype" toggle switches BOTH Watch Battle and Simulate ×N
|
||
// together onto VegaCombatV2.js's per-ship engine and VegaCombatViewV2.js's
|
||
// zoomable per-ship view — never one on each, so the fight you watch and the
|
||
// trial stats you run always agree. Live mode is untouched, byte-for-byte
|
||
// the same tool this always was; V2 is an experimental parallel engine (see
|
||
// docs/mastervega-build-plan.md) that does not affect real player battles.
|
||
//
|
||
// Boots straight past GameMenuScene/OpponentSelect, so it has to load its own
|
||
// slice of the mastervega asset manifest (rules JSON + ship art) instead of
|
||
// relying on GameRoomScene's lazy fetch — same trick as the other *Editor
|
||
// scenes.
|
||
|
||
import * as Phaser from 'phaser';
|
||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||
import { Button } from './VegaButton.js';
|
||
import { queueGameAssets } from '../../services/assetLoader.js';
|
||
import { compileRules, markNumeral } from './VegaRules.js';
|
||
import { ensureSheets } from './VegaArt.js';
|
||
import { designFor, stackPower } from './VegaShips.js';
|
||
import { createBattle, runBattle } from './VegaCombat.js';
|
||
import { openCombatView } from './VegaCombatView.js';
|
||
import * as CombatV2 from './VegaCombatV2.js';
|
||
import { openCombatViewV2 } from './VegaCombatViewV2.js';
|
||
import { formationName } from './VegaFormations.js';
|
||
import { VegaMusic } from './VegaMusic.js';
|
||
import { FONT, uiClick, openFormationPicker } from './VegaScreens.js';
|
||
|
||
const PANEL_BG = 0x0b1220;
|
||
const PANEL_LINE = 0x2c435f;
|
||
const ATTACKER_COLOR = '#9fd8ff';
|
||
const DEFENDER_COLOR = '#ffb0a0';
|
||
|
||
const TOP = 170;
|
||
const COL_W = 800;
|
||
const COL_A_X = 90;
|
||
const COL_D_X = GAME_WIDTH - 90 - COL_W;
|
||
|
||
// Starter compositions for the "Small / Medium / Large" one-click presets.
|
||
// Hand-tuned, not derived — they exist to get a plausible fleet on the grid
|
||
// fast; every count is still a manual stepper afterwards.
|
||
const FLEET_PRESETS = {
|
||
small: {
|
||
frigate: 4, destroyer: 2, cruiser: 0, battleship: 0, starbase: 0,
|
||
},
|
||
medium: {
|
||
frigate: 6, destroyer: 6, cruiser: 3, battleship: 1, starbase: 0,
|
||
},
|
||
large: {
|
||
frigate: 10, destroyer: 10, cruiser: 8, battleship: 5, starbase: 1,
|
||
},
|
||
};
|
||
const PLANET_PRESETS = {
|
||
small: { defenseHp: 100, shieldBonus: 0 },
|
||
medium: { defenseHp: 400, shieldBonus: 2 },
|
||
large: { defenseHp: 1200, shieldBonus: 4 },
|
||
};
|
||
|
||
const TRIAL_COUNTS = [10, 50, 200, 1000];
|
||
|
||
// The tech tier (0..9, same scale as rules.techs[].tier) needed in every
|
||
// component field to land VegaShips.markFor() on a given Mark. Derived by
|
||
// inverting markFor's `1 + floor(avg*0.7)` — see VegaShips.js — so a chosen
|
||
// Mark here produces a design identical to an empire that actually researched
|
||
// its way there, not an approximation.
|
||
const MARK_TIER = {
|
||
1: 0, 2: 2, 3: 3, 4: 5, 5: 6, 6: 8, 7: 9,
|
||
};
|
||
|
||
function knownForMark(rules, mark) {
|
||
const tier = MARK_TIER[mark] ?? 9;
|
||
const known = {};
|
||
for (const t of rules.techList) if (t.tier <= tier) known[t.id] = true;
|
||
return known;
|
||
}
|
||
|
||
const totalLosses = (losses) => losses.reduce((t, l) => t + l.lost, 0);
|
||
|
||
export default class VegaCombatSim extends Phaser.Scene {
|
||
constructor() { super('MasterVegaSim'); }
|
||
|
||
preload() {
|
||
// Boots straight out of PreloadScene, so the lazy game art (ships sheet,
|
||
// sfx, soundtrack) GameRoomScene would normally fetch isn't loaded yet.
|
||
queueGameAssets(this, 'mastervega');
|
||
}
|
||
|
||
create() {
|
||
this.cameras.main.setBackgroundColor('#03060d');
|
||
this.rules = compileRules(this.cache.json.get('mastervega-rules'));
|
||
const artwork = this.cache.json.get('mastervega-artwork') ?? { sheets: {} };
|
||
this.art = ensureSheets(this, this.rules, artwork).keys;
|
||
|
||
try {
|
||
this.music = new VegaMusic(this, this.cache.json.get('masterofvega-music'));
|
||
this.music.setCategory('combat');
|
||
} catch (err) { /* music is optional */ }
|
||
|
||
// Only the hulls a tactical battle actually cares about — scouts,
|
||
// transports and colony ships never fire a shot.
|
||
this.combatHulls = this.rules.hullList.filter((h) => h.role === 'warship' || h.role === 'base');
|
||
this.speciesIds = this.rules.speciesList.map((s) => s.id);
|
||
|
||
this.battleType = 'ship'; // 'ship' | 'planet'
|
||
this.engineMode = 'live'; // 'live' | 'v2' — see the header comment
|
||
this.trialCount = 200;
|
||
this.sideA = this.freshSide();
|
||
this.sideD = this.freshSide();
|
||
this.sideD.defenseHp = PLANET_PRESETS.medium.defenseHp;
|
||
this.sideD.shieldBonus = PLANET_PRESETS.medium.shieldBonus;
|
||
this.applyFleetPreset(this.sideA, 'medium', false);
|
||
this.applyFleetPreset(this.sideD, 'medium', false);
|
||
|
||
this.add.text(GAME_WIDTH / 2, 44, 'MASTER OF VEGA — COMBAT SIMULATOR', {
|
||
fontFamily: FONT, fontSize: '34px', color: '#cfe8ff',
|
||
}).setOrigin(0.5);
|
||
this.add.text(GAME_WIDTH / 2, 82,
|
||
'Configure both sides, then watch an animated battle or run a batch of instant trials.', {
|
||
fontFamily: FONT, fontSize: '17px', color: '#6f8aa8',
|
||
}).setOrigin(0.5);
|
||
new Button(this, GAME_WIDTH - 150, 44, 'Exit', uiClick(this, () => { window.location.search = ''; }),
|
||
{ width: 200, height: 44, fontSize: 16, variant: 'ghost' });
|
||
|
||
this.panelLayer = this.add.container(0, 0);
|
||
|
||
const resultsBg = this.add.rectangle(GAME_WIDTH / 2, 962, 1620, 170, PANEL_BG, 0.6)
|
||
.setStrokeStyle(2, PANEL_LINE, 0.9);
|
||
this.resultsText = this.add.text(GAME_WIDTH / 2, 962, 'Run a battle to see results here.', {
|
||
fontFamily: FONT, fontSize: '20px', color: '#cfe8ff', align: 'center', lineSpacing: 10,
|
||
}).setOrigin(0.5);
|
||
this.resultsBg = resultsBg;
|
||
|
||
this.rebuild();
|
||
}
|
||
|
||
freshSide() {
|
||
const counts = {};
|
||
for (const h of this.combatHulls) counts[h.id] = 0;
|
||
return { species: 'human', mark: 4, counts };
|
||
}
|
||
|
||
// ------------------------------------------------------------- mutators
|
||
|
||
applyFleetPreset(side, size, rebuild = true) {
|
||
const p = FLEET_PRESETS[size];
|
||
for (const h of this.combatHulls) side.counts[h.id] = p[h.id] ?? 0;
|
||
if (rebuild) this.rebuild();
|
||
}
|
||
|
||
applyPlanetPreset(size) {
|
||
const p = PLANET_PRESETS[size];
|
||
this.sideD.defenseHp = p.defenseHp;
|
||
this.sideD.shieldBonus = p.shieldBonus;
|
||
this.rebuild();
|
||
}
|
||
|
||
clearSide(side) {
|
||
for (const h of this.combatHulls) side.counts[h.id] = 0;
|
||
this.rebuild();
|
||
}
|
||
|
||
cycleSpecies(side) {
|
||
const i = this.speciesIds.indexOf(side.species);
|
||
side.species = this.speciesIds[(i + 1) % this.speciesIds.length];
|
||
this.rebuild();
|
||
}
|
||
|
||
cycleMark(side) {
|
||
side.mark = side.mark >= 7 ? 1 : side.mark + 1;
|
||
this.rebuild();
|
||
}
|
||
|
||
adjustCount(side, hullId, delta) {
|
||
side.counts[hullId] = Phaser.Math.Clamp((side.counts[hullId] ?? 0) + delta, 0, 999);
|
||
this.rebuild();
|
||
}
|
||
|
||
// --------------------------------------------------------- battle setup
|
||
|
||
sidePower(side) {
|
||
const known = knownForMark(this.rules, side.mark);
|
||
const traits = this.rules.species[side.species]?.traits ?? {};
|
||
let ships = 0;
|
||
let power = 0;
|
||
for (const h of this.combatHulls) {
|
||
const c = side.counts[h.id] ?? 0;
|
||
if (c <= 0) continue;
|
||
ships += c;
|
||
power += stackPower(designFor(this.rules, known, h.id, traits), c);
|
||
}
|
||
return { ships, power };
|
||
}
|
||
|
||
buildSideShips(side) {
|
||
return this.combatHulls
|
||
.filter((h) => (side.counts[h.id] ?? 0) > 0)
|
||
.map((h) => ({ hullId: h.id, mark: side.mark, count: side.counts[h.id] }));
|
||
}
|
||
|
||
buildBattleOpts() {
|
||
const a = this.sideA;
|
||
const d = this.sideD;
|
||
const attacker = {
|
||
empireIdx: 0,
|
||
name: 'Attacker',
|
||
empire: { known: knownForMark(this.rules, a.mark), traits: this.rules.species[a.species]?.traits ?? {} },
|
||
ships: this.buildSideShips(a),
|
||
};
|
||
const defender = {
|
||
empireIdx: 1,
|
||
name: this.battleType === 'planet' ? 'Planetary Defenses' : 'Defender',
|
||
empire: { known: knownForMark(this.rules, d.mark), traits: this.rules.species[d.species]?.traits ?? {} },
|
||
ships: this.battleType === 'planet' ? [] : this.buildSideShips(d),
|
||
};
|
||
const colony = this.battleType === 'planet' ? { defenseHp: d.defenseHp, shieldBonus: d.shieldBonus } : null;
|
||
return { attacker, defender, colony };
|
||
}
|
||
|
||
battleReady() {
|
||
const { attacker, defender, colony } = this.buildBattleOpts();
|
||
return attacker.ships.length > 0 && (defender.ships.length > 0 || (colony && colony.defenseHp > 0));
|
||
}
|
||
|
||
// ---------------------------------------------------------------- run
|
||
|
||
watchBattle() {
|
||
if (!this.battleReady()) return;
|
||
if (this.engineMode === 'v2') {
|
||
// V2 only: a pre-battle prompt for the attacker's ("your") formation
|
||
// strategy. The defender ("AI") gets no prompt at all — it picks
|
||
// silently, which CombatV2.createBattle already does on its own for
|
||
// any side that doesn't supply a valid choice.
|
||
openFormationPicker(this, (formationStrategy) => this.launchV2Battle(formationStrategy));
|
||
return;
|
||
}
|
||
const { attacker, defender, colony } = this.buildBattleOpts();
|
||
const battle = createBattle(this.rules, {
|
||
attacker, defender, colony, starIdx: -1, rnd: Math.random,
|
||
});
|
||
this.music?.setCategory('combat');
|
||
openCombatView(this, this.rules, battle, this.art, {
|
||
attackerSpecies: this.sideA.species,
|
||
defenderSpecies: this.sideD.species,
|
||
playerSide: null,
|
||
onDone: (result) => this.showWatchResult(result),
|
||
});
|
||
}
|
||
|
||
launchV2Battle(attackerFormation) {
|
||
const { attacker, defender, colony } = this.buildBattleOpts();
|
||
attacker.formationStrategy = attackerFormation;
|
||
const battle = CombatV2.createBattle(this.rules, {
|
||
attacker, defender, colony, starIdx: -1, rnd: Math.random,
|
||
});
|
||
this.music?.setCategory('combat');
|
||
openCombatViewV2(this, this.rules, battle, this.art, {
|
||
attackerSpecies: this.sideA.species,
|
||
defenderSpecies: this.sideD.species,
|
||
playerSide: null,
|
||
onDone: (result) => this.showWatchResult(result, battle),
|
||
});
|
||
}
|
||
|
||
runTrials(n) {
|
||
if (!this.battleReady()) return;
|
||
const { attacker, defender, colony } = this.buildBattleOpts();
|
||
const engine = this.engineMode === 'v2' ? CombatV2 : { createBattle, runBattle };
|
||
let aWins = 0;
|
||
let dWins = 0;
|
||
let draws = 0;
|
||
let roundsSum = 0;
|
||
let aLossSum = 0;
|
||
let dLossSum = 0;
|
||
let planetKills = 0;
|
||
for (let i = 0; i < n; i += 1) {
|
||
const battle = engine.createBattle(this.rules, {
|
||
attacker, defender, colony, starIdx: -1, rnd: Math.random,
|
||
});
|
||
const result = engine.runBattle(battle);
|
||
if (result.winner === 'attacker') aWins += 1;
|
||
else if (result.winner === 'defender') dWins += 1;
|
||
else draws += 1;
|
||
roundsSum += result.rounds;
|
||
aLossSum += totalLosses(result.attackerLosses);
|
||
dLossSum += totalLosses(result.defenderLosses);
|
||
if (result.planetDestroyed) planetKills += 1;
|
||
}
|
||
this.showTrialResults({
|
||
n, aWins, dWins, draws, roundsSum, aLossSum, dLossSum, planetKills,
|
||
});
|
||
}
|
||
|
||
showWatchResult(result, battle = null) {
|
||
const aLoss = totalLosses(result.attackerLosses);
|
||
const dLoss = totalLosses(result.defenderLosses);
|
||
const winner = result.winner === 'draw' ? 'Draw — mutual stalemate'
|
||
: result.winner === 'attacker' ? 'Attacker wins' : 'Defender wins';
|
||
let line2 = `Attacker lost ${aLoss} ship${aLoss === 1 ? '' : 's'} · Defender lost ${dLoss} ship${dLoss === 1 ? '' : 's'}`;
|
||
if (this.battleType === 'planet') {
|
||
line2 += result.planetDestroyed
|
||
? ' · Planetary defenses destroyed'
|
||
: ` · Planetary defenses at ${Math.round(result.planetDefenseLeft)} HP`;
|
||
}
|
||
let text = `LAST BATTLE — ${winner} in ${result.rounds} round${result.rounds === 1 ? '' : 's'}\n${line2}`;
|
||
if (battle?.attackerFormation) {
|
||
text += `\nAttacker formation: ${formationName(battle.attackerFormation)}`
|
||
+ ` · Defender formation: ${formationName(battle.defenderFormation)}`;
|
||
}
|
||
this.resultsText.setText(text);
|
||
}
|
||
|
||
showTrialResults({
|
||
n, aWins, dWins, draws, roundsSum, aLossSum, dLossSum, planetKills,
|
||
}) {
|
||
const pct = (x) => ((x / n) * 100).toFixed(1);
|
||
let text = `${n} TRIALS — Attacker wins ${pct(aWins)}% · Defender wins ${pct(dWins)}% · Draws ${pct(draws)}%\n`
|
||
+ `Avg rounds ${(roundsSum / n).toFixed(1)} · Avg attacker losses ${(aLossSum / n).toFixed(1)} ships`
|
||
+ ` · Avg defender losses ${(dLossSum / n).toFixed(1)} ships`;
|
||
if (this.battleType === 'planet') text += `\nPlanetary defenses destroyed in ${pct(planetKills)}% of trials`;
|
||
this.resultsText.setText(text);
|
||
}
|
||
|
||
// --------------------------------------------------------------- draw
|
||
|
||
rebuild() {
|
||
this.panelLayer.removeAll(true);
|
||
this.drawModeToggle();
|
||
this.drawEngineToggle();
|
||
this.drawSide(COL_A_X, this.sideA, false, 'ATTACKER FLEET', ATTACKER_COLOR);
|
||
this.drawSide(COL_D_X, this.sideD, true, this.battleType === 'planet' ? 'PLANETARY DEFENSES' : 'DEFENDER FLEET', DEFENDER_COLOR);
|
||
this.drawRunControls();
|
||
}
|
||
|
||
// Side by side in one row (not a second row) so neither the side panels
|
||
// below (which start at TOP=170) nor the run controls further down need
|
||
// to move for it.
|
||
drawModeToggle() {
|
||
const label = this.battleType === 'ship' ? 'Battle Type: Fleet vs Fleet ▸' : 'Battle Type: Fleet vs Planet ▸';
|
||
const btn = new Button(this, GAME_WIDTH / 2 - 235, 130, label, uiClick(this, () => {
|
||
this.battleType = this.battleType === 'ship' ? 'planet' : 'ship';
|
||
this.rebuild();
|
||
}), { width: 440, height: 48, fontSize: 18, variant: 'ghost' });
|
||
this.panelLayer.add(btn);
|
||
}
|
||
|
||
// Live and V2 are two completely separate engines/views (see the header
|
||
// comment) — Watch Battle and Simulate both follow this one switch, so
|
||
// what you watch and what the trial stats measure never disagree.
|
||
drawEngineToggle() {
|
||
const isV2 = this.engineMode === 'v2';
|
||
const label = isV2 ? 'Engine: V2 Prototype ▸' : 'Engine: Live ▸';
|
||
const btn = new Button(this, GAME_WIDTH / 2 + 235, 130, label, uiClick(this, () => {
|
||
this.engineMode = isV2 ? 'live' : 'v2';
|
||
this.rebuild();
|
||
}), {
|
||
width: 440, height: 48, fontSize: 18, variant: isV2 ? 'solid' : 'ghost',
|
||
});
|
||
this.panelLayer.add(btn);
|
||
}
|
||
|
||
rowLabel(x0, y, label) {
|
||
const t = this.add.text(x0 + 24, y, label, { fontFamily: FONT, fontSize: '19px', color: '#cfe0f0' }).setOrigin(0, 0.5);
|
||
this.panelLayer.add(t);
|
||
}
|
||
|
||
stepperControl(cx, y, value, onDelta, step) {
|
||
const minus = new Button(this, cx - 95, y, '−', uiClick(this, () => onDelta(-step)), { width: 40, height: 38, fontSize: 20, variant: 'ghost' });
|
||
const valText = this.add.text(cx, y, String(value), { fontFamily: FONT, fontSize: '20px', color: '#f2ead8' }).setOrigin(0.5);
|
||
const plus = new Button(this, cx + 95, y, '+', uiClick(this, () => onDelta(step)), { width: 40, height: 38, fontSize: 20, variant: 'ghost' });
|
||
this.panelLayer.add([minus, valText, plus]);
|
||
}
|
||
|
||
presetRow(x0, width, y, onPick) {
|
||
const gap = 12;
|
||
const bw = (width - 40 - gap * 2) / 3;
|
||
['small', 'medium', 'large'].forEach((size, i) => {
|
||
const bx = x0 + 20 + bw / 2 + i * (bw + gap);
|
||
const btn = new Button(this, bx, y, size[0].toUpperCase() + size.slice(1),
|
||
uiClick(this, () => onPick(size)), { width: bw, height: 44, fontSize: 16, variant: 'ghost' });
|
||
this.panelLayer.add(btn);
|
||
});
|
||
}
|
||
|
||
drawSide(x0, side, isDefender, title, accentHex) {
|
||
const cx = x0 + COL_W / 2;
|
||
const isPlanet = isDefender && this.battleType === 'planet';
|
||
const panelH = isPlanet ? 420 : 600;
|
||
|
||
const bg = this.add.rectangle(cx, TOP + panelH / 2, COL_W, panelH, PANEL_BG, 0.55)
|
||
.setStrokeStyle(2, PANEL_LINE, 0.85);
|
||
this.panelLayer.add(bg);
|
||
this.panelLayer.add(this.add.text(cx, TOP + 26, title, { fontFamily: FONT, fontSize: '24px', color: accentHex }).setOrigin(0.5));
|
||
|
||
let y = TOP + 76;
|
||
|
||
if (!isPlanet) {
|
||
const spName = this.rules.species[side.species]?.name ?? side.species;
|
||
this.rowLabel(x0, y, 'Species');
|
||
const spBtn = new Button(this, x0 + COL_W - 140, y, spName, uiClick(this, () => this.cycleSpecies(side)),
|
||
{ width: 220, height: 40, fontSize: 17, variant: 'ghost' });
|
||
this.panelLayer.add(spBtn);
|
||
y += 56;
|
||
|
||
this.rowLabel(x0, y, 'Tech Level');
|
||
const markBtn = new Button(this, x0 + COL_W - 140, y, `Mark ${markNumeral(side.mark)}`, uiClick(this, () => this.cycleMark(side)),
|
||
{ width: 220, height: 40, fontSize: 17, variant: 'ghost' });
|
||
this.panelLayer.add(markBtn);
|
||
y += 70;
|
||
|
||
for (const h of this.combatHulls) {
|
||
this.rowLabel(x0, y, h.name);
|
||
const count = side.counts[h.id] ?? 0;
|
||
this.stepperControl(x0 + COL_W - 170, y, count, (d) => this.adjustCount(side, h.id, d), 1);
|
||
y += 56;
|
||
}
|
||
y += 14;
|
||
this.presetRow(x0, COL_W, y, (size) => this.applyFleetPreset(side, size));
|
||
y += 60;
|
||
const clearBtn = new Button(this, cx, y, 'Clear', uiClick(this, () => this.clearSide(side)),
|
||
{ width: 160, height: 38, fontSize: 15, variant: 'ghost' });
|
||
this.panelLayer.add(clearBtn);
|
||
y += 48;
|
||
|
||
const { ships, power } = this.sidePower(side);
|
||
this.panelLayer.add(this.add.text(cx, y, `${ships} ship${ships === 1 ? '' : 's'} · power score ${power.toLocaleString()}`, {
|
||
fontFamily: FONT, fontSize: '16px', color: '#6f8aa8',
|
||
}).setOrigin(0.5));
|
||
} else {
|
||
this.rowLabel(x0, y, 'Defense HP');
|
||
this.stepperControl(x0 + COL_W - 170, y, side.defenseHp, (d) => {
|
||
side.defenseHp = Phaser.Math.Clamp(side.defenseHp + d, 0, 5000);
|
||
this.rebuild();
|
||
}, 50);
|
||
y += 56;
|
||
|
||
this.rowLabel(x0, y, 'Shield Bonus');
|
||
this.stepperControl(x0 + COL_W - 170, y, side.shieldBonus, (d) => {
|
||
side.shieldBonus = Phaser.Math.Clamp(side.shieldBonus + d, 0, 10);
|
||
this.rebuild();
|
||
}, 1);
|
||
y += 70;
|
||
|
||
this.presetRow(x0, COL_W, y, (size) => this.applyPlanetPreset(size));
|
||
y += 80;
|
||
this.panelLayer.add(this.add.text(cx, y,
|
||
'No orbiting ships — the attacker must break the planetary\ndefenses alone before the colony can be invaded.', {
|
||
fontFamily: FONT, fontSize: '15px', color: '#5a7086', align: 'center', lineSpacing: 6,
|
||
}).setOrigin(0.5));
|
||
}
|
||
}
|
||
|
||
drawRunControls() {
|
||
const ready = this.battleReady();
|
||
const y = 830;
|
||
|
||
const watchBtn = new Button(this, GAME_WIDTH / 2 - 260, y, 'Watch Battle ▶', uiClick(this, () => this.watchBattle()),
|
||
{ width: 300, height: 58, fontSize: 20 });
|
||
watchBtn.setEnabled(ready);
|
||
this.panelLayer.add(watchBtn);
|
||
|
||
const trialBtn = new Button(this, GAME_WIDTH / 2 + 10, y, `Trials: ${this.trialCount}`, uiClick(this, () => {
|
||
const i = TRIAL_COUNTS.indexOf(this.trialCount);
|
||
this.trialCount = TRIAL_COUNTS[(i + 1) % TRIAL_COUNTS.length];
|
||
this.rebuild();
|
||
}), { width: 180, height: 58, fontSize: 18, variant: 'ghost' });
|
||
this.panelLayer.add(trialBtn);
|
||
|
||
const simBtn = new Button(this, GAME_WIDTH / 2 + 260, y, `Simulate ×${this.trialCount}`, uiClick(this, () => this.runTrials(this.trialCount)),
|
||
{ width: 300, height: 58, fontSize: 20 });
|
||
simBtn.setEnabled(ready);
|
||
this.panelLayer.add(simBtn);
|
||
|
||
if (!ready) {
|
||
const { attacker } = this.buildBattleOpts();
|
||
const msg = attacker.ships.length === 0
|
||
? 'Attacker needs at least one ship.'
|
||
: 'Defender needs ships, or a Battle Type of Fleet vs Planet with Defense HP above 0.';
|
||
this.panelLayer.add(this.add.text(GAME_WIDTH / 2, y + 48, msg, {
|
||
fontFamily: FONT, fontSize: '16px', color: '#e08a8a',
|
||
}).setOrigin(0.5));
|
||
}
|
||
}
|
||
}
|