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