/ concepts / ship-data.jsx
ship-data.jsx
  1  // Shared ship data + deterministic pseudo-random helpers.
  2  // Every ship shown across concepts is seeded, so the same serial always
  3  // produces the same specs — selling the "infinite catalog" feel.
  4  
  5  function mulberry32(seed) {
  6    let a = seed >>> 0;
  7    return function () {
  8      a = (a + 0x6D2B79F5) >>> 0;
  9      let t = a;
 10      t = Math.imul(t ^ (t >>> 15), t | 1);
 11      t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
 12      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
 13    };
 14  }
 15  function hashStr(s) {
 16    let h = 2166136261 >>> 0;
 17    for (let i = 0; i < s.length; i++) {
 18      h ^= s.charCodeAt(i);
 19      h = Math.imul(h, 16777619);
 20    }
 21    return h >>> 0;
 22  }
 23  function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
 24  function range(rng, a, b, step = 1) {
 25    const v = a + rng() * (b - a);
 26    return Math.round(v / step) * step;
 27  }
 28  
 29  // Ship classes, tiers, manufacturers.
 30  const CLASSES = [
 31    { code: "IC", name: "Interceptor",   role: "Light combat",      hull: [40,  80]  },
 32    { code: "FR", name: "Frigate",       role: "Escort",            hull: [200, 400] },
 33    { code: "CH", name: "Cargo Hauler",  role: "Logistics",         hull: [500, 1200]},
 34    { code: "YT", name: "Pleasure Yacht",role: "Civilian",          hull: [120, 300] },
 35    { code: "MN", name: "Mining Rig",    role: "Industrial",        hull: [800, 2000]},
 36    { code: "CR", name: "Cruiser",       role: "Heavy combat",      hull: [1500,3000]},
 37    { code: "EX", name: "Explorer",      role: "Survey / long-range",hull:[300, 700] },
 38    { code: "DR", name: "Dreadnought",   role: "Capital",           hull: [4000,9000]},
 39    { code: "SC", name: "Science Vessel",role: "Research",          hull: [250, 500] },
 40    { code: "GB", name: "Gunboat",       role: "Fast attack",       hull: [100, 220] },
 41  ];
 42  
 43  const MFG = [
 44    { code: "KRV", name: "Krovas Yards",         loc: "Ceres-II"      },
 45    { code: "AUR", name: "Aurelia Dynamics",     loc: "High Solis"    },
 46    { code: "OBS", name: "Obsidian Forge",       loc: "Erebus Belt"   },
 47    { code: "HEL", name: "Helion Shipworks",     loc: "Tau-Ceti Outer"},
 48    { code: "VNT", name: "Vantari Collective",   loc: "Kepler Ring"   },
 49    { code: "NXS", name: "Nexus Propulsion",     loc: "Luna Sector 4" },
 50    { code: "KNT", name: "Kontos & Sons",        loc: "Mars Orbital"  },
 51    { code: "IOTA",name: "Iota Stellar",         loc: "Proxima Gate"  },
 52  ];
 53  
 54  const DRIVES = ["Fusion Torch", "Ion Rift", "Quantum Fold", "Antimatter Core", "Plasma Pulse", "Grav-Slip"];
 55  const FACTIONS = ["Coalition", "Free-Port", "Syndicate", "Civilian", "Independent"];
 56  const AVAIL = ["IN STOCK", "PRE-ORDER", "BUILD TO ORDER", "LAST UNIT", "BROKERED"];
 57  const HULL_COND = ["FACTORY SEALED", "CERTIFIED", "OVERHAULED", "SALVAGED"];
 58  
 59  const NAME_PREFIX = ["Vel","Kor","Ara","Myr","Zen","Drax","Lyr","Orn","Vex","Tal","Noct","Hes","Cyr","Fen","Ixo","Sol","Rho","Thal","Umb","Eph"];
 60  const NAME_SUFFIX = ["-strike","-wing","-arc","-spire","-crest","-forge","-hawk","-veil","-rune","-ward","-thorn","-drift","-fang","-void","-mark"];
 61  
 62  function makeShip(seedStr) {
 63    const rng = mulberry32(hashStr(seedStr));
 64    const cls = pick(rng, CLASSES);
 65    const mfg = pick(rng, MFG);
 66    const drive = pick(rng, DRIVES);
 67    const faction = pick(rng, FACTIONS);
 68    const avail = pick(rng, AVAIL);
 69    const cond = pick(rng, HULL_COND);
 70    const serial = `${mfg.code}-${cls.code}-${(Math.floor(rng()*9000)+1000)}-${String.fromCharCode(65+Math.floor(rng()*26))}${String.fromCharCode(65+Math.floor(rng()*26))}`;
 71    const model = `${pick(rng, NAME_PREFIX)}${pick(rng, NAME_SUFFIX)} ${pick(rng, ["Mk.II","Mk.III","Mk.IV","R","X","XR","Tactical","Civic","Export"])}`;
 72    const mass = Math.round(range(rng, cls.hull[0], cls.hull[1], 1));
 73    const length_m = Math.round(Math.pow(mass, 0.48) * (6 + rng()*4));
 74    const crew = Math.max(1, Math.round(Math.pow(mass, 0.42) * (0.3 + rng()*0.6)));
 75    const price = Math.round(mass * (80 + rng()*450)) * 1000;
 76    const topSpeed = Math.round(600 + rng()*2400); // m/s sublight
 77    const jumpRange = +(2 + rng()*38).toFixed(1);
 78    const shields = Math.round(400 + rng()*1600);
 79    const armor = Math.round(200 + rng()*1800);
 80    const maneuver = Math.round(20 + rng()*80);
 81    const thrust = Math.round(30 + rng()*70);
 82    const signature = Math.round(10 + rng()*90);
 83    const cargo_t = Math.round(range(rng, mass*0.2, mass*2, 1));
 84    const hardpoints = Math.floor(rng()*8);
 85    const year = 2382 + Math.floor(rng()*14);
 86    const delivery = Math.max(1, Math.round(rng()*28));
 87    const warranty = pick(rng, ["STANDARD 10Y", "EXTENDED 25Y", "AS-IS", "LIMITED 5Y", "FULL FLEET"]);
 88  
 89    return {
 90      seed: seedStr, rng: mulberry32(hashStr(seedStr)),
 91      cls, mfg, drive, faction, avail, cond, serial, model,
 92      mass, length_m, crew, price, topSpeed, jumpRange,
 93      shields, armor, maneuver, thrust, signature, cargo_t, hardpoints,
 94      year, delivery, warranty,
 95    };
 96  }
 97  
 98  // Deterministic-random ship silhouette renderers. Stylized — not trying to be a
 99  // 3D model. Each returns SVG paths based on class + seed.
100  function ShipSilhouette({ ship, stroke = "#5df0ff", fill = "none", glow = true, strokeWidth = 1.2, detail = "full" }) {
101    const rng = mulberry32(hashStr(ship.seed + "::silhouette"));
102    const w = 480, h = 240;
103    const cx = w/2, cy = h/2;
104    const kind = ship.cls.code;
105  
106    // Body length/width depends on class
107    const bodyL = 0.4 + rng()*0.35;  // 0.4..0.75 of width
108    const bodyW = 0.12 + rng()*0.18; // 0.12..0.30 of height
109    const L = w*bodyL, W = h*bodyW;
110  
111    // Wing sweep
112    const sweep = 0.3 + rng()*0.6;
113    const wingSpan = 0.6 + rng()*0.35;
114    const wingChord = 0.2 + rng()*0.3;
115  
116    const paths = [];
117  
118    // Build a body polygon based on class
119    const bodyPts = [];
120    if (["IC","GB","FR"].includes(kind)) {
121      // Sleek dart
122      bodyPts.push([cx - L/2, cy]);
123      bodyPts.push([cx - L/2 + L*0.15, cy - W/2]);
124      bodyPts.push([cx + L/2 - L*0.1, cy - W/2.5]);
125      bodyPts.push([cx + L/2, cy]);
126      bodyPts.push([cx + L/2 - L*0.1, cy + W/2.5]);
127      bodyPts.push([cx - L/2 + L*0.15, cy + W/2]);
128    } else if (["CH","MN","DR","CR"].includes(kind)) {
129      // Boxy / capital
130      bodyPts.push([cx - L/2, cy - W/2]);
131      bodyPts.push([cx + L/2 - L*0.08, cy - W/2]);
132      bodyPts.push([cx + L/2, cy - W/3]);
133      bodyPts.push([cx + L/2, cy + W/3]);
134      bodyPts.push([cx + L/2 - L*0.08, cy + W/2]);
135      bodyPts.push([cx - L/2, cy + W/2]);
136      bodyPts.push([cx - L/2 - L*0.06, cy]);
137    } else if (["YT","EX","SC"].includes(kind)) {
138      // Spindle
139      bodyPts.push([cx - L/2, cy]);
140      bodyPts.push([cx - L/2 + L*0.25, cy - W/2]);
141      bodyPts.push([cx + L/2 - L*0.2, cy - W/2]);
142      bodyPts.push([cx + L/2, cy]);
143      bodyPts.push([cx + L/2 - L*0.2, cy + W/2]);
144      bodyPts.push([cx - L/2 + L*0.25, cy + W/2]);
145    }
146    paths.push(<polygon key="body" points={bodyPts.map(p=>p.join(",")).join(" ")} fill={fill} stroke={stroke} strokeWidth={strokeWidth}/>);
147  
148    // Wings (not for capitals)
149    if (!["DR","CR","MN","CH"].includes(kind)) {
150      const wy = cy;
151      const wx0 = cx - L*0.1;
152      const wx1 = cx - L*0.5 + L*wingChord;
153      const span = h*wingSpan/2;
154      const sweepX = L*sweep*0.3;
155      const wingTop = [
156        [wx0, wy - W/2],
157        [wx0 - sweepX, wy - span],
158        [wx1 - sweepX, wy - span],
159        [wx1, wy - W/2],
160      ];
161      const wingBot = wingTop.map(p => [p[0], 2*cy - p[1]]);
162      paths.push(<polygon key="wt" points={wingTop.map(p=>p.join(",")).join(" ")} fill={fill} stroke={stroke} strokeWidth={strokeWidth}/>);
163      paths.push(<polygon key="wb" points={wingBot.map(p=>p.join(",")).join(" ")} fill={fill} stroke={stroke} strokeWidth={strokeWidth}/>);
164    }
165  
166    // Engines at rear
167    const enCount = 2 + Math.floor(rng()*3);
168    for (let i = 0; i < enCount; i++) {
169      const ey = cy - W/2 + (W/(enCount+1))*(i+1) - (W*0.04);
170      const ex = cx - L/2;
171      paths.push(<rect key={"en"+i} x={ex-10} y={ey} width={12} height={W*0.08} fill={stroke} opacity={0.7}/>);
172      if (detail !== "min") {
173        paths.push(<line key={"et"+i} x1={ex-14} y1={ey+W*0.04} x2={ex-34-rng()*30} y2={ey+W*0.04} stroke={stroke} strokeWidth={0.8} opacity={0.5} strokeDasharray="3,3"/>);
174      }
175    }
176  
177    // Hardpoints / detail ticks
178    if (detail === "full") {
179      const ticks = 4 + Math.floor(rng()*6);
180      for (let i = 0; i < ticks; i++) {
181        const tx = cx - L/2 + rng()*L;
182        const ty = cy + (rng() > 0.5 ? -1 : 1) * W * (0.3 + rng()*0.15);
183        paths.push(<circle key={"t"+i} cx={tx} cy={ty} r={1.5} fill={stroke}/>);
184      }
185      // Cockpit
186      paths.push(<ellipse key="cpt" cx={cx + L*0.25} cy={cy} rx={L*0.05} ry={W*0.22} fill={fill} stroke={stroke} strokeWidth={strokeWidth}/>);
187      // Hull segment lines
188      const segs = 2 + Math.floor(rng()*3);
189      for (let i = 1; i <= segs; i++) {
190        const x = cx - L/2 + (L/(segs+1))*i;
191        paths.push(<line key={"hs"+i} x1={x} y1={cy-W*0.35} x2={x} y2={cy+W*0.35} stroke={stroke} strokeWidth={0.5} opacity={0.45}/>);
192      }
193    }
194  
195    const id = "glow_" + (hashStr(ship.seed) % 100000);
196    return (
197      <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" style={{width:"100%", height:"100%", display:"block", filter: glow ? `drop-shadow(0 0 8px ${stroke}) drop-shadow(0 0 2px ${stroke})` : "none"}}>
198        {paths}
199      </svg>
200    );
201  }
202  
203  function formatCred(n) {
204    if (n >= 1e9) return (n/1e9).toFixed(2) + "B";
205    if (n >= 1e6) return (n/1e6).toFixed(2) + "M";
206    if (n >= 1e3) return (n/1e3).toFixed(1) + "k";
207    return String(n);
208  }
209  
210  Object.assign(window, { makeShip, ShipSilhouette, formatCred, mulberry32, hashStr });