feat(mastervega): add third-party peace requests, pre-battle formation picker, and combat V2 planet fixes
- Add "peace request" diplomacy: player can ask an AI to end a war with a third empire, and AIs can ask the player the same. Refusing costs attitude. Implemented in VegaDiplomacy.js (wouldAcceptPeaceRequest/requestPeace/offerPeaceRequest), VegaAudience.js (Demand Peace button + war picker), and full chat lines for all species in VegaChat.js. - Extract openFormationPicker to VegaScreens.js and wire it into MasterOfVegaGame.js so the player chooses a formation before every battle. AI opponents still pick silently. - Fix Combat V2 planet positioning (fixed at worldWidth * 0.75 instead of formation-relative), pass typeId for real planet art, and apply the Planetary Shield building's shieldBonus (was hardcoded to 0). - Add icon images to research screen tech field headers. - Add comprehensive tests for peace requests and prepareBattleAt fixes.
This commit is contained in:
parent
590bca1721
commit
57a8a14ade
|
|
@ -433,6 +433,12 @@
|
||||||
"attitudeThreshold": 45,
|
"attitudeThreshold": 45,
|
||||||
"aiProposeChance": 0.25
|
"aiProposeChance": 0.25
|
||||||
},
|
},
|
||||||
|
"warRequest": {
|
||||||
|
"_readme": "\"Please make peace with them\" — a request about a THIRD PARTY's war (VegaDiplomacy.js's wouldAcceptPeaceRequest/requestPeace/offerPeaceRequest). friendThreshold is how much the one being asked has to like the requester to grant it as a favor when they wouldn't otherwise; refuseBelowAttitude is how bad their own war with the third party can be before even a friend's plea is waved off. aiProposeChance is per-friend-per-turn, checked only once the human is already at war with that friend.",
|
||||||
|
"friendThreshold": 40,
|
||||||
|
"refuseBelowAttitude": -70,
|
||||||
|
"aiProposeChance": 0.15
|
||||||
|
},
|
||||||
"espionage": {
|
"espionage": {
|
||||||
"_readme": "Sabotage magnitude when a runEspionage roll succeeds with mission 'sabotage' (VegaLogic.js). Targeting/frequency/odds are unchanged from the steal-tech path; this only controls what a sabotage success does. A successful mission (sabotage or tech theft) carries no attitude penalty — nobody publicly knows who did it, so there's nobody for the victim to be angry at; getting CAUGHT is the one outcome that still costs relations.",
|
"_readme": "Sabotage magnitude when a runEspionage roll succeeds with mission 'sabotage' (VegaLogic.js). Targeting/frequency/odds are unchanged from the steal-tech path; this only controls what a sabotage success does. A successful mission (sabotage or tech theft) carries no attitude penalty — nobody publicly knows who did it, so there's nobody for the victim to be angry at; getting CAUGHT is the one outcome that still costs relations.",
|
||||||
"sabotageFactoriesFraction": 0.25,
|
"sabotageFactoriesFraction": 0.25,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import { openSystemView } from './VegaSystemView.js';
|
||||||
import { openCombatViewV2 } from './VegaCombatViewV2.js';
|
import { openCombatViewV2 } from './VegaCombatViewV2.js';
|
||||||
import {
|
import {
|
||||||
FONT, D, openDiplomacyScreen, openCouncilScreen, openLeaderScreen,
|
FONT, D, openDiplomacyScreen, openCouncilScreen, openLeaderScreen,
|
||||||
openSaveScreen, openLoadScreen, showVictoryOverlay,
|
openSaveScreen, openLoadScreen, showVictoryOverlay, openFormationPicker,
|
||||||
} from './VegaScreens.js';
|
} from './VegaScreens.js';
|
||||||
import { openColoniesScreen } from './VegaColoniesScreen.js';
|
import { openColoniesScreen } from './VegaColoniesScreen.js';
|
||||||
import { openResearchScreen } from './VegaResearchScreen.js';
|
import { openResearchScreen } from './VegaResearchScreen.js';
|
||||||
|
|
@ -1167,23 +1167,29 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
||||||
const next = (i) => {
|
const next = (i) => {
|
||||||
if (i >= pending.length) { this.refreshAll(); done(); return; }
|
if (i >= pending.length) { this.refreshAll(); done(); return; }
|
||||||
const { starIdx, other } = pending[i];
|
const { starIdx, other } = pending[i];
|
||||||
const prepared = Logic.prepareBattleAt(this.rules, this.state, starIdx, me, other);
|
|
||||||
if (!prepared) { next(i + 1); return; }
|
|
||||||
this.map?.panToStar(starIdx, 260);
|
this.map?.panToStar(starIdx, 260);
|
||||||
this.modalOpen = true;
|
this.modalOpen = true;
|
||||||
this.music?.setCategory('combat');
|
// Forced (closable: false) — there's no sensible "cancel" once fleets
|
||||||
openCombatViewV2(this, this.rules, prepared.battle, this.art, {
|
// are already committed to this fight. Only the human's own side is
|
||||||
attackerSpecies: this.state.empires[prepared.attackerIdx].speciesId,
|
// ever asked; the AI opponent always picks silently (Logic
|
||||||
defenderSpecies: this.state.empires[prepared.defenderIdx].speciesId,
|
// .prepareBattleAt's own comment).
|
||||||
playerSide: prepared.attackerIdx === me ? 'attacker' : 'defender',
|
openFormationPicker(this, (humanFormation) => {
|
||||||
onDone: (result) => {
|
const prepared = Logic.prepareBattleAt(this.rules, this.state, starIdx, me, other, { humanFormation });
|
||||||
Logic.applyBattleOutcome(this.rules, this.state, prepared, result);
|
if (!prepared) { this.modalOpen = false; next(i + 1); return; }
|
||||||
this.modalOpen = false;
|
this.music?.setCategory('combat');
|
||||||
this.music?.setCategory('peace');
|
openCombatViewV2(this, this.rules, prepared.battle, this.art, {
|
||||||
this.refreshAll();
|
attackerSpecies: this.state.empires[prepared.attackerIdx].speciesId,
|
||||||
next(i + 1);
|
defenderSpecies: this.state.empires[prepared.defenderIdx].speciesId,
|
||||||
},
|
playerSide: prepared.attackerIdx === me ? 'attacker' : 'defender',
|
||||||
});
|
onDone: (result) => {
|
||||||
|
Logic.applyBattleOutcome(this.rules, this.state, prepared, result);
|
||||||
|
this.modalOpen = false;
|
||||||
|
this.music?.setCategory('peace');
|
||||||
|
this.refreshAll();
|
||||||
|
next(i + 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, { closable: false });
|
||||||
};
|
};
|
||||||
next(0);
|
next(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import {
|
||||||
import { atWar, grantTech } from './VegaLogic.js';
|
import { atWar, grantTech } from './VegaLogic.js';
|
||||||
import {
|
import {
|
||||||
attitudeOf, moodOf, videoMood, canNegotiate, declareWar, proposeTreaty, respondToOffer, tradeTech, giveGift,
|
attitudeOf, moodOf, videoMood, canNegotiate, declareWar, proposeTreaty, respondToOffer, tradeTech, giveGift,
|
||||||
|
requestPeace,
|
||||||
} from './VegaDiplomacy.js';
|
} from './VegaDiplomacy.js';
|
||||||
import { CHAT, pickLine } from './VegaChat.js';
|
import { CHAT, pickLine } from './VegaChat.js';
|
||||||
|
|
||||||
|
|
@ -535,10 +536,14 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
}
|
}
|
||||||
const openingOffer = state.empires[me].pendingOffers[otherIdx];
|
const openingOffer = state.empires[me].pendingOffers[otherIdx];
|
||||||
if (openingOffer) {
|
if (openingOffer) {
|
||||||
const key = openingOffer.kind === 'peace' ? chat.offerPeaceOpener
|
if (openingOffer.kind === 'peaceRequest') {
|
||||||
: openingOffer.kind === 'alliance' ? chat.offerAllianceOpener
|
pushChat('o', pickLine(chat.offerPeaceRequestOpener, { ...vars, third: state.empires[openingOffer.thirdParty]?.name }));
|
||||||
: chat.offerTradeAgreementOpener;
|
} else {
|
||||||
pushChat('o', pickLine(key, vars));
|
const key = openingOffer.kind === 'peace' ? chat.offerPeaceOpener
|
||||||
|
: openingOffer.kind === 'alliance' ? chat.offerAllianceOpener
|
||||||
|
: chat.offerTradeAgreementOpener;
|
||||||
|
pushChat('o', pickLine(key, vars));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A fresh (this-turn-or-last) fleet complaint gets its own opener line,
|
// A fresh (this-turn-or-last) fleet complaint gets its own opener line,
|
||||||
|
|
@ -568,9 +573,29 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
}
|
}
|
||||||
|
|
||||||
function doPending(accept) {
|
function doPending(accept) {
|
||||||
pushChat('p', pickLine(accept ? chat.acceptOffer : chat.rejectOffer, vars));
|
const offer = state.empires[me].pendingOffers[otherIdx];
|
||||||
|
const isPeaceRequest = offer?.kind === 'peaceRequest';
|
||||||
|
const thirdVars = isPeaceRequest ? { ...vars, third: state.empires[offer.thirdParty]?.name } : vars;
|
||||||
|
pushChat('p', pickLine(
|
||||||
|
isPeaceRequest ? (accept ? chat.acceptPeaceRequest : chat.rejectPeaceRequest)
|
||||||
|
: (accept ? chat.acceptOffer : chat.rejectOffer),
|
||||||
|
thirdVars,
|
||||||
|
));
|
||||||
respondToOffer(rules, state, me, otherIdx, accept);
|
respondToOffer(rules, state, me, otherIdx, accept);
|
||||||
say(pickLine(accept ? chat.afterAccepted : chat.afterRejected, vars));
|
if (isPeaceRequest) {
|
||||||
|
// respondToOffer's own true/false means "was there an offer to
|
||||||
|
// answer," not "did the treaty succeed" (existing callers depend on
|
||||||
|
// that contract) — the actual outcome is read back off the treaty
|
||||||
|
// state instead, only when accepted.
|
||||||
|
const madePeace = accept && !atWar(state, me, offer.thirdParty);
|
||||||
|
say(pickLine(
|
||||||
|
!accept ? chat.afterPeaceRequestRejected
|
||||||
|
: madePeace ? chat.afterPeaceRequestAcceptedSuccess : chat.afterPeaceRequestAcceptedTried,
|
||||||
|
thirdVars,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
say(pickLine(accept ? chat.afterAccepted : chat.afterRejected, thirdVars));
|
||||||
|
}
|
||||||
afterAction();
|
afterAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,6 +653,18 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
afterAction();
|
afterAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Asking `otherIdx` to end a war with a THIRD empire — see
|
||||||
|
// VegaDiplomacy.js's requestPeace for why this is kept separate from
|
||||||
|
// proposeTreaty/wouldAccept (it's a favor about someone else's war, not a
|
||||||
|
// treaty between the two people actually talking).
|
||||||
|
function doRequestPeace(thirdPartyIdx) {
|
||||||
|
const reqVars = { ...vars, third: state.empires[thirdPartyIdx].name };
|
||||||
|
pushChat('p', pickLine(chat.requestPeaceThird, reqVars));
|
||||||
|
const accepted = requestPeace(rules, state, me, otherIdx, thirdPartyIdx);
|
||||||
|
say(pickLine(accepted ? chat.replyRequestPeaceAccept : chat.replyRequestPeaceReject, reqVars));
|
||||||
|
afterAction();
|
||||||
|
}
|
||||||
|
|
||||||
function doTechTrade(giveId, wantId) {
|
function doTechTrade(giveId, wantId) {
|
||||||
pushChat('p', pickLine(chat.tradeTechOffer, { ...vars, tech: rules.techs[wantId].name }));
|
pushChat('p', pickLine(chat.tradeTechOffer, { ...vars, tech: rules.techs[wantId].name }));
|
||||||
const ok = tradeTech(rules, state, me, otherIdx, giveId, wantId);
|
const ok = tradeTech(rules, state, me, otherIdx, giveId, wantId);
|
||||||
|
|
@ -668,7 +705,10 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
say(pickLine(chat.storyLore, vars));
|
say(pickLine(chat.storyLore, vars));
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickFromList(title, ids, onPick) {
|
// `labelFn` defaults to the tech-name lookup every existing caller wants;
|
||||||
|
// doRequestWhichWar (below) is the one caller that isn't picking a tech,
|
||||||
|
// so it passes an empire-name lookup instead.
|
||||||
|
function pickFromList(title, ids, onPick, labelFn = (id) => rules.techs[id].name) {
|
||||||
const box = scene.add.container(0, 0);
|
const box = scene.add.container(0, 0);
|
||||||
root.add(box);
|
root.add(box);
|
||||||
const bx = GAME_WIDTH / 2;
|
const bx = GAME_WIDTH / 2;
|
||||||
|
|
@ -678,7 +718,7 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
box.add(scene.add.rectangle(bx, by, 420, h, PANEL, 0.98).setStrokeStyle(2, ACCENT));
|
box.add(scene.add.rectangle(bx, by, 420, h, PANEL, 0.98).setStrokeStyle(2, ACCENT));
|
||||||
box.add(scene.add.text(bx, by - h / 2 + 26, title, { fontFamily: FONT, fontSize: '19px', color: '#ffd88a' }).setOrigin(0.5));
|
box.add(scene.add.text(bx, by - h / 2 + 26, title, { fontFamily: FONT, fontSize: '19px', color: '#ffd88a' }).setOrigin(0.5));
|
||||||
ids.slice(0, 12).forEach((id, i) => {
|
ids.slice(0, 12).forEach((id, i) => {
|
||||||
const t = scene.add.text(bx, by - h / 2 + 66 + i * 34, rules.techs[id].name, {
|
const t = scene.add.text(bx, by - h / 2 + 66 + i * 34, labelFn(id), {
|
||||||
fontFamily: FONT, fontSize: '16px', color: '#e8f4ff',
|
fontFamily: FONT, fontSize: '16px', color: '#e8f4ff',
|
||||||
}).setOrigin(0.5).setInteractive({ useHandCursor: true });
|
}).setOrigin(0.5).setInteractive({ useHandCursor: true });
|
||||||
t.on('pointerover', () => t.setColor('#ffd88a'));
|
t.on('pointerover', () => t.setColor('#ffd88a'));
|
||||||
|
|
@ -691,6 +731,12 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
box.add(cancel);
|
box.add(cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens the "which war?" picker for Demand Peace, listing every empire
|
||||||
|
* `otherIdx` is at war with (that a request could ever succeed against). */
|
||||||
|
function openWarPicker(warTargets) {
|
||||||
|
pickFromList('END WHICH WAR?', warTargets, doRequestPeace, (idx) => state.empires[idx].name);
|
||||||
|
}
|
||||||
|
|
||||||
function openTechPicker(giveOptions, wantOptions) {
|
function openTechPicker(giveOptions, wantOptions) {
|
||||||
pickFromList('OFFER WHICH TECHNOLOGY?', giveOptions, (giveId) => {
|
pickFromList('OFFER WHICH TECHNOLOGY?', giveOptions, (giveId) => {
|
||||||
pickFromList('ASK FOR WHICH TECHNOLOGY?', wantOptions, (wantId) => doTechTrade(giveId, wantId));
|
pickFromList('ASK FOR WHICH TECHNOLOGY?', wantOptions, (wantId) => doTechTrade(giveId, wantId));
|
||||||
|
|
@ -768,6 +814,14 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
||||||
const want = Object.keys(state.empires[otherIdx].known).filter((t) => !state.empires[me].known[t]);
|
const want = Object.keys(state.empires[otherIdx].known).filter((t) => !state.empires[me].known[t]);
|
||||||
if (give.length && want.length) items.push(['Trade Tech', () => openTechPicker(give, want)]);
|
if (give.length && want.length) items.push(['Trade Tech', () => openTechPicker(give, want)]);
|
||||||
if (giftAffordable) items.push(['Send Gift', openGiftPicker]);
|
if (giftAffordable) items.push(['Send Gift', openGiftPicker]);
|
||||||
|
// Wars otherIdx is fighting against a THIRD empire — one the request
|
||||||
|
// could ever actually reach (canNegotiate both ways), never our own
|
||||||
|
// war with them (that's Sue for Peace, above).
|
||||||
|
const theirWars = state.empires
|
||||||
|
.filter((o) => o.alive && o.idx !== me && o.idx !== otherIdx
|
||||||
|
&& atWar(state, otherIdx, o.idx) && canNegotiate(rules, state, otherIdx, o.idx))
|
||||||
|
.map((o) => o.idx);
|
||||||
|
if (theirWars.length) items.push(['Demand Peace', () => openWarPicker(theirWars)]);
|
||||||
}
|
}
|
||||||
const treatyRows = layoutRow(items, BTN_Y);
|
const treatyRows = layoutRow(items, BTN_Y);
|
||||||
let nextY = BTN_Y + treatyRows * (BTN_H + BTN_GAP);
|
let nextY = BTN_Y + treatyRows * (BTN_H + BTN_GAP);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
// {you} — the human empire's display name
|
// {you} — the human empire's display name
|
||||||
// {me} — this species' empire name
|
// {me} — this species' empire name
|
||||||
// {tech} — a technology name, for the tech-trade situations
|
// {tech} — a technology name, for the tech-trade situations
|
||||||
|
// {third} — a third empire's display name, for the "make peace with them"
|
||||||
|
// request situations
|
||||||
// Resolved (already-substituted) text is what VegaAudience.js pushes into
|
// Resolved (already-substituted) text is what VegaAudience.js pushes into
|
||||||
// state.chatLog, not the templates.
|
// state.chatLog, not the templates.
|
||||||
//
|
//
|
||||||
|
|
@ -36,6 +38,23 @@
|
||||||
// offer from this species.
|
// offer from this species.
|
||||||
// afterAccepted / afterRejected — this species' reaction to the player's
|
// afterAccepted / afterRejected — this species' reaction to the player's
|
||||||
// answer to their own offer.
|
// answer to their own offer.
|
||||||
|
// requestPeaceThird / replyRequestPeaceAccept / replyRequestPeaceReject —
|
||||||
|
// the PLAYER asking THIS species to make peace with a third empire
|
||||||
|
// (VegaDiplomacy.js's requestPeace), and their reply. Uses a third
|
||||||
|
// token, {third}, alongside the usual {me}/{you}.
|
||||||
|
// offerPeaceRequestOpener — this species' own opening line when THEY ask
|
||||||
|
// the PLAYER to make peace with a third empire (offerPeaceRequest,
|
||||||
|
// held exactly like offerPeaceOpener/offerAllianceOpener above).
|
||||||
|
// acceptPeaceRequest / rejectPeaceRequest — the PLAYER's line answering
|
||||||
|
// that request (same Accept/Reject buttons a treaty offer uses —
|
||||||
|
// respondToOffer's 'peaceRequest' branch is what makes accepting mean
|
||||||
|
// "I'll propose peace to the third party," not a treaty with the asker).
|
||||||
|
// afterPeaceRequestAcceptedSuccess / afterPeaceRequestAcceptedTried —
|
||||||
|
// this species' reaction once the player agreed to try: Success if the
|
||||||
|
// third party actually accepted the resulting peace proposal, Tried if
|
||||||
|
// the third party turned it down anyway (not the player's fault).
|
||||||
|
// afterPeaceRequestRejected — this species' reaction to the player
|
||||||
|
// declining to even try.
|
||||||
//
|
//
|
||||||
// Additionally, three pure-flavor "conversation" topics, present for every
|
// Additionally, three pure-flavor "conversation" topics, present for every
|
||||||
// diplomacy-capable species, with ZERO effect on attitude — gating lives in
|
// diplomacy-capable species, with ZERO effect on attitude — gating lives in
|
||||||
|
|
@ -189,6 +208,42 @@ export const CHAT = {
|
||||||
'I\'ve got a standing proposal for you, {you} — a trade agreement, ongoing, good for us both.',
|
'I\'ve got a standing proposal for you, {you} — a trade agreement, ongoing, good for us both.',
|
||||||
'Let\'s formalize something, {you}: regular trade, on the record.',
|
'Let\'s formalize something, {you}: regular trade, on the record.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Do us both a favor, {me} — make peace with {third}. A war on your border is bad for trade on mine.',
|
||||||
|
'I\'d rather see you and {third} at peace, {me}. Fighting\'s bad for everyone\'s bottom line.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'You\'ve got a point, {you}. I\'ll open talks with {third}.',
|
||||||
|
'Fine, {you} — for you, I\'ll make the offer to {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'Not this time, {you}. My war with {third} isn\'t yours to settle.',
|
||||||
|
'I appreciate the thought, {you}, but {third} hasn\'t earned peace yet.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'A favor, {you}: make peace with {third}. It\'s bad business watching a friend bleed money on a war.',
|
||||||
|
'I\'m asking you to end things with {third}, {you}. Consider it a personal request.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'Fair enough, {me}. I\'ll make the offer to {third}.',
|
||||||
|
'For you, {me} — I\'ll try to settle things with {third}.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'Not this time, {me}. That war is mine to end, on my terms.',
|
||||||
|
'I\'ll pass, {me}. {third} hasn\'t earned it from me yet.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Good business, {you}. Peace with {third} suits everyone\'s ledger.',
|
||||||
|
'Well done, {you} — {third} and I can both stop paying for this war now.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'Appreciated, {you}, even if {third} wasn\'t ready to listen.',
|
||||||
|
'You tried, {you} — that\'s worth something, even though {third} said no.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'Disappointing, {you}. I thought we understood each other better.',
|
||||||
|
'Noted, {you}. I won\'t forget you turned down a simple favor.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your fleet\'s been sitting over my colony a little too long, {you}. Care to explain, or just move it?',
|
'Your fleet\'s been sitting over my colony a little too long, {you}. Care to explain, or just move it?',
|
||||||
'I\'ll be direct, {you}: that fleet parked over my world is bad for business. Move it.',
|
'I\'ll be direct, {you}: that fleet parked over my world is bad for business. Move it.',
|
||||||
|
|
@ -357,6 +412,42 @@ export const CHAT = {
|
||||||
'The aerie proposes something lasting, {you} — a standing trade agreement, not merely a single exchange.',
|
'The aerie proposes something lasting, {you} — a standing trade agreement, not merely a single exchange.',
|
||||||
'Let us formalize our commerce, {you}. I offer an ongoing arrangement.',
|
'Let us formalize our commerce, {you}. I offer an ongoing arrangement.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Fold your wings toward {third}, {me}. This war does not become you.',
|
||||||
|
'I ask a favor of the aerie, {me}: make peace with {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'For you, {you}, we will offer {third} our terms.',
|
||||||
|
'A reasonable request. The aerie will approach {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'No, {you}. {third} has not yet earned our mercy.',
|
||||||
|
'That quarrel is ours to end, {you}, not yours to arrange.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'A request, {you}: make your peace with {third}. It troubles the aerie to watch a friend circle so low.',
|
||||||
|
'I ask this of you, {you} — end your war with {third}. Do it for us, if not for yourself.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'Since you ask it, {me}, I will approach {third}.',
|
||||||
|
'For the aerie\'s sake, {me}, I will try.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'No, {me}. That sky is mine to clear.',
|
||||||
|
'I decline, {me}. {third} has not yet fallen far enough.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Well flown, {you}. The aerie rests easier with {third} at peace.',
|
||||||
|
'Gracefully done, {you}. Two wars end today.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'You tried, {you}, even if {third}\'s wings proved too proud to land.',
|
||||||
|
'The effort is noted, {you}, though {third} was not yet ready to listen.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'A groundling\'s answer, {you}. The aerie remembers who refuses a simple favor.',
|
||||||
|
'Then the sky between us grows a little colder, {you}.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your ships circle our nest longer than courtesy allows, {you}. Explain yourself, or withdraw them.',
|
'Your ships circle our nest longer than courtesy allows, {you}. Explain yourself, or withdraw them.',
|
||||||
'A fleet that lingers over our aerie is no longer a guest, {you}. Move it.',
|
'A fleet that lingers over our aerie is no longer a guest, {you}. Move it.',
|
||||||
|
|
@ -524,6 +615,42 @@ export const CHAT = {
|
||||||
'We offer something that holds, {you} — not a single exchange, but a standing arrangement.',
|
'We offer something that holds, {you} — not a single exchange, but a standing arrangement.',
|
||||||
'Let our trade stand steady, {you}. We propose an agreement.',
|
'Let our trade stand steady, {you}. We propose an agreement.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Stop fighting {third}, {me}. Enough blood spilled there.',
|
||||||
|
'Make peace with {third}, {me}. I ask it plainly.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'Fine, {you}. I will offer {third} an end to it.',
|
||||||
|
'For you, {you} — I\'ll put down the axe against {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'No, {you}. That fight is not finished.',
|
||||||
|
'{third} has not bled enough yet, {you}. Not your call to make.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'Make peace with {third}, {you}. I ask it as one who stands beside you.',
|
||||||
|
'End it with {third}, {you}. A war you carry hurts us both.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'For you, {me}, I\'ll offer {third} peace.',
|
||||||
|
'Fine, {me}. I\'ll try with {third}.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'No, {me}. {third} has not earned it.',
|
||||||
|
'That fight stays mine, {me}.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Good. {third} and you, at peace — stronger for it, {you}.',
|
||||||
|
'Well done, {you}. One less war weighs on us both.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'You tried, {you}. {third} refused — not your failing.',
|
||||||
|
'Effort noted, {you}, even though {third} would not yield.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'Weak answer, {you}. I will remember it.',
|
||||||
|
'Then we remember, {you}, who refuses a simple ask.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your ships sit over our ground too long, {you}. Explain the weight of that, or lift it.',
|
'Your ships sit over our ground too long, {you}. Explain the weight of that, or lift it.',
|
||||||
'A fleet that does not move becomes an occupation, {you}. Move it.',
|
'A fleet that does not move becomes an occupation, {you}. Move it.',
|
||||||
|
|
@ -692,6 +819,42 @@ export const CHAT = {
|
||||||
'We propose something lasting, {you} — a trade agreement. Sincerely, for whatever that\'s worth from us.',
|
'We propose something lasting, {you} — a trade agreement. Sincerely, for whatever that\'s worth from us.',
|
||||||
'A standing arrangement, {you}. We\'d like to make this formal, believe it or not.',
|
'A standing arrangement, {you}. We\'d like to make this formal, believe it or not.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Wear a kinder face for {third}, {me}. Make peace.',
|
||||||
|
'I ask you to end it with {third}, {me} — even we tire of watching, sometimes.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'How generous of me, {you}. I\'ll offer {third} peace — this once.',
|
||||||
|
'Very well, {you}. I\'ll play the peacemaker with {third}. It\'s a good look on me.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'No, {you}. That mask doesn\'t suit me yet.',
|
||||||
|
'I\'ll keep the war with {third}, {you}. It amuses me still.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'A strange request from me, {you}, I know — make peace with {third}. Even we grow tired of some faces.',
|
||||||
|
'I ask a favor, {you}: end your war with {third}. Trust me, if you can manage it.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'If it pleases you, {me}, I\'ll offer {third} peace.',
|
||||||
|
'Fine, {me}. I\'ll wear the face of a peacemaker with {third}.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'No, {me}. I like the war with {third} exactly as it is.',
|
||||||
|
'I\'ll decline, {me}. Not every mask fits.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'How refreshing, {you} — peace with {third}, and you actually meant it.',
|
||||||
|
'Well played, {you}. {third} never saw it coming, and neither did I.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'You tried, {you}. {third} wasn\'t buying it — I almost respect that.',
|
||||||
|
'A convincing effort, {you}, even if {third} saw through it.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'How predictable, {you}. I\'ll remember the face you showed me.',
|
||||||
|
'Refused, {you}? I\'ll wear that against you later.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your fleet has worn out its welcome over our world, {you}. Move it, or explain why it stays.',
|
'Your fleet has worn out its welcome over our world, {you}. Move it, or explain why it stays.',
|
||||||
'We\'ve watched your ships linger long enough, {you}. That\'s not a face we appreciate.',
|
'We\'ve watched your ships linger long enough, {you}. That\'s not a face we appreciate.',
|
||||||
|
|
@ -860,6 +1023,42 @@ export const CHAT = {
|
||||||
'Continuous exchange protocol calculated as favorable, {you}. Proposed.',
|
'Continuous exchange protocol calculated as favorable, {you}. Proposed.',
|
||||||
'Standing trade function available, {you}. Proposing.',
|
'Standing trade function available, {you}. Proposing.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Cease hostilities with {third}. We request it.',
|
||||||
|
'Terminate conflict-state with {third}, {me}. Request logged.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'Request accepted. Peace-protocol will be offered to {third}.',
|
||||||
|
'Acknowledged. We will approach {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'Request denied. Conflict-state with {third} continues.',
|
||||||
|
'Negative. {third} has not met termination-conditions.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'Request: terminate your conflict-state with {third}. Reason: mutual inefficiency.',
|
||||||
|
'We require this of you: cease hostilities with {third}.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'Acknowledged. Peace-protocol will be offered to {third}.',
|
||||||
|
'Request accepted. We will attempt termination with {third}.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'Request denied. Conflict-state with {third} continues.',
|
||||||
|
'Negative. Termination not authorized.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Termination confirmed. Efficiency restored.',
|
||||||
|
'Conflict-state with {third} resolved. Function approves.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'Attempt logged. {third} rejected termination. Compliance noted regardless.',
|
||||||
|
'Request processed. Outcome with {third} negative. Effort acknowledged.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'Request denial logged. Cooperation-rating reduced.',
|
||||||
|
'Non-compliance recorded.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Foreign vessel detected over colony function beyond acceptable duration, {you}. Withdraw it.',
|
'Foreign vessel detected over colony function beyond acceptable duration, {you}. Withdraw it.',
|
||||||
'Occupation pattern logged at colony coordinates, {you}. Explain function or withdraw.',
|
'Occupation pattern logged at colony coordinates, {you}. Explain function or withdraw.',
|
||||||
|
|
@ -1027,6 +1226,42 @@ export const CHAT = {
|
||||||
'A recurring exchange has been calculated as favorable, {you}. Proposing a standing agreement.',
|
'A recurring exchange has been calculated as favorable, {you}. Proposing a standing agreement.',
|
||||||
'Continuous trade improves both systems, {you}. Proposal offered.',
|
'Continuous trade improves both systems, {you}. Proposal offered.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Decommission your conflict with {third}, {me}. It is an inefficient allocation of resources.',
|
||||||
|
'End the war with {third}, {me}. I recommend it as an upgrade to your situation.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'Optimization accepted, {you}. I will offer {third} peace.',
|
||||||
|
'Logical. I will initiate peace with {third}, {you}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'Rejected, {you}. The war with {third} is not yet obsolete.',
|
||||||
|
'No. {third} has not yet reached the threshold for peace.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'A recommendation, {you}: decommission your war with {third}. It is wasted output.',
|
||||||
|
'I propose an upgrade to your foreign policy, {you} — peace with {third}.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'Recommendation accepted, {me}. I will offer {third} peace.',
|
||||||
|
'Logical. I will attempt peace with {third}.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'Rejected, {me}. That conflict remains operational.',
|
||||||
|
'No. The war with {third} has not reached obsolescence.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Optimization complete, {you}. {third} at peace — efficiency restored.',
|
||||||
|
'Confirmed. Your foreign policy is now upgraded, {you}.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'Attempt logged, {you}. {third} rejected the upgrade. Compliance noted.',
|
||||||
|
'Process completed, output negative — {third} declined. Effort registered.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'Rejection logged, {you}. Cooperation index lowered.',
|
||||||
|
'Non-compliance noted, {you}. Recalculating trust.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your fleet occupies orbital space above my colony beyond acceptable parameters, {you}. Relocate it.',
|
'Your fleet occupies orbital space above my colony beyond acceptable parameters, {you}. Relocate it.',
|
||||||
'Unauthorized presence logged at my colony, {you}. Justify it or withdraw.',
|
'Unauthorized presence logged at my colony, {you}. Justify it or withdraw.',
|
||||||
|
|
@ -1194,6 +1429,42 @@ export const CHAT = {
|
||||||
'I offer something that holds, {you} — not one trade, but a standing arrangement.',
|
'I offer something that holds, {you} — not one trade, but a standing arrangement.',
|
||||||
'Let this go beyond a single exchange, {you}. A trade agreement, proposed.',
|
'Let this go beyond a single exchange, {you}. A trade agreement, proposed.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Sheathe your claws against {third}, {me}. I ask it.',
|
||||||
|
'End the hunt with {third}, {me}. Enough blood.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'Fine, {you}. I\'ll offer {third} its life.',
|
||||||
|
'For you, {you} — I\'ll call off the hunt against {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'No, {you}. {third} has not yet earned mercy.',
|
||||||
|
'That hunt is mine, {you}. Not yours to end.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'Sheathe your claws against {third}, {you}. I ask this of you.',
|
||||||
|
'End your hunt with {third}, {you}. Do it, and I\'ll remember it.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'Fine, {me}. I\'ll offer {third} its life.',
|
||||||
|
'For you, {me} — I\'ll call off my hunt.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'No, {me}. {third} has not earned mercy.',
|
||||||
|
'That hunt is mine to finish.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Good. {third} lives, and you\'ve proven you keep your word, {you}.',
|
||||||
|
'Well done, {you}. A clean kill would have been simpler — this was harder.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'You bared your throat and asked, {you}. {third} refused. Not your failure.',
|
||||||
|
'You tried, {you}. {third} wasn\'t ready to yield. I respect the attempt.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'A coward\'s answer, {you}. I\'ll remember it.',
|
||||||
|
'Refused? Then remember I asked, {you}.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your ships have circled my territory too long, {you}. Move them, or give me a reason not to treat them as prey.',
|
'Your ships have circled my territory too long, {you}. Move them, or give me a reason not to treat them as prey.',
|
||||||
'A fleet that lingers this long isn\'t visiting, {you}. It\'s hunting. Explain, or withdraw.',
|
'A fleet that lingers this long isn\'t visiting, {you}. It\'s hunting. Explain, or withdraw.',
|
||||||
|
|
@ -1361,6 +1632,42 @@ export const CHAT = {
|
||||||
'I have modeled the benefit of a continuous arrangement, {you}. It exceeds a single exchange. Proposed.',
|
'I have modeled the benefit of a continuous arrangement, {you}. It exceeds a single exchange. Proposed.',
|
||||||
'A logical next step, {you}: a standing trade agreement, rather than isolated trades.',
|
'A logical next step, {you}: a standing trade agreement, rather than isolated trades.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'The optimal solution, {me}, is peace with {third}. I recommend you see it.',
|
||||||
|
'Consider the equation, {me}: your war with {third} solves nothing. End it.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'A logical request, {you}. I will offer {third} the same solution.',
|
||||||
|
'Correctly reasoned, {you}. I will approach {third}.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'An incomplete argument, {you}. The variables with {third} have not yet resolved.',
|
||||||
|
'No, {you}. The equation is not yet solvable.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'A simple proof, {you}: your war with {third} has no winning solution. End it.',
|
||||||
|
'I offer you the answer, {you} — make peace with {third}. The logic is plain.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'The proof holds, {me}. I will offer {third} peace.',
|
||||||
|
'Correctly reasoned, {me}. I will attempt it.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'An incomplete proof, {me}. I decline.',
|
||||||
|
'No, {me}. The variables have not resolved.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Solved elegantly, {you}. {third} at peace — the correct answer, reached.',
|
||||||
|
'As predicted, {you}. The logical outcome, achieved.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'The proof was sound, {you}, even if {third} could not follow it. Noted regardless.',
|
||||||
|
'You reasoned correctly, {you}. {third} simply lacked the capacity to agree.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'A flawed conclusion, {you}. I will remember the error.',
|
||||||
|
'Disappointing, {you}. I expected better logic from you.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your fleet\'s continued presence over my colony no longer registers as accidental, {you}. Explain, or withdraw it.',
|
'Your fleet\'s continued presence over my colony no longer registers as accidental, {you}. Explain, or withdraw it.',
|
||||||
'I have recalculated your intentions given the duration of that fleet\'s presence, {you}, and the results are not favorable. Move it.',
|
'I have recalculated your intentions given the duration of that fleet\'s presence, {you}, and the results are not favorable. Move it.',
|
||||||
|
|
@ -1528,6 +1835,42 @@ export const CHAT = {
|
||||||
'The brood proposes something lasting, {you} — a standing trade agreement, not a single exchange.',
|
'The brood proposes something lasting, {you} — a standing trade agreement, not a single exchange.',
|
||||||
'Let our clutches trade as neighbors do, {you}. We offer an ongoing arrangement.',
|
'Let our clutches trade as neighbors do, {you}. We offer an ongoing arrangement.',
|
||||||
],
|
],
|
||||||
|
requestPeaceThird: [
|
||||||
|
'Withdraw your claws from {third}\'s brood, {me}. Make peace.',
|
||||||
|
'I ask you to end your war with {third}, {me}, for the good of both clutches.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceAccept: [
|
||||||
|
'For you, {you}, I will offer {third}\'s brood peace.',
|
||||||
|
'Very well, {you}. I will spare {third}\'s clutch, this once.',
|
||||||
|
],
|
||||||
|
replyRequestPeaceReject: [
|
||||||
|
'No, {you}. {third}\'s brood has not yet paid enough.',
|
||||||
|
'That territory is not yet settled, {you}. Not yours to decide.',
|
||||||
|
],
|
||||||
|
offerPeaceRequestOpener: [
|
||||||
|
'A request from one clutch to another, {you}: make peace with {third}.',
|
||||||
|
'I ask you to spare {third}\'s brood, {you}, and end this war.',
|
||||||
|
],
|
||||||
|
acceptPeaceRequest: [
|
||||||
|
'For the clutch\'s sake, {me}, I will offer {third} peace.',
|
||||||
|
'Very well, {me}. I will spare {third}, this once.',
|
||||||
|
],
|
||||||
|
rejectPeaceRequest: [
|
||||||
|
'No, {me}. {third}\'s brood has not paid enough yet.',
|
||||||
|
'That territory remains contested, {me}.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedSuccess: [
|
||||||
|
'Good. {third}\'s brood lives, and our clutches both grow stronger for it, {you}.',
|
||||||
|
'Wisely done, {you}. Peace with {third} serves every brood better than war did.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestAcceptedTried: [
|
||||||
|
'You asked on our behalf, {you}. {third} refused — the fault is theirs, not yours.',
|
||||||
|
'The attempt is noted, {you}, though {third}\'s brood was not yet ready to yield.',
|
||||||
|
],
|
||||||
|
afterPeaceRequestRejected: [
|
||||||
|
'A poor answer, {you}. The clutch remembers who refuses a simple request.',
|
||||||
|
'Then remember, {you}, that we asked, and you said no.',
|
||||||
|
],
|
||||||
fleetComplaintOpener: [
|
fleetComplaintOpener: [
|
||||||
'Your fleet crowds our territory too long, {you}. The brood does not tolerate lingering guests. Withdraw it.',
|
'Your fleet crowds our territory too long, {you}. The brood does not tolerate lingering guests. Withdraw it.',
|
||||||
'That fleet sits over our clutch longer than courtesy allows, {you}. Explain yourself, or move it.',
|
'That fleet sits over our clutch longer than courtesy allows, {you}. Explain yourself, or move it.',
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,9 @@ import { createBattle, runBattle } from './VegaCombat.js';
|
||||||
import { openCombatView } from './VegaCombatView.js';
|
import { openCombatView } from './VegaCombatView.js';
|
||||||
import * as CombatV2 from './VegaCombatV2.js';
|
import * as CombatV2 from './VegaCombatV2.js';
|
||||||
import { openCombatViewV2 } from './VegaCombatViewV2.js';
|
import { openCombatViewV2 } from './VegaCombatViewV2.js';
|
||||||
import { FORMATION_STRATEGIES, formationName } from './VegaFormations.js';
|
import { formationName } from './VegaFormations.js';
|
||||||
import { VegaMusic } from './VegaMusic.js';
|
import { VegaMusic } from './VegaMusic.js';
|
||||||
import { FONT, uiClick, modalShell } from './VegaScreens.js';
|
import { FONT, uiClick, openFormationPicker } from './VegaScreens.js';
|
||||||
|
|
||||||
const PANEL_BG = 0x0b1220;
|
const PANEL_BG = 0x0b1220;
|
||||||
const PANEL_LINE = 0x2c435f;
|
const PANEL_LINE = 0x2c435f;
|
||||||
|
|
@ -238,7 +238,7 @@ export default class VegaCombatSim extends Phaser.Scene {
|
||||||
// strategy. The defender ("AI") gets no prompt at all — it picks
|
// strategy. The defender ("AI") gets no prompt at all — it picks
|
||||||
// silently, which CombatV2.createBattle already does on its own for
|
// silently, which CombatV2.createBattle already does on its own for
|
||||||
// any side that doesn't supply a valid choice.
|
// any side that doesn't supply a valid choice.
|
||||||
this.openFormationPicker((formationStrategy) => this.launchV2Battle(formationStrategy));
|
openFormationPicker(this, (formationStrategy) => this.launchV2Battle(formationStrategy));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { attacker, defender, colony } = this.buildBattleOpts();
|
const { attacker, defender, colony } = this.buildBattleOpts();
|
||||||
|
|
@ -269,38 +269,6 @@ export default class VegaCombatSim extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// A simple two-card picker: click a strategy to commit it immediately and
|
|
||||||
// start the battle (no separate confirm step — this is a "real quick" dev
|
|
||||||
// tool, not a game screen). Shown fresh every Watch Battle press in V2
|
|
||||||
// mode; nothing is remembered between battles.
|
|
||||||
openFormationPicker(onPick) {
|
|
||||||
const shell = modalShell(this, 'Choose Formation Strategy', null, { width: 980, height: 420 });
|
|
||||||
const { body } = shell;
|
|
||||||
shell.add(this.add.text(body.x + body.w / 2, body.y, 'Your fleet’s tactical doctrine for this battle. The enemy fleet picks its own, silently.', {
|
|
||||||
fontFamily: FONT, fontSize: '16px', color: '#6f8aa8', align: 'center',
|
|
||||||
}).setOrigin(0.5, 0));
|
|
||||||
|
|
||||||
const cardW = (body.w - 40) / 2;
|
|
||||||
const cardY = body.y + body.h / 2 + 20;
|
|
||||||
FORMATION_STRATEGIES.forEach((f, i) => {
|
|
||||||
const cx = body.x + cardW / 2 + i * (cardW + 40);
|
|
||||||
const card = this.add.rectangle(cx, cardY, cardW, 160, PANEL_BG, 0.7)
|
|
||||||
.setStrokeStyle(2, PANEL_LINE, 0.9);
|
|
||||||
shell.add(card);
|
|
||||||
shell.add(this.add.text(cx, cardY - 48, f.name, {
|
|
||||||
fontFamily: FONT, fontSize: '22px', color: '#cfe8ff',
|
|
||||||
}).setOrigin(0.5));
|
|
||||||
shell.add(this.add.text(cx, cardY - 4, f.desc, {
|
|
||||||
fontFamily: FONT, fontSize: '15px', color: '#9fb6cc', align: 'center', wordWrap: { width: cardW - 40 },
|
|
||||||
}).setOrigin(0.5, 0));
|
|
||||||
const btn = new Button(this, cx, cardY + 55, 'Choose', uiClick(this, () => {
|
|
||||||
shell.destroy();
|
|
||||||
onPick(f.id);
|
|
||||||
}), { width: 160, height: 44, fontSize: 16 });
|
|
||||||
shell.add(btn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
runTrials(n) {
|
runTrials(n) {
|
||||||
if (!this.battleReady()) return;
|
if (!this.battleReady()) return;
|
||||||
const { attacker, defender, colony } = this.buildBattleOpts();
|
const { attacker, defender, colony } = this.buildBattleOpts();
|
||||||
|
|
|
||||||
|
|
@ -448,10 +448,9 @@ function layoutFormation(rules, ships) {
|
||||||
// frame closely, and a big one spans proportionally more of the (now much
|
// frame closely, and a big one spans proportionally more of the (now much
|
||||||
// bigger) world.
|
// bigger) world.
|
||||||
// Returns the layout too (not just a side effect on `ships`) — createBattle
|
// Returns the layout too (not just a side effect on `ships`) — createBattle
|
||||||
// uses `defenderEndX` to anchor a defended planet just past the defender's
|
// reads `centerY` to keep a defended planet vertically level with the
|
||||||
// own fleet, rather than at a fixed world-edge position that would now sit
|
// fleets (the planet's own X is fixed independently of this layout — see
|
||||||
// far off in empty space, disconnected from a fleet placed near the world's
|
// createBattle's own comment on why).
|
||||||
// center.
|
|
||||||
//
|
//
|
||||||
// The proportional gap is clamped to never push the total span past the
|
// The proportional gap is clamped to never push the total span past the
|
||||||
// world's own width. Without this, a large mixed-hull fleet (many ships,
|
// world's own width. Without this, a large mixed-hull fleet (many ships,
|
||||||
|
|
@ -576,10 +575,15 @@ export function createBattle(rules, opts) {
|
||||||
const layout = placeFleets(rules, aShips, dShips);
|
const layout = placeFleets(rules, aShips, dShips);
|
||||||
|
|
||||||
// A defended colony fights as an extra immobile entity that cannot be
|
// A defended colony fights as an extra immobile entity that cannot be
|
||||||
// boarded — killing it is what clears the way for an invasion. Anchored
|
// boarded — killing it is what clears the way for an invasion. Fixed at
|
||||||
// just past the defender fleet's own footprint (not a fixed world-edge
|
// the midpoint between the world's centre and the defender-side edge
|
||||||
// position) so it stays visually coherent with where the defender's
|
// (Brian: "about equal distance from the edge of the screen and the
|
||||||
// ships actually ended up, and inside the camera's initial framing.
|
// center on its side") rather than anchored off the defender fleet's own
|
||||||
|
// footprint — a formation-relative anchor put the planet at very
|
||||||
|
// different depths depending on fleet size/spread, which read as the
|
||||||
|
// planet drifting around from battle to battle instead of holding a
|
||||||
|
// stable "home world" position. Defender is always placed on the +X side
|
||||||
|
// (placeFleets), so worldWidth is that side's edge.
|
||||||
let planet = null;
|
let planet = null;
|
||||||
if (colony && colony.defenseHp > 0) {
|
if (colony && colony.defenseHp > 0) {
|
||||||
planet = {
|
planet = {
|
||||||
|
|
@ -588,10 +592,11 @@ export function createBattle(rules, opts) {
|
||||||
side: 'defender',
|
side: 'defender',
|
||||||
isPlanet: true,
|
isPlanet: true,
|
||||||
name: 'Planetary Defences',
|
name: 'Planetary Defences',
|
||||||
|
typeId: colony.typeId,
|
||||||
hp: colony.defenseHp,
|
hp: colony.defenseHp,
|
||||||
hpMax: colony.defenseHp,
|
hpMax: colony.defenseHp,
|
||||||
shield: colony.shieldBonus ?? 0,
|
shield: colony.shieldBonus ?? 0,
|
||||||
x: layout.defenderEndX + 220,
|
x: C2.worldWidth * 0.75,
|
||||||
y: layout.centerY,
|
y: layout.centerY,
|
||||||
facing: Math.PI,
|
facing: Math.PI,
|
||||||
angularVelocity: 0,
|
angularVelocity: 0,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import VegaFx from './VegaFx.js';
|
||||||
import {
|
import {
|
||||||
advance, runBattle, battleResult, shipBounds, SIM_DT,
|
advance, runBattle, battleResult, shipBounds, SIM_DT,
|
||||||
} from './VegaCombatV2.js';
|
} from './VegaCombatV2.js';
|
||||||
import { shipFrame } from './VegaArt.js';
|
import { shipFrame, planetFrame } from './VegaArt.js';
|
||||||
import { buildParallax, bindZoomPan } from './VegaCombatCamera.js';
|
import { buildParallax, bindZoomPan } from './VegaCombatCamera.js';
|
||||||
import { formationName } from './VegaFormations.js';
|
import { formationName } from './VegaFormations.js';
|
||||||
import { makeCommanderPortrait } from './VegaShipMedia.js';
|
import { makeCommanderPortrait } from './VegaShipMedia.js';
|
||||||
|
|
@ -36,6 +36,11 @@ import { makeCommanderPortrait } from './VegaShipMedia.js';
|
||||||
// multiplied by the firing ship's hull.sizeScale instead.
|
// multiplied by the firing ship's hull.sizeScale instead.
|
||||||
const SHIP_BASE_SIZE = 58;
|
const SHIP_BASE_SIZE = 58;
|
||||||
|
|
||||||
|
// A defended planet reads as bigger than any single ship (even a maxed-out
|
||||||
|
// battleship at SHIP_BASE_SIZE * 2.5 ≈ 145) without dominating the frame.
|
||||||
|
const PLANET_MARKER_SIZE = 110;
|
||||||
|
const SHIELD_RING_COLOR = 0x4fd8ff;
|
||||||
|
|
||||||
// Fire events now arrive as a steady trickle (each ship's own ~2s cooldown,
|
// Fire events now arrive as a steady trickle (each ship's own ~2s cooldown,
|
||||||
// staggered by whenever it entered range) rather than one big per-round
|
// staggered by whenever it entered range) rather than one big per-round
|
||||||
// batch, so every event gets both its visual FX AND its sound cue — no
|
// batch, so every event gets both its visual FX AND its sound cue — no
|
||||||
|
|
@ -343,13 +348,25 @@ export function openCombatViewV2(scene, rules, battle, art, opts = {}) {
|
||||||
markers.clear();
|
markers.clear();
|
||||||
for (const s of battle.ships) {
|
for (const s of battle.ships) {
|
||||||
const c = scene.add.container(s.x, s.y);
|
const c = scene.add.container(s.x, s.y);
|
||||||
|
let shieldRing = null;
|
||||||
if (s.isPlanet) {
|
if (s.isPlanet) {
|
||||||
const g = scene.add.graphics();
|
const img = scene.add.image(0, 0, art.planets, planetFrame(rules, s.typeId))
|
||||||
g.fillStyle(0x8a6a3a, 1);
|
.setDisplaySize(PLANET_MARKER_SIZE, PLANET_MARKER_SIZE);
|
||||||
g.fillCircle(0, 0, 26);
|
c.add(img);
|
||||||
g.lineStyle(2, 0xffd88a, 0.8);
|
// A planetary shield's magnitude (from the Planetary Shield building,
|
||||||
g.strokeCircle(0, 0, 32);
|
// rules.buildings.planetaryshield's shieldBonus effect) is fixed for
|
||||||
c.add(g);
|
// the whole battle — only WHETHER it's still up changes, tracked by
|
||||||
|
// toggling this ring's visibility in syncMarkers as the planet's own
|
||||||
|
// hp falls, rather than redrawing it every frame.
|
||||||
|
if (s.shield > 0) {
|
||||||
|
shieldRing = scene.add.graphics();
|
||||||
|
const r = PLANET_MARKER_SIZE / 2 + 14;
|
||||||
|
shieldRing.fillStyle(SHIELD_RING_COLOR, 0.18);
|
||||||
|
shieldRing.fillCircle(0, 0, r);
|
||||||
|
shieldRing.lineStyle(2.5, SHIELD_RING_COLOR, 0.75);
|
||||||
|
shieldRing.strokeCircle(0, 0, r);
|
||||||
|
c.add(shieldRing);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const species = s.side === 'attacker' ? attackerSpecies : defenderSpecies;
|
const species = s.side === 'attacker' ? attackerSpecies : defenderSpecies;
|
||||||
const size = SHIP_BASE_SIZE * (s.design.hull.sizeScale ?? 1);
|
const size = SHIP_BASE_SIZE * (s.design.hull.sizeScale ?? 1);
|
||||||
|
|
@ -367,7 +384,7 @@ export function openCombatViewV2(scene, rules, battle, art, opts = {}) {
|
||||||
c.add(img);
|
c.add(img);
|
||||||
}
|
}
|
||||||
shipLayer.add(c);
|
shipLayer.add(c);
|
||||||
markers.set(s.uid, { ship: s, container: c });
|
markers.set(s.uid, { ship: s, container: c, shieldRing });
|
||||||
}
|
}
|
||||||
refreshSummaries();
|
refreshSummaries();
|
||||||
}
|
}
|
||||||
|
|
@ -383,6 +400,9 @@ export function openCombatViewV2(scene, rules, battle, art, opts = {}) {
|
||||||
m.container.setAlpha(alive ? 1 : 0.25);
|
m.container.setAlpha(alive ? 1 : 0.25);
|
||||||
m.container.setPosition(m.ship.x, m.ship.y);
|
m.container.setPosition(m.ship.x, m.ship.y);
|
||||||
if (!m.ship.isPlanet) m.container.setRotation(m.ship.facing + Math.PI / 2);
|
if (!m.ship.isPlanet) m.container.setRotation(m.ship.facing + Math.PI / 2);
|
||||||
|
// Hide the shield the instant the planet's own hp is gone — "unless
|
||||||
|
// it's destroyed" (Brian's ask).
|
||||||
|
m.shieldRing?.setVisible(m.ship.hp > 0);
|
||||||
}
|
}
|
||||||
if (aliveCount !== lastSummaryAlive) {
|
if (aliveCount !== lastSummaryAlive) {
|
||||||
lastSummaryAlive = aliveCount;
|
lastSummaryAlive = aliveCount;
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,61 @@ export function proposeTreaty(rules, state, a, b, kind) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// "Please make peace with them" — a request about a THIRD PARTY's war, not a
|
||||||
|
// bilateral treaty between the two empires actually negotiating. Kept
|
||||||
|
// separate from wouldAccept/proposeTreaty/proposeOrOffer above since their
|
||||||
|
// shape (an offer resolves into a treaty between exactly the two parties
|
||||||
|
// talking) doesn't fit a favor that's about someone else's relationship.
|
||||||
|
// Only ever runs in one of two directions — the player asking an AI
|
||||||
|
// (requestPeace, resolved immediately, same as every other player-initiated
|
||||||
|
// audience action) or an AI asking the human (offerPeaceRequest, always
|
||||||
|
// held — mirrors proposeOrOffer's own reasoning: resolving it unilaterally
|
||||||
|
// would give the Audience screen's Accept/Reject buttons nothing to decide).
|
||||||
|
// AI-to-AI never happens; nothing in this file needs it, so it's not built.
|
||||||
|
|
||||||
|
// Would `target` agree to make peace with `thirdParty`, asked by `requester`?
|
||||||
|
// Baseline is whatever `target` would decide on the war's own merits (the
|
||||||
|
// same read wouldAccept('peace') already uses) — a friend's request tips the
|
||||||
|
// balance for a target that WOULDN'T otherwise sue for peace, but only if
|
||||||
|
// they like the requester enough and the war isn't going so badly that even
|
||||||
|
// a friend's plea gets waved off.
|
||||||
|
export function wouldAcceptPeaceRequest(rules, state, requester, target, thirdParty) {
|
||||||
|
if (!atWar(state, target, thirdParty)) return false;
|
||||||
|
if (!canNegotiate(rules, state, target, thirdParty)) return false;
|
||||||
|
if (wouldAccept(rules, state, thirdParty, target, 'peace')) return true;
|
||||||
|
const cfg = rules.diplomacy.warRequest;
|
||||||
|
return attitudeOf(state, target, requester) >= cfg.friendThreshold
|
||||||
|
&& attitudeOf(state, target, thirdParty) > cfg.refuseBelowAttitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The player asking `target` (always an AI) to make peace with `thirdParty`.
|
||||||
|
// Refusal costs the same small sting proposeTreaty's own refusal already
|
||||||
|
// costs elsewhere in this file — being told no by someone you leaned on is a
|
||||||
|
// real, if minor, friction point. Success also warms `target` toward the
|
||||||
|
// player a little on top of makePeace's own bilateral bump: gratitude for
|
||||||
|
// having been asked nicely rather than just left to fight.
|
||||||
|
export function requestPeace(rules, state, requester, target, thirdParty) {
|
||||||
|
if (!wouldAcceptPeaceRequest(rules, state, requester, target, thirdParty)) {
|
||||||
|
adjust(state, target, requester, -3);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
makePeace(rules, state, target, thirdParty);
|
||||||
|
adjust(state, target, requester, 6);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An AI (`a`) asking the HUMAN (`b`) to make peace with `thirdParty`. Held
|
||||||
|
// exactly like proposeOrOffer's human-targeted branch; answered through
|
||||||
|
// respondToOffer, which special-cases offer.kind === 'peaceRequest' since
|
||||||
|
// accepting it doesn't create a treaty between `a` and `b` at all.
|
||||||
|
export function offerPeaceRequest(rules, state, a, b, thirdParty) {
|
||||||
|
if (state.empires[b].pendingOffers[a]) return 'pending';
|
||||||
|
state.empires[b].pendingOffers[a] = { kind: 'peaceRequest', thirdParty, turn: state.turn };
|
||||||
|
state.events.push({ type: 'offerReceived', empire: a, other: b, kind: 'peaceRequest', turn: state.turn });
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
// When an AI proposes peace or an alliance TO THE HUMAN, resolving it
|
// When an AI proposes peace or an alliance TO THE HUMAN, resolving it
|
||||||
// unilaterally (like AI-vs-AI) would give the Audience screen's Accept/Reject
|
// unilaterally (like AI-vs-AI) would give the Audience screen's Accept/Reject
|
||||||
// buttons nothing to actually decide. So a proposal aimed at the human is
|
// buttons nothing to actually decide. So a proposal aimed at the human is
|
||||||
|
|
@ -166,6 +221,21 @@ export function respondToOffer(rules, state, b, a, accept) {
|
||||||
if (offer.kind === 'peace') makePeace(rules, state, a, b);
|
if (offer.kind === 'peace') makePeace(rules, state, a, b);
|
||||||
else if (offer.kind === 'alliance') formAlliance(rules, state, a, b);
|
else if (offer.kind === 'alliance') formAlliance(rules, state, a, b);
|
||||||
else if (offer.kind === 'tradeAgreement') formTradeAgreement(rules, state, a, b);
|
else if (offer.kind === 'tradeAgreement') formTradeAgreement(rules, state, a, b);
|
||||||
|
else if (offer.kind === 'peaceRequest') {
|
||||||
|
// Agreeing means `b` (the human) commits to actually proposing peace to
|
||||||
|
// the third party — same proposeTreaty check a personal Sue for Peace
|
||||||
|
// visit would run, so this can still fail to land if the third party
|
||||||
|
// itself isn't willing. Either way `a` is pleased the human tried;
|
||||||
|
// more so if it actually worked. madePeace rides on the event (not the
|
||||||
|
// return value here — respondToOffer's own true/false already means
|
||||||
|
// "was there an offer to answer", not "did the treaty succeed", and
|
||||||
|
// existing callers depend on that).
|
||||||
|
const madePeace = proposeTreaty(rules, state, b, offer.thirdParty, 'peace');
|
||||||
|
adjust(state, a, b, madePeace ? 15 : 4);
|
||||||
|
state.events.push({
|
||||||
|
type: 'peaceRequestResolved', empire: b, other: a, thirdParty: offer.thirdParty, madePeace, turn: state.turn,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
adjust(state, a, b, -8); // the proposer stings at being refused
|
adjust(state, a, b, -8); // the proposer stings at being refused
|
||||||
adjust(state, b, a, -2); // refusing is itself a small friction cost
|
adjust(state, b, a, -2); // refusing is itself a small friction cost
|
||||||
|
|
@ -334,6 +404,22 @@ export function runDiplomacyTurn(rules, state, e) {
|
||||||
&& rand(state) < allianceCfg.aiProposeChance) {
|
&& rand(state) < allianceCfg.aiProposeChance) {
|
||||||
proposeOrOffer(rules, state, e, other.idx, 'alliance');
|
proposeOrOffer(rules, state, e, other.idx, 'alliance');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask the human to make peace with a friend of ours, if the human is at
|
||||||
|
// war with them — a favor request about someone ELSE's war, not about
|
||||||
|
// e's own relationship with the human (see VegaDiplomacy.js's
|
||||||
|
// offerPeaceRequest, above). `other` here is guaranteed not at war with
|
||||||
|
// e (the atWar(e, other) branch above already `continue`d) and liked
|
||||||
|
// enough by e (`att`) to count as "a friend."
|
||||||
|
const warCfg = rules.diplomacy.warRequest;
|
||||||
|
if (other.idx !== state.humanIndex && emp.contacted[state.humanIndex]
|
||||||
|
&& canNegotiate(rules, state, e, state.humanIndex)
|
||||||
|
&& atWar(state, state.humanIndex, other.idx)
|
||||||
|
&& att > warCfg.friendThreshold
|
||||||
|
&& !human?.pendingOffers?.[e]
|
||||||
|
&& rand(state) < warCfg.aiProposeChance) {
|
||||||
|
offerPeaceRequest(rules, state, e, state.humanIndex, other.idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1334,7 +1334,13 @@ function applyBattleLosses(rules, state, starIdx, e, survivors) {
|
||||||
// Split out of fightAt so the player can drive a battle tick by tick through
|
// Split out of fightAt so the player can drive a battle tick by tick through
|
||||||
// VegaCombatViewV2 and then hand the outcome back — the interactive battle
|
// VegaCombatViewV2 and then hand the outcome back — the interactive battle
|
||||||
// and auto-resolve therefore run the same engine and cannot diverge.
|
// and auto-resolve therefore run the same engine and cannot diverge.
|
||||||
export function prepareBattleAt(rules, state, starIdx, a, b) {
|
// `humanFormation` (VegaFormations.js id) is stamped onto whichever side is
|
||||||
|
// state.humanIndex, so a human-participated battle can carry the tactic the
|
||||||
|
// player actually chose (MasterOfVegaGame.js's playPlayerBattles, via its
|
||||||
|
// pre-battle formation picker) instead of createBattle's own silent-random
|
||||||
|
// fallback for an unset side. AI-vs-AI battles (fightAt, below) never pass
|
||||||
|
// this and are unaffected — both sides stay silently random as before.
|
||||||
|
export function prepareBattleAt(rules, state, starIdx, a, b, { humanFormation = null } = {}) {
|
||||||
const colony = colonyAt(state, starIdx);
|
const colony = colonyAt(state, starIdx);
|
||||||
const defenderIdx = colony && (colony.empireIdx === a || colony.empireIdx === b) ? colony.empireIdx : b;
|
const defenderIdx = colony && (colony.empireIdx === a || colony.empireIdx === b) ? colony.empireIdx : b;
|
||||||
const attackerIdx = defenderIdx === a ? b : a;
|
const attackerIdx = defenderIdx === a ? b : a;
|
||||||
|
|
@ -1350,14 +1356,29 @@ export function prepareBattleAt(rules, state, starIdx, a, b) {
|
||||||
};
|
};
|
||||||
const attacker = mkSide(attackerIdx);
|
const attacker = mkSide(attackerIdx);
|
||||||
const defender = mkSide(defenderIdx);
|
const defender = mkSide(defenderIdx);
|
||||||
|
if (humanFormation) {
|
||||||
|
if (attackerIdx === state.humanIndex) attacker.formationStrategy = humanFormation;
|
||||||
|
else if (defenderIdx === state.humanIndex) defender.formationStrategy = humanFormation;
|
||||||
|
}
|
||||||
const defColony = colony && colony.empireIdx === defenderIdx ? colony : null;
|
const defColony = colony && colony.empireIdx === defenderIdx ? colony : null;
|
||||||
if (!attacker.ships.length) return null;
|
if (!attacker.ships.length) return null;
|
||||||
if (!defender.ships.length && !(defColony && defColony.defenseHp > 0)) return null;
|
if (!defender.ships.length && !(defColony && defColony.defenseHp > 0)) return null;
|
||||||
|
|
||||||
|
// The planet's typeId rides along purely for the tactical view's art (the
|
||||||
|
// real planets spritesheet instead of a generic icon) — the combat engine
|
||||||
|
// itself never reads it. shieldBonus was hard-coded to 0 here until now,
|
||||||
|
// which meant the Planetary Shield building's own effect (rules.buildings
|
||||||
|
// .planetaryshield's shieldBonus:5) was silently never applied to a real
|
||||||
|
// battle, only ever exercised through the standalone ?movsim simulator's
|
||||||
|
// manual stepper — found while wiring the shield ring into the view.
|
||||||
const battle = createBattle(rules, {
|
const battle = createBattle(rules, {
|
||||||
attacker,
|
attacker,
|
||||||
defender,
|
defender,
|
||||||
colony: defColony ? { defenseHp: defColony.defenseHp, shieldBonus: 0 } : null,
|
colony: defColony ? {
|
||||||
|
defenseHp: defColony.defenseHp,
|
||||||
|
shieldBonus: buildingEffect(rules, defColony, 'shieldBonus'),
|
||||||
|
typeId: state.galaxy.stars[starIdx]?.planets[defColony.orbit]?.typeId,
|
||||||
|
} : null,
|
||||||
starIdx,
|
starIdx,
|
||||||
rnd: () => rand(state),
|
rnd: () => rand(state),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,10 @@ export function openResearchScreen(scene, rules, state, e, art, onClose, { initi
|
||||||
gridLayer.add(bg);
|
gridLayer.add(bg);
|
||||||
|
|
||||||
const lvl = fieldTechLevel(rules, state, e, field.id);
|
const lvl = fieldTechLevel(rules, state, e, field.id);
|
||||||
gridLayer.add(scene.add.text(bx + 12, by + 8, `${field.name.toUpperCase()} · Lv ${lvl}`, {
|
const fieldIconSize = 26;
|
||||||
|
gridLayer.add(scene.add.image(bx + 12 + fieldIconSize / 2, by + 8 + 10, art.techfields, field.iconFrame)
|
||||||
|
.setDisplaySize(fieldIconSize, fieldIconSize));
|
||||||
|
gridLayer.add(scene.add.text(bx + 12 + fieldIconSize + 8, by + 8, `${field.name.toUpperCase()} · Lv ${lvl}`, {
|
||||||
fontFamily: FONT, fontSize: '18px', color: '#cfe8ff',
|
fontFamily: FONT, fontSize: '18px', color: '#cfe8ff',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -312,7 +315,10 @@ export function openResearchScreen(scene, rules, state, e, art, onClose, { initi
|
||||||
|
|
||||||
const field = rules.techFields[selectedField];
|
const field = rules.techFields[selectedField];
|
||||||
const detailLvl = fieldTechLevel(rules, state, e, selectedField);
|
const detailLvl = fieldTechLevel(rules, state, e, selectedField);
|
||||||
detailLayer.add(scene.add.text(detailX, headingY, `${field.name.toUpperCase()} — TECH TREE (Level ${detailLvl})`, {
|
const detailIconSize = 22;
|
||||||
|
detailLayer.add(scene.add.image(detailX + detailIconSize / 2, headingY + 9, art.techfields, field.iconFrame)
|
||||||
|
.setDisplaySize(detailIconSize, detailIconSize));
|
||||||
|
detailLayer.add(scene.add.text(detailX + detailIconSize + 8, headingY, `${field.name.toUpperCase()} — TECH TREE (Level ${detailLvl})`, {
|
||||||
fontFamily: FONT, fontSize: '18px', color: '#cfe8ff',
|
fontFamily: FONT, fontSize: '18px', color: '#cfe8ff',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { leaderOffers } from './VegaLeaders.js';
|
||||||
|
|
||||||
import { makeSpeciesPortrait } from './VegaArt.js';
|
import { makeSpeciesPortrait } from './VegaArt.js';
|
||||||
import { openAudienceScreen } from './VegaAudience.js';
|
import { openAudienceScreen } from './VegaAudience.js';
|
||||||
|
import { FORMATION_STRATEGIES } from './VegaFormations.js';
|
||||||
|
|
||||||
export const FONT = '"Julius Sans One"';
|
export const FONT = '"Julius Sans One"';
|
||||||
// `colony` sits above `modal` so the colony screen can stack over the system
|
// `colony` sits above `modal` so the colony screen can stack over the system
|
||||||
|
|
@ -157,6 +158,47 @@ export function slider(scene, x, y, w, label, value, onChange, colour = ACCENT)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple two-card picker for the player's pre-battle formation strategy
|
||||||
|
* (VegaFormations.js). The opposing side always picks silently — an unset
|
||||||
|
* side falls back to VegaCombatV2.createBattle's own seeded-random choice —
|
||||||
|
* this screen only ever asks about the player's own side. Shared by
|
||||||
|
* VegaCombatSim.js's dev tool (closable, a "real quick" testing aid) and the
|
||||||
|
* real pre-battle flow (MasterOfVegaGame.js's playPlayerBattles, forced —
|
||||||
|
* `closable: false`, since there is no sensible "cancel" once fleets are
|
||||||
|
* already committed to a fight this turn).
|
||||||
|
*/
|
||||||
|
export function openFormationPicker(scene, onPick, { closable = true } = {}) {
|
||||||
|
const shell = modalShell(scene, 'Choose Formation Strategy', null, { width: 980, height: 420, closable });
|
||||||
|
const { body } = shell;
|
||||||
|
shell.add(scene.add.text(body.x + body.w / 2, body.y,
|
||||||
|
"Your fleet's tactical doctrine for this battle. The enemy fleet picks its own, silently.", {
|
||||||
|
fontFamily: FONT, fontSize: '16px', color: '#8fa8c0', align: 'center',
|
||||||
|
}).setOrigin(0.5, 0));
|
||||||
|
|
||||||
|
const cardW = (body.w - 40) / 2;
|
||||||
|
const cardY = body.y + body.h / 2 + 20;
|
||||||
|
FORMATION_STRATEGIES.forEach((f, i) => {
|
||||||
|
const cx = body.x + cardW / 2 + i * (cardW + 40);
|
||||||
|
const card = scene.add.rectangle(cx, cardY, cardW, 160, PANEL, 0.7).setStrokeStyle(2, ACCENT, 0.9);
|
||||||
|
shell.add(card);
|
||||||
|
shell.add(scene.add.text(cx, cardY - 48, f.name, {
|
||||||
|
fontFamily: FONT, fontSize: '22px', color: '#cfe8ff',
|
||||||
|
}).setOrigin(0.5));
|
||||||
|
shell.add(scene.add.text(cx, cardY - 4, f.desc, {
|
||||||
|
fontFamily: FONT, fontSize: '15px', color: '#9fb6cc', align: 'center', wordWrap: { width: cardW - 40 },
|
||||||
|
}).setOrigin(0.5, 0));
|
||||||
|
const btn = new Button(scene, cx, cardY + 55, 'Choose', uiClick(scene, () => {
|
||||||
|
shell.destroy();
|
||||||
|
onPick(f.id);
|
||||||
|
}), { width: 160, height: 44, fontSize: 16 });
|
||||||
|
shell.add(btn);
|
||||||
|
});
|
||||||
|
return shell;
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// Research has its own file, VegaResearchScreen.js — it grew past this
|
// Research has its own file, VegaResearchScreen.js — it grew past this
|
||||||
// module's shared factory-function pattern the same way Audience and the
|
// module's shared factory-function pattern the same way Audience and the
|
||||||
|
|
|
||||||
|
|
@ -2344,6 +2344,122 @@ section('7. Diplomacy and the Galactic Council');
|
||||||
st3.events.some((e) => e.type === 'offerExpired' && e.empire === 1 && e.other === 0));
|
st3.events.some((e) => e.type === 'offerExpired' && e.empire === 1 && e.other === 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- "make peace with them" third-party requests (VegaDiplomacy.js's
|
||||||
|
// wouldAcceptPeaceRequest/requestPeace/offerPeaceRequest) — a favor about
|
||||||
|
// someone ELSE's war, not a treaty between the two people actually
|
||||||
|
// talking. Brian's ask: the player can ask an AI to end a war with a
|
||||||
|
// third empire, an AI can ask the player the same, and refusing either
|
||||||
|
// direction costs the relationship.
|
||||||
|
{
|
||||||
|
const stP = Logic.createGame(RULES, {
|
||||||
|
sizeId: 'small', shapeId: 'cluster', seed: 7373, difficultyId: 'normal',
|
||||||
|
speciesIds: ['human', 'kkrix', 'ursaal'], humanIndex: 0,
|
||||||
|
});
|
||||||
|
stP.rules = RULES;
|
||||||
|
const player = 0;
|
||||||
|
const target = 1;
|
||||||
|
const third = 2;
|
||||||
|
for (const e of stP.empires) {
|
||||||
|
e.totalPop = 100;
|
||||||
|
for (const o of stP.empires) if (o.idx !== e.idx) { e.contacted[o.idx] = true; e.attitude[o.idx] = 0; }
|
||||||
|
}
|
||||||
|
Diplo.declareWar(RULES, stP, target, third);
|
||||||
|
check('fixture: target is at war with third', Logic.atWar(stP, target, third));
|
||||||
|
|
||||||
|
stP.empires[target].attitude[third] = -50; // not losing badly, not devoted either
|
||||||
|
stP.empires[target].attitude[player] = 0; // not yet a friend
|
||||||
|
check('a stranger\'s request is refused when the target has no independent reason to accept',
|
||||||
|
!Diplo.wouldAcceptPeaceRequest(RULES, stP, player, target, third));
|
||||||
|
|
||||||
|
stP.empires[target].attitude[player] = 60;
|
||||||
|
check('a friend\'s request succeeds as a favor even when the target would not sue for peace unprompted',
|
||||||
|
Diplo.wouldAcceptPeaceRequest(RULES, stP, player, target, third));
|
||||||
|
|
||||||
|
stP.empires[target].attitude[third] = -90;
|
||||||
|
check('even a friend\'s request is refused when the target\'s own war attitude is bad enough',
|
||||||
|
!Diplo.wouldAcceptPeaceRequest(RULES, stP, player, target, third));
|
||||||
|
|
||||||
|
// requestPeace: success actually makes peace and warms target toward the
|
||||||
|
// requester beyond makePeace's own bilateral bump; failure stings instead.
|
||||||
|
stP.empires[target].attitude[third] = -50;
|
||||||
|
stP.empires[target].attitude[player] = 60;
|
||||||
|
const beforeAtt = Diplo.attitudeOf(stP, target, player);
|
||||||
|
const ok = Diplo.requestPeace(RULES, stP, player, target, third);
|
||||||
|
check('requestPeace succeeds when wouldAcceptPeaceRequest is true', ok === true);
|
||||||
|
check('a successful request actually makes peace', !Logic.atWar(stP, target, third));
|
||||||
|
check('a successful request warms the target toward the requester',
|
||||||
|
Diplo.attitudeOf(stP, target, player) > beforeAtt);
|
||||||
|
|
||||||
|
Diplo.declareWar(RULES, stP, target, third); // reset the war for the failure case
|
||||||
|
stP.empires[target].attitude[player] = -50; // no longer a friend
|
||||||
|
stP.empires[target].attitude[third] = -90; // and losing badly
|
||||||
|
const beforeAtt2 = Diplo.attitudeOf(stP, target, player);
|
||||||
|
const failed = Diplo.requestPeace(RULES, stP, player, target, third);
|
||||||
|
check('requestPeace fails when wouldAcceptPeaceRequest is false', failed === false);
|
||||||
|
check('a failed request still leaves them at war', Logic.atWar(stP, target, third));
|
||||||
|
check('a failed request costs the requester a small sting',
|
||||||
|
Diplo.attitudeOf(stP, target, player) < beforeAtt2);
|
||||||
|
|
||||||
|
// offerPeaceRequest: held exactly like proposeOrOffer's human-targeted
|
||||||
|
// branch, and respondToOffer's peaceRequest case resolves acceptance as
|
||||||
|
// "try to make peace with the third party," not a treaty with the asker.
|
||||||
|
Diplo.declareWar(RULES, stP, player, third); // now the HUMAN is at war with third
|
||||||
|
const heldReq = Diplo.offerPeaceRequest(RULES, stP, target, player, third);
|
||||||
|
check('offerPeaceRequest holds, same contract as proposeOrOffer', heldReq === 'pending');
|
||||||
|
check('the held peaceRequest offer carries its thirdParty',
|
||||||
|
stP.empires[player].pendingOffers[target]?.thirdParty === third);
|
||||||
|
const heldAgain = Diplo.offerPeaceRequest(RULES, stP, target, player, third);
|
||||||
|
check('a second peaceRequest from the same asker does not clobber the first', heldAgain === 'pending');
|
||||||
|
|
||||||
|
stP.empires[third].attitude[player] = 50; // so the resulting proposeTreaty to third can land
|
||||||
|
const beforeAskerAtt = Diplo.attitudeOf(stP, target, player);
|
||||||
|
const respAccept = Diplo.respondToOffer(RULES, stP, player, target, true);
|
||||||
|
check('respondToOffer resolves a held peaceRequest (true, same contract as any offer)',
|
||||||
|
respAccept === true);
|
||||||
|
check('accepting a peaceRequest attempts peace with the third party',
|
||||||
|
!Logic.atWar(stP, player, third));
|
||||||
|
check('accepting clears the pending offer', !stP.empires[player].pendingOffers[target]);
|
||||||
|
check('the asker is pleased the human tried',
|
||||||
|
Diplo.attitudeOf(stP, target, player) > beforeAskerAtt);
|
||||||
|
check('accepting a peaceRequest pushes a peaceRequestResolved event',
|
||||||
|
stP.events.some((e) => e.type === 'peaceRequestResolved' && e.empire === player && e.other === target
|
||||||
|
&& e.thirdParty === third && e.madePeace === true));
|
||||||
|
|
||||||
|
// Rejecting reuses the exact generic refusal path every other offer kind
|
||||||
|
// already uses — refusing costs the relationship, Brian's explicit ask.
|
||||||
|
Diplo.declareWar(RULES, stP, player, third);
|
||||||
|
Diplo.offerPeaceRequest(RULES, stP, target, player, third);
|
||||||
|
const beforeReject = Diplo.attitudeOf(stP, target, player);
|
||||||
|
Diplo.respondToOffer(RULES, stP, player, target, false);
|
||||||
|
check('rejecting a peaceRequest costs the relationship, same as any other refused offer',
|
||||||
|
Diplo.attitudeOf(stP, target, player) < beforeReject);
|
||||||
|
check('rejecting a peaceRequest never touches the third party\'s treaty',
|
||||||
|
Logic.atWar(stP, player, third));
|
||||||
|
|
||||||
|
// AI-initiation: an AI that likes a friend enough, with the human at war
|
||||||
|
// with that friend, eventually offers the human a peaceRequest.
|
||||||
|
const stAI = Logic.createGame(RULES, {
|
||||||
|
sizeId: 'small', shapeId: 'cluster', seed: 8181, difficultyId: 'normal',
|
||||||
|
speciesIds: ['human', 'kkrix', 'ursaal'], humanIndex: 0,
|
||||||
|
});
|
||||||
|
stAI.rules = RULES;
|
||||||
|
for (const e of stAI.empires) {
|
||||||
|
e.totalPop = 100;
|
||||||
|
for (const o of stAI.empires) if (o.idx !== e.idx) { e.contacted[o.idx] = true; e.attitude[o.idx] = 0; }
|
||||||
|
}
|
||||||
|
const asker = 1;
|
||||||
|
const friend = 2;
|
||||||
|
Diplo.declareWar(RULES, stAI, stAI.humanIndex, friend);
|
||||||
|
let anyOffered = false;
|
||||||
|
for (let i = 0; i < 100 && !anyOffered; i += 1) {
|
||||||
|
stAI.empires[asker].attitude[friend] = 80; // pin above friendThreshold — isolate the dice roll
|
||||||
|
Diplo.runDiplomacyTurn(RULES, stAI, asker);
|
||||||
|
anyOffered = !!stAI.empires[stAI.humanIndex].pendingOffers[asker];
|
||||||
|
}
|
||||||
|
check('runDiplomacyTurn eventually offers the human a peaceRequest for a war against a liked empire',
|
||||||
|
anyOffered, `pendingOffers=${JSON.stringify(stAI.empires[stAI.humanIndex].pendingOffers)}`);
|
||||||
|
}
|
||||||
|
|
||||||
// --- colonize() must trigger contact even when nobody's fleet just moved.
|
// --- colonize() must trigger contact even when nobody's fleet just moved.
|
||||||
{
|
{
|
||||||
const stC = Logic.createGame(RULES, {
|
const stC = Logic.createGame(RULES, {
|
||||||
|
|
@ -2714,6 +2830,9 @@ section('7. Diplomacy and the Galactic Council');
|
||||||
'offerTradeAgreementOpener',
|
'offerTradeAgreementOpener',
|
||||||
'fleetComplaintOpener', 'acknowledgeComplaintWithdraw', 'acknowledgeComplaintDefy',
|
'fleetComplaintOpener', 'acknowledgeComplaintWithdraw', 'acknowledgeComplaintDefy',
|
||||||
'afterComplaintWithdrawPromise', 'afterComplaintDefy',
|
'afterComplaintWithdrawPromise', 'afterComplaintDefy',
|
||||||
|
'requestPeaceThird', 'replyRequestPeaceAccept', 'replyRequestPeaceReject',
|
||||||
|
'offerPeaceRequestOpener', 'acceptPeaceRequest', 'rejectPeaceRequest',
|
||||||
|
'afterPeaceRequestAcceptedSuccess', 'afterPeaceRequestAcceptedTried', 'afterPeaceRequestRejected',
|
||||||
];
|
];
|
||||||
// Lore reply pools are meant to carry real variety — enforce a minimum
|
// Lore reply pools are meant to carry real variety — enforce a minimum
|
||||||
// beyond "non-empty" since pickLine's empty-pool fallback is silent and
|
// beyond "non-empty" since pickLine's empty-pool fallback is silent and
|
||||||
|
|
@ -3454,10 +3573,11 @@ section('11. Combat V2 (per-ship prototype)');
|
||||||
{
|
{
|
||||||
// Headless like everything else here — VegaCombatV2.js is Phaser-free, so
|
// Headless like everything else here — VegaCombatV2.js is Phaser-free, so
|
||||||
// this whole section is a plain Node exercise, same pattern as section 5.
|
// this whole section is a plain Node exercise, same pattern as section 5.
|
||||||
// This engine is NOT used by real player battles (wired only into
|
// This IS the resolver real player battles use (VegaLogic.js imports
|
||||||
// VegaCombatSim.js's ?movsim Live/V2 toggle) — see
|
// createBattle/runBattle from here, not from VegaCombat.js — V1 is kept
|
||||||
// docs/mastervega-build-plan.md for why it's a parallel prototype rather
|
// only for resolveInvasion and the ?movsim Live/V2 comparison toggle) —
|
||||||
// than a rewrite of VegaCombat.js in place.
|
// see docs/mastervega-build-plan.md for the history of why it started as
|
||||||
|
// a parallel prototype.
|
||||||
const techsUpToV2 = (tier) => {
|
const techsUpToV2 = (tier) => {
|
||||||
const k = {};
|
const k = {};
|
||||||
for (const t of RULES.techList) if (t.tier <= tier) k[t.id] = true;
|
for (const t of RULES.techList) if (t.tier <= tier) k[t.id] = true;
|
||||||
|
|
@ -3545,6 +3665,64 @@ section('11. Combat V2 (per-ship prototype)');
|
||||||
both1.attackerFormation === both2.attackerFormation && both1.defenderFormation === both2.defenderFormation);
|
both1.attackerFormation === both2.attackerFormation && both1.defenderFormation === both2.defenderFormation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logic.prepareBattleAt — the real-game seam between the engine and a
|
||||||
|
// player battle. Scripted through a real state (not a hand-built fixture)
|
||||||
|
// since it reads fleets/colonies/buildings/galaxy data no synthetic object
|
||||||
|
// could cheaply fake correctly. Covers three things fixed together in one
|
||||||
|
// pass: the player's own pre-battle formation choice actually reaching the
|
||||||
|
// battle, the Planetary Shield building's shieldBonus actually reaching
|
||||||
|
// the defended planet (it was hard-coded to 0 before, so the building's
|
||||||
|
// effect was silently never applied to a real battle), and the planet's
|
||||||
|
// typeId riding along for the tactical view's real planet art.
|
||||||
|
{
|
||||||
|
const st = Logic.createGame(RULES, {
|
||||||
|
sizeId: 'small', shapeId: 'cluster', seed: 5151, difficultyId: 'normal',
|
||||||
|
speciesIds: ['human', 'kkrix'], humanIndex: 0,
|
||||||
|
});
|
||||||
|
st.rules = RULES;
|
||||||
|
const attackerIdx = st.humanIndex;
|
||||||
|
const defenderIdx = attackerIdx === 0 ? 1 : 0;
|
||||||
|
const defColony = st.colonies.find((c) => c.empireIdx === defenderIdx);
|
||||||
|
const starIdx = defColony.starIdx;
|
||||||
|
defColony.defenseHp = 200;
|
||||||
|
defColony.buildings.push('planetaryshield');
|
||||||
|
Diplo.declareWar(RULES, st, attackerIdx, defenderIdx);
|
||||||
|
Logic.addFleet(RULES, st, attackerIdx, starIdx, [{ hullId: 'frigate', mark: 1, count: 2 }]);
|
||||||
|
|
||||||
|
const prepared = Logic.prepareBattleAt(RULES, st, starIdx, attackerIdx, defenderIdx, { humanFormation: 'speed_swarm' });
|
||||||
|
check('prepareBattleAt finds a valid battle for the scripted attack-a-defended-colony scenario', !!prepared);
|
||||||
|
|
||||||
|
const humanIsAttacker = prepared.attackerIdx === st.humanIndex;
|
||||||
|
const humanFormationOnBattle = humanIsAttacker ? prepared.battle.attackerFormation : prepared.battle.defenderFormation;
|
||||||
|
check("the human's chosen formation reaches battle.*Formation on the human's own side",
|
||||||
|
humanFormationOnBattle === 'speed_swarm', humanFormationOnBattle);
|
||||||
|
const validIds = FORMATION_STRATEGIES.map((f) => f.id);
|
||||||
|
const aiFormationOnBattle = humanIsAttacker ? prepared.battle.defenderFormation : prepared.battle.attackerFormation;
|
||||||
|
check("the AI opponent's formation is still a silent (but valid) pick, not forced",
|
||||||
|
validIds.includes(aiFormationOnBattle));
|
||||||
|
|
||||||
|
const planetEntity = prepared.battle.ships.find((s) => s.isPlanet);
|
||||||
|
check('the defended planet entity exists in the prepared battle', !!planetEntity);
|
||||||
|
check("the Planetary Shield building's shieldBonus (5) reaches the planet entity's shield — was hard-coded to 0 before",
|
||||||
|
planetEntity?.shield === RULES.buildings.planetaryshield.effects.shieldBonus,
|
||||||
|
`${planetEntity?.shield}`);
|
||||||
|
const expectedTypeId = st.galaxy.stars[starIdx].planets[defColony.orbit].typeId;
|
||||||
|
check("the planet entity's typeId matches the actual colonised planet",
|
||||||
|
planetEntity?.typeId === expectedTypeId, `${planetEntity?.typeId} vs ${expectedTypeId}`);
|
||||||
|
check('the planet sits at the fixed midpoint between world centre and its side\'s edge (worldWidth * 0.75)',
|
||||||
|
Math.abs(planetEntity.x - RULES.combatV2.worldWidth * 0.75) < 1e-9, `${planetEntity.x}`);
|
||||||
|
|
||||||
|
// A colony with no Planetary Shield still fights, with no shield at all
|
||||||
|
// (not a stale nonzero default) — the ring the view draws is entirely
|
||||||
|
// conditional on this being > 0.
|
||||||
|
const unshieldedColony = st.colonies.find((c) => c.empireIdx === defenderIdx);
|
||||||
|
unshieldedColony.buildings = unshieldedColony.buildings.filter((b) => b !== 'planetaryshield');
|
||||||
|
const preparedNoShield = Logic.prepareBattleAt(RULES, st, starIdx, attackerIdx, defenderIdx, {});
|
||||||
|
const planetNoShield = preparedNoShield.battle.ships.find((s) => s.isPlanet);
|
||||||
|
check('a colony with no Planetary Shield building reaches battle with shield 0',
|
||||||
|
planetNoShield.shield === 0, `${planetNoShield.shield}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Determinism, and auto-resolve agreeing with a played-out battle — same
|
// Determinism, and auto-resolve agreeing with a played-out battle — same
|
||||||
// structural guarantee as the live engine's equivalent check.
|
// structural guarantee as the live engine's equivalent check.
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue