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;
}