{ "_comment": "ASTEROID CLUSTERS — loose groups of slowly tumbling rocks drifting in the system's void. texture = spritesheet of frameWidth×frameHeight asteroid frames (frameCount frames). cluster = how a group looks & moves: groupSize = rocks per cluster (minFullSize guarantees at least one full-size rock), sizes = per-rock diameter px, spread = how far a rock may sit from the cluster center, gapFactor = the small edge-gap between ANY two rocks, as a multiple of their combined radii (1.0 = touching, 1.12 = a subtle gap — the generator pushes apart just the overlapping pairs, each by half, so groups stay compact), spin = each rock's OWN very slow tumble (deg/s, direction per-rock random), groupSpin = the whole loose group drifts around its center (deg/s), tint = subtle warm/cool starlight per cluster (anchors blended toward white by strength), halo = soft glow behind the group, debris = a halo of fine dust motes orbiting just outside the rocks. distribution = how many clusters a NON-barren system gets: targetObjects − planetCount, ±jitter, clamped to [minClusters, maxClusters] (more planets ⇒ fewer clusters); the starting system always gets at least startingSystemMinClusters, and those first ones are placed INSIDE the player's initial tether so they're reachable from the spawn. barren = the dead-end (gate-only) systems: the cluster drifts INSIDE the arrival gate's level-1 tether (tether.json → level1Radius, minus the cluster's extent + tetherMargin) — the stop's only payload: mine it, then head back (clusters = how many, minRadius = nearest a cluster center may sit to the gate, px). placement = where clusters may sit: minObjectSpacing = no cluster may be closer (center-to-center) than this to ANY other object (home world, planets, free-space stations, jump gates, other clusters); minRadius/maxRadius = the annulus around the origin random clusters live in (the starting system's tether-inside clusters and the barren gate clusters are exempt — they sit in their own tether annulus); tetherMargin = how far inside the tether rim a tether-bound cluster must sit (whole group stays reachable). shipClearance = how close (edge-to-edge) the ship may get to a rock — clusters are solid, like worlds.", "enabled": true, "texture": "assets/images/asteroids.png", "frameWidth": 128, "frameHeight": 128, "frameCount": 10, "typeLabel": "Asteroid Cluster", "compassColor": "#c8d2e0", "shipClearance": 50, "mining": { "_comment": "MINING (v0.4) — the ship's energy arm. armExtendMs = the 'Extending Mining Arm...' reach before the beam fires (the ship is locked while it extends). beam = the arm's line: inMs/outMs = the beam extends (ship→rock) / retracts (rock→ship); glowWidth/midWidth/mainWidth/coreWidth = the white core in its blue glow (px, additive under the core); ghostOffset = the console RGB fringe split off the beam sides. ore = the mineral stream: motes ride the beam rock→ship (speedPxPerSec along the beam), each with a travelling wobble + a tail, absorbed at the hull. impact = the molten bite point: glowScale × rock radius (glowMinPx floor), the hot core, shock rings, and chipping sparks. surge = the big ore packets riding the beam home (the arm brightens while one is in flight). popup = the context menu a rock click opens (world-anchored at the click). economy = the rock IS its minerals: its size (width = height, px) at the start of mining is the minerals it holds; ratePerSec = minerals extracted per second (× the ship's stats.miningSpeed multiplier) — each mineral shrinks the rock by 1 px (width AND height; the beam tracks the live rock, so the impact point stays latched as it shrinks) and loads the ship's hold (capped at stats.mineralStorage — a full hold ends the run). splitLostFraction = once the rock has lost this fraction of its ORIGINAL size it breaks: ≤ smallSizeMaxPx it shatters into smallPieces of floor(size × pieceFraction) px each and ALL pieces are sucked into the ship (their minerals land on impact, capped); > smallSizeMaxPx it splits in HALF (floor(size/2) each, both starting fresh with their own original size) and the beam re-latches onto one half to keep mining. suckSpeedPxPerSec / suckStaggerMs = the shattered pieces' ride home. Every break plays sfx mining_split.", "armExtendMs": 1500, "beam": { "inMs": 420, "outMs": 380, "glowWidth": 30, "glowColor": "#2f7bff", "glowAlpha": 0.1, "midWidth": 15, "midColor": "#00b3ff", "midAlpha": 0.22, "mainWidth": 7, "mainColor": "#00e5ff", "mainAlpha": 0.5, "coreWidth": 2.6, "coreColor": "#ffffff", "coreAlpha": 0.95, "ghostOffset": 2.6, "ghostCyan": "#00e5ff", "ghostMag": "#ff2d6f", "ghostAlpha": 0.2 }, "ore": { "maxParticles": 28, "spawnEveryMs": [55, 110], "speedPxPerSec": [140, 280], "wobblePx": [1.5, 4], "sizePx": [5, 12], "tailPx": [8, 16] }, "impact": { "glowScale": 0.6, "glowMinPx": 30, "coreSizePx": 12, "ringEveryMs": [550, 1100], "sparkEveryMs": [90, 240] }, "surge": { "enabled": true, "everyMs": [1800, 4200], "durationMs": 320, "bandLen": 46 }, "popup": { "width": 248, "padTop": 10, "padBottom": 12, "gap": 8, "headerHeight": 20, "lift": 14 }, "economy": { "ratePerSec": 1, "splitLostFraction": 0.33, "smallSizeMaxPx": 64, "smallPieces": 4, "pieceFraction": 0.25, "suckSpeedPxPerSec": 460, "suckStaggerMs": 70 } }, "cluster": { "groupSize": { "min": 4, "max": 8 }, "sizes": { "min": 64, "max": 128 }, "minFullSize": 1, "spread": 140, "gapFactor": 1.12, "spin": { "minDegPerSec": 0.3, "maxDegPerSec": 1.6 }, "groupSpin": { "minDegPerSec": 0.12, "maxDegPerSec": 0.4 }, "tint": { "enabled": true, "anchors": ["#dfe9ff", "#ffe9d6", "#e8e4f8"], "strength": 0.5 }, "halo": { "enabled": true, "scale": 2.8, "alpha": 0.26, "color": "#7fa8d8" }, "debris": { "enabled": true, "count": [18, 36], "inner": 0.9, "outer": 2.4, "size": [2.5, 6], "alpha": [0.4, 0.85] } }, "distribution": { "targetObjects": 9, "jitter": 1, "minClusters": 1, "maxClusters": 6, "startingSystemMinClusters": 2 }, "barren": { "clusters": [1, 2], "minRadius": 1024 }, "placement": { "minObjectSpacing": 1024, "minRadius": 2048, "maxRadius": 18432, "tetherMargin": 96, "maxPlacementAttempts": 400 } }