Death242 commited on
Commit
64cf76e
·
verified ·
1 Parent(s): cb0e58a

Static site

Browse files
Files changed (4) hide show
  1. index.html +4 -3
  2. static/game.js +0 -0
  3. static/goals.js +4 -1
  4. static/main.js +12 -6
index.html CHANGED
@@ -8,7 +8,7 @@
8
  <meta name="theme-color" content="#10131b">
9
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;800;900&family=Inter:wght@400;500;600;700&family=Noto+Kufi+Arabic:wght@700&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="/static/style.css?v=1793116888">
12
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Crect width=%2764%27 height=%2764%27 rx=%2714%27 fill=%27%23050506%27/%3E%3Ccircle cx=%2732%27 cy=%2732%27 r=%2716%27 fill=%27none%27 stroke=%27%23fff%27 stroke-width=%275%27/%3E%3C/svg%3E">
13
  <script type="importmap">
14
  { "imports": {
@@ -158,6 +158,7 @@
158
  <label class="set">Living city (day/night + weather change)<span class="seg" data-set="cycle"><button data-v="off">Off</button><button data-v="on">On</button></span></label>
159
  <label class="set">Handling<span class="seg" data-set="handling"><button data-v="arcade">Arcade</button><button data-v="realistic">Realistic</button></span></label>
160
  <label class="set">Screen shake and speed blur<span class="seg" data-set="motion"><button data-v="on">On</button><button data-v="off">Off</button></span></label>
 
161
  <label class="set">Text size<span class="seg" data-set="text"><button data-v="normal">Normal</button><button data-v="large">Large</button></span></label>
162
  <label class="set">Touch steering<span class="seg" data-set="steer"><button data-v="stick">Joystick</button><button data-v="buttons">Buttons</button></span></label>
163
  <div class="set save-row">Secret code<span><input id="gCode" maxlength="16" placeholder="type a code" autocomplete="off"><button class="pill" id="gCodeGo">Enter</button></span></div>
@@ -168,7 +169,7 @@
168
  <section class="wide"><h4>Contracts</h4><div class="tiles con-tiles" id="gContracts"></div></section>
169
  <section class="wide"><h4>Routes and ghosts</h4><div class="routes" id="gRoutes"></div></section>
170
  <section class="wide"><h4>Challenges</h4><div class="goals" id="gGoals"></div></section>
171
- <section class="wide"><h4>Controls</h4><p class="small"><b>Drive:</b> W A S D or arrows · Space handbrake/drift · on a jump: A/D spin, X (Drift) backflip · Shift nitro · F fly · L land · C camera · R reset · H horn · T time · M GPS · O autopilot · Enter starts a race at a 🏁 marker · B drops a gate in the route builder · Y hoverboard (Space jump, A/D spin and X kickflip in the air). <b>Mountains:</b> take the highway over the bridge and through Brooklyn; trucks from VOID Motors handle the rough ground best. <b>On foot:</b> stop and press E to get out · WASD walk · Shift run · Space jump · E next to any car to drive it (taking traffic cars brings the police). VOID Motors in the park sells 50 cars. <b>Look:</b> click the game, then move the mouse. <b>Phone:</b> joystick on the left, pedals on the right, drag anywhere to look. <b>Fuel:</b> about 10 min driving, 5 min flying; stop at ⛽ to fill up and repair, or drive through a green 🔧.</p></section>
172
  </div>
173
  <div class="menu-foot"><button class="pill" id="gExit">Exit to home</button></div>
174
  </div>
@@ -215,6 +216,6 @@
215
  <div id="toast" role="status"></div>
216
  <noscript><p style="position:fixed;inset:auto 0 0;padding:16px;background:#111;color:#fff;text-align:center">VOID needs JavaScript for the 3D car.</p></noscript>
217
  <script src="/static/hub.js" data-app="void" defer></script>
218
- <script type="module" src="/static/main.js?v=1793116888"></script>
219
  </body>
220
  </html>
 
8
  <meta name="theme-color" content="#10131b">
9
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;800;900&family=Inter:wght@400;500;600;700&family=Noto+Kufi+Arabic:wght@700&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="/static/style.css?v=1793285738">
12
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Crect width=%2764%27 height=%2764%27 rx=%2714%27 fill=%27%23050506%27/%3E%3Ccircle cx=%2732%27 cy=%2732%27 r=%2716%27 fill=%27none%27 stroke=%27%23fff%27 stroke-width=%275%27/%3E%3C/svg%3E">
13
  <script type="importmap">
14
  { "imports": {
 
158
  <label class="set">Living city (day/night + weather change)<span class="seg" data-set="cycle"><button data-v="off">Off</button><button data-v="on">On</button></span></label>
159
  <label class="set">Handling<span class="seg" data-set="handling"><button data-v="arcade">Arcade</button><button data-v="realistic">Realistic</button></span></label>
160
  <label class="set">Screen shake and speed blur<span class="seg" data-set="motion"><button data-v="on">On</button><button data-v="off">Off</button></span></label>
161
+ <label class="set">Hoverboard<span class="seg" data-set="board"><button data-v="ion">Ion</button><button data-v="blaze">Blaze</button><button data-v="toxic">Toxic</button><button data-v="violet">Violet</button><button data-v="gold">Gold</button><button data-v="ghost">Ghost</button></span></label>
162
  <label class="set">Text size<span class="seg" data-set="text"><button data-v="normal">Normal</button><button data-v="large">Large</button></span></label>
163
  <label class="set">Touch steering<span class="seg" data-set="steer"><button data-v="stick">Joystick</button><button data-v="buttons">Buttons</button></span></label>
164
  <div class="set save-row">Secret code<span><input id="gCode" maxlength="16" placeholder="type a code" autocomplete="off"><button class="pill" id="gCodeGo">Enter</button></span></div>
 
169
  <section class="wide"><h4>Contracts</h4><div class="tiles con-tiles" id="gContracts"></div></section>
170
  <section class="wide"><h4>Routes and ghosts</h4><div class="routes" id="gRoutes"></div></section>
171
  <section class="wide"><h4>Challenges</h4><div class="goals" id="gGoals"></div></section>
172
+ <section class="wide"><h4>Controls</h4><p class="small"><b>Drive:</b> W A S D or arrows · Space handbrake/drift · on a jump: A/D spin, X (Drift) backflip · Shift nitro · F fly · L land · C camera · R reset · H horn · T time · M GPS · O autopilot · Enter starts a race at a 🏁 marker · B drops a gate in the route builder · Y hoverboard: hold Space and let go to ollie, Space again in the air to double jump; in the air A/D spin, X kickflip, Q heelflip, Shift grab; land on a rail to grind (stunt park, boardwalk, mountain foot). <b>Mountains:</b> take the highway over the bridge and through Brooklyn; trucks from VOID Motors handle the rough ground best. <b>On foot:</b> stop and press E to get out · WASD walk · Shift run · Space jump · E next to any car to drive it (taking traffic cars brings the police). VOID Motors in the park sells 50 cars. <b>Look:</b> click the game, then move the mouse. <b>Phone:</b> joystick on the left, pedals on the right, drag anywhere to look. <b>Fuel:</b> about 10 min driving, 5 min flying; stop at ⛽ to fill up and repair, or drive through a green 🔧.</p></section>
173
  </div>
174
  <div class="menu-foot"><button class="pill" id="gExit">Exit to home</button></div>
175
  </div>
 
216
  <div id="toast" role="status"></div>
217
  <noscript><p style="position:fixed;inset:auto 0 0;padding:16px;background:#111;color:#fff;text-align:center">VOID needs JavaScript for the 3D car.</p></noscript>
218
  <script src="/static/hub.js" data-app="void" defer></script>
219
+ <script type="module" src="/static/main.js?v=1793285738"></script>
220
  </body>
221
  </html>
static/game.js CHANGED
The diff for this file is too large to render. See raw diff
 
static/goals.js CHANGED
@@ -15,10 +15,13 @@ export const GOALS = [
15
  { id: "coins", icon: "🪙", name: "Collector", text: "Find 20 hidden coins", stat: "coins", need: 20 },
16
  { id: "taxi", icon: "🚕", name: "Cabbie", text: "Complete 5 taxi fares", stat: "fares", need: 5 },
17
  { id: "tuner", icon: "🔧", name: "Tuner", text: "Buy 4 garage upgrades", stat: "upg", need: 4 },
 
 
 
18
  { id: "brooklyn", icon: "🌉", name: "Bridge and tunnel", text: "Cross the bridge to Brooklyn", stat: "brooklyn", need: 1 },
19
  ];
20
  export function createGoals(onDone) {
21
- const S = Object.assign({ top: 0, smashed: 0, drift: 0, air: 0, pads: 0, butter: 0, wins: 0, escapes: 0, rings: 0, km: 0, refuels: 0, brooklyn: 0, best: 0, coins: 0, fares: 0, upg: 0, steals: 0, meds: 0 }, store.get("void.stats") || {});
22
  const done = new Set(store.get("void.goals") || []);
23
  let saveT = 0, lastSmash = null, lastRings = null, lastFuel = null, pads = new Set();
24
  function check() { for (const g of GOALS) if (!done.has(g.id) && S[g.stat] >= g.need) { done.add(g.id); store.set("void.goals", [...done]); onDone(g); } }
 
15
  { id: "coins", icon: "🪙", name: "Collector", text: "Find 20 hidden coins", stat: "coins", need: 20 },
16
  { id: "taxi", icon: "🚕", name: "Cabbie", text: "Complete 5 taxi fares", stat: "fares", need: 5 },
17
  { id: "tuner", icon: "🔧", name: "Tuner", text: "Buy 4 garage upgrades", stat: "upg", need: 4 },
18
+ { id: "grind", icon: "🛹", name: "Rail slider", text: "Grind 100 m of rails on the hoverboard", stat: "grind", need: 100 },
19
+ { id: "b720", icon: "🌀", name: "Sky skater", text: "Land a 720 on the hoverboard", stat: "b720", need: 1 },
20
+ { id: "bcombo", icon: "🔥", name: "Combo king", text: "Land 5 flip combos on the hoverboard", stat: "combo", need: 5 },
21
  { id: "brooklyn", icon: "🌉", name: "Bridge and tunnel", text: "Cross the bridge to Brooklyn", stat: "brooklyn", need: 1 },
22
  ];
23
  export function createGoals(onDone) {
24
+ const S = Object.assign({ top: 0, smashed: 0, drift: 0, air: 0, pads: 0, butter: 0, wins: 0, escapes: 0, rings: 0, km: 0, refuels: 0, brooklyn: 0, best: 0, coins: 0, fares: 0, upg: 0, steals: 0, meds: 0, grind: 0, b720: 0, combo: 0 }, store.get("void.stats") || {});
25
  const done = new Set(store.get("void.goals") || []);
26
  let saveT = 0, lastSmash = null, lastRings = null, lastFuel = null, pads = new Set();
27
  function check() { for (const g of GOALS) if (!done.has(g.id) && S[g.stat] >= g.need) { done.add(g.id); store.set("void.goals", [...done]); onDone(g); } }
static/main.js CHANGED
@@ -60,11 +60,11 @@ setInterval(() => { if (document.hidden) net.tick(.5, null); }, 500); // a h
60
  const GPU = (() => { try { const c = renderer.getContext(), e = c.getExtension("WEBGL_debug_renderer_info"); return e ? c.getParameter(e.UNMASKED_RENDERER_WEBGL) : ""; } catch { return ""; } })();
61
  const STRONG = /RTX|GTX 1[06-9]|GTX 9[78]|Radeon RX|Radeon Pro|Apple M[1-9]/i.test(GPU) && !COARSE;
62
  const TOP = /RTX\s?(40[6-9]0|50[5-9]0)|RTX\s?A?[45]\d{3}/i.test(GPU) && !COARSE; // recent RTX cards start on Ultra
63
- const SET = Object.assign({ q: LITE ? "lite" : TOP ? "ultra" : STRONG ? "high" : COARSE ? "low" : "medium", sens: 1, autogas: "off", steer: "stick", weather: "clear", radio: "score", cycle: "off", handling: "arcade", motion: "on", text: "normal" }, store.get("void.settings") || {});
64
  const gameOpts = {
65
  handling: () => SET.handling, buyFood: () => { if (cash < 10) return false; cash -= 10; store.set("void.cash", cash); earn(0); return true; }, heroOwned: () => !!store.get("void.hero"), starter: () => { const m = life.models.find(m => m.id === "compact-3") || life.models.find(m => m.kind.id === "compact"); return { model: { ...m, top: 31, acc: .7, grip: .9 }, color: 0x6b5a44, rust: .75, owned: true }; }, onSteal: c => { police && police.event("steal"); goals.stats.steals = (goals.stats.steals || 0) + 1; },
66
  garage: () => openGarage(true), cycleTime, hornType: () => build.horn, autoGas: () => SET.autogas === "on", onRepair: () => car.repair && car.repair(),
67
- onScore: (n, why, pts) => { if (why === "Drift") goals.drift(n); if (/^(🥇|🥈|🥉)/.test(why)) goals.stats.meds = (goals.stats.meds || 0) + 1; goals.note(why); earn(pts ?? n); },
68
  onSmash: kind => police && police.event("smash", kind), policeClear: () => police && police.clear(),
69
  autopilot: (g, dt, over) => nav ? nav.controls(g, dt, over) : null, openGps: () => document.getElementById("gGpsBtn").click(),
70
  mapExtra(mx, g, MM) {
@@ -106,6 +106,7 @@ menuEl.addEventListener("click", e => { const t = e.target.closest(".tile"); if
106
  menuEl.querySelectorAll("[data-veh]").forEach(b => b.onclick = () => game.api.setVehicle(b.dataset.veh));
107
  document.getElementById("gFuelNav").onclick = () => { const st = game.state; if (!st) return; const best = W.city.stations.slice().sort((a, b) => Math.hypot(a.x - st.x, a.z - st.z) - Math.hypot(b.x - st.x, b.z - st.z))[0]; nav.setDest({ name: "⛽ Fuel station", x: best.x, z: best.z }); };
108
  function applySettings(save = true) {
 
109
  if (typeof life !== "undefined" && life.setVoidDetail) life.setVoidDetail(SET.q !== "lite"); // Lite: simple flying VOID cars
110
  document.body.classList.toggle("big-ui", SET.text === "large");
111
  for (const seg of menuEl.querySelectorAll(".seg[data-set]")) for (const b of seg.children) b.classList.toggle("on", SET[seg.dataset.set] === b.dataset.v);
@@ -174,8 +175,10 @@ const boardMesh = (() => {
174
  const under = new THREE.Mesh(new THREE.PlaneGeometry(1.2, .3).rotateX(Math.PI / 2), glow); under.position.y = .005; g.add(under);
175
  for (const x of [-.4, .4]) { const pod = new THREE.Mesh(new THREE.CylinderGeometry(.11, .09, .1, 12), new THREE.MeshStandardMaterial({ color: 0x2a2c32, metalness: .7, roughness: .3 })); pod.position.set(x, -.03, 0); g.add(pod); const ring = new THREE.Mesh(new THREE.TorusGeometry(.1, .02, 6, 16).rotateX(Math.PI / 2), glow); ring.position.set(x, -.08, 0); g.add(ring); }
176
  const strip = new THREE.Mesh(new THREE.BoxGeometry(1.32, .015, .02), glow); strip.position.set(0, .085, .19); g.add(strip); const strip2 = strip.clone(); strip2.position.z = -.19; g.add(strip2);
177
- g.visible = false; scene.add(g); return g;
178
  })();
 
 
179
  const qSide = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2), boardUp = new THREE.Vector3();
180
  // cars that aren't your VOID car: the one you're driving and the one you left parked, drawn from the traffic shapes
181
  const rideMesh = () => { const g = new THREE.Group(); g.visible = false; scene.add(g); return { g, key: "" }; };
@@ -335,7 +338,8 @@ applyBuild();
335
  const UPG = { engine: ["⚙️", "Engine", "More power and top speed"], grip: ["🛞", "Tyres", "Grip in corners, longer drifts"], nitro: ["🔥", "Nitro", "A bigger nitro tank"], armor: ["🛡️", "Armour", "Takes less damage"] };
336
  const PRICE = [2500, 6000, 15000];
337
  const upg = Object.assign({ engine: 0, grip: 0, nitro: 0, armor: 0 }, store.get("void.upg") || {});
338
- let cash = store.get("void.cash") ?? 3000, cashSaveT = 0;
 
339
  gameOpts.upg = upg;
340
  addEventListener("pagehide", () => store.set("void.cash", cash)); // phones kill tabs without warning: save on the way out
341
  function earn(n) { cash += Math.max(0, Math.round(n)); clearTimeout(cashSaveT); cashSaveT = setTimeout(() => store.set("void.cash", cash), 800); const el = document.getElementById("gCash"); if (el) el.textContent = "$" + cash.toLocaleString(); }
@@ -546,9 +550,11 @@ function frameLoop(fixed) {
546
  if (veh === "board") { // stand sideways on the board, knees apart, arms out for balance
547
  boardMesh.position.copy(S.pos); boardMesh.quaternion.copy(S.quat);
548
  walker.g.position.copy(boardUp.set(0, .1, 0).applyQuaternion(S.quat).add(S.pos)); walker.g.quaternion.copy(S.quat).multiply(qSide);
549
- walker.legs[0].rotation.set(-.24, 0, 0); walker.legs[1].rotation.set(.24, 0, 0); walker.arms[0].rotation.set(-.95, 0, Math.sin(time * 2) * .1); walker.arms[1].rotation.set(.95, 0, -Math.sin(time * 2) * .1);
 
 
550
  }
551
- if (veh === "foot") { walker.legs[0].rotation.x = walker.legs[1].rotation.x = walker.arms[0].rotation.x = walker.arms[1].rotation.x = 0; walker.g.position.copy(S.pos); walker.g.rotation.set(0, -gsx.yaw, 0); const sw = Math.sin(S.walk || 0) * Math.min(.7, S.speed / 14) * (S.alt > .3 ? .3 : 1); walker.legs[0].rotation.z = sw; walker.legs[1].rotation.z = -sw; walker.arms[0].rotation.z = -sw * .8; walker.arms[1].rotation.z = sw * .8; }
552
  vehicles.show(veh === "car" || veh === "foot" || veh === "board" ? null : veh, S.pos, S.quat, S, time, dt);
553
  if (S.splash) for (let k = 0; k < 6; k++) { const pf = fx.puffs[fx.puffI++ % fx.puffs.length]; pf.s.position.copy(S.pos).add(new THREE.Vector3((Math.random() - .5) * 3, .3, (Math.random() - .5) * 3)); pf.life = 1; pf.s.visible = true; pf.grow = .8; pf.s.scale.setScalar(.8); pf.s.material.color.set(0xeaf6ff); pf.v.set((Math.random() - .5) * 6, 4 + Math.random() * 4, (Math.random() - .5) * 6); }
554
  if (veh === "boat" && S.speed > 12 && Math.random() < .6) { const back = new THREE.Vector3(-3.6, .1, (Math.random() - .5) * 2).applyQuaternion(S.quat).add(S.pos); const pf = fx.puffs[fx.puffI++ % fx.puffs.length]; pf.s.position.copy(back); pf.life = 1; pf.s.visible = true; pf.grow = .5; pf.s.scale.setScalar(.5); pf.s.material.color.set(0xf4fbff); pf.v.set((Math.random() - .5) * 2, .6, (Math.random() - .5) * 2); }
 
60
  const GPU = (() => { try { const c = renderer.getContext(), e = c.getExtension("WEBGL_debug_renderer_info"); return e ? c.getParameter(e.UNMASKED_RENDERER_WEBGL) : ""; } catch { return ""; } })();
61
  const STRONG = /RTX|GTX 1[06-9]|GTX 9[78]|Radeon RX|Radeon Pro|Apple M[1-9]/i.test(GPU) && !COARSE;
62
  const TOP = /RTX\s?(40[6-9]0|50[5-9]0)|RTX\s?A?[45]\d{3}/i.test(GPU) && !COARSE; // recent RTX cards start on Ultra
63
+ const SET = Object.assign({ q: LITE ? "lite" : TOP ? "ultra" : STRONG ? "high" : COARSE ? "low" : "medium", sens: 1, autogas: "off", steer: "stick", weather: "clear", radio: "score", cycle: "off", handling: "arcade", motion: "on", text: "normal", board: "ion" }, store.get("void.settings") || {});
64
  const gameOpts = {
65
  handling: () => SET.handling, buyFood: () => { if (cash < 10) return false; cash -= 10; store.set("void.cash", cash); earn(0); return true; }, heroOwned: () => !!store.get("void.hero"), starter: () => { const m = life.models.find(m => m.id === "compact-3") || life.models.find(m => m.kind.id === "compact"); return { model: { ...m, top: 31, acc: .7, grip: .9 }, color: 0x6b5a44, rust: .75, owned: true }; }, onSteal: c => { police && police.event("steal"); goals.stats.steals = (goals.stats.steals || 0) + 1; },
66
  garage: () => openGarage(true), cycleTime, hornType: () => build.horn, autoGas: () => SET.autogas === "on", onRepair: () => car.repair && car.repair(),
67
+ onScore: (n, why, pts) => { if (why === "Drift") goals.drift(n); if (/^(🥇|🥈|🥉)/.test(why)) goals.stats.meds = (goals.stats.meds || 0) + 1; { const gm = /^Grind (\d+) m/.exec(why); if (gm) goals.stats.grind = (goals.stats.grind || 0) + +gm[1]; const sp = /^(\d+)( |$)/.exec(why); if (sp && +sp[1] >= 720) goals.stats.b720 = 1; if (/Kickflip|Heelflip/.test(why) && / \+ /.test(why)) goals.stats.combo = (goals.stats.combo || 0) + 1; } goals.note(why); earn(pts ?? n); },
68
  onSmash: kind => police && police.event("smash", kind), policeClear: () => police && police.clear(),
69
  autopilot: (g, dt, over) => nav ? nav.controls(g, dt, over) : null, openGps: () => document.getElementById("gGpsBtn").click(),
70
  mapExtra(mx, g, MM) {
 
106
  menuEl.querySelectorAll("[data-veh]").forEach(b => b.onclick = () => game.api.setVehicle(b.dataset.veh));
107
  document.getElementById("gFuelNav").onclick = () => { const st = game.state; if (!st) return; const best = W.city.stations.slice().sort((a, b) => Math.hypot(a.x - st.x, a.z - st.z) - Math.hypot(b.x - st.x, b.z - st.z))[0]; nav.setDest({ name: "⛽ Fuel station", x: best.x, z: best.z }); };
108
  function applySettings(save = true) {
109
+ if (typeof boardMesh !== "undefined") paintBoard();
110
  if (typeof life !== "undefined" && life.setVoidDetail) life.setVoidDetail(SET.q !== "lite"); // Lite: simple flying VOID cars
111
  document.body.classList.toggle("big-ui", SET.text === "large");
112
  for (const seg of menuEl.querySelectorAll(".seg[data-set]")) for (const b of seg.children) b.classList.toggle("on", SET[seg.dataset.set] === b.dataset.v);
 
175
  const under = new THREE.Mesh(new THREE.PlaneGeometry(1.2, .3).rotateX(Math.PI / 2), glow); under.position.y = .005; g.add(under);
176
  for (const x of [-.4, .4]) { const pod = new THREE.Mesh(new THREE.CylinderGeometry(.11, .09, .1, 12), new THREE.MeshStandardMaterial({ color: 0x2a2c32, metalness: .7, roughness: .3 })); pod.position.set(x, -.03, 0); g.add(pod); const ring = new THREE.Mesh(new THREE.TorusGeometry(.1, .02, 6, 16).rotateX(Math.PI / 2), glow); ring.position.set(x, -.08, 0); g.add(ring); }
177
  const strip = new THREE.Mesh(new THREE.BoxGeometry(1.32, .015, .02), glow); strip.position.set(0, .085, .19); g.add(strip); const strip2 = strip.clone(); strip2.position.z = -.19; g.add(strip2);
178
+ g.visible = false; scene.add(g); g.userData = { glow, deck: deck.material }; return g;
179
  })();
180
+ const BOARDS = { ion: [[.3, 2.2, 2.6], 0x15161a], blaze: [[2.6, .9, .2], 0x1a1210], toxic: [[.8, 2.6, .3], 0x111a12], violet: [[1.6, .6, 2.6], 0x15121c], gold: [[2.6, 1.9, .5], 0x2a2418], ghost: [[2.4, 2.4, 2.4], 0xe8e8ea] };
181
+ function paintBoard() { const b = BOARDS[SET.board] || BOARDS.ion; boardMesh.userData.glow.color.setRGB(...b[0]); boardMesh.userData.deck.color.setHex(b[1]); }
182
  const qSide = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2), boardUp = new THREE.Vector3();
183
  // cars that aren't your VOID car: the one you're driving and the one you left parked, drawn from the traffic shapes
184
  const rideMesh = () => { const g = new THREE.Group(); g.visible = false; scene.add(g); return { g, key: "" }; };
 
338
  const UPG = { engine: ["⚙️", "Engine", "More power and top speed"], grip: ["🛞", "Tyres", "Grip in corners, longer drifts"], nitro: ["🔥", "Nitro", "A bigger nitro tank"], armor: ["🛡️", "Armour", "Takes less damage"] };
339
  const PRICE = [2500, 6000, 15000];
340
  const upg = Object.assign({ engine: 0, grip: 0, nitro: 0, armor: 0 }, store.get("void.upg") || {});
341
+ let cash = store.get("void.cash") ?? 100000, cashSaveT = 0;
342
+ if (!store.get("void.start100k")) { store.set("void.start100k", 1); if (cash < 100000) { cash = 100000; store.set("void.cash", cash); setTimeout(() => toast("You now start with $100,000. Go shopping at VOID Motors!"), 3000); } }
343
  gameOpts.upg = upg;
344
  addEventListener("pagehide", () => store.set("void.cash", cash)); // phones kill tabs without warning: save on the way out
345
  function earn(n) { cash += Math.max(0, Math.round(n)); clearTimeout(cashSaveT); cashSaveT = setTimeout(() => store.set("void.cash", cash), 800); const el = document.getElementById("gCash"); if (el) el.textContent = "$" + cash.toLocaleString(); }
 
550
  if (veh === "board") { // stand sideways on the board, knees apart, arms out for balance
551
  boardMesh.position.copy(S.pos); boardMesh.quaternion.copy(S.quat);
552
  walker.g.position.copy(boardUp.set(0, .1, 0).applyQuaternion(S.quat).add(S.pos)); walker.g.quaternion.copy(S.quat).multiply(qSide);
553
+ const cr = S.crouch || 0; walker.g.position.add(boardUp.set(0, -.22 * cr, 0).applyQuaternion(S.quat)); walker.g.scale.set(1, 1 - .14 * cr, 1);
554
+ walker.legs[0].rotation.set(-.24 - .2 * cr, 0, .5 * cr); walker.legs[1].rotation.set(.24 + .2 * cr, 0, .5 * cr); walker.arms[0].rotation.set(-.95 + .5 * cr, 0, Math.sin(time * 2) * .1 + 1.1 * cr); walker.arms[1].rotation.set(.95 - .5 * cr, 0, -Math.sin(time * 2) * .1 + 1.1 * cr);
555
+ if (S.speed > 25 && Math.random() < .5) { const pf = fx.puffs[fx.puffI++ % fx.puffs.length], gc = boardMesh.userData.glow.color; pf.s.position.copy(S.pos).add(boardUp.set((Math.random() - .5) * .3, .02, (Math.random() - .5) * .3)); pf.life = .6; pf.s.visible = true; pf.grow = .15; pf.s.scale.setScalar(.15); pf.s.material.color.setRGB(Math.min(1, gc.r / 2.6), Math.min(1, gc.g / 2.6), Math.min(1, gc.b / 2.6)); pf.v.set(0, .3, 0); }
556
  }
557
+ if (veh === "foot") { walker.legs[0].rotation.x = walker.legs[1].rotation.x = walker.arms[0].rotation.x = walker.arms[1].rotation.x = 0; walker.g.scale.set(1, 1, 1); walker.g.position.copy(S.pos); walker.g.rotation.set(0, -gsx.yaw, 0); const sw = Math.sin(S.walk || 0) * Math.min(.7, S.speed / 14) * (S.alt > .3 ? .3 : 1); walker.legs[0].rotation.z = sw; walker.legs[1].rotation.z = -sw; walker.arms[0].rotation.z = -sw * .8; walker.arms[1].rotation.z = sw * .8; }
558
  vehicles.show(veh === "car" || veh === "foot" || veh === "board" ? null : veh, S.pos, S.quat, S, time, dt);
559
  if (S.splash) for (let k = 0; k < 6; k++) { const pf = fx.puffs[fx.puffI++ % fx.puffs.length]; pf.s.position.copy(S.pos).add(new THREE.Vector3((Math.random() - .5) * 3, .3, (Math.random() - .5) * 3)); pf.life = 1; pf.s.visible = true; pf.grow = .8; pf.s.scale.setScalar(.8); pf.s.material.color.set(0xeaf6ff); pf.v.set((Math.random() - .5) * 6, 4 + Math.random() * 4, (Math.random() - .5) * 6); }
560
  if (veh === "boat" && S.speed > 12 && Math.random() < .6) { const back = new THREE.Vector3(-3.6, .1, (Math.random() - .5) * 2).applyQuaternion(S.quat).add(S.pos); const pf = fx.puffs[fx.puffI++ % fx.puffs.length]; pf.s.position.copy(back); pf.life = 1; pf.s.visible = true; pf.grow = .5; pf.s.scale.setScalar(.5); pf.s.material.color.set(0xf4fbff); pf.v.set((Math.random() - .5) * 2, .6, (Math.random() - .5) * 2); }