Spaces:
Paused
Paused
File size: 8,738 Bytes
79c7af7 22a180c 79c7af7 6db3533 79c7af7 6db3533 79c7af7 6db3533 79c7af7 6db3533 79c7af7 22a180c 6db3533 22a180c 79c7af7 6db3533 22a180c 6db3533 22a180c 79c7af7 22a180c 79c7af7 6db3533 79c7af7 22a180c 79c7af7 6db3533 79c7af7 22a180c 79c7af7 | 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 | // @ts-check
/* ============================================================
Recycling: the player can scrap an OWNED, completed building or a unit to
reclaim part of what it cost. Not instant β like construction, it runs a
timer in place (engine/commands.js issueRecycle starts it, updateUnitRecycle/
updateBuildingRecycle below tick it down from sim.js) so scrapping something
under fire is a real risk, not a free undo button: the target stays fully
itself and fully targetable the whole time, it just can't act (a unit) or
ever be re-recycled (either) until the timer completes or the player cancels
it (engine/commands.js issueCancelRecycle) β cancelling is free, since nothing
was ever paid or disabled beyond that.
The refund fraction is 40% for ordinary stock, or 50% for pricier
industrial/capital assets β the "depends on type" half of the ask β then
boosted by research up to an 80% cap: the SAME upgrade id ("recycling") in
both the skirmish doctrine tree (engine/entities.js UPGRADES, Logistics tier
3, researched at a Refinery) and the Odyssey tech tree (engine/techtree.js
TECHS, researched at a Datacenter) β whichever mode you're in stamps the same
flag in player.upgrades, so recycleFrac only ever needs to check the one id,
the same way FREIGHTER_AI_TECH is read directly rather than through a
generic multiplier (engine/haul.js).
That fraction applies to the build COST only. Whatever inventory the entity is
physically holding when it finishes recycling β a factory's input larder and
output backlog, a freighter's freight hold, a worker's carried cargo β is
recovered in FULL (recycleValue below), separately: that stock was already
gathered/produced, not spent constructing the thing, so scrapping it shouldn't
also erase whatever it happened to be carrying.
============================================================ */
"use strict";
import { UNITS, BUILDINGS } from "./entities.js";
import { removeEntity } from "./state.js";
export const RECYCLE_TECH = "recycling";
const BASE_FRAC = 0.4;
const ADVANCED_FRAC = 0.5;
const TECH_BONUS = 0.3;
const FRAC_CAP = 0.8;
// A little more than half the entity's own build time to scrap it β quicker to tear down than
// put up, same call most demolition mechanics make β floored so even the cheapest worker isn't a
// free instant cash-out.
const MIN_RECYCLE_TIME = 3;
const RECYCLE_TIME_FRAC = 0.5;
// A building counts as "advanced" by the exact same signal isElectrifiable already uses to mark
// it as real industrial/strategic infrastructure rather than a basic shell: it runs a recipe,
// grants Power, digs a rig, is the endgame wonder, or burns fuel (Combustion Generator).
function isAdvancedBuilding(def) {
return !!(def.recipe || def.energyGrants || def.rig || def.wonder || def.combust);
}
// A unit counts as "advanced" if it's a big capital asset β a cargo freighter or a support
// Mender β rather than disposable line troops or plain labour.
function isAdvancedUnit(def) {
return def.role === "freighter" || def.role === "support";
}
function defOf(entity) {
return entity.kind === "building" ? BUILDINGS[entity.type] : UNITS[entity.type];
}
/** The fraction (0.4-0.8) of `entity`'s cost recycling it refunds right now, for its owner.
* @param {State} state @param {string} owner @param {Unit|Building} entity @returns {number} */
export function recycleFrac(state, owner, entity) {
const def = defOf(entity);
if (!def) return 0;
const advanced = entity.kind === "building" ? isAdvancedBuilding(def) : isAdvancedUnit(def);
const base = advanced ? ADVANCED_FRAC : BASE_FRAC;
const researched = !!state.players[owner]?.upgrades[RECYCLE_TECH];
return Math.min(FRAC_CAP, researched ? base + TECH_BONUS : base);
}
// A Command Center can't be recycled β scrapping your only (or, mid-Odyssey, any) base for parts
// is a footgun with no play value, unlike everything else, which is a real, rebuildable choice.
/** @param {Unit|Building} entity @returns {boolean} */
export function canRecycle(entity) {
const def = defOf(entity);
if (!def || entity.recycling) return false;
if (entity.kind === "building") return !entity.constructing && !def.isCommandCenter;
return true;
}
// Start recycling `entity` in place. Pure state mutation β engine/commands.js's issueRecycle
// checks ownership/canRecycle and handles the unit-order-dispatch side (squad release, clearing
// Hold) before calling this.
/** @param {Unit|Building} entity @returns {void} */
export function beginRecycle(entity) {
const def = defOf(entity);
const time = Math.max(MIN_RECYCLE_TIME, (def.buildTime || 0) * RECYCLE_TIME_FRAC);
entity.recycling = { progress: 0, time };
if (entity.kind === "unit") { entity.order = { type: "recycle" }; entity.orderQueue = []; }
}
// Cancel an in-progress recycle β free: the target was never disabled beyond refusing new orders
// (a building) or acting at all (a unit), so this just drops the timer, no partial refund.
/** @param {Unit|Building} entity @returns {void} */
export function cancelRecycle(entity) {
entity.recycling = null;
if (entity.kind === "unit" && entity.order?.type === "recycle") entity.order = null;
}
// Every commodity a recycle would return RIGHT NOW, merged into one commodityβqty map: the
// type/research-scaled fraction of its build cost (recycleFrac), PLUS every bit of its own
// current inventory at full (1:1) value β a factory's input larder and output backlog
// (engine/haul.js), a freighter's freight hold, a worker's carried cargo. That inventory is stock
// the player already gathered or produced, sitting in the thing being scrapped rather than spent
// on building it, so recycling recovers all of it rather than letting it vanish with the
// structure. The single source of truth for both the actual bank (below) and the HUD's refund
// preview (hudSelection.js), so the two can never drift apart.
/** @param {State} state @param {Unit|Building} entity @returns {Resources} */
export function recycleValue(state, entity) {
const def = defOf(entity);
const out = /** @type {Resources} */ ({});
const add = (com, qty) => { if (com && qty > 0) out[com] = (out[com] || 0) + qty; };
if (def?.cost) {
const frac = recycleFrac(state, entity.owner, entity);
for (const [com, qty] of Object.entries(def.cost)) add(com, qty * frac);
}
const addBuffer = buf => { if (buf) for (const com in buf) add(com, buf[com] || 0); };
// Probe all four inventory buffers off ONE widened reference rather than narrowing by
// entity.kind: a Building carries store/input, a Unit carries freight/cargo, and addBuffer
// ignores whichever are absent. Keeping it a single unconditional pass means a future entity
// type that carries both still refunds both β narrowing by kind would silently skip one.
const inv = /** @type {{store?: Resources, input?: Resources, freight?: Resources, cargo?: Cargo}} */ (entity);
addBuffer(inv.store);
addBuffer(inv.input);
addBuffer(inv.freight);
if (inv.cargo) add(inv.cargo.com, inv.cargo.qty || 0);
return out;
}
function grantRecycleValue(state, entity) {
const res = state.players[entity.owner].resources;
for (const [com, qty] of Object.entries(recycleValue(state, entity))) res[com] = (res[com] || 0) + qty;
}
/** Advance a recycling BUILDING by dt β called from sim.js's per-building loop. A no-op unless
* it's actually recycling; removes + refunds it once the timer completes.
* @param {State} state @param {Building} building @param {number} dt @returns {void} */
export function updateBuildingRecycle(state, building, dt) {
if (!building.recycling) return;
building.recycling.progress += dt / building.recycling.time;
if (building.recycling.progress >= 1) {
grantRecycleValue(state, building);
removeEntity(state, building.id);
state.events.push({ type: "recycled", x: building.x, y: building.y, owner: building.owner });
}
}
/** Advance a recycling UNIT by dt. Called early from sim.js's updateUnit, same convention as
* updateBombFuse: returns true whenever the unit is mid-recycle (so the caller skips every other
* per-tick behaviour β movement, combat, gather, haul β for it this tick), false otherwise.
* @param {State} state @param {Unit} unit @param {number} dt @returns {boolean} */
export function updateUnitRecycle(state, unit, dt) {
if (!unit.recycling) return false;
unit.recycling.progress += dt / unit.recycling.time;
if (unit.recycling.progress >= 1) {
grantRecycleValue(state, unit);
removeEntity(state, unit.id);
state.events.push({ type: "recycled", x: unit.x, y: unit.y, owner: unit.owner });
}
return true;
}
|