Spaces:
Paused
Paused
File size: 13,019 Bytes
db667bc eaccb6e 3e3db8e db667bc 91ad84b db667bc 3e3db8e db667bc 6772f54 db667bc 6772f54 db667bc 6772f54 db667bc 6772f54 db667bc 6772f54 b22641b 6772f54 b22641b 6772f54 b22641b 6772f54 db667bc 91ad84b db667bc b22641b db667bc 3e3db8e db667bc b22641b 8cd6f2e db667bc eaccb6e 8cd6f2e 91ad84b eaccb6e db667bc 8cd6f2e db667bc 3e3db8e db667bc b22641b 6772f54 db667bc 91ad84b db667bc b22641b db667bc eaccb6e db667bc 91ad84b db667bc | 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 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | /* ============================================================
PLASMA RIG (Odyssey) β deep-core extraction. Unlike a resource node (which is
a finite deposit workers haul from), a Plasma Rig is an UNLIMITED, perpetual
source of one raw commodity, banked straight into the owner's stockpile.
It digs in cycles at a PROBABILISTIC yield tier β low, medium, good, or
overwhelming β whose odds rise with how rich the rig's spot (and the planet)
are, so WHERE you build it matters. The "randomness" is fully deterministic:
every roll is a hash of the rig's id + its dig counter (engine/rng.js hashStr),
so a same-seed galaxy strikes the same seams on a replay β the engine's
determinism law holds (no unseeded RNG, no wall clock).
Costs, so it's an engine, not free money: it draws heavy Power (the plasma arc
β routed through industry.js's grid, so it competes with factories and the Gate
and its digs slow when power is short), and it burns radioactives per dig
("nuclear to exploit"). Odyssey-only: updatePlasmaRig is a no-op for any
building without a `rig` def, so the skirmish tick is untouched.
Ice coolant (industry.js iceCoolantMult): with any ice banked in the treasury,
both those costs are halved β the nuclear burn per dig here, and the plasma
arc's Power draw over in industry.js's powerDraw. Running the coolant isn't
free either: industry.js's chargeIceUpkeep drains a little ice per tick for
as long as a dig actually completes, so the discount lasts only as long as
the ice supply does.
============================================================ */
"use strict";
import { BUILDINGS, storeTotal, storeRoom, storeCapOf } from "./entities.js";
import { hashStr } from "./rng.js";
import { powerThrottle, cachedPowerThrottle, planetIndustryScale, iceCoolantMult, chargeIceUpkeep } from "./industry.js";
// The raw commodities a rig can strike. Which one a given rig mines β its VEIN β is chosen by WHERE
// it's built: the SURFACE deposits nearby bias what lies below (a rig among ore fields usually
// strikes ore), so late-game placement is an educated guess off the visible map rather than a blind
// gamble. A barren spot falls back to a position hash, so it always strikes SOMETHING.
export const PLASMA_VEINS = ["ore", "crystals", "radioactives", "ice", "gas", "biomass"];
const VEIN_SET = new Set(PLASMA_VEINS);
// How far out a rig "reads" the surface. Deposits within this radius hint at the seam below; the
// hint fades linearly with distance. Nudged with placement, not the sim, so it's a UX-scale number.
export const SURVEY_RADIUS = 280;
const BASE_VEIN_WEIGHT = 0.05; // every vein's hashed floor β keeps a barren spot a gamble and leaves room for a surprise
const RICH_DENSITY_SCALE = 600; // node.max is ~hundreds; this soft-caps surface density into [0,1)
// Yield tiers: each dig strikes one, its multiplier over the rig's base yield, and the cumulative
// probability boundary a richness-biased roll must fall under. Richer ground reaches the fat tiers.
export const YIELD_TIERS = [
{ name: "low", mult: 1, p: 0.44 },
{ name: "medium", mult: 2.4, p: 0.74 },
{ name: "good", mult: 4.5, p: 0.93 },
{ name: "overwhelming", mult: 9, p: 1 },
];
const RICH_CELL = 96; // richness varies on this grid β a rig's tile has its own luck
const MAX_CYCLES_PER_TICK = 4; // a real tick advances <1 cycle; a cap guards a huge dt or a tampered save
const frac01 = s => (hashStr(s) % 100000) / 100000; // a stable pseudo-random [0,1) from a string
// Read the surface at (x, y): a proximity-weighted tally of nearby PLASMA-vein deposits. `weights`
// is per-commodity (which raws are nearby, and how close), `density` folds in each deposit's SIZE
// (node.max β the ORIGINAL amount, which never depletes, so the reading is STABLE as nodes are mined
// out). Pure over the given node list, so the sim can survey ALL nodes (deterministic) while the UI
// surveys only the ones the player can see (an honest guess β a hidden cache stays a surprise).
function surfaceSurvey(nodes, x, y) {
const weights = {};
let density = 0;
for (const n of nodes) {
if (!VEIN_SET.has(n.com)) continue;
const d = Math.hypot(n.x - x, n.y - y);
if (d >= SURVEY_RADIUS) continue;
const prox = 1 - d / SURVEY_RADIUS; // 1 right on top of it, 0 at the survey edge
weights[n.com] = (weights[n.com] || 0) + prox;
density += prox * (n.max || 0);
}
return { weights, density };
}
// Seam richness in [0,1) from surface density blended with the old per-TILE / per-PLANET hash β so a
// resource-dense spot digs richer, but two dense spots still differ (luck) and some worlds run richer
// all over (the core). Shared by the sim and the placement survey.
function richnessFrom(density, planetId, x, y) {
const surface = density / (density + RICH_DENSITY_SCALE); // soft-cap to [0,1)
const tile = frac01(`${planetId}:${Math.floor(x / RICH_CELL)}:${Math.floor(y / RICH_CELL)}`);
const core = frac01(`core:${planetId}`);
return Math.min(1, 0.6 * surface + 0.4 * (0.55 * tile + 0.45 * core));
}
function richLabelFor(r) {
return r < 0.35 ? "poor" : r < 0.6 ? "fair" : r < 0.82 ? "rich" : "mother lode";
}
// The weighted-random vein pick given an already-surveyed `weights` tally β factored out of rigVein
// so a caller that needs BOTH the vein and the richness of the same spot (see surveySpot below) can
// share a single surfaceSurvey scan instead of each public helper re-surveying identical ground.
function veinFromWeights(weights, x, y) {
const key = `${Math.round(x)},${Math.round(y)}`;
let total = 0;
const scored = PLASMA_VEINS.map(com => {
const w = (weights[com] || 0) + BASE_VEIN_WEIGHT * (0.5 + frac01(`${key}:${com}`));
total += w;
return { com, w };
});
let roll = frac01(key) * total;
for (const e of scored) { roll -= e.w; if (roll <= 0) return e.com; }
return scored[scored.length - 1].com;
}
/**
* Which raw a rig strikes β a deterministic weighted-random pick BIASED by the surface: nearby
* deposits weight their own commodity, so a rig among ore fields usually strikes ore, but a hashed
* floor on every vein leaves room for a surprise (and makes a barren spot a genuine gamble). Stable
* for a given rig: the weights come from node positions/sizes, which never move or deplete.
*/
export function rigVein(state, building) {
const { weights } = surfaceSurvey(state.map?.nodes || [], building.x, building.y);
return veinFromWeights(weights, building.x, building.y);
}
/** The richness of a rig's spot in [0,1), surface-biased. Pure β no clock, no unseeded RNG. */
export function locationRichness(state, x, y) {
const { density } = surfaceSurvey(state.map?.nodes || [], x, y);
return richnessFrom(density, state.planetId, x, y);
}
// The vein AND richness of the SAME spot, sharing one surfaceSurvey scan β surfaceSurvey walks every
// map node, so a rig tick that needs both (updatePlasmaRig, rigInfo) surveyed the identical ground
// twice calling rigVein then locationRichness separately. Byte-identical to that pair: same
// surfaceSurvey inputs, same veinFromWeights/richnessFrom math, just computed from one scan instead
// of two. Public rigVein/locationRichness are untouched β still the right call when only one is needed.
function surveySpot(state, x, y) {
const { weights, density } = surfaceSurvey(state.map?.nodes || [], x, y);
return { vein: veinFromWeights(weights, x, y), richness: richnessFrom(density, state.planetId, x, y) };
}
/**
* A placement-time READING of the surface for the player: from a given node list (pass the VISIBLE
* ones), the most-likely vein below, how confident that read is, and the seam richness. `likelyVein`
* is null when no deposits are in range β a blind spot. Distinct from rigVein: this is the best guess
* off the visible map; the actual strike is the weighted roll over ALL nodes, so it can still surprise.
*/
export function rigSurvey(nodes, planetId, x, y) {
const { weights, density } = surfaceSurvey(nodes, x, y);
let likelyVein = null, best = 0, sum = 0;
for (const com of PLASMA_VEINS) { const w = weights[com] || 0; sum += w; if (w > best) { best = w; likelyVein = com; } }
const richness = richnessFrom(density, planetId, x, y);
return { likelyVein, confidence: sum > 0 ? best / sum : 0, richness, richLabel: richLabelFor(richness) };
}
/** The tier a given dig strikes β a per-dig roll (stable from the rig id + dig counter) biased up by richness. */
export function rollTier(building, richness) {
const roll = frac01(`${building.id}:${building.digCount || 0}`);
const score = Math.min(0.99999, roll * (0.6 + 0.4 * richness) + 0.35 * richness);
for (const t of YIELD_TIERS) if (score < t.p) return t;
return YIELD_TIERS[YIELD_TIERS.length - 1];
}
/**
* Advance one Plasma Rig by dt. The plasma arc's Power draw (industry.js powerDraw) throttles the
* dig speed; each completed cycle burns radioactives and banks a probabilistic tier of the rig's
* vein into its FINITE output buffer (building.store, capped at storeCap). When the buffer is full
* the rig stalls at the brink until a worker hauls it to a Command Center (engine/haul.js) β it's
* an unlimited SOURCE, not an unlimited SINK. Deterministic. A no-op for any building without a
* `rig` def.
*/
export function updatePlasmaRig(state, building, dt) {
const def = BUILDINGS[building.type];
if (building.constructing || !def || !def.rig || building.paused) return;
const rig = def.rig;
const throttle = cachedPowerThrottle(state, building.owner); // short power β slower digs; owner-wide, cached for this tick (industry.js)
if (throttle <= 0) return;
const res = state.players[building.owner].resources;
const iceMult = iceCoolantMult(state, building.owner);
const nuclearCost = rig.nuclear * iceMult; // banked ice halves the nuclear burn per dig
building.digProgress = (building.digProgress || 0) + (dt / rig.digTime) * planetIndustryScale(state) * throttle;
const { vein, richness } = surveySpot(state, building.x, building.y); // one surface scan shared by both
// BOTH sides pile the dig into a FINITE output buffer (building.store) that workers must haul off to
// a Command Center β it stalls at the brink when full. The AI's workers haul its rig exactly like
// the player's (engine/ai.js assignAiLogistics / engine/haul.js), so it's an unlimited SOURCE, not
// an unlimited SINK, for either owner. Deterministic; a no-op for any building without a `rig` def.
let cycles = 0;
while (building.digProgress >= 1 && cycles < MAX_CYCLES_PER_TICK) {
if ((res.radioactives || 0) < nuclearCost) { building.digProgress = 1; break; } // out of nuclear β stall at the brink
const room = storeRoom(building);
if (room <= 1e-9) { building.digProgress = 1; break; } // output buffer full β stall until hauled
const tier = rollTier(building, richness);
const amount = Math.min(rig.base * tier.mult, room); // top off to exactly full on the last dig (overflow spills)
res.radioactives -= nuclearCost;
building.digProgress -= 1;
building.digCount = (building.digCount || 0) + 1;
building.store = building.store || {};
building.store[vein] = (building.store[vein] || 0) + amount; // into the finite buffer, hauled to a CC later
building.lastTier = tier.name; // transient display state, for the HUD
building.lastYield = amount;
state.events.push({ type: "rigDig", com: vein, amount, tier: tier.name, x: building.x, y: building.y, owner: building.owner });
cycles++;
}
// The coolant itself costs ice while it's actively cutting the nuclear bill β charged once per
// tick (not per dig) the same flat rate a factory/power station pays, regardless of how many of
// this tick's (up to MAX_CYCLES_PER_TICK) digs completed.
if (cycles > 0 && iceMult < 1) chargeIceUpkeep(state, building.owner, dt);
}
/** A read-only snapshot of a rig's state for the HUD: what it mines, how rich the spot is, progress, last strike. */
export function rigInfo(state, building) {
const def = BUILDINGS[building.type];
if (!def || !def.rig) return null;
const { vein, richness } = surveySpot(state, building.x, building.y); // one surface scan shared by both
const richLabel = richLabelFor(richness);
const res = state.players[building.owner].resources;
const cap = storeCapOf(building.type);
const stored = storeTotal(building);
return {
vein,
richness, richLabel,
progress: Math.min(1, building.digProgress || 0),
lastTier: building.lastTier || null,
lastYield: building.lastYield || 0,
nuclearOk: (res.radioactives || 0) >= def.rig.nuclear * iceCoolantMult(state, building.owner),
throttle: powerThrottle(state, building.owner),
stored, storeCap: cap,
storeFull: cap > 0 && stored >= cap - 1e-6, // buffer full β the rig is stalled until it's hauled off
};
}
|