Spaces:
Paused
Paused
File size: 11,795 Bytes
7e764b0 17641e3 e847942 17641e3 04b2d3e c2a29e2 d228ca7 17641e3 e735d2f 7e764b0 e735d2f 17641e3 ac7d42f 17641e3 d228ca7 3195ab6 e735d2f 430c532 9eded26 17641e3 7e764b0 17641e3 785c4de d228ca7 3195ab6 af15726 3195ab6 af15726 c2a29e2 17641e3 340ed0f 53ddc42 340ed0f 4f1cb54 340ed0f 4f1cb54 17641e3 9eded26 17641e3 7e764b0 ccc9661 17641e3 9eded26 17641e3 ccc9661 24f7ec5 e3fdeef 17641e3 ccc9661 04b2d3e ccc9661 17641e3 9eded26 cae3dc6 7e764b0 cae3dc6 ccc9661 cae3dc6 4b95948 7e764b0 9eded26 d69dcb5 4b95948 d69dcb5 4b95948 d69dcb5 4b95948 d69dcb5 9eded26 4b95948 9eded26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | // @ts-check
/* ============================================================
Building construction progress and unit production queues.
============================================================ */
"use strict";
import { BUILDINGS, UNITS, UPGRADES, canAfford, payCost, prereqsMet, committedDoctrine, upgradeMult, structureMult, isElectrifiable, canGatherType, negate } from "./entities.js";
import { makeUnit } from "./state.js";
import { supplyUsed, supplyCap } from "./supply.js";
import { electrifyBoost } from "./industry.js";
import { sideMod } from "./map.js";
// How close a worker has to stand to actually count as building, not just
// walking over. Shared with sim.js's arrival check for the 'build' order.
export const BUILD_REACH = 24;
// More workers can pile onto the same construction site to speed it up β
// capped so a single building can't be insta-rushed by massing an
// unlimited crowd of workers on it.
const MAX_BUILDERS = 4;
function countBuilders(state, building) {
let n = 0;
for (const u of state.units.values()) {
if (u.owner !== building.owner) continue;
if (!u.order || u.order.type !== "build" || u.order.buildingId !== building.id) continue;
if (Math.hypot(u.x - building.x, u.y - building.y) <= BUILD_REACH) n++;
}
return n;
}
/** @param {State} state @param {Building} building @param {number} dt @returns {void} */
export function updateBuildingConstruction(state, building, dt) {
if (!building.constructing) return;
const def = BUILDINGS[building.type];
// The instant-build early-exit stays on the RAW buildTime β a modifier
// never turns a real build time into a zero-time one.
if (def.buildTime <= 0) {
building.constructing = false;
building.buildProgress = 1;
building.hp = def.hp;
return;
}
// A world's build-time modifier speeds (or slows) construction; per-side on an
// asymmetric world (sideMod reads the default 1 for map-less test states). The
// Logistics doctrine's production-speed upgrade compounds on top.
const bt = def.buildTime * sideMod(state, building.owner, "buildTimeMult")
* upgradeMult(state.players?.[building.owner]?.upgrades, "produceTimeMult");
// The founding worker alone still builds at the original pace even if
// it wanders off or dies β extra workers on-site are a bonus, not a
// requirement.
const builders = Math.min(countBuilders(state, building), MAX_BUILDERS);
const rate = Math.max(1, builders);
const before = building.buildProgress;
building.buildProgress = Math.min(1, before + (rate * dt) / bt);
// Construction ADDS the hp built this tick (a ceiling of def.hp * progress),
// it never overwrites hp downward β so damage taken mid-build persists and a
// building under sustained fire can actually be destroyed before it finishes,
// instead of healing every hit away each tick. A building that survives to
// completion while damaged finishes at def.hp minus the damage it soaked.
const gain = def.hp * (building.buildProgress - before);
building.hp = Math.min(building.hp + gain, def.hp * building.buildProgress);
if (building.buildProgress >= 1) {
building.constructing = false;
state.events.push({ type: "buildingComplete", x: building.x, y: building.y, owner: building.owner });
}
}
/** @param {State} state @param {Building} building @param {number} dt @returns {void} */
export function updateProductionQueue(state, building, dt) {
if (building.constructing || building.queue.length === 0) return;
const job = building.queue[0];
const def = UNITS[job.unitType];
// A job with an unknown unitType (localStorage bit-rot or a hand-edited save that slipped past load
// coercion) would otherwise deref `undefined.buildTime` and throw on the FIRST tick β inside the rAF
// loop, past load's try/catch β bricking the autosaved game on every reload. Drop it and move on,
// matching updateResearch's guard for its own queue (engine/techtree.js).
if (!def) { building.queue.shift(); return; }
// Same build-time modifier applies to unit production (a factory world trains
// faster too), per-side on an asymmetric world β and the same Logistics
// production-speed upgrade compounds here too.
// Foundry/Arsenal standing bonus (entities.js structureMult): while completed, each speeds
// MILITARY unit production ~8% (they stack), live-scanned so razing either loses the bonus
// immediately. Scoped to role:"combat" jobs only β a Worker/Ranger/freighter at the Command
// Center is untouched, matching the proposal's "military unit production" wording exactly.
const bt = def.buildTime * sideMod(state, building.owner, "buildTimeMult")
* upgradeMult(state.players?.[building.owner]?.upgrades, "produceTimeMult")
* (def.role === "combat" ? structureMult(state, building.owner, "produceTimeMult") : 1);
// Electrification (Odyssey): a producer wired into the power grid trains 30% faster β the boost
// tapers with the grid throttle (engine/industry.js). Guarded on the flag so the skirmish path,
// which never electrifies, advances the queue byte-identically (boost stays 0, no branch taken).
const boost = (building.electrified && isElectrifiable(building.type))
? electrifyBoost(state, building.owner) : 0;
job.progress += dt * (1 + boost) / bt;
if (job.progress >= 1) {
building.queue.shift();
const spawn = { x: building.x + building.radius + 10, y: building.y + building.radius + 10 };
const u = makeUnit(job.unitType, building.owner, spawn.x, spawn.y);
// Rally-to-resource: if the rally sits on a live node and this unit can actually gather
// (canGatherType, not just cargo presence β a cargo-carrying unit with no gatherRate/cargoCap
// would otherwise NaN-poison unit.cargo.qty/node.amount, see gather.js's miningEfficiency), it
// spawns already mining instead of idling at the point. Everything else β and a rally on a
// drained node β just walks to the rally point as before.
const rallyNode = building.rally.nodeId
? (state.map.nodesById ? state.map.nodesById.get(building.rally.nodeId)
: state.map.nodes.find(n => n.id === building.rally.nodeId))
: null;
u.order = rallyNode && rallyNode.amount > 0 && canGatherType(u.type)
? { type: "gather", nodeId: building.rally.nodeId }
: { type: "move", x: building.rally.x, y: building.rally.y };
state.units.set(u.id, u);
state.events.push({ type: "unitSpawned", x: u.x, y: u.y, owner: u.owner });
}
}
/** @param {State} state @param {string} buildingId @param {string} unitType @param {boolean} [alt] @returns {boolean} */
export function queueProduction(state, buildingId, unitType, alt = false) {
const building = state.buildings.get(buildingId);
if (!building || building.constructing) return false;
const def = UNITS[unitType];
const producable = def && BUILDINGS[building.type].produces?.includes(unitType);
if (!producable) return false;
// A unit with an `altCost` (the Worker) can be trained on its alternative price β the queued job
// remembers which it paid so a cancel refunds the right commodity. Falls back to the base cost if
// this unit has no alternative.
const useAlt = alt && !!def.altCost;
const cost = useAlt ? def.altCost : def.cost;
// An Odyssey-only unit (e.g. the Colony Ship) can never be built in a skirmish,
// regardless of menu wiring β hard-guarantees it stays out of the byte-identical
// skirmish path. A no-op for existing skirmish (no odysseyOnly unit is reachable).
if (def.odysseyOnly && !state.endless) return false;
// Tech gate: a locked unit (its prereq building not yet completed) can't be
// queued. Checked before affordability so "locked" outranks "too expensive".
if (!prereqsMet(state, building.owner, def)) {
state.events.push({ type: "productionBlocked", reason: "prereq",
x: building.x, y: building.y, owner: building.owner });
return false;
}
const player = state.players[building.owner];
if (!canAfford(player.resources, cost)) return false;
// Supply check sits AFTER canAfford (so being broke stays the silent
// today-UX, and the supply beep only fires when supply is the real
// blocker) and BEFORE payCost (reject before charging β nothing to
// refund). All queued jobs already count toward supplyUsed, so this
// is what actually caps queue-stuffing.
if (supplyUsed(state, building.owner) + (def.supplyCost || 0) > supplyCap(state, building.owner)) {
state.events.push({ type: "productionBlocked", reason: "supply",
x: building.x, y: building.y, owner: building.owner });
return false;
}
payCost(player.resources, cost);
building.queue.push({ unitType, progress: 0, ...(useAlt ? { alt: true } : {}) });
return true;
}
// Pulls a job out of the queue (in progress or still waiting, either
// one) and fully refunds its cost β the simplest, most player-friendly
// convention, and consistent with nothing having been spent on it yet
// beyond the ore itself (a part-built unit doesn't exist as an entity,
// unlike a part-built building, so there's no partial-progress asset to
// account for).
/** @param {State} state @param {string} buildingId @param {number} queueIndex @returns {boolean} */
export function cancelProduction(state, buildingId, queueIndex) {
const building = state.buildings.get(buildingId);
if (!building) return false;
const job = building.queue[queueIndex];
if (!job) return false;
building.queue.splice(queueIndex, 1);
const player = state.players[building.owner];
const def = UNITS[job.unitType];
const refund = (job.alt && def.altCost) ? def.altCost : def.cost; // refund the commodity actually charged
payCost(player.resources, negate(refund));
return true;
}
// A Refinery's player-wide doctrine research β see UPGRADES in entities.js for
// what each one does. Queued and TIMED, exactly like the Datacenter's tech tree:
// pays up front on enqueue, then develops over ~25-40s (engine/techtree.js
// updateResearch, which resolves UPGRADES for a Refinery the same way it resolves
// TECHS for a Datacenter) before it lands in player.upgrades and takes effect live,
// army- (and base-) wide.
/** @param {State} state @param {string} buildingId @param {string} upgradeId @returns {boolean} */
export function researchUpgrade(state, buildingId, upgradeId) {
const building = state.buildings.get(buildingId);
if (!building || building.type !== "refinery" || building.constructing) return false;
const player = state.players[building.owner];
if (player.upgrades[upgradeId]) return false;
const def = UPGRADES[upgradeId];
if (!def) return false;
// Doctrine lock: once committed to one doctrine β researched OR still developing,
// see entities.js committedDoctrine β the other is off-limits.
const chosen = committedDoctrine(state, building.owner);
if (chosen && def.doctrine && chosen !== def.doctrine) return false;
// Tier gate: a Tier-2 upgrade needs its Tier-1 already RESEARCHED, not merely
// queued (prereqsMet reads player.upgrades via the requires token, same as the
// tech tree) β unlike researchTech's Datacenter queue, a Refinery job queued
// ahead does NOT count as meeting its own successor's prereq (see the comment on
// researchTech in techtree.js for why).
if (!prereqsMet(state, building.owner, def)) return false;
const queue = building.researchQueue || (building.researchQueue = []);
if (queue.some(j => j.techId === upgradeId)) return false; // already queued β no dupes
if (!canAfford(player.resources, def.cost)) return false;
payCost(player.resources, def.cost);
queue.push({ techId: upgradeId, progress: 0 });
return true;
}
|