feat(mastervega): add morale bonus system and espionage restraint diplomacy
- Add morale bonus from buildings (e.g., Holo Simulator): flat +N% to total colony production, uniformly boosting construction, defense, industry, ecology, and research - Add espionage restraint (offDrift): setting Intelligence to 'off' toward an empire with neutral-or-better opinion drifts their attitude upward by 1/turn, giving a way to improve relations without gifting (gated on not being at war and their opinion of you being >= 0) - Display morale bonus in colony view when applicable - Reduce planetShotDamageMult from 10 to 6 - Add verification tests for both new mechanics
This commit is contained in:
parent
46a2979555
commit
09a62f12cc
|
|
@ -450,9 +450,10 @@
|
|||
"aiProposeChance": 0.15
|
||||
},
|
||||
"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. offDrift (Brian's ask, 2026-08-15) is a separate mechanic: setting Intelligence to 'off' toward an empire whose current opinion of you is already neutral or better reads as a diplomatic gesture (I'm choosing not to spy on you), so their attitude toward you drifts up by this much per turn (VegaDiplomacy.js's driftAttitudes) — gated on not being at war, and deliberately smaller than even a Modest Gift's one-time +4 so restraint alone can't outperform actually gifting people.",
|
||||
"sabotageFactoriesFraction": 0.25,
|
||||
"sabotageDefenseFraction": 0.25
|
||||
"sabotageDefenseFraction": 0.25,
|
||||
"offDrift": 1
|
||||
},
|
||||
"borderTension": {
|
||||
"colonyRange": 6,
|
||||
|
|
@ -513,7 +514,7 @@
|
|||
"disengageRound": 25,
|
||||
"planetDefenseScale": 0.06,
|
||||
"planetDefensePerShot": 5,
|
||||
"planetShotDamageMult": 10,
|
||||
"planetShotDamageMult": 6,
|
||||
"bombardPopKill": 0.22,
|
||||
"bombardFactoryKillMult": 1,
|
||||
"bombardBuildingDestroyMult": 1,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import { openLeaderDetail } from './VegaLeaderDetail.js';
|
|||
import { leaderOf } from './VegaLeaders.js';
|
||||
import { describeBuildingTooltip, describeLeaderTooltip, describeAllocationTooltip } from './VegaTooltips.js';
|
||||
import {
|
||||
CHANNELS, colonyMaxPop, colonyProduction, colonyFactoryCap, effectiveFactories,
|
||||
CHANNELS, colonyMaxPop, colonyProduction, colonyMoraleBonus, colonyFactoryCap, effectiveFactories,
|
||||
colonyDefenseCap, colonyTrade, colonyBuildRate, setSlider, enqueue, enqueueMany,
|
||||
dequeue, collapseQueue, moveQueueRun, queueItemCost, queueEtas, empireDesign,
|
||||
empireColonies, maxSendablePopulation, sendPopulation, etaTo,
|
||||
|
|
@ -412,6 +412,14 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
text(`Trade ${colonyTrade(rules, state, colony).toFixed(1)} BC`, 17, '#c8dcf0', 2);
|
||||
text(`Defences ${Math.round(colony.defenseHp)} / ${colonyDefenseCap(rules, state, colony)}`,
|
||||
17, '#c8dcf0', 2);
|
||||
// Only worth a line when a building is actually granting it — same
|
||||
// "don't clutter the summary with a zero" convention as the mothballed/
|
||||
// waste lines just below.
|
||||
const moraleBonus = colonyMoraleBonus(rules, colony);
|
||||
if (moraleBonus > 0) {
|
||||
text(`Morale +${moraleBonus}% to all output (construction/defense/industry/ecology/research)`,
|
||||
14, '#9fd8ff', 2);
|
||||
}
|
||||
// Factories nobody is left to staff are mothballed rather than demolished —
|
||||
// worth saying out loud, because the number simply reads as wrong otherwise.
|
||||
if (colony.factories > effF + 0.5) {
|
||||
|
|
|
|||
|
|
@ -315,6 +315,19 @@ export function driftAttitudes(rules, state, e) {
|
|||
if (Math.abs(cur - base) > 1) adjust(state, e, other.idx, pull);
|
||||
if (atWar(state, e, other.idx)) adjust(state, e, other.idx, -2);
|
||||
else if (emp.treaties[other.idx] === 'alliance') adjust(state, e, other.idx, 1);
|
||||
|
||||
// Espionage restraint (Brian's ask, 2026-08-15): choosing 'off' toward
|
||||
// an empire that already thinks reasonably well of us is a diplomatic
|
||||
// gesture — the one and only thing that used to make attitude drift
|
||||
// upward on its own was gifts, so an empire that never gifts anyone
|
||||
// (or runs out of BC to) had no way to counter the baseline's downward
|
||||
// pull at all. Gated on THEIR current opinion of US (not war, not our
|
||||
// opinion of them) — this rewards maintaining an already-decent
|
||||
// relationship, not buying back one that already hates us.
|
||||
if ((emp.espionageMission[other.idx] ?? 'steal') === 'off'
|
||||
&& !atWar(state, e, other.idx) && (other.attitude[e] ?? 0) >= 0) {
|
||||
adjust(state, other.idx, e, rules.diplomacy.espionage.offDrift ?? 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,14 @@ export function effectiveFactories(rules, state, colony) {
|
|||
return Math.min(colony.factories, colonyFactoryCap(rules, state, colony));
|
||||
}
|
||||
|
||||
// Total morale bonus points from this colony's buildings (currently only
|
||||
// the Holo Simulator's moraleBonus: 10) — a raw point value, not yet turned
|
||||
// into a multiplier. Exported so the colony screen can show it directly,
|
||||
// same shape as buildingEffect's other consumers.
|
||||
export function colonyMoraleBonus(rules, colony) {
|
||||
return buildingEffect(rules, colony, 'moraleBonus');
|
||||
}
|
||||
|
||||
export function colonyProduction(rules, state, colony) {
|
||||
const emp = state.empires[colony.empireIdx];
|
||||
const spec = rules.species[emp.speciesId];
|
||||
|
|
@ -202,7 +210,14 @@ export function colonyProduction(rules, state, colony) {
|
|||
* spec.traits.industryMult
|
||||
* buildingMult(rules, colony, 'industry')
|
||||
* (skills.industryMult ?? 1);
|
||||
return fromPop + fromFactories;
|
||||
// Morale (Brian's ask, 2026-08-15 — Holo Simulator's moraleBonus effect
|
||||
// previously did nothing at all) is a flat percentage applied to this
|
||||
// colony's TOTAL output, before it gets split across the five sliders in
|
||||
// processColony — so it lifts construction, defense, industry, ecology,
|
||||
// AND research uniformly, exactly as a broad "happier citizens work
|
||||
// better at everything" bonus should, rather than favouring one channel.
|
||||
const moraleMult = 1 + colonyMoraleBonus(rules, colony) / 100;
|
||||
return (fromPop + fromFactories) * moraleMult;
|
||||
}
|
||||
|
||||
export function colonyDefenseCap(rules, state, colony) {
|
||||
|
|
|
|||
|
|
@ -2591,6 +2591,29 @@ section('5g. AI bombard/invade at a contested multi-colony star');
|
|||
// ---------------------------------------------------------------------------
|
||||
section('6. Colony economy');
|
||||
// ---------------------------------------------------------------------------
|
||||
// --- morale (Brian's ask, 2026-08-15): the Holo Simulator's moraleBonus
|
||||
// effect previously did nothing at all — a flat +N% now applies uniformly
|
||||
// to a colony's TOTAL production, before the five sliders split it, so
|
||||
// construction/defense/industry/ecology/research all benefit equally.
|
||||
{
|
||||
const st = Logic.createGame(RULES, {
|
||||
sizeId: 'medium', shapeId: 'spiral', seed: 31, difficultyId: 'normal',
|
||||
speciesIds: ['human', 'kkrix', 'lithox'], humanIndex: -1,
|
||||
});
|
||||
st.rules = RULES;
|
||||
const colony = st.colonies[0];
|
||||
check('a colony with no Holo Simulator has zero morale bonus',
|
||||
Logic.colonyMoraleBonus(RULES, colony) === 0);
|
||||
const prodBefore = Logic.colonyProduction(RULES, st, colony);
|
||||
colony.buildings.push('holosimulator');
|
||||
check("the Holo Simulator's moraleBonus (10) is now readable",
|
||||
Logic.colonyMoraleBonus(RULES, colony) === 10);
|
||||
const prodAfter = Logic.colonyProduction(RULES, st, colony);
|
||||
check('building it lifts total colony production by exactly the morale percentage',
|
||||
Math.abs(prodAfter - prodBefore * 1.1) < 1e-9, `${prodBefore} -> ${prodAfter}`);
|
||||
colony.buildings.pop();
|
||||
}
|
||||
|
||||
{
|
||||
const st = Logic.createGame(RULES, {
|
||||
sizeId: 'medium', shapeId: 'spiral', seed: 31, difficultyId: 'normal',
|
||||
|
|
@ -3398,6 +3421,54 @@ section('7. Diplomacy and the Galactic Council');
|
|||
Diplo.makePeace(RULES, st, 0, 1);
|
||||
check('peace is mutual', !Logic.atWar(st, 0, 1) && !Logic.atWar(st, 1, 0));
|
||||
|
||||
// Espionage restraint drift (Brian's ask, 2026-08-15): setting Intelligence
|
||||
// to 'off' toward an empire that already thinks reasonably well of us
|
||||
// should nudge their opinion of us up a little every turn — the one
|
||||
// mechanic besides gifts that can push attitude UP rather than just decay
|
||||
// toward baseline. Isolated by diffing WITH vs WITHOUT the 'off' setting
|
||||
// from the identical starting attitude, so the pre-existing baseline-pull
|
||||
// noise (which applies either way) cancels out and only offDrift remains.
|
||||
{
|
||||
const st2 = Logic.createGame(RULES, {
|
||||
sizeId: 'medium', shapeId: 'elliptical', seed: 78, difficultyId: 'normal',
|
||||
speciesIds: ['human', 'kkrix'], humanIndex: 0,
|
||||
});
|
||||
st2.rules = RULES;
|
||||
st2.empires[0].contacted[1] = true;
|
||||
st2.empires[1].contacted[0] = true;
|
||||
|
||||
st2.empires[1].attitude[0] = 10; // kkrix's opinion of the human: neutral-or-better
|
||||
Diplo.driftAttitudes(RULES, st2, 0);
|
||||
const withoutOff = st2.empires[1].attitude[0];
|
||||
|
||||
st2.empires[1].attitude[0] = 10; // reset to the same starting point
|
||||
st2.empires[0].espionageMission[1] = 'off';
|
||||
Diplo.driftAttitudes(RULES, st2, 0);
|
||||
const withOff = st2.empires[1].attitude[0];
|
||||
|
||||
check("'off' toward an empire that already likes us at least neutrally drifts their opinion up by offDrift",
|
||||
withOff - withoutOff === (RULES.diplomacy.espionage.offDrift ?? 1), `${withoutOff} -> ${withOff}`);
|
||||
|
||||
// Control: no drift if THEIR opinion of us is already negative — this
|
||||
// rewards maintaining good relations, not buying back a rival that
|
||||
// already dislikes us.
|
||||
st2.empires[1].attitude[0] = -10;
|
||||
Diplo.driftAttitudes(RULES, st2, 0);
|
||||
check("'off' grants no drift when their opinion of us is already negative",
|
||||
st2.empires[1].attitude[0] === -10, `${st2.empires[1].attitude[0]}`);
|
||||
|
||||
// Control: no drift while at war, even if attitude somehow still reads
|
||||
// neutral-or-better this same turn. driftAttitudes(e=0) never touches
|
||||
// empires[1].attitude[0] except through the 'off' block (every other
|
||||
// line in it writes to empires[0]'s own attitude array instead), so a
|
||||
// truly-gated 'off' bonus should leave it perfectly unchanged.
|
||||
Diplo.declareWar(RULES, st2, 0, 1);
|
||||
st2.empires[1].attitude[0] = 10; // declareWar itself dings attitude — reset AFTER, to isolate the war gate
|
||||
Diplo.driftAttitudes(RULES, st2, 0);
|
||||
check("'off' grants no drift while at war",
|
||||
st2.empires[1].attitude[0] === 10, `${st2.empires[1].attitude[0]}`);
|
||||
}
|
||||
|
||||
check('attitudes stay in range', (() => {
|
||||
for (let i = 0; i < 400; i += 1) {
|
||||
for (const e of st.empires) Diplo.driftAttitudes(RULES, st, e.idx);
|
||||
|
|
|
|||
Loading…
Reference in New Issue