feat(total-annihilation): add unit/building repair system
Allow builders to repair damaged friendly units and buildings by right-clicking them. Repair cost and duration are proportional to the damage healed (X% HP restoration costs X% of build cost and takes X% of build time). Key changes: - New 'repair' order type with validation (own units only, must be damaged, requires a builder) - Separate _repairPower accumulator so repairing a damaged factory doesn't silently speed up its production queue - Repair drains energy/mass through the same economy machinery as construction, including brownout throttling - HUD hints guide players to right-click damaged allies to repair - Repair order auto-clears when complete; unqueued orders interrupt it while queued orders wait behind it - Visual waypoint color for repair orders - Comprehensive test coverage in verifyTotalAnnihilation.js
This commit is contained in:
parent
1f4f8d1b30
commit
4b86a85358
|
|
@ -27,7 +27,7 @@ const BTN_W = 88, BTN_H = 74; // build-menu button size
|
|||
|
||||
// CTRL is the queue modifier (as in the original game); SHIFT adds to the selection and
|
||||
// buys x5 from a factory. Kept in one place so the hint and the bindings cannot drift.
|
||||
const HINT = 'LMB select · drag box · RMB order · CTRL+RMB queue · A attack-move · X stop · H hold · WASD pan';
|
||||
const HINT = 'LMB select · drag box · RMB order · RMB a damaged ally to repair · CTRL+RMB queue · A attack-move · X stop · H hold';
|
||||
|
||||
/** Build-button captions have ~7 characters of room; "Vehicle Plant" needs shortening. */
|
||||
function shortName(name) {
|
||||
|
|
@ -350,6 +350,13 @@ export default class TAHud {
|
|||
lines.push(`HP ${Math.ceil(lead.hp)} / ${lead.maxHp}`);
|
||||
if (lead.site) lines.push(`Under construction — ${Math.round((lead.progress ?? 0) * 100)}%`);
|
||||
if (lead.orders?.length > 1) lines.push(`${lead.orders.length} orders queued`);
|
||||
const job = lead.orders?.[0];
|
||||
if (job?.type === 'repair') lines.push('Repairing');
|
||||
else if (job?.type === 'build' || job?.type === 'assist') lines.push('Building');
|
||||
// A builder is the only thing that can repair, so say so where the player is looking.
|
||||
if (!lead.site && (this.rules.defById[lead.defId].builds ?? []).length && !lead.isBuilding) {
|
||||
lines.push('Right-click a damaged ally to repair it');
|
||||
}
|
||||
if (!lead.site && lead.hasRally) lines.push('Rally point set');
|
||||
for (const w of def.weaponDefs ?? []) lines.push(`${w.name}${w.manual ? ' — manual (attack order)' : ''}`);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ function baseEntity(state, army, def) {
|
|||
burstLeft: (def.weaponDefs ?? []).map(() => 0),
|
||||
buildTargetId: 0,
|
||||
queue: [], jobProgress: 0, rallyX: 0, rallyY: 0, hasRally: false,
|
||||
_power: 0,
|
||||
_power: 0, _repairPower: 0,
|
||||
produceE: 0, produceM: 0, upkeepE: 0, storeE: 0, storeM: 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -238,7 +238,7 @@ function defOf(rules, e) { return rules.defById[e.defId]; }
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ORDER_TYPES = new Set([
|
||||
'move', 'attackMove', 'attack', 'stop', 'hold', 'patrol', 'guard', 'build', 'assist',
|
||||
'move', 'attackMove', 'attack', 'stop', 'hold', 'patrol', 'guard', 'build', 'assist', 'repair',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -278,6 +278,13 @@ export function issueOrder(state, rules, cmd) {
|
|||
if (!target || target.army !== army) return { ok: false, error: 'can only assist your own' };
|
||||
if (!canBuild(rules, units[0]) ) return { ok: false, error: 'unit cannot assist' };
|
||||
}
|
||||
if (order.type === 'repair') {
|
||||
const target = entityById(state, order.targetId);
|
||||
if (!target || target.army !== army) return { ok: false, error: 'can only repair your own' };
|
||||
if (target.site) return { ok: false, error: 'use assist on a building under construction' };
|
||||
if (!units.some((u) => canBuild(rules, u))) return { ok: false, error: 'no builder selected' };
|
||||
if (target.hp >= target.maxHp) return { ok: false, error: 'already at full health' };
|
||||
}
|
||||
if (order.type === 'guard') {
|
||||
const target = entityById(state, order.targetId);
|
||||
if (!target || target.army !== army) return { ok: false, error: 'can only guard your own' };
|
||||
|
|
@ -306,6 +313,10 @@ function pushOrder(e, order, queue) {
|
|||
e.path = null;
|
||||
e.wantPath = false;
|
||||
e.noPath = false;
|
||||
// Drop the nanolathe link here rather than waiting for stepOrders to notice. Economy runs
|
||||
// before orders within a tick, so leaving it set would bill the player for one more tick
|
||||
// of a build or repair they just cancelled.
|
||||
e.buildTargetId = 0;
|
||||
}
|
||||
e.orders.push(order);
|
||||
}
|
||||
|
|
@ -417,7 +428,7 @@ function stepEconomy(state, rules) {
|
|||
a.eIncome = 0; a.mIncome = 0; a.eUpkeep = 0; a.eDrain = 0; a.mDrain = 0;
|
||||
a._mFree = 0; a._mGated = 0; a.upkeepFactor = 1;
|
||||
}
|
||||
for (const e of state.entities) e._power = 0;
|
||||
for (const e of state.entities) { e._power = 0; e._repairPower = 0; }
|
||||
|
||||
// 1. Income and upkeep from completed entities. Mass production is split into what runs
|
||||
// for free and what is gated behind an energy upkeep (the Mass Generator), because
|
||||
|
|
@ -443,7 +454,12 @@ function stepEconomy(state, rules) {
|
|||
const target = entityById(state, e.buildTargetId);
|
||||
if (target && target.army === e.army) {
|
||||
const d = Math.hypot(target.x - e.x, target.y - e.y);
|
||||
if (d <= def.buildRange + target.radius) target._power += def.buildPower;
|
||||
if (d > def.buildRange + target.radius) continue;
|
||||
// Repair power is kept in its OWN accumulator. A damaged factory has build power of
|
||||
// its own for the unit it is producing; folding an incoming repair into the same
|
||||
// number would silently speed up that unit's production.
|
||||
if (target.site) target._power += def.buildPower;
|
||||
else if (target.hp < target.maxHp) target._repairPower += def.buildPower;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -451,19 +467,37 @@ function stepEconomy(state, rules) {
|
|||
// 3. Demand.
|
||||
const jobs = [];
|
||||
for (const e of state.entities) {
|
||||
if (e.dead || e._power <= 0) continue;
|
||||
let jobDef = null;
|
||||
if (e.site) jobDef = defOf(rules, e);
|
||||
else if (e.isBuilding && e.queue.length) jobDef = rules.unitById[e.queue[0].defId];
|
||||
if (!jobDef || !(jobDef.buildTime > 0)) continue;
|
||||
const rate = (e._power / nominal) / jobDef.buildTime; // progress fraction per second
|
||||
const a = state.armies[e.army];
|
||||
if (!a) continue;
|
||||
const dE = (jobDef.cost.energy ?? 0) * rate;
|
||||
const dM = (jobDef.cost.mass ?? 0) * rate;
|
||||
a.eDrain += dE;
|
||||
a.mDrain += dM;
|
||||
jobs.push({ e, rate, army: e.army });
|
||||
if (e.dead || !a) continue;
|
||||
|
||||
if (e._power > 0) {
|
||||
let jobDef = null;
|
||||
if (e.site) jobDef = defOf(rules, e);
|
||||
else if (e.isBuilding && e.queue.length) jobDef = rules.unitById[e.queue[0].defId];
|
||||
if (jobDef && jobDef.buildTime > 0) {
|
||||
const rate = (e._power / nominal) / jobDef.buildTime; // progress fraction per second
|
||||
a.eDrain += (jobDef.cost.energy ?? 0) * rate;
|
||||
a.mDrain += (jobDef.cost.mass ?? 0) * rate;
|
||||
jobs.push({ e, rate, army: e.army, kind: e.site ? 'site' : 'factory' });
|
||||
}
|
||||
}
|
||||
|
||||
// Repair is priced as a fraction of a fresh build: restoring X% of a unit's HP costs X%
|
||||
// of its build cost and takes X% of its build time. It therefore runs through exactly the
|
||||
// same rate, drain and stall machinery as construction — including being throttled in a
|
||||
// brownout rather than proceeding for free.
|
||||
if (e._repairPower > 0 && !e.site && e.hp < e.maxHp) {
|
||||
const def = defOf(rules, e);
|
||||
if (def.buildTime > 0) {
|
||||
const full = (e._repairPower / nominal) / def.buildTime;
|
||||
// Never bill for more than the damage actually outstanding.
|
||||
const remaining = (e.maxHp - e.hp) / e.maxHp;
|
||||
const rate = Math.min(full, remaining / dt);
|
||||
a.eDrain += (def.cost.energy ?? 0) * rate;
|
||||
a.mDrain += (def.cost.mass ?? 0) * rate;
|
||||
jobs.push({ e, rate, army: e.army, kind: 'repair' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Stall factors, then apply.
|
||||
|
|
@ -499,7 +533,7 @@ function stepEconomy(state, rules) {
|
|||
|
||||
for (const j of jobs) {
|
||||
const a = state.armies[j.army];
|
||||
j.e.__adv = j.rate * a.buildEff * dt;
|
||||
j.adv = j.rate * a.buildEff * dt;
|
||||
}
|
||||
|
||||
for (const a of state.armies) {
|
||||
|
|
@ -513,12 +547,24 @@ function stepEconomy(state, rules) {
|
|||
a.mass = Math.max(0, Math.min(a.massCap, mNext));
|
||||
}
|
||||
|
||||
// 5. Advance construction.
|
||||
// 5. Advance construction and repair.
|
||||
for (const j of jobs) {
|
||||
const e = j.e;
|
||||
const adv = e.__adv ?? 0;
|
||||
e.__adv = 0;
|
||||
const adv = j.adv ?? 0;
|
||||
if (adv <= 0) continue;
|
||||
if (j.kind === 'repair') {
|
||||
e.hp = Math.min(e.maxHp, e.hp + e.maxHp * adv);
|
||||
state.events.push({ t: 'nanolathe', id: e.id, x: e.x, y: e.y, army: e.army });
|
||||
if (e.hp >= e.maxHp) {
|
||||
// Release the builders so they fall through to whatever they were told to do next.
|
||||
for (const b of state.entities) {
|
||||
if (b.buildTargetId !== e.id) continue;
|
||||
b.buildTargetId = 0;
|
||||
if (b.orders[0]?.type === 'repair' && b.orders[0].targetId === e.id) b.orders.shift();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (e.site) {
|
||||
const def = defOf(rules, e);
|
||||
e.progress = Math.min(1, e.progress + adv);
|
||||
|
|
@ -627,6 +673,14 @@ function stepOrders(state, rules) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// A builder only holds a nanolathe target while its CURRENT order is a construction one.
|
||||
// Without this, giving a repairing Commander a move order left buildTargetId pointing at
|
||||
// the patient, and the economy kept pouring resources into the repair — for as long as
|
||||
// the builder happened to stay in range — even though the player had cancelled it.
|
||||
if (order.type !== 'build' && order.type !== 'assist' && order.type !== 'repair') {
|
||||
e.buildTargetId = 0;
|
||||
}
|
||||
|
||||
switch (order.type) {
|
||||
case 'stop':
|
||||
e.orders.length = 0;
|
||||
|
|
@ -699,12 +753,18 @@ function stepOrders(state, rules) {
|
|||
}
|
||||
|
||||
case 'build':
|
||||
case 'assist': {
|
||||
case 'assist':
|
||||
case 'repair': {
|
||||
const target = entityById(state, order.targetId);
|
||||
if (!target) { e.orders.shift(); e.buildTargetId = 0; e.path = null; break; }
|
||||
if (order.type === 'assist' && !target.site && !target.queue?.length) {
|
||||
e.orders.shift(); e.buildTargetId = 0; break;
|
||||
}
|
||||
// Nothing left to mend — either it was topped up by someone else or it was never
|
||||
// damaged by the time we arrived.
|
||||
if (order.type === 'repair' && (target.hp >= target.maxHp || target.site)) {
|
||||
e.orders.shift(); e.buildTargetId = 0; break;
|
||||
}
|
||||
const reach = (def.buildRange ?? 0) + target.radius;
|
||||
const d = Math.hypot(target.x - e.x, target.y - e.y);
|
||||
if (d > reach * 0.9) {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const MAX_QUEUE_LINES = 24; // selected units whose order queue is drawn
|
|||
/** Waypoint colour per order type, so a queue reads at a glance. */
|
||||
const ORDER_COLORS = {
|
||||
move: 0x7dff9b, attackMove: 0xff8a5a, attack: 0xff5a5a, patrol: 0x8ad4ff,
|
||||
guard: 0xffd27a, assist: 0x8affd0, build: 0x8ad4ff,
|
||||
guard: 0xffd27a, assist: 0x8affd0, build: 0x8ad4ff, repair: 0x8affd0,
|
||||
};
|
||||
|
||||
export const DEPTHS = {
|
||||
|
|
|
|||
|
|
@ -550,6 +550,13 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
|
|||
this._order({ type: 'assist', targetId: target.id }, queue);
|
||||
return;
|
||||
}
|
||||
// Any unit that can build can also repair, so a future construction unit picks this up
|
||||
// with no code change — the capability comes from its def, not from being the Commander.
|
||||
if (target && target.army === this.playerArmy && target.hp < target.maxHp
|
||||
&& this._selectedEntities().some((e) => this.rules.defById[e.defId].builds?.length && !e.isBuilding)) {
|
||||
this._order({ type: 'repair', targetId: target.id }, queue);
|
||||
return;
|
||||
}
|
||||
this._order({ type: 'move', x: w.x, y: w.y }, queue);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -756,6 +756,150 @@ section('6b. Commander self-repair');
|
|||
check('a reload does not hand back a free heal', c6b.hp === hpAfterLoad);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
section('6c. Repair');
|
||||
// ---------------------------------------------------------------------------
|
||||
{
|
||||
const raw = JSON.parse(readFileSync(join(ROOT, 'data/totalannihilation-rules.json'), 'utf8'));
|
||||
raw.constants.eliminateWhenUnrecoverable = false;
|
||||
// Self-repair would confound the cost measurements below, so it is off for this fixture.
|
||||
for (const u of raw.units) delete u.selfHeal;
|
||||
const rr = compileRules(raw);
|
||||
const map = generateMap(rr, { seed: 81, size: 'small', symmetry: 'mirror-x' });
|
||||
|
||||
const setup = (defId, hpFrac) => {
|
||||
const st = L.createMatch(rr, { seed: 81, map, armies: [{ armyId: 'arm' }, { armyId: 'core' }] });
|
||||
st.over = null;
|
||||
for (const a of st.armies) a.alive = true;
|
||||
const builder = st.entities.find((e) => e.army === 0);
|
||||
const patient = L.spawnUnit(st, rr, 0, defId, builder.x + 80, builder.y);
|
||||
patient.hp = patient.maxHp * hpFrac;
|
||||
const army = st.armies[0];
|
||||
army.mass = 99999; army.energy = 99999; army.massCap = 99999; army.energyCap = 99999;
|
||||
// Silence the builder's own income, so a drop in stored resources IS the repair bill and
|
||||
// nothing else. Measuring the net change instead would net off the Commander's output.
|
||||
builder.produceE = 0; builder.produceM = 0;
|
||||
return { st, builder, patient, army };
|
||||
};
|
||||
|
||||
// A damaged friendly is a legal repair target; a healthy one is not.
|
||||
{
|
||||
const { st, builder, patient } = setup('tank', 0.5);
|
||||
const ok = L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: patient.id } });
|
||||
check('repair order accepted on a damaged friendly', ok.ok, ok.error);
|
||||
patient.hp = patient.maxHp;
|
||||
const full = L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: patient.id } });
|
||||
check('repair is refused at full health', !full.ok, full.error);
|
||||
const foe = st.entities.find((e) => e.army === 1);
|
||||
const enemy = L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: foe.id } });
|
||||
check('repair is refused on an enemy', !enemy.ok, enemy.error);
|
||||
}
|
||||
|
||||
// Cost and duration are both proportional to the damage healed.
|
||||
for (const [defId, frac] of [['tank', 0.5], ['tank', 0.25], ['rockettank', 0.5]]) {
|
||||
const { st, builder, patient, army } = setup(defId, frac);
|
||||
const def = rr.unitById[defId];
|
||||
const missing = 1 - frac;
|
||||
const m0 = army.mass, e0 = army.energy;
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: patient.id } });
|
||||
let ticks = 0;
|
||||
const cap = 400 * HZ;
|
||||
while (ticks < cap && patient.hp < patient.maxHp) { L.tick(st, rr); ticks++; }
|
||||
check(`${defId} at ${frac * 100}% is repaired to full`, patient.hp >= patient.maxHp - 1e-6,
|
||||
`${patient.hp.toFixed(0)}/${patient.maxHp}`);
|
||||
|
||||
const spentM = m0 - army.mass, spentE = e0 - army.energy;
|
||||
const wantM = def.cost.mass * missing, wantE = def.cost.energy * missing;
|
||||
check(`${defId} repair mass cost is ~${(missing * 100) | 0}% of build cost`,
|
||||
Math.abs(spentM - wantM) < wantM * 0.06 + 1, `${spentM.toFixed(0)} vs ${wantM.toFixed(0)}`);
|
||||
check(`${defId} repair energy cost is ~${(missing * 100) | 0}% of build cost`,
|
||||
Math.abs(spentE - wantE) < wantE * 0.06 + 1, `${spentE.toFixed(0)} vs ${wantE.toFixed(0)}`);
|
||||
|
||||
// Time: same build power, so healing X% takes X% of the build time.
|
||||
const bp = rr.unitById.commander.buildPower / rr.constants.buildPowerNominal;
|
||||
const wantSecs = (def.buildTime * missing) / bp;
|
||||
const gotSecs = ticks / HZ;
|
||||
check(`${defId} repair takes ~${(missing * 100) | 0}% of build time`,
|
||||
Math.abs(gotSecs - wantSecs) < wantSecs * 0.25 + 1.5, `${gotSecs.toFixed(1)}s vs ${wantSecs.toFixed(1)}s`);
|
||||
}
|
||||
|
||||
// Buildings repair too.
|
||||
{
|
||||
const { st, builder, army } = setup('tank', 0.99);
|
||||
const gen = rr.buildingById.energygen;
|
||||
let site = null;
|
||||
for (let d = 2; d < 20 && !site; d++) {
|
||||
const tx = worldToTileX(st.nav, builder.x) + d, ty = worldToTileY(st.nav, builder.y);
|
||||
if (L.canPlaceAt(st, rr, tx, ty, gen).ok) site = L.placeBuilding(st, rr, 0, 'energygen', tx, ty);
|
||||
}
|
||||
check('found room for a building to repair', !!site);
|
||||
site.site = false; site.progress = 1; site.hp = gen.hp * 0.4;
|
||||
const before = site.hp;
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: site.id } });
|
||||
for (let i = 0; i < 200 * HZ && site.hp < site.maxHp; i++) { army.mass = 99999; army.energy = 99999; L.tick(st, rr); }
|
||||
check('a damaged building can be repaired', site.hp > before && site.hp >= site.maxHp - 1e-6,
|
||||
`${site.hp.toFixed(0)}/${site.maxHp}`);
|
||||
}
|
||||
|
||||
// The order ends by itself once the patient is whole, and an unqueued order breaks it.
|
||||
{
|
||||
const { st, builder, patient } = setup('tank', 0.5);
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: patient.id } });
|
||||
for (let i = 0; i < 400 * HZ && builder.orders.length; i++) L.tick(st, rr);
|
||||
check('the repair order clears itself when done', builder.orders.length === 0);
|
||||
check('the builder releases its build target', builder.buildTargetId === 0);
|
||||
}
|
||||
{
|
||||
const { st, builder, patient } = setup('tank', 0.5);
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: patient.id } });
|
||||
for (let i = 0; i < 3 * HZ; i++) L.tick(st, rr);
|
||||
const mid = patient.hp;
|
||||
check('repair is under way', mid > patient.maxHp * 0.5);
|
||||
// A plain (unqueued) order must break the heal outright.
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'move', x: builder.x + 600, y: builder.y } });
|
||||
check('a new unqueued order replaces the repair', builder.orders.length === 1
|
||||
&& builder.orders[0].type === 'move');
|
||||
for (let i = 0; i < 5 * HZ; i++) L.tick(st, rr);
|
||||
check('the interrupted patient stops healing', Math.abs(patient.hp - mid) < 1e-6,
|
||||
`${patient.hp.toFixed(1)} vs ${mid.toFixed(1)}`);
|
||||
}
|
||||
// A QUEUED order must not break it — the repair stays at the head of the queue.
|
||||
{
|
||||
const { st, builder, patient } = setup('tank', 0.5);
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: patient.id } });
|
||||
for (let i = 0; i < 2 * HZ; i++) L.tick(st, rr);
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id],
|
||||
order: { type: 'move', x: builder.x + 600, y: builder.y }, queue: true });
|
||||
const mid = patient.hp;
|
||||
for (let i = 0; i < 3 * HZ; i++) L.tick(st, rr);
|
||||
check('a queued order leaves the repair running', patient.hp > mid,
|
||||
`${patient.hp.toFixed(1)} vs ${mid.toFixed(1)}`);
|
||||
check('the queued order is still waiting behind it', builder.orders.length === 2
|
||||
&& builder.orders[0].type === 'repair');
|
||||
}
|
||||
|
||||
// A damaged factory being repaired must not have its production accelerated.
|
||||
{
|
||||
const { st, builder, army } = setup('tank', 0.99);
|
||||
const plant = L.placeBuilding(st, rr, 0, 'vehicleplant',
|
||||
worldToTileX(st.nav, builder.x) + 6, worldToTileY(st.nav, builder.y));
|
||||
check('placed a factory for the mixing test', !!plant);
|
||||
if (plant) {
|
||||
plant.site = false; plant.progress = 1;
|
||||
plant.hp = rr.buildingById.vehicleplant.hp * 0.5;
|
||||
L.issueOrder(st, rr, { army: 0, order: { type: 'factoryEnqueue', factoryId: plant.id, defId: 'tank', count: 1 } });
|
||||
L.issueOrder(st, rr, { army: 0, unitIds: [builder.id], order: { type: 'repair', targetId: plant.id } });
|
||||
let t = 0;
|
||||
for (; t < 30 * HZ; t++) { army.mass = 99999; army.energy = 99999; L.tick(st, rr); }
|
||||
const bp = rr.buildingById.vehicleplant.buildPower / rr.constants.buildPowerNominal;
|
||||
const expected = Math.min(1, (t / HZ) * bp / rr.unitById.tank.buildTime);
|
||||
check('repairing a factory does not speed up its production',
|
||||
Math.abs(plant.jobProgress - expected) < 0.08 || plant.jobProgress < expected + 0.08,
|
||||
`progress ${plant.jobProgress.toFixed(2)} vs expected ${expected.toFixed(2)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
section('7. Fog of war');
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue