Spaces:
Paused
Paused
Download engine/rig.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/engine/rig.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities/engine/rig.js
-
curl -L -o rig.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/engine/rig.js
13 kB
| /* ============================================================ | |
| 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. | |
| ============================================================ */ | |
| ; | |
| 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 | |
| }; | |
| } | |