// Turfbreak: a first-person tactical shooter in Port Varo, a made-up rain-soaked harbour city. Two crews (ASHEN vs // COLDWIRE) fight over three zones; eliminations chain into killstreaks (Recon Drone, Sentry, Skyfall Strike). // Desktop: pointer-lock mouse look, WASD, Shift sprint, Space jump, C crouch (slide while sprinting), R reload, right // button aims down sights, 1/2 or the wheel switch weapons, G charge, F killstreak, Tab scoreboard, Esc pause. // Phone: left thumb stick, drag the right half to look, fire / aim / reload / jump / crouch buttons, optional auto-fire // and aim assist. Bots on both teams, a daily Contract (same map + objective for everyone), a kill-cam from your // killer's eyes, a FINAL KILLCAM, an automatic "Play of the match" replay recorded on the device as a clip, a score // card, challenge-a-friend links and a Telegram Mini App. Rules in sim.js, city in core.js, looks in render.js + fpv.js. // (c) 2026 CyberMax. All rights reserved. import { count, store, toast, share, intents, canvasBlob, download, pwa } from './cmx.js'; import * as FP from './funpass.js'; import * as skins from './skins.js'; import * as TGM from './tg.js'; import { genMap, MAPS, MAP_IDS, WEAPONS, WEAPON_IDS, ARMS, PERKS, PERK_IDS, STREAKS, TEAMS, BODY, contract, dayNumber, fmt } from './core.js'; import * as SIM from './sim.js'; const GAME = 'turfbreak'; const SITE = 'https://cybermaxtools.com'; const PATH = '/games/turfbreak/'; const $ = (id) => document.getElementById(id); const Q = new URLSearchParams(location.search); const CAP = Q.has('cap'); // trailer / screenshot capture: frame-stepped from outside const COARSE = matchMedia('(pointer: coarse)').matches || /[?&]touch=1/.test(location.search); if (COARSE) document.body.classList.add('coarse'); const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); const angD = (a, b) => { let d = b - a; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; return d; }; // ------------------------------------------------------------------ cosmetics (Operator Pack, $1.99 once, cosmetic only) let SKINS = []; // filled from render.js after the dynamic import skins.capture({ game: GAME, name: 'Turfbreak', skins: 6, price: '$1.99', preview: ['#B36BFF', '#FF5A1F', '#7FE3FF', '#FFE08A', '#4CFFB0', '#FF2E4C'], perks: ['6 operator looks: Nightshade, Ember, Arctic, Gilded, Static and Carbon', 'Matching sleeves, weapon glow and tracer colour in first person', 'Shown in your kill-cams, clips and score cards'] }); // The full game ($2.99 once, Stripe "Turfbreak: full game, one payment"): exactly what it opens. Six lines, so the shared // sheet adds no generic ones. Free for everyone: daily Contract, quick matches, every weapon and perk, challenge links. const FULL_PERKS = ['SHOWDOWN mode: 3 vs 3, one life per round, first to 4', 'Veteran bots (faster, sharper)', 'Pick any map for Quick Match', 'Contract archive: replay every past daily Contract', 'Yours for good on any device (receipt code)', 'Free stays free: daily Contract, quick matches, every weapon']; FP.init({ game: GAME, name: 'Turfbreak', perks: FULL_PERKS, exact: true, title: 'Turfbreak full game', short: 'Full game', free: 'The daily Contract, quick matches, every weapon and perk, and challenge links stay free for everyone.', extra: 'Turfbreak full game, $2.99 once: Showdown (3 vs 3, one life per round), Veteran bots, any map on demand and the Contract archive.' }); // ------------------------------------------------------------------ saved stats + settings (on this device only) const S = Object.assign({ plays: 0, best: {}, streak: 0, lastDay: 0, name: '', weapon: 'vk9', perk: 'plated', skin: 0, vet: false, coached: false, map: 'daily', kills: 0, fov: 90, sens: 1, tsens: 1, adsSens: 1, invert: false, autofire: true, assist: true, adsToggle: false, portraitOk: false, bob: true, skillAdj: 0, wins: 0 }, store('turfbreak:s') || {}); // saves from the first first-person build (17 Oct): new mouse scale (sens 1 now = 0.0012 rad per count, half the old // speed) and the new 90 degree default field of view; everything else in the save is kept as it was if ((S.feelV || 1) < 2) { if (S.fov === 84) S.fov = 90; S.sens = Math.max(0.1, Math.min(4, S.sens || 1)); S.feelV = 2; } if (S.adsSens == null) S.adsSens = 1; if (S.bob == null) S.bob = true; if (!S.name) S.name = ['VIPER', 'GHOST', 'NOVA', 'REAPER', 'SHADE', 'TALON', 'BLITZ', 'COBRA'][Math.floor(Math.random() * 8)] + '-' + (10 + Math.floor(Math.random() * 89)); const save = () => store('turfbreak:s', S); const TODAY = dayNumber(); const DAY = Math.max(1, Number(Q.get('day')) || TODAY); let chal = null; // challenge from a friend: { d, n, s, k, w } try { const c = Q.get('c'); if (c) { const j = JSON.parse(decodeURIComponent(escape(atob(c.replace(/-/g, '+').replace(/_/g, '/'))))); if (j && j.d && j.s != null) chal = { d: Math.max(1, j.d | 0), n: String(j.n || 'A FRIEND').toUpperCase().replace(/[^A-Z0-9-]/g, '').slice(0, 12), s: Math.max(0, j.s | 0), k: Math.max(0, j.k | 0), w: WEAPONS[j.w] ? j.w : 'vk9' }; } } catch { chal = null; } if (!chal && Q.get('cd') && Q.get('cs')) chal = { d: Math.max(1, Number(Q.get('cd')) | 0), n: 'A FRIEND', s: Math.max(0, Number(Q.get('cs')) | 0), k: 0, w: 'vk9' }; // Telegram challenge deep link (t.me/?start=tb_d_vs) // ------------------------------------------------------------------ audio (WebAudio, all synthesised, original) let AC = null, master = null, noiseB = null, muted = store('turfbreak:mute') === true, musicT = null, rainG = null; function audio() { if (AC || CAP) return; try { AC = new (window.AudioContext || window.webkitAudioContext)(); master = AC.createGain(); master.gain.value = muted ? 0 : 0.8; master.connect(AC.destination); noiseB = AC.createBuffer(1, AC.sampleRate, AC.sampleRate); const d = noiseB.getChannelData(0); for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; const n = AC.createBufferSource(); n.buffer = noiseB; n.loop = true; const lp = AC.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 1600; const hp = AC.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 500; rainG = AC.createGain(); rainG.gain.value = 0.05; n.connect(lp); lp.connect(hp); hp.connect(rainG); rainG.connect(master); n.start(); } catch { AC = null; } } function env(node, t, a, d, peak) { const g = AC.createGain(); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(peak, t + a); g.gain.exponentialRampToValueAtTime(0.0001, t + a + d); node.connect(g); return g; } function out(node, pan = 0) { if (AC.createStereoPanner) { const p = AC.createStereoPanner(); p.pan.value = clamp(pan, -1, 1); node.connect(p); p.connect(master); } else node.connect(master); } const noise = (t, f, q, a, d, peak, pan = 0, type = 'bandpass') => { const n = AC.createBufferSource(); n.buffer = noiseB; const bp = AC.createBiquadFilter(); bp.type = type; bp.frequency.value = f; bp.Q.value = q; n.connect(bp); out(env(bp, t, a, d, peak), pan); n.start(t, Math.random() * 0.5); n.stop(t + a + d + 0.05); }; const tone = (t, f0, f1, type, a, d, peak, pan = 0) => { const o = AC.createOscillator(); o.type = type; o.frequency.setValueAtTime(f0, t); if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(f1, t + a + d); out(env(o, t, a, d, peak), pan); o.start(t); o.stop(t + a + d + 0.05); }; const SFX = { shot(w, vol = 1, pan = 0, near = false) { if (!AC || vol < 0.03) return; const t = AC.currentTime; const f = { vk9: 1900, wasp: 2600, breach: 900, lancer: 1400, p9: 2200, sentry: 3000 }[w] || 1800; const dec = { breach: 0.34, lancer: 0.3, p9: 0.12 }[w] || 0.11; noise(t, near ? f : f * 0.6, 0.7, 0.002, dec, 0.55 * vol, pan); tone(t, w === 'breach' ? 140 : w === 'lancer' ? 120 : 180, 40, 'sine', 0.002, w === 'breach' || w === 'lancer' ? 0.2 : 0.08, 0.6 * vol, pan); if (near) noise(t + 0.01, 6200, 1.2, 0.001, 0.03, 0.12 * vol, pan, 'highpass'); // mechanical snap if (!near && vol > 0.1) noise(t + 0.06, 500, 0.5, 0.02, 0.4, 0.08 * vol, pan); // street echo }, hit(kind) { if (!AC) return; const t = AC.currentTime; if (kind === 'kill') { // confirm: a heavy thud, then a bright two-note chime tone(t, 150, 55, 'sine', 0.002, 0.16, 0.5); noise(t, 900, 0.8, 0.001, 0.06, 0.22); tone(t + 0.05, 1568, 1568, 'triangle', 0.002, 0.16, 0.16); tone(t + 0.05, 2093, 2093, 'sine', 0.002, 0.22, 0.12); tone(t + 0.13, 2637, 2637, 'sine', 0.002, 0.26, 0.1); } else if (kind === 'head') { tone(t, 3300, 2900, 'triangle', 0.001, 0.1, 0.2); tone(t, 1650, 1650, 'square', 0.001, 0.035, 0.06); noise(t, 5000, 2, 0.001, 0.02, 0.1); } else { tone(t, 2400, 2100, 'triangle', 0.001, 0.045, 0.13); noise(t, 3800, 2.5, 0.001, 0.018, 0.08); tone(t, 180, 120, 'sine', 0.001, 0.05, 0.12); } }, hurt() { if (!AC) return; const t = AC.currentTime; tone(t, 160, 70, 'sine', 0.002, 0.18, 0.4); noise(t, 400, 0.6, 0.002, 0.12, 0.25); }, boom(vol = 1, pan = 0) { if (!AC || vol < 0.03) return; const t = AC.currentTime; const n = AC.createBufferSource(); n.buffer = noiseB; n.loop = true; const lp = AC.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(1400, t); lp.frequency.exponentialRampToValueAtTime(120, t + 0.9); n.connect(lp); out(env(lp, t, 0.004, 1.0, 0.9 * vol), pan); n.start(t); n.stop(t + 1.2); tone(t, 70, 30, 'sine', 0.005, 0.6, 0.8 * vol, pan); }, cap(good) { if (!AC) return; const t = AC.currentTime; (good ? [523, 659, 784] : [392, 330, 262]).forEach((f, i) => { const o = AC.createOscillator(); o.type = 'sawtooth'; const lp = AC.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 2200; o.frequency.value = f; o.connect(lp); out(env(lp, t + i * 0.09, 0.01, 0.35, 0.09)); o.start(t + i * 0.09); o.stop(t + i * 0.09 + 0.5); }); }, streak() { if (!AC) return; const t = AC.currentTime; const o = AC.createOscillator(); o.type = 'sawtooth'; o.frequency.setValueAtTime(180, t); o.frequency.exponentialRampToValueAtTime(900, t + 0.5); const lp = AC.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 3000; o.connect(lp); out(env(lp, t, 0.02, 0.6, 0.14)); o.start(t); o.stop(t + 0.7); }, ui() { if (!AC) return; tone(AC.currentTime, 660, 660, 'triangle', 0.002, 0.05, 0.08); }, dash() { if (!AC) return; const t = AC.currentTime; const n = AC.createBufferSource(); n.buffer = noiseB; const bp = AC.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.setValueAtTime(600, t); bp.frequency.exponentialRampToValueAtTime(2400, t + 0.2); n.connect(bp); out(env(bp, t, 0.01, 0.25, 0.2)); n.start(t); n.stop(t + 0.35); }, step(vol = 1) { if (!AC) return; const t = AC.currentTime; noise(t, 220 + Math.random() * 80, 1.4, 0.002, 0.07, 0.11 * vol); noise(t, 2600, 2, 0.001, 0.025, 0.03 * vol); }, jump() { if (!AC) return; noise(AC.currentTime, 300, 1, 0.003, 0.08, 0.12); }, land(v = 1) { if (!AC) return; const t = AC.currentTime; noise(t, 160, 1, 0.002, 0.14, 0.25 * v); tone(t, 90, 50, 'sine', 0.002, 0.1, 0.2 * v); }, click() { if (!AC) return; noise(AC.currentTime, 4000, 3, 0.001, 0.02, 0.12); }, magOut() { if (!AC) return; const t = AC.currentTime; noise(t, 1800, 4, 0.001, 0.04, 0.14); noise(t + 0.05, 900, 3, 0.001, 0.05, 0.08); }, magIn() { if (!AC) return; const t = AC.currentTime; noise(t, 1300, 5, 0.001, 0.05, 0.18); noise(t + 0.12, 2600, 5, 0.001, 0.03, 0.14); }, swap() { if (!AC) return; const t = AC.currentTime; noise(t, 1500, 3, 0.002, 0.06, 0.1); noise(t + 0.09, 3200, 4, 0.001, 0.03, 0.1); }, }; function music(on) { if (!AC) return; clearInterval(musicT); musicT = null; if (!on || muted) return; const notes = [55, 55, 65.4, 55, 49, 55, 73.4, 65.4]; let i = 0; const step = 60 / 104 / 2; musicT = setInterval(() => { if (!AC) return; const t = AC.currentTime + 0.02; const f = notes[i % notes.length]; i++; const o = AC.createOscillator(); o.type = 'sawtooth'; o.frequency.value = f; const lp = AC.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(700, t); lp.frequency.exponentialRampToValueAtTime(140, t + step); o.connect(lp); out(env(lp, t, 0.01, step * 0.9, 0.035)); o.start(t); o.stop(t + step); }, step * 1000); } function setMute(m) { muted = m; store('turfbreak:mute', m); $('mute').textContent = m ? 'MUTED' : 'SND'; if (master) master.gain.value = m ? 0 : 0.8; music(!m && state === 'play'); } // ------------------------------------------------------------------ boot const cv = $('c'); let World = null, world = null, G = null, attract = null, mapCache = new Map(), builtKey = '', state = 'loading'; let mode = 'daily', con = contract(DAY), curMap = null, seen = new Set(), seenT = 0; const lb = (p) => { const b = $('lbar'); if (b) b.style.width = `${p}%`; }; function getMap(id, seed) { const k = `${id}:${seed}`; let m = mapCache.get(k); if (!m) { m = genMap(id, seed); mapCache.set(k, m); if (mapCache.size > 4) mapCache.delete(mapCache.keys().next().value); } return m; } function ensureWorld(map) { const k = `${map.id}:${map.seed}`; if (builtKey !== k) { world.build(map); builtKey = k; } } async function boot() { lb(10); const BT = window.__turf.boot = {}; const mk = (k) => { BT[k] = Math.round(performance.now()); }; let ok = false; try { const c = document.createElement('canvas'); ok = !!c.getContext('webgl2'); } catch { ok = false; } if (!ok) { $('loading').hidden = true; $('nogl').hidden = false; count(GAME, 'nogl'); return; } mk('start'); const mod = await import('./render.js'); lb(45); World = mod.World; SKINS = mod.SKINS; mk('import'); world = new World(cv, { record: CAP, tier: Q.get('hq') ? 'high' : Q.get('lq') ? 'low' : undefined }); fit(); lb(60); mk('stage'); if (TGM.inTG) { await TGM.init({ onResize: fit, onBack: () => { if (state === 'play') pause(); else if (state === 'pause') toMenu(); } }); if (TGM.app()) { claimTG(); TGM.hello().then((r) => { tgInfo = { ...tgInfo, ...r }; renderMenu(); }); } } con = contract(chal ? chal.d : DAY); curMap = getMap(con.map, con.seed); mk('map'); ensureWorld(curMap); lb(85); mk('city'); startAttract(curMap); mk('attract'); // compile every shader program (the first-person ones too: your weapon in hand, bullet marks) so DEPLOY never // stalls. Where the GPU compiles in parallel this is quick and is awaited; where it cannot (software GL, some older // phones) the menu shows first and the compile runs right behind it, so the player is never staring at a loading bar. const warm = () => { try { world.vm.use(S.weapon); world.vm.root.visible = true; world.decal(0, -50, 0, 0, 1, 0); world.draw(liveView(attract, { x: 0, z: 0, mode: 'orbit', r: 64, h: 52 }, true), 0); world.vm.root.visible = true; world.stage.r.compile(world.vm.root, world.cam, world.scene); } catch { /* compiles on first draw */ } world.vm.root.visible = false; }; const par = world.stage.r.extensions.has('KHR_parallel_shader_compile'); if (par) { warm(); try { await world.stage.r.compileAsync(world.scene, world.cam); } catch { /* */ } } mk('compile'); lb(95); $('loading').hidden = true; toMenu(); lb(100); window.__turf.ready = true; window.__turf.readyAt = performance.now(); if (!par) setTimeout(() => { if (state === 'menu') warm(); mk('compileLate'); }, 60); if (!CAP) requestAnimationFrame(loop); pwa(GAME, $('install')); count(GAME, 'view', TGM.app() ? 'telegram' : chal ? 'challenge' : ''); } function fit() { if (world) world.stage.fit(); } window.addEventListener('resize', () => { fit(); rotateCheck(); }); // ------------------------------------------------------------------ input: keyboard + pointer-lock mouse look const keys = new Set(); const mouse = { down: false, rdown: false, dx: 0, dy: 0, in: false }; const pend = { dash: false, nade: false, streak: false, reload: false, jump: false, slot: null, slide: false }; const look = { yaw: -Math.PI / 2, pitch: 0, rc: 0, ry: 0, fdx: 0, fdy: 0 }; // rc/ry: recoil offset that settles back let adsOn = false; // toggle mode state (and the touch ADS button) const lockInfo = { requested: 0, locked: false, error: '', changes: 0, at: 0, raw: false }; const typing = () => /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement && document.activeElement.tagName); const locked = () => document.pointerLockElement === cv; function requestLock() { if (COARSE || CAP || !cv.requestPointerLock) return; lockInfo.requested++; try { const p = cv.requestPointerLock({ unadjustedMovement: true }); if (p && p.then) p.then(() => { lockInfo.raw = true; }, () => {}); if (p && p.catch) p.catch(() => { try { const p2 = cv.requestPointerLock(); if (p2 && p2.catch) p2.catch((e) => { lockInfo.error = String(e && e.name || e); }); } catch (e) { lockInfo.error = String(e && e.name || e); } }); } catch (e) { lockInfo.error = String(e && e.name || e); } } document.addEventListener('pointerlockchange', () => { lockInfo.changes++; lockInfo.locked = locked(); lockInfo.at = performance.now(); document.body.classList.toggle('locked', lockInfo.locked); if (!lockInfo.locked && state === 'play' && !CAP) pause(); // Esc releases the mouse: that is a pause }); document.addEventListener('pointerlockerror', () => { lockInfo.error = 'pointerlockerror'; }); window.addEventListener('keydown', (e) => { if (typing()) return; if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space', 'Tab', 'KeyC', 'ControlLeft'].includes(e.code) && (state === 'play' || isCine())) e.preventDefault(); const first = !keys.has(e.code); keys.add(e.code); if (isCine() && (e.code === 'Space' || e.code === 'Enter' || e.code === 'Escape')) { skipCine(); return; } if (state === 'play') { if (!first) return; if (e.code === 'Space') pend.jump = true; if (e.code === 'KeyC' || e.code === 'ControlLeft') { if (G && (G.you.sprint || G.you.moving > 5.6)) pend.slide = true; } if (e.code === 'KeyV') pend.dash = true; if (e.code === 'KeyG') pend.nade = true; if (e.code === 'KeyF' || e.code === 'KeyQ') pend.streak = true; if (e.code === 'KeyR') pend.reload = true; if (e.code === 'Digit1') pend.slot = 0; if (e.code === 'Digit2') pend.slot = 1; if (e.code === 'Escape' || e.code === 'KeyP') pause(); if (e.code === 'Tab') board(true); } else if (state === 'pause') { if (e.code === 'Escape' || e.code === 'KeyP') resume(); if (e.code === 'KeyR') restart(); } else if (state === 'over' && e.code === 'KeyR') restart(); else if (state === 'menu' && e.code === 'Enter') startMatch('daily'); if (e.code === 'KeyM') setMute(!muted); }); window.addEventListener('keyup', (e) => { keys.delete(e.code); if (e.code === 'Tab') board(false); }); window.addEventListener('blur', () => { keys.clear(); mouse.down = mouse.rdown = false; if (state === 'play' && !CAP) pause(); }); document.addEventListener('visibilitychange', () => { if (document.hidden && state === 'play' && !CAP) pause(); }); document.addEventListener('mousemove', (e) => { if (state !== 'play' || COARSE) return; // locked: raw movement (unadjustedMovement where the browser has it: no OS acceleration, no smoothing); // not locked (lock refused, e.g. inside some embeds): the cursor over the game still turns. // No clamping, so fast flicks land where your hand went; only the known one-off spike some browsers send right // after the lock is taken (hundreds of px in one event while the lock settles) is dropped. if (!(locked() || mouse.in)) return; const mx = e.movementX || 0, my = e.movementY || 0; if (performance.now() - lockInfo.at < 120 && (Math.abs(mx) > 250 || Math.abs(my) > 250)) return; if (Math.abs(mx) > 1500 || Math.abs(my) > 1500) return; mouse.dx += mx; mouse.dy += my; }); cv.addEventListener('mouseenter', () => { mouse.in = true; }); cv.addEventListener('mouseleave', () => { mouse.in = false; }); cv.addEventListener('mousedown', (e) => { if (isCine()) { skipCine(); return; } audio(); if (state === 'play' && !locked()) requestLock(); if (e.button === 0) mouse.down = true; if (e.button === 2) { mouse.rdown = true; if (S.adsToggle) adsOn = !adsOn; } cv.focus({ preventScroll: true }); }); window.addEventListener('mouseup', (e) => { if (e.button === 0) mouse.down = false; if (e.button === 2) mouse.rdown = false; }); document.addEventListener('mousedown', (e) => { if (locked() && e.target !== cv) { if (e.button === 0) mouse.down = true; if (e.button === 2) { mouse.rdown = true; if (S.adsToggle) adsOn = !adsOn; } } }); cv.addEventListener('contextmenu', (e) => e.preventDefault()); cv.addEventListener('wheel', (e) => { if (state === 'play' && G) { e.preventDefault(); pend.slot = G.you.slot ? 0 : 1; } }, { passive: false }); // ------------------------------------------------------------------ input: touch (left stick, right-half look, buttons) const touch = { stick: null, look: null, fire: null, firing: false, crouch: false, lookDX: 0, lookDY: 0 }; const STICK_R = 52; const rel = (t) => { const r = cv.getBoundingClientRect(); return { x: t.clientX - r.left, y: t.clientY - r.top, w: r.width, h: r.height }; }; cv.addEventListener('touchstart', (e) => { e.preventDefault(); audio(); if (isCine()) { skipCine(); return; } for (const t of e.changedTouches) { const p = rel(t); if (p.x < p.w * 0.42 && !touch.stick) { touch.stick = { id: t.identifier, ox: p.x, oy: p.y, dx: 0, dy: 0 }; const el = $('stickL'); el.style.left = `${p.x}px`; el.style.top = `${p.y}px`; el.classList.add('on'); el.firstChild.style.transform = ''; } else if (!touch.look) touch.look = { id: t.identifier, x: t.clientX, y: t.clientY }; } }, { passive: false }); window.addEventListener('touchmove', (e) => { if (state === 'play') e.preventDefault(); for (const t of e.changedTouches) { const s = touch.stick; if (s && s.id === t.identifier) { // floating stick: 12 % dead zone (no drift), full speed at 52 px, and the base follows a thumb that slides past the rim const p = rel(t); let dx = (p.x - s.ox) / STICK_R, dy = (p.y - s.oy) / STICK_R; let l = Math.hypot(dx, dy); if (l > 1.25) { s.ox += (dx / l) * (l - 1.25) * STICK_R; s.oy += (dy / l) * (l - 1.25) * STICK_R; const el = $('stickL'); el.style.left = `${s.ox}px`; el.style.top = `${s.oy}px`; dx /= l / 1.25; dy /= l / 1.25; l = 1.25; } const vis = Math.min(1, l); $('stickL').firstChild.style.transform = `translate(${(dx / (l || 1)) * vis * 44}px,${(dy / (l || 1)) * vis * 44}px)`; const k = l < 0.12 ? 0 : Math.min(1, (l - 0.12) / 0.88) / (l || 1); s.dx = dx * k; s.dy = dy * k; } for (const k of ['look', 'fire']) { const L = touch[k]; if (L && L.id === t.identifier) { touch.lookDX += t.clientX - L.x; touch.lookDY += t.clientY - L.y; L.x = t.clientX; L.y = t.clientY; } } } }, { passive: false }); const endTouch = (e) => { for (const t of e.changedTouches) { if (touch.stick && touch.stick.id === t.identifier) { touch.stick = null; $('stickL').classList.remove('on'); } if (touch.look && touch.look.id === t.identifier) touch.look = null; if (touch.fire && touch.fire.id === t.identifier) { touch.fire = null; touch.firing = false; $('t-fire').classList.remove('on'); } } }; window.addEventListener('touchend', endTouch); window.addEventListener('touchcancel', endTouch); $('t-fire').addEventListener('touchstart', (e) => { e.preventDefault(); audio(); const t = e.changedTouches[0]; touch.fire = { id: t.identifier, x: t.clientX, y: t.clientY }; touch.firing = true; $('t-fire').classList.add('on'); }, { passive: false }); const tbtn = (id, fn) => { const el = $(id); el.addEventListener('touchstart', (e) => { e.preventDefault(); e.stopPropagation(); audio(); fn(); }, { passive: false }); el.addEventListener('click', fn); }; tbtn('t-ads', () => { adsOn = !adsOn; $('t-ads').classList.toggle('on', adsOn); }); tbtn('t-reload', () => { pend.reload = true; }); tbtn('t-jump', () => { pend.jump = true; }); tbtn('t-crouch', () => { if (G && (G.you.sprint || G.you.moving > 5.6)) pend.slide = true; else { touch.crouch = !touch.crouch; $('t-crouch').classList.toggle('on', touch.crouch); } }); tbtn('t-swap', () => { if (G) pend.slot = G.you.slot ? 0 : 1; }); tbtn('t-nade', () => { pend.nade = true; }); tbtn('t-streak', () => { pend.streak = true; }); // ------------------------------------------------------------------ per-step input for your operator let assistT = null; /** Mouse: radians per raw count at sensitivity 1.0 (~16 cm per 360 at 800 DPI; Krunker-like casual default). */ const MOUSE_K = 0.0012, TOUCH_K = 0.0046; /** * Look, applied every rendered frame (not only on 60 Hz sim steps), so the view answers the mouse on the very next * frame at any refresh rate. No smoothing or acceleration for the mouse. While aiming down sights the turn rate scales * with the zoom (the target moves the same distance under the crosshair), times the ADS sensitivity setting. */ function applyLook(dt) { if (!G || state !== 'play') { mouse.dx = mouse.dy = 0; touch.lookDX = touch.lookDY = 0; return; } const y = G.you; const W = SIM.armOf(y); const z = 1 + (W.zoom - 1) * y.ads; const adsK = y.ads > 0.01 ? lerpN(1, S.adsSens / z, Math.min(1, y.ads * 1.4)) : 1; const inv = S.invert ? -1 : 1; let dyaw = mouse.dx * MOUSE_K * S.sens * adsK, dpit = -mouse.dy * MOUSE_K * S.sens * adsK * inv; // touch: a gentle speed curve (slow drags stay precise, quick swipes turn fast) const tv = Math.hypot(touch.lookDX, touch.lookDY) / Math.max(0.004, dt) / 1000; // px per ms const curve = 0.8 + Math.min(0.9, tv * 0.55); let tx = touch.lookDX * TOUCH_K * S.tsens * adsK * curve, ty = -touch.lookDY * TOUCH_K * S.tsens * adsK * inv * curve; // touch aim assist: slow the look near an enemy and pull gently towards it while you turn, move or fire assistT = null; if (COARSE && S.assist) { const best = assistTarget(y, 0.085); if (best) { assistT = best.ent; tx *= 0.5; ty *= 0.5; const active = Math.abs(tx) + Math.abs(ty) > 0.0003 || touch.stick || touch.firing; if (active) { look.yaw += angD(look.yaw, best.yaw) * Math.min(1, dt * 6); look.pitch += (best.pitch - look.pitch - look.rc) * Math.min(1, dt * 4.5); } } } dyaw += tx; dpit += ty; look.fdx += (mouse.dx + touch.lookDX); look.fdy += (mouse.dy + touch.lookDY); mouse.dx = mouse.dy = 0; touch.lookDX = touch.lookDY = 0; look.yaw += dyaw; look.pitch = clamp(look.pitch + dpit, -1.45, 1.45); } const lerpN = (a, b, t) => a + (b - a) * t; const smooth01 = (t) => t * t * (3 - 2 * t); function readInput(dt) { const y = G.you; const W = ARMS[SIM.armOf(y).id]; // recoil settles back look.rc *= Math.exp(-dt * 7.5); look.ry *= Math.exp(-dt * 9); const inp = { mx: 0, mz: 0, yaw: look.yaw + look.ry, pitch: clamp(look.pitch + look.rc, -1.45, 1.45), fire: false }; let fwd = 0, str = 0; if (keys.has('KeyW') || keys.has('ArrowUp')) fwd += 1; if (keys.has('KeyS') || keys.has('ArrowDown')) fwd -= 1; if (keys.has('KeyD') || keys.has('ArrowRight')) str += 1; if (keys.has('KeyA') || keys.has('ArrowLeft')) str -= 1; if (touch.stick) { fwd = -touch.stick.dy; str = touch.stick.dx; } const cy = Math.cos(look.yaw), sy = Math.sin(look.yaw); inp.mx = cy * fwd - sy * str; inp.mz = sy * fwd + cy * str; inp.sprint = keys.has('ShiftLeft') || keys.has('ShiftRight') || (touch.stick && -touch.stick.dy > 0.9 && Math.abs(touch.stick.dx) < 0.45); inp.crouch = keys.has('KeyC') || keys.has('ControlLeft') || touch.crouch; inp.ads = S.adsToggle || COARSE ? adsOn : mouse.rdown; inp.fire = mouse.down || touch.firing; let held = touch.firing; if (COARSE && S.autofire && !inp.fire) { const t = SIM.aimTarget(G, { ...y, yaw: inp.yaw, pitch: inp.pitch }, W.range * 1.5); if (t) { inp.fire = true; held = true; } } if (held && !W.auto) inp.fire = (G.tick % 10) < 5; // a held touch button keeps a semi-auto sidearm firing (about 6 shots a second) if (howtoOn && (fwd || str || inp.fire)) hideHowto(); // the how-to card closes the moment you move or shoot inp.jump = pend.jump; inp.reload = pend.reload; inp.nade = pend.nade; inp.streak = pend.streak; inp.dash = pend.dash; inp.slide = pend.slide; if (pend.slot != null) inp.slot = pend.slot; pend.jump = pend.reload = pend.nade = pend.streak = pend.dash = pend.slide = false; pend.slot = null; return inp; } /** The enemy closest to the crosshair (angle under `cone`) that you can see, with the yaw/pitch to its chest. */ function assistTarget(y, cone) { const eye = SIM.eyeOf(y); let best = null, bd = cone; for (const o of G.ents) { if (o.team === y.team || !o.alive || !seen.has(o.id)) continue; const dx = o.x - eye[0], dz = o.z - eye[2], d = Math.hypot(dx, dz); if (d > 45 || d < 0.5) continue; const yw = Math.atan2(dz, dx), pt = Math.atan2(o.y + 1.1 - 0.3 * o.crouch - eye[1], d); const a = Math.hypot(angD(look.yaw, yw), pt - look.pitch - look.rc) * Math.max(1, 6 / d); if (a < bd) { bd = a; best = { ent: o, yaw: yw, pitch: pt }; } } return best; } // ------------------------------------------------------------------ matches /** * How sharp the enemy bots are (0..1): the first match is a rookie lobby, then it ramps over about 6 matches to * normal (0.5) and adapts a little to how you do (S.skillAdj moves +-0.05 after each match). Veteran = 1. */ function botSkill() { const base = Math.min(0.5, 0.1 + S.plays * 0.07); return clamp(base + (S.skillAdj || 0), 0.05, 0.7); } function newMatch(m) { mode = m; let id, seed; if (m === 'daily') { con = contract(chal ? chal.d : DAY); id = con.map; seed = con.seed; } else { const pick = FP.isPro() && S.map !== 'daily' ? S.map : null; id = pick || MAP_IDS[Math.floor(Math.random() * MAP_IDS.length)]; seed = (Math.random() * 1e9) >>> 0; if (S.map === 'daily' || !FP.isPro()) { if (m === 'quick') { id = MAP_IDS[Math.floor(Math.random() * MAP_IDS.length)]; } } } if (m === 'showdown' && S.map === 'daily') { id = contract(DAY).map; } curMap = getMap(id, seed); ensureWorld(curMap); const skin = S.skin > 0 && skins.owned() ? S.skin : 0; G = SIM.createMatch({ map: curMap, seed: m === 'daily' ? con.seed : seed, mode: m === 'showdown' ? 'showdown' : 'turf', hard: S.vet && FP.isPro(), skill: botSkill(), rookie: S.plays === 0 && m !== 'showdown', you: { name: S.name, weapon: S.weapon, perk: S.perk, skin } }); prevPos = null; world.setOps(G.ents, skin); look.yaw = G.you.yaw; look.pitch = 0; look.rc = look.ry = 0; adsOn = false; touch.crouch = false; for (const id2 of ['t-ads', 't-crouch']) $(id2).classList.remove('on'); seen = new Set(); seenT = 0; feedRows = []; kc = null; potm = null; clip = null; frameShot = null; lastHud = {}; nextCine = null; deathT = 0; bob = { ph: 0, land: 0, step: 0 }; buildZonesHud(); drawMiniBase(); } function startMatch(m) { audio(); SFX.ui(); if (m === 'showdown' && !FP.isPro()) { openFull('mode-showdown'); return; } if (!world) return; const t0 = performance.now(); newMatch(m); for (const id of ['menu', 'over', 'pause', 'settings']) $(id).hidden = true; $('hud').hidden = false; document.body.classList.add('playing'); $('cine').hidden = true; fit(); state = 'play'; backdrop(false); G.startedMs = performance.now() - t0; if (!window.__turf.playAt) window.__turf.playAt = performance.now(); requestLock(); goFull(); const map = MAPS[curMap.id]; banner(mode === 'daily' ? `CONTRACT #${con.day}` : mode === 'showdown' ? 'SHOWDOWN' : 'QUICK MATCH', `${map.name} · ${mode === 'showdown' ? 'FIRST TO 4 ROUNDS' : 'TAKE THE ZONES'}`); if ((S.howto || 0) < 3 && !CAP) showHowto(); S.plays++; save(); updUnder(); rotateCheck(); if (!muted) music(true); cv.focus({ preventScroll: true }); count(GAME, 'play', mode === 'daily' ? (chal ? 'challenge' : DAY !== TODAY ? 'archive' : 'daily') : mode); try { window.CMXPortal && window.CMXPortal.gameplayStart && window.CMXPortal.gameplayStart(); } catch { /* */ } } // one-screen how-to (first 3 matches): every control on one card; it closes on your first move or shot let howtoOn = false, howtoT = 0; function showHowto() { const k = (key, what) => `${key}${what}`; $('coach').innerHTML = COARSE ? `

HOW TO PLAY

${k('LEFT THUMB', 'move (push up to sprint)')}${k('DRAG RIGHT', 'aim')}${k('FIRE', 'or just aim: auto-fire is on')}${k('ADS', 'aim down sights')}${k('JUMP · CROUCH', 'crouch while running = slide')}${k('ZONES A B C', 'stand in one to take it')}

Move to start

` : `

HOW TO PLAY

${k('WASD', 'move')}${k('MOUSE', 'aim')}${k('LEFT CLICK', 'fire')}${k('RIGHT CLICK', 'aim down sights')}${k('SHIFT', 'sprint')}${k('SPACE', 'jump · climb crates')}${k('C', 'crouch · slide while sprinting')}${k('R · 1 2 · G · F', 'reload · weapons · charge · streak')}

Take the zones A B C · move to start

`; $('coach').classList.add('show'); howtoOn = true; howtoT = 0; // closes on the first move or shot, or after 14 s of play } function hideHowto() { if (!howtoOn) return; howtoOn = false; $('coach').classList.remove('show'); S.howto = (S.howto || 0) + 1; save(); } function goFull() { // phones: full screen + landscape when the browser allows it (silently ignored otherwise) if (!COARSE || CAP || TGM.app() || document.querySelector('meta[name="cmx-portal"]')) return; // portals run their own fullscreen (CrazyGames forbids a custom one) try { const el = document.documentElement; if (!document.fullscreenElement && el.requestFullscreen) el.requestFullscreen({ navigationUI: 'hide' }).then(() => { try { screen.orientation && screen.orientation.lock && screen.orientation.lock('landscape').catch(() => {}); } catch { /* */ } }).catch(() => {}); } catch { /* */ } } function rotateCheck() { const r = $('rotate'); if (!r) return; r.hidden = !(COARSE && state === 'play' && innerHeight > innerWidth * 1.05 && !S.portraitOk && !CAP); } $('rot-ok').onclick = () => { S.portraitOk = true; save(); rotateCheck(); }; function restart() { const t0 = performance.now(); startMatch(mode); window.__turf.restartMs = performance.now() - t0; } function pause() { if (state !== 'play') return; state = 'pause'; $('pause').hidden = false; music(false); mouse.down = mouse.rdown = false; keys.clear(); board(false); if (locked()) try { document.exitPointerLock(); } catch { /* */ } } function resume() { state = 'play'; $('pause').hidden = true; $('settings').hidden = true; if (!muted) music(true); requestLock(); cv.focus({ preventScroll: true }); } function backdrop(on) { if (!world || CAP) return; const st = world.stage; if (on) { st.auto = false; if (st.q.scale !== 0.7) st.setQuality(0.7); } else { st.auto = !/[?&](fixedq|3d)=1/.test(location.search); if (st.q.scale < 0.85) st.setQuality(world.low ? 0.85 : 1); } } function toMenu() { state = 'menu'; backdrop(true); for (const id of ['over', 'pause', 'hud', 'cine', 'settings']) $(id).hidden = true; $('menu').hidden = false; document.body.classList.remove('playing'); fit(); music(false); rotateCheck(); if (locked()) try { document.exitPointerLock(); } catch { /* */ } if (attract && curMap && attract.map !== curMap) startAttract(curMap); renderMenu(); } function startAttract(map) { attract = SIM.createMatch({ map, seed: map.seed + 3, size: 4 }); for (let i = 0; i < 60 * 9; i++) SIM.step(attract, {}); world.setOps(attract.ents, 0); } // 9 s in: bots already moving up, cheap to simulate on load // ------------------------------------------------------------------ cinematics: kill-cam (killer's eyes), final kill-cam, play of the match let kc = null, potm = null, nextCine = null, clip = null, frameShot = null, deathT = 0; const isCine = () => state === 'killcam' || state === 'final' || state === 'potm'; function beginCine(kind, t0, t1, follow, title, sub, slowAt = -1) { if (t1 - t0 < 30) return false; const f0 = SIM.histAt(G, t0); if (!G.hist[f0]) return false; kc = { kind, t0, t1, p: t0, follow, slowAt, last: t0, rec: null, cy: null }; state = kind; world.stage.parts.clear(); mouse.down = false; touch.firing = false; const f = G.ents[follow]; const sk = f.you ? (SKINS[G.you.skin] || SKINS[0]) : null; world.vm.setLook(sk && sk.jacket ? sk.jacket : f.team === 0 ? '#45473A' : '#34465A', sk && sk.visor ? sk.visor : TEAMS[f.team].color); $('hud').hidden = true; $('cine').hidden = false; const T = $('cine-t'); T.textContent = title; T.className = `cine-t${kind === 'potm' ? ' potm' : ''}`; $('cine-s').innerHTML = sub; if (kind === 'potm' && !CAP) startRecording(); return true; } function skipCine() { if (!kc) return; kc.p = kc.t1; } function endCine() { const k = kc; kc = null; $('cine').hidden = true; const sk = SKINS[G.you.skin] || SKINS[0]; world.vm.setLook(sk.jacket || '#45473A', sk.visor || TEAMS[0].color); if (k.rec) stopRecording(k.rec); if (k.kind === 'killcam') { state = 'play'; $('hud').hidden = false; if (G.you.respawn < 1e8) G.you.respawn = Math.min(G.you.respawn, 1.0); return; } if (k.kind === 'final') { if (!startPotm()) showOver(); return; } if (k.kind === 'potm') showOver(); } function startPotm() { const b = SIM.bestMoment(G); if (!b) return false; potm = b; const w = weaponName(b.w); const what = b.multi >= 3 ? 'TRIPLE BREAK' : b.multi === 2 ? 'DOUBLE BREAK' : b.streak >= 5 ? `${b.streak} STREAK` : b.hs ? 'HEADSHOT' : 'ELIMINATION'; return beginCine('potm', b.t0, b.t1, G.you.id, 'PLAY OF THE MATCH', `${esc(G.you.name)} · ${what} · ${w}`, -1); } function stepCine(dt) { const k = kc; const near = k.slowAt > 0 && Math.abs(k.p - k.slowAt) < 40; const speed = k.kind === 'final' ? (near ? 0.35 : 1) : k.kind === 'potm' ? 0.85 : 0.8; const prev = k.p; k.p = Math.min(k.t1, k.p + dt * 60 * speed); const view = histView(k.p, k.follow, k); for (const e of SIM.histEvents(G, prev, k.p)) { world.fx(e, view); cineSound(e, view); } world.draw(view, dt * speed); if (k.rec) recFrame(k.rec); if (k.kind === 'potm' && !frameShot && k.p > (k.t0 + k.t1) / 2) grabFrame(); if (k.p >= k.t1) endCine(); } /** A view of the match at a (fractional) tick from history, seen through the eyes of `follow` (interpolated). */ function histView(tick, follow, k) { const i1 = Math.min(SIM.histAt(G, tick), G.hist.length - 1), i0 = Math.max(0, i1 - 1); const A = G.hist[i0], B = G.hist[i1]; const f = B.tick > A.tick ? clamp((tick - A.tick) / (B.tick - A.tick), 0, 1) : 1; const n = G.ents.length; const ents = []; const H = SIM.HS; const L = (o) => A.s[o] + (B.s[o] - A.s[o]) * f; for (let i = 0; i < n; i++) { const o = i * H; const ya = A.s[o + 2], yb = B.s[o + 2]; ents.push({ x: L(o), z: L(o + 1), yaw: ya + angD(ya, yb) * f, alive: B.s[o + 3] > 0.5, moving: L(o + 4), firing: B.s[o + 5], hp: B.s[o + 6], dash: B.s[o + 7] > 0.5, kick: B.s[o + 8], y: L(o + 9), pitch: L(o + 10), crouch: L(o + 11), slot: B.s[o + 12] % 2 >= 1 ? 1 : 0, reload: B.s[o + 12] >= 2, team: G.ents[i].team, you: G.ents[i].you, show: true, mark: 0 }); } const e = ents[follow] || ents[0]; const ent = G.ents[follow]; if (k) { if (k.cy == null) { k.cy = e.yaw; k.cp = e.pitch; } k.cy += angD(k.cy, e.yaw) * 0.5; k.cp += (e.pitch - k.cp) * 0.5; } const yaw = k ? k.cy : e.yaw, pitch = k ? k.cp : e.pitch; const w = e.slot ? 'p9' : ent.weapon; return { ents, zones: B.zones.map((z) => [z[0], z[1], false]), sentries: B.sentries, charges: B.charges, cam: { mode: 'fps', follow, team: e.team, x: e.x, y: e.y + BODY.eye + (BODY.eyeC - BODY.eye) * e.crouch, z: e.z, yaw, pitch, fov: S.fov, zoom: 1, weapon: e.alive ? w : null, alive: e.alive, moving: e.moving, ground: true, crouch: e.crouch, ads: 0, reload: e.reload ? 0.5 : -1 } }; } function cineSound(e) { if (e.k === 'shot') SFX.shot(e.w, 0.35, 0, kc && e.from === kc.follow); else if (e.k === 'boom') SFX.boom(0.6); else if (e.k === 'kill') SFX.hit('kill'); } // on-device clip of the Play of the match (watermarked); nothing is uploaded function startRecording() { try { if (!window.MediaRecorder || !HTMLCanvasElement.prototype.captureStream) return; const types = ['video/mp4;codecs=avc1', 'video/mp4', 'video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']; const type = types.find((t) => MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(t)); if (!type) return; const src = world.stage.canvas; const sc = Math.min(1, 1280 / Math.max(src.width, src.height)); const c = document.createElement('canvas'); c.width = Math.round(src.width * sc / 2) * 2; c.height = Math.round(src.height * sc / 2) * 2; const stream = c.captureStream(30); const chunks = []; const r = new MediaRecorder(stream, { mimeType: type, videoBitsPerSecond: 5_000_000 }); r.ondataavailable = (ev) => { if (ev.data && ev.data.size) chunks.push(ev.data); }; const g0 = G; r.onstop = () => { if (chunks.length && G === g0) { clip = { blob: new Blob(chunks, { type: type.split(';')[0] }), ext: type.includes('mp4') ? 'mp4' : 'webm' }; if (state === 'over') showClip(); } }; r.start(); kc.rec = { r, c, g: c.getContext('2d'), sub: $('cine-s').textContent }; } catch { /* recording is optional */ } } function recFrame(rec) { const g = rec.g, c = rec.c, W = c.width, H = c.height; g.drawImage(world.stage.canvas, 0, 0, W, H); g.fillStyle = 'rgba(0,0,0,.85)'; g.fillRect(0, 0, W, H * 0.09); g.fillRect(0, H * 0.91, W, H * 0.09); g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillStyle = '#FF8A1F'; g.font = `italic 800 ${Math.round(H * 0.055)}px TB, "Arial Narrow", sans-serif`; g.fillText('PLAY OF THE MATCH', W / 2, H * 0.047); g.fillStyle = '#FFFFFF'; g.font = `800 ${Math.round(H * 0.035)}px TB, "Arial Narrow", sans-serif`; g.fillText(rec.sub.toUpperCase(), W / 2, H * 0.955); g.textAlign = 'right'; g.font = `800 ${Math.round(H * 0.026)}px TB, "Arial Narrow", sans-serif`; g.fillStyle = 'rgba(255,255,255,.75)'; g.fillText('TURFBREAK · cybermaxtools.com/games/turfbreak', W - 12, H * 0.87); } function stopRecording(rec) { try { if (rec.r.state !== 'inactive') rec.r.stop(); } catch { /* */ } } function grabFrame() { try { const src = world.stage.canvas; const c = document.createElement('canvas'); c.width = src.width; c.height = src.height; c.getContext('2d').drawImage(src, 0, 0); frameShot = c; } catch { frameShot = null; } } function showClip() { const v = $('clipv'), b = $('clip'); if (!clip) { b.hidden = true; v.hidden = true; return; } const u = URL.createObjectURL(clip.blob); v.src = u; v.hidden = false; b.hidden = false; v.play().catch(() => {}); } // ------------------------------------------------------------------ the loop let lastT = 0, acc = 0, attractAcc = 0, bob = { ph: 0, land: 0, step: 0 }, autoCam = null, deadCam = null; // render interpolation: bodies and your eyes are drawn between the last two 60 Hz sim steps, so motion is smooth on // 90/120/144 Hz screens and never judders when a frame runs 0 or 2 steps let prevPos = null, camY = null; function snapPrev() { const n = G.ents.length; if (!prevPos || prevPos.length !== n * 4) prevPos = new Float32Array(n * 4); G.ents.forEach((e, i) => { prevPos[i * 4] = e.x; prevPos[i * 4 + 1] = e.y; prevPos[i * 4 + 2] = e.z; prevPos[i * 4 + 3] = e.crouch; }); } /** Interpolated [x, y, z, crouch] of an operator for drawing (falls back to the sim value after a teleport). */ function ipos(e, i) { if (!prevPos || G.over || state !== 'play' || CAP) return [e.x, e.y, e.z, e.crouch]; const a = clamp(acc / SIM.DT, 0, 1); const o = i * 4; if (Math.abs(e.x - prevPos[o]) + Math.abs(e.z - prevPos[o + 2]) > 3) return [e.x, e.y, e.z, e.crouch]; return [prevPos[o] + (e.x - prevPos[o]) * a, prevPos[o + 1] + (e.y - prevPos[o + 1]) * a, prevPos[o + 2] + (e.z - prevPos[o + 2]) * a, prevPos[o + 3] + (e.crouch - prevPos[o + 3]) * a]; } function loop(t) { const dt = lastT ? Math.min(0.1, (t - lastT) / 1000) : 1 / 60; lastT = t; frame(dt); requestAnimationFrame(loop); } function frame(dt) { if (!world) return; if (state === 'menu' || state === 'loading') { attractAcc += dt; let n = 0; while (attractAcc >= SIM.DT && n < 6) { SIM.step(attract, {}); attractAcc -= SIM.DT; n++; for (const e of attract.events) world.fx(e, { ents: attract.ents, cam: { x: 0, z: 0 } }); if (attract.over) startAttract(attract.map); } if (attractAcc > SIM.DT * 6) attractAcc = 0; world.draw(liveView(attract, { x: 0, z: 0, mode: 'orbit', r: 64, h: 52 }, true), dt); return; } if (isCine()) { stepCine(dt); return; } if (state === 'play') { if (!G.you.bot) applyLook(dt); if (howtoOn && (howtoT += dt) > 14) hideHowto(); acc += dt; let n = 0; while (acc >= SIM.DT && n < 6) { const inp = G.you.bot ? {} : readInput(SIM.DT); snapPrev(); SIM.step(G, inp); acc -= SIM.DT; n++; handleEvents(); if (state !== 'play') break; } if (acc > SIM.DT * 6) acc = 0; if (state === 'play' && G.over && !nextCine) { nextCine = 'final'; deathT = 0.9; } if (nextCine) { deathT -= dt; if (deathT <= 0) { const c = nextCine; nextCine = null; if (c === 'kill') startKillcam(); else startFinal(); } } } if (!G) return; const view = liveView(G, fpsCam(dt)); world.draw(view, state === 'pause' ? 0 : dt); look.fdx = look.fdy = 0; if (state === 'play' || state === 'pause') hud(view); } /** First-person camera for the live match: your eyes (bob, landing dip), a teammate's when you are out in Showdown. */ function fpsCam(dt) { const y = G.you; if (state === 'over') return { x: y.x, z: y.z, mode: 'orbit', r: 40, h: 34 }; let f = y; if (!y.alive && G.mode === 'showdown') { const m = G.ents.find((e) => e.team === y.team && e.alive); if (m) f = m; } const W = SIM.armOf(f); const mv = f.ground ? Math.min(1.3, f.moving / 6.4) : 0; bob.ph += dt * (7.2 + (f.sprint ? 3 : 0)) * (mv > 0.05 ? 1 : 0); bob.land *= Math.exp(-dt * 8); const ip = ipos(f, f.id); const eye = [ip[0], ip[1] + BODY.eye + (BODY.eyeC - BODY.eye) * ip[3], ip[2]]; // ease the eyes over small steps (kerbs, platforms) instead of popping; jumps, falls and crouch stay 1:1 if (camY == null || !f.ground || Math.abs(eye[1] - camY) > 0.6) camY = eye[1]; else camY = eye[1] + (camY - eye[1]) * Math.exp(-dt * 22); eye[1] = camY; let yaw, pitch; if (f === y && !y.bot) { yaw = look.yaw + look.ry; pitch = look.pitch + look.rc; } else { // autopilot / spectating: a smoothed view of the bot's aim (steady for videos) if (!autoCam || autoCam.id !== f.id) autoCam = { id: f.id, yaw: f.yaw, pitch: f.pitch }; autoCam.yaw += angD(autoCam.yaw, f.yaw) * Math.min(1, dt * 7); autoCam.pitch += (f.pitch - autoCam.pitch) * Math.min(1, dt * 6); yaw = autoCam.yaw; pitch = autoCam.pitch; } const dead = !f.alive; if (dead) { // redeploy view: raised behind where you fell, turned towards whoever took you out const k = G.ents[f.killer]; const tx = k && k.alive ? k.x : f.x + Math.cos(yaw) * 10, tz = k && k.alive ? k.z : f.z + Math.sin(yaw) * 10; const a = Math.atan2(tz - f.z, tx - f.x); const cx = f.x - Math.cos(a) * 2.2, cz = f.z - Math.sin(a) * 2.2, cyy = f.y + 3.1; if (!deadCam) deadCam = { yaw, pitch }; deadCam.yaw += angD(deadCam.yaw, a) * Math.min(1, dt * 3); deadCam.pitch += (Math.atan2((k ? k.y + 1.2 : 1) - cyy, Math.hypot(tx - cx, tz - cz)) - deadCam.pitch) * Math.min(1, dt * 3); return { mode: 'fps', follow: f.id, team: f.team, x: cx, y: cyy, z: cz, yaw: deadCam.yaw, pitch: deadCam.pitch, fov: S.fov, zoom: 1, weapon: null, alive: false, deadView: true }; } deadCam = null; return { mode: 'fps', follow: f.id, team: f.team, x: eye[0], y: eye[1] + (S.bob ? Math.sin(bob.ph * 2) * 0.012 * mv - bob.land * 0.08 : 0), z: eye[2], yaw, pitch, fov: S.fov, zoom: 1 + (W.zoom - 1) * smooth01(f.ads), weapon: W.id, alive: true, bob: S.bob, slide: f.slide > 0, moving: f.moving, sprint: f.sprint, ads: f.ads, ground: f.ground, crouch: f.crouch, reload: f.reload > 0 ? 1 - f.reload / (f.reloadMax || 1) : -1, lookDX: look.fdx, lookDY: look.fdy, }; } function liveView(g, cam, all = false) { if (!all && g === G && (G.tick - seenT >= 6 || G.tick < seenT)) { seenT = G.tick; seen = new Set(G.ents.filter((e) => e.team === G.you.team || SIM.spotted(G, G.you.team, e)).map((e) => e.id)); } const recon = !all && g === G && G.recon[G.you.team] > 0; return { ents: g.ents.map((e, i) => { const p = g === G ? ipos(e, i) : [e.x, e.y, e.z, e.crouch]; return { x: p[0], y: p[1], z: p[2], yaw: e.yaw, pitch: e.pitch, crouch: p[3], hpAbs: e.hp, name: e.name, prot: g.t < e.prot, flinch: g.t - e.lastHit < 0.12, alive: e.alive, moving: e.moving, firing: e.firing, hp: e.hp / e.maxHp, dash: e.dash > 0, kick: e.kick, team: e.team, you: e.you, show: true, mark: all ? 0 : e.team === G.you.team ? 1 : recon && seen.has(e.id) ? 2 : 0 }; }), zones: g.zones.map((z) => [z.owner, z.prog, z.contest]), sentries: g.sentries.map((s) => [s.x, s.z, s.yaw, s.team, s.firing]), charges: g.charges.map((c) => [c.x0, c.z0, c.x, c.z, c.t, c.fly, c.y0, c.y]), cam: cam || { x: 0, z: 0, mode: 'orbit' }, }; } function startKillcam() { const y = G.you; const killer = G.ents[y.killer]; if (!killer || G.over) return; const t1 = G.tick; const t0 = Math.max(0, t1 - 60 * 3.6); const last = G.feed.length ? G.feed[G.feed.length - 1] : null; const w = last ? last.w : killer.weapon; if (!beginCine('killcam', t0, t1, killer.id, 'KILLCAM', `${esc(killer.name)} · ${weaponName(w)}${last && last.hs ? ' · HEADSHOT' : ''} · ${killer.hp > 0 ? Math.ceil(killer.hp) : 0} HP LEFT`)) return; } function startFinal() { music(false); const lk = G.lastKill; if (lk && G.tick - lk.tick < 60 * 30) { const t1 = Math.min(G.tick, lk.tick + 60); const t0 = Math.max(0, lk.tick - 60 * 2.6); const k = G.ents[lk.by]; if (beginCine('final', t0, t1, lk.by, 'FINAL KILLCAM', `${esc(k.name)} · ${weaponName(lk.w)}${lk.hs ? ' · HEADSHOT' : ''} → ${esc(G.ents[lk.victim].name)}`, lk.tick)) return; } if (!startPotm()) showOver(); } const weaponName = (w) => (w === 'skyfall' ? 'SKYFALL STRIKE' : w === 'sentry' ? 'SENTRY' : w === 'charge' ? 'CHARGE' : ARMS[w] ? ARMS[w].short : ''); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); // ------------------------------------------------------------------ events -> fx, sound, HUD let feedRows = []; function handleEvents() { const y = G.you; const camF = { mode: 'fps', follow: y.id, x: y.x, z: y.z }; const view = { ents: G.ents, cam: camF }; for (const e of G.events) { world.fx(e, view); const rel = (x, z) => angD(look.yaw, Math.atan2(z - y.z, x - y.x)); const near = (x, z) => Math.max(0, 1 - Math.hypot(x - y.x, z - y.z) / 45); const pan = (x, z) => clamp(Math.sin(rel(x, z)), -1, 1) * 0.85; if (e.k === 'shot') { const src = e.from >= 0 ? G.ents[e.from] : null; const mine = src && src.you; SFX.shot(e.w, mine ? 0.7 : near(e.x0, e.z0) * 0.6, mine ? 0 : pan(e.x0, e.z0), mine); if (mine && !y.bot) { const W = ARMS[e.w] || WEAPONS.vk9; const k = (1 - 0.35 * y.ads) * (y.crouch > 0.5 ? 0.8 : 1); look.rc += W.recoil * k / (W.pellets > 1 ? W.pellets : 1); look.ry += (Math.random() - 0.5) * W.recoil * 0.5 * k / (W.pellets > 1 ? W.pellets : 1); bloomKick = 1; } // bullets cracking past your head if (!mine && src && src.team !== y.team && e.hit !== y.id) { const d = distToSeg(y.x, y.y + 1.5, y.z, e.x0, e.y0, e.z0, e.x1, e.y1, e.z1); if (d < 1.2 && Math.random() < 0.5) SFX.click(); } } else if (e.k === 'boom') SFX.boom(near(e.x, e.z), pan(e.x, e.z)); else if ((e.k === 'dash' || e.k === 'slide') && G.ents[e.id].you) { SFX.dash(); if (e.k === 'slide') world.stage.shake(0.05); } else if (e.k === 'jump' && G.ents[e.id].you) SFX.jump(); else if (e.k === 'mantle' && G.ents[e.id].you) { SFX.jump(); bob.land = 0.4; } else if (e.k === 'land' && G.ents[e.id].you) { SFX.land(Math.min(1, -e.v / 10)); bob.land = Math.min(1, -e.v / 9); } else if (e.k === 'reloaded' && G.ents[e.id].you) SFX.magIn(); else if (e.k === 'hit') { if (e.you) { hitMark(e.hs ? 'head' : 'body'); SFX.hit(e.hs ? 'head' : 'body'); } if (e.id === y.id) { const d = $('dmg'); d.classList.add('on'); setTimeout(() => d.classList.remove('on'), 60); world.stage.shake(0.12); dmgDir(Math.atan2(e.fz - y.z, e.fx - y.x)); SFX.hurt(); look.rc += 0.012; } } else if (e.k === 'kill') { const k = G.ents[e.by], v = G.ents[e.victim]; feed(k, v, e.w, e.hs); if (e.by === y.id) { hitMark('kill'); SFX.hit('kill'); killConfirm(v, e.hs); pop(e.hs ? `HEADSHOT · ${esc(v.name)}` : e.multi >= 3 ? 'TRIPLE BREAK' : e.multi === 2 ? 'DOUBLE BREAK' : `ELIMINATED · ${esc(v.name)}`, (e.multi >= 2 ? 100 + 50 * (e.multi - 1) : 100) + (e.hs ? 25 : 0)); if (e.multi >= 2) banner(e.multi >= 3 ? 'TRIPLE BREAK' : 'DOUBLE BREAK', `${e.streak} IN A ROW`); world.stage.punch(0.003); } if (e.victim === y.id && !G.over) { nextCine = 'kill'; deathT = 0.75; world.stage.flash('#200000', 0.4); } } else if (e.k === 'streak') { const s = STREAKS.find((q) => q.id === e.s); const who = G.ents[e.id]; if (who.you) { SFX.streak(); banner(s.name, e.s === 'recon' ? 'ONLINE · ENEMIES MARKED' : COARSE ? 'TAP STREAK TO CALL IT IN' : 'PRESS F TO CALL IT IN'); } else if (who.team !== y.team && e.s !== 'recon') banner(`ENEMY ${s.name}`, '', true); } else if (e.k === 'recon' && e.team !== y.team && y.perk !== 'ghost') banner('ENEMY RECON DRONE', 'YOU ARE ON THEIR MAP', true); else if (e.k === 'skyfall') { SFX.streak(); if (G.ents[e.id].team !== y.team) banner('INCOMING SKYFALL', 'MOVE', true); } else if (e.k === 'cap') { const good = e.team === y.team; SFX.cap(good); const zz = G.zones.find((z) => z.id === e.zone); const mine = Math.hypot(y.x - zz.x, y.z - zz.z) < 6 && good; if (mine) pop(`ZONE ${e.zone} CAPTURED`, 150); const all = G.zones.every((z) => z.owner === e.team); if (G.mode === 'turf') banner(all ? (good ? 'TURFBREAK' : 'TURF LOST') : `ZONE ${e.zone} ${good ? 'TAKEN' : 'LOST'}`, all ? (good ? 'ALL THREE ZONES ARE YOURS' : 'THEY HOLD ALL THREE') : '', !good); } else if (e.k === 'round') { const good = e.team === y.team; SFX.cap(good); banner(good ? 'ROUND WON' : 'ROUND LOST', `${G.rounds[y.team]} - ${G.rounds[1 - y.team]}`, !good); } else if (e.k === 'roundStart') { banner(`ROUND ${e.round}`, 'ONE LIFE · TAKE B OR WIPE THEM'); look.yaw = y.yaw; look.pitch = 0; } else if (e.k === 'spawn' && e.id === y.id) { look.yaw = y.yaw; look.pitch = 0; look.rc = look.ry = 0; } } // footsteps + reload start sound if (y.alive && y.ground && y.moving > 1.5 && !y.bot) { bob.step -= SIM.DT * (y.moving / 2.4); if (bob.step <= 0) { bob.step = 1; SFX.step(y.crouch > 0.5 ? 0.4 : y.sprint ? 1.2 : 0.8); } } if (y.reload > 0 && !reloadWas) SFX.magOut(); reloadWas = y.reload > 0; if (y.slot !== slotWas) { if (slotWas != null) SFX.swap(); slotWas = y.slot; } } let reloadWas = false, slotWas = null, bloomKick = 0; function distToSeg(px, py, pz, ax, ay, az, bx, by, bz) { const dx = bx - ax, dy = by - ay, dz = bz - az; const L = dx * dx + dy * dy + dz * dz || 1; const t = clamp(((px - ax) * dx + (py - ay) * dy + (pz - az) * dz) / L, 0, 1); return Math.hypot(ax + dx * t - px, ay + dy * t - py, az + dz * t - pz); } function feed(k, v, w, hs) { const cls = (e) => (e.team === 0 ? 'a' : 'c'); const me = k.you || v.you; feedRows.push({ t: performance.now(), html: `

${esc(k.name)}${weaponName(w)}${hs ? '' : ''}${esc(v.name)}

` }); feedRows = feedRows.slice(-5); renderFeed(); } function renderFeed() { const now = performance.now(); feedRows = feedRows.filter((r) => now - r.t < 6000); $('feed').innerHTML = feedRows.map((r) => r.html).join(''); } function pop(text, pts) { const p = document.createElement('p'); p.innerHTML = `${text}+${pts}`; $('pops').appendChild(p); setTimeout(() => p.remove(), 1650); } let banT = 0; function banner(t, sub = '', bad = false) { const b = $('banner'); b.innerHTML = `${esc(t)}${sub ? `${esc(sub)}` : ''}`; b.style.color = bad ? '#FF6A5A' : '#FFFFFF'; b.classList.remove('show'); void b.offsetWidth; b.classList.add('show'); clearTimeout(banT); banT = setTimeout(() => b.classList.remove('show'), 2100); } /** Kill confirm: a red skull-free "X" badge that punches in under the crosshair with the match kill count. */ let kcT = 0; function killConfirm(v, hs) { const el = $('kconf'); if (!el) return; el.innerHTML = `${G.you.kills}${hs ? 'HEADSHOT' : 'KILL'}`; el.className = `kconf${hs ? ' hs' : ''}`; void el.offsetWidth; el.classList.add('on'); clearTimeout(kcT); kcT = setTimeout(() => el.classList.remove('on'), 900); world.stage.flash(hs ? '#3A2A00' : '#2A0400', 0.12); } let hitT = 0; function hitMark(kind) { const h = $('hitm'); h.className = `hitm on ${kind}`; clearTimeout(hitT); hitT = setTimeout(() => { h.className = `hitm ${kind}`; }, kind === 'kill' ? 260 : 110); } function dmgDir(a) { const host = $('dmgdir'); const el = document.createElement('i'); el.dataset.a = String(a); host.appendChild(el); setTimeout(() => el.remove(), 1100); if (host.children.length > 6) host.firstChild.remove(); } function board(on) { const b = $('board'); if (!b) return; if (!on || !G) { b.hidden = true; return; } const rows = G.ents.slice().sort((p, q) => q.score - p.score); b.innerHTML = `${rows.map((e) => ``).join('')}
OPERATORKDSCORE
${esc(e.name)}${e.kills}${e.deaths}${fmt(e.score)}
`; b.hidden = false; } // ------------------------------------------------------------------ HUD let lastHud = {}; function buildZonesHud() { $('zones').innerHTML = G.zones.map((z) => `${z.id}`).join(''); $('zmark').innerHTML = G.zones.map((z) => `
${z.id}
`).join(''); } function hud(view) { const y = G.you; const W = SIM.armOf(y); const set = (id, v, f) => { if (lastHud[id] !== v) { lastHud[id] = v; f(v); } }; if (G.mode === 'turf') { set('sc0', Math.floor(G.score[0]), (v) => { $('sc0').textContent = v; }); set('sc1', Math.floor(G.score[1]), (v) => { $('sc1').textContent = v; }); } else { set('sc0', G.rounds[0], (v) => { $('sc0').textContent = v; }); set('sc1', G.rounds[1], (v) => { $('sc1').textContent = v; }); } const sec = Math.ceil(G.left); set('tm', sec, (v) => { $('timer').textContent = `${Math.floor(v / 60)}:${String(v % 60).padStart(2, '0')}`; }); G.zones.forEach((z, i) => { const el = $('zones').children[i]; if (!el) return; const k = `${z.owner}${z.contest ? 'c' : ''}${Math.round(z.prog * 20)}`; set(`z${i}`, k, () => { el.className = `${z.owner >= 0 ? `o${z.owner}` : ''}${z.contest ? ' ct' : ''}`; const bar = el.firstElementChild; bar.style.width = `${Math.abs(z.prog) * 100}%`; bar.style.background = z.prog > 0 ? TEAMS[0].color : TEAMS[1].color; }); }); set('hp', Math.ceil(y.hp), (v) => { $('hpf').style.width = `${(v / y.maxHp) * 100}%`; $('hpn').textContent = Math.max(0, v); $('hpf').parentElement.classList.toggle('low', v < y.maxHp * 0.35); }); set('ammo', `${y.ammo}|${y.reload > 0}`, () => { $('ammo').textContent = y.reload > 0 ? '--' : y.ammo; $('ammo').classList.toggle('low', y.ammo <= Math.ceil(W.mag * 0.25) && y.reload <= 0); }); $('rlf').style.width = y.reload > 0 ? `${(1 - y.reload / (y.reloadMax || 1)) * 100}%` : '0%'; set('wn', `${W.id}`, () => { $('wname').textContent = W.name; $('mag').textContent = `/ ${W.mag}`; $('callsign').textContent = y.name; $('slots').innerHTML = [WEAPONS[y.weapon], ARMS.p9].map((w, i) => `${i + 1}${w.short}`).join(''); }); set('pips', `${y.streak}|${y.owned.join()}`, () => { $('pips').innerHTML = [1, 2, 3, 4, 5, 6, 7].map((i) => ``).join(''); const top = y.owned[y.owned.length - 1]; const s = STREAKS.find((q) => q.id === top); $('k-streak').hidden = !s; $('t-streak').hidden = !s; if (s) { $('k-streak').textContent = `F · ${s.name}`; $('t-streak').textContent = s.id === 'skyfall' ? 'SKYFALL' : s.name; } }); set('nade', y.nades, (v) => { $('k-nade').textContent = `G · CHARGE ${v}`; $('t-nade').textContent = `CHG ${v}`; }); let c = ''; if (!y.alive) c = G.mode === 'showdown' ? 'ELIMINATED · SPECTATING' : `REDEPLOYING ${Math.max(0, y.respawn).toFixed(1)}`; else if (G.roundOver > 0) c = 'NEXT ROUND'; else if (G.left < 10.5 && G.mode === 'turf') c = `${Math.ceil(G.left)}`; set('center', c, (v) => { $('center').textContent = v; }); // crosshair: gap follows the real spread (it blooms while you fire, move or jump); hidden while aiming down sights const H = cv.clientHeight || 600; const vf = (world.cam.fov * Math.PI) / 180; bloomKick *= 0.82; const gap = clamp((Math.tan(SIM.spreadOf(y)) / Math.tan(vf / 2)) * (H / 2) + bloomKick * 3, 3, 90); const xh = $('xh'); xh.style.setProperty('--g', `${gap.toFixed(1)}px`); const hideX = y.ads > 0.55 || !y.alive || y.sprint; xh.classList.toggle('off', hideX); const at = y.alive ? SIM.aimTarget(G, { ...y, yaw: view.cam.yaw, pitch: view.cam.pitch }, W.range * 1.6) : null; xh.classList.toggle('foe', !!at); // target info under the crosshair: who you are on, how much health they have left, and how far const tg = $('tgt'); const tk = at ? `${at.ent.id}|${Math.ceil(at.ent.hp)}|${Math.round(at.dist)}` : ''; set('tgt', tk, () => { if (!at) { tg.classList.remove('on'); return; } tg.classList.add('on'); tg.innerHTML = `${esc(at.ent.name)}${Math.round(at.dist)} M`; }); $('scope').hidden = !(W.sight === 'scope' && y.ads > 0.86 && y.alive); // damage direction arcs (relative to where you look now) for (const el of $('dmgdir').children) el.style.transform = `rotate(${(angD(look.yaw, Number(el.dataset.a)) * 180) / Math.PI}deg)`; // zone markers: projected on screen, clamped to the edges when off screen, with distance const Wd = cv.clientWidth || 800; G.zones.forEach((z, i) => { const el = $('zmark').children[i]; if (!el) return; const p = world.toScreen(z.x, 3.2, z.z, Wd, H); const d = Math.hypot(z.x - y.x, z.z - y.z); let px = p.x, py = p.y; const behind = !p.on || p.behind; if (behind) { const a = angD(look.yaw, Math.atan2(z.z - y.z, z.x - y.x)); px = Wd / 2 + Math.sin(a) * Wd * 0.46; py = H * 0.5 - Math.cos(a) * H * 0.3; } px = clamp(px, 26, Wd - 26); py = clamp(py, 70, COARSE ? H * 0.6 : H - 90); el.style.transform = `translate(${px.toFixed(0)}px,${py.toFixed(0)}px)`; el.className = `zm${z.owner >= 0 ? ` o${z.owner}` : ''}${z.contest ? ' ct' : ''}${d < z.r ? ' in' : ''}`; el.lastChild.textContent = d < z.r ? (z.contest ? 'CONTESTED' : z.owner === y.team ? 'HOLDING' : 'CAPTURING') : `${Math.round(d)}M`; }); if ((G.tick & 3) === 0) { drawMini(); renderFeed(); if (!$('board').hidden) board(true); } } let miniBase = null; function drawMiniBase() { const c = document.createElement('canvas'); c.width = c.height = 520; const g = c.getContext('2d'); const s = 5; // 5 px per metre, map centred at 260 g.fillStyle = 'rgba(10,12,18,.92)'; g.fillRect(0, 0, 520, 520); for (const o of curMap.obstacles) { if (Math.abs(o.x) > 60 || Math.abs(o.z) > 60) continue; g.fillStyle = o.kind === 'bldg' ? '#2C3140' : o.h > 1.7 ? '#475062' : '#3A4150'; g.fillRect((o.x - o.w / 2) * s + 260, (o.z - o.d / 2) * s + 260, o.w * s, o.d * s); } miniBase = c; } function drawMini() { const g = $('mini').getContext('2d'); const y = G.you; const R = 150; const s = 5 * 0.55; // radar: 30 m around you, forward is up g.save(); g.clearRect(0, 0, 300, 300); g.beginPath(); g.arc(R, R, R - 2, 0, 7); g.clip(); g.fillStyle = 'rgba(8,10,16,.9)'; g.fillRect(0, 0, 300, 300); g.translate(R, R); g.rotate(-Math.PI / 2 - look.yaw); g.scale(s / 5, s / 5); g.translate(-y.x * 5, -y.z * 5); g.drawImage(miniBase, -260, -260); for (const z of G.zones) { g.strokeStyle = z.owner >= 0 ? TEAMS[z.owner].color : '#AAB'; g.lineWidth = 8; g.beginPath(); g.arc(z.x * 5, z.z * 5, z.r * 5, 0, 7); g.stroke(); } for (const e of G.ents) { if (!e.alive || e.you || (e.team !== y.team && !seen.has(e.id))) continue; g.fillStyle = e.team === 0 ? TEAMS[0].color : TEAMS[1].color; g.beginPath(); g.arc(e.x * 5, e.z * 5, 11, 0, 7); g.fill(); } g.restore(); g.save(); g.translate(R, R); g.fillStyle = '#FFFFFF'; g.beginPath(); g.moveTo(0, -13); g.lineTo(-9, 9); g.lineTo(0, 4); g.lineTo(9, 9); g.fill(); g.strokeStyle = 'rgba(255,255,255,.22)'; g.lineWidth = 2; g.beginPath(); g.moveTo(0, 0); g.lineTo(-R * 0.7, -R * 0.75); g.moveTo(0, 0); g.lineTo(R * 0.7, -R * 0.75); g.stroke(); g.restore(); g.font = '800 26px TB, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle'; G.zones.forEach((z) => { const a = Math.atan2(z.z - y.z, z.x - y.x) - look.yaw; const d = Math.min(R - 22, Math.hypot(z.x - y.x, z.z - y.z) * s); g.fillStyle = z.owner >= 0 ? TEAMS[z.owner].color : '#E8EAF0'; g.fillText(z.id, R + Math.sin(a) * d, R - Math.cos(a) * d); }); } // ------------------------------------------------------------------ results, share, challenge let R = null; function showOver() { state = 'over'; music(false); $('hud').hidden = true; $('cine').hidden = true; board(false); if (locked()) try { document.exitPointerLock(); } catch { /* */ } R = SIM.result(G); { // difficulty follows you a little: a clear win nudges the bots up, a rough loss nudges them down const kd = R.kills / Math.max(1, R.deaths); if (R.win) S.wins = (S.wins || 0) + 1; if (mode !== 'showdown') S.skillAdj = clamp((S.skillAdj || 0) + (R.win && kd >= 1.5 ? 0.05 : !R.win && kd < 0.7 ? -0.05 : 0), -0.2, 0.2); } const passed = mode === 'daily' && con.test(R); if (mode === 'daily') { const d = chal ? chal.d : DAY; S.best[d] = Math.max(S.best[d] || 0, R.score); if (d === TODAY) { if (S.lastDay === TODAY - 1) S.streak++; else if (S.lastDay !== TODAY) S.streak = 1; S.lastDay = TODAY; } const keys2 = Object.keys(S.best).map(Number).sort((a, b) => b - a).slice(0, 40); S.best = Object.fromEntries(keys2.map((k) => [k, S.best[k]])); } S.kills += R.kills; if (!S.coached) S.coached = true; save(); updUnder(); const res = R.draw ? 'DRAW' : R.win ? 'VICTORY' : 'DEFEAT'; $('o-res').textContent = res; $('o-res').className = `o-res ${R.win ? 'w' : 'l'}`; $('o-sub').textContent = `${mode === 'daily' ? `CONTRACT #${con.day} · ` : mode === 'showdown' ? 'SHOWDOWN · ' : ''}${MAPS[curMap.id].name} · ${G.mode === 'turf' ? `${R.team[0]} - ${R.team[1]}` : `${R.rounds[0]} - ${R.rounds[1]} ROUNDS`}`; const kd = R.deaths ? (R.kills / R.deaths).toFixed(2) : R.kills.toFixed(2); $('o-stats').innerHTML = `
${fmt(R.score)}SCORE${R.mvp ? ' · MVP' : ` · #${R.rank} OF ${R.board.length}`}
${R.kills}ELIMS
${R.deaths}DEATHS
${kd}K/D
${R.heads}HEADSHOTS
${R.bestStreak}BEST STREAK
${R.caps}CAPTURES
`; $('o-board').innerHTML = 'OPERATORKDSCORE' + R.board.map((b) => `${esc(b.name)}${b.k}${b.d}${fmt(b.s)}`).join(''); const oc = $('o-con'); if (mode === 'daily') { oc.hidden = false; oc.textContent = `CONTRACT: ${con.text} · ${passed ? 'COMPLETE' : 'NOT YET'}`; oc.className = `o-con ${passed ? 'ok' : 'no'}`; } else oc.hidden = true; const vs = $('o-vs'); if (chal && mode === 'daily') { vs.hidden = false; vs.textContent = R.score > chal.s ? `YOU BEAT ${chal.n} BY ${fmt(R.score - chal.s)}` : `${chal.n} STILL LEADS BY ${fmt(chal.s - R.score)} · RUN IT BACK`; } else vs.hidden = true; const text = shareText(); const url = challengeUrl(); const it = intents(GAME, text, url.replace(SITE, '')); $('sx').href = it.x || `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`; $('sw').href = it.whatsapp || `https://wa.me/?text=${encodeURIComponent(`${text} ${url}`)}`; $('over').hidden = false; showClip(); FP.afterWin($('over-fp'), 'match'); count(GAME, 'done', `${mode}-${R.win ? 'win' : 'loss'}`); if (TGM.app() && S.plays === 1) TGM.played().then((r) => { tgInfo = { ...tgInfo, ...r }; }); try { window.CMXPortal && window.CMXPortal.gameplayStop && window.CMXPortal.gameplayStop(); } catch { /* */ } } function shareText() { const res = R.draw ? 'DRAW' : R.win ? 'VICTORY' : 'DEFEAT'; return `TURFBREAK ${mode === 'daily' ? `Contract #${con.day}` : mode === 'showdown' ? 'Showdown' : 'Quick match'} · ${MAPS[curMap.id].name}: ${res} · ${R.kills} elims · ${R.heads} headshots · best streak ${R.bestStreak} · ${fmt(R.score)} pts. Beat me:`; } function challengeUrl() { const j = { d: chal ? chal.d : DAY, n: S.name, s: R ? R.score : 0, k: R ? R.kills : 0, w: S.weapon }; const b = btoa(unescape(encodeURIComponent(JSON.stringify(j)))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); return `${SITE}${PATH}?c=${b}`; } async function scoreCard() { const W = 1080, H = 1350, c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d'); g.fillStyle = '#05060A'; g.fillRect(0, 0, W, H); const src = frameShot || world.stage.canvas; const s = Math.max(W / src.width, (H * 0.62) / src.height); try { g.drawImage(src, (W - src.width * s) / 2, 0, src.width * s, src.height * s); } catch { /* */ } const gr = g.createLinearGradient(0, H * 0.3, 0, H * 0.68); gr.addColorStop(0, 'rgba(5,6,10,0)'); gr.addColorStop(1, 'rgba(5,6,10,1)'); g.fillStyle = gr; g.fillRect(0, 0, W, H); g.fillStyle = '#05060A'; g.fillRect(0, H * 0.68, W, H); const F = (w, sz, it = false) => `${it ? 'italic ' : ''}${w} ${sz}px TB, "Arial Narrow", sans-serif`; g.textAlign = 'left'; g.fillStyle = '#F2F2F0'; g.font = F(800, 92, true); g.fillText('TURF', 60, 120); g.fillStyle = '#FF8A1F'; g.fillText('BREAK', 60 + g.measureText('TURF').width, 120); g.fillStyle = '#AEB4C2'; g.font = F(700, 30); g.fillText(`${mode === 'daily' ? `CONTRACT #${con.day}` : mode.toUpperCase()} · ${MAPS[curMap.id].name}`, 64, 168); const res = R.draw ? 'DRAW' : R.win ? 'VICTORY' : 'DEFEAT'; g.fillStyle = R.win ? '#FF8A1F' : '#D8DCE4'; g.font = F(800, 170, true); g.fillText(res, 56, H * 0.66); const stats = [[fmt(R.score), 'SCORE'], [R.kills, 'ELIMS'], [R.heads, 'HEADSHOTS'], [R.bestStreak, 'STREAK']]; stats.forEach(([v, l], i) => { const x = 60 + i * 250; g.fillStyle = '#FFFFFF'; g.font = F(800, 84); g.fillText(String(v), x, H * 0.79); g.fillStyle = '#8A90A0'; g.font = F(700, 28); g.fillText(l, x + 2, H * 0.825); }); g.fillStyle = '#FFFFFF'; g.font = F(800, 38); g.fillText(`${S.name}${R.mvp ? ' · MVP' : ''}`, 60, H * 0.885); if (mode === 'daily') { g.fillStyle = con.test(R) ? '#5CFFB0' : '#8A90A0'; g.font = F(700, 30); g.fillText(`CONTRACT: ${con.text.toUpperCase()} · ${con.test(R) ? 'COMPLETE' : 'NOT YET'}`, 60, H * 0.925); } g.fillStyle = '#FF8A1F'; g.fillRect(0, H - 70, W, 70); g.fillStyle = '#120A02'; g.font = F(800, 34); g.textAlign = 'center'; g.fillText('PLAY FREE · CYBERMAXTOOLS.COM/GAMES/TURFBREAK', W / 2, H - 24); return canvasBlob(c); } $('retry').onclick = () => restart(); $('o-menu').onclick = () => toMenu(); $('p-resume').onclick = () => resume(); $('p-menu').onclick = () => toMenu(); $('h-pause').addEventListener('click', () => pause()); $('mute').onclick = () => setMute(!muted); $('share').onclick = async () => { const blob = await scoreCard(); if (TGM.app()) { TGM.share(shareText(), challengeUrl()); count(GAME, 'share', 'telegram'); return; } share(GAME, shareText(), challengeUrl().replace(SITE, ''), blob, `turfbreak-${mode === 'daily' ? `contract-${con.day}` : 'match'}.png`); }; $('card').onclick = async () => { const blob = await scoreCard(); download(blob, `turfbreak-${mode === 'daily' ? `contract-${con.day}` : 'match'}.png`); count(GAME, 'share', 'card'); toast('Score card saved.'); }; $('clip').onclick = () => { if (!clip) return; download(clip.blob, `turfbreak-play-of-the-match.${clip.ext}`); count(GAME, 'share', 'clip'); toast('Clip saved. Post it anywhere.'); }; $('beat').onclick = async () => { const url = challengeUrl(); const text = `${S.name} scored ${fmt(R.score)} on Turfbreak Contract #${chal ? chal.d : DAY}. Beat it:`; count(GAME, 'share', 'challenge'); if (TGM.app()) { TGM.challenge(chal ? chal.d : DAY, R.score, text); return; } if (navigator.share) { try { await navigator.share({ title: 'Turfbreak challenge', text, url }); return; } catch { /* cancelled */ } } try { await navigator.clipboard.writeText(`${text} ${url}`); toast('Challenge link copied. Send it to a friend.'); } catch { prompt('Copy your challenge link:', url); } }; // ------------------------------------------------------------------ settings (field of view, sensitivity, invert, touch assists) function renderSettings() { const row = (id, label, html) => ``; const tog = (k) => ``; $('st-body').innerHTML = [ row('st-fov', `FIELD OF VIEW ${S.fov}°`, ``), row('st-sens', `MOUSE SENSITIVITY ${S.sens.toFixed(2)}`, ``), row('st-ads', `AIM-DOWN-SIGHTS SENSITIVITY ${S.adsSens.toFixed(2)}`, ``), row('st-tsens', `TOUCH LOOK SPEED ${S.tsens.toFixed(2)}`, ``), `
INVERT LOOK UP/DOWN${tog('invert')}
`, `
HEAD BOB${tog('bob')}
`, `
AIM DOWN SIGHTS: TOGGLE (OFF = HOLD)${tog('adsToggle')}
`, `
TOUCH: AUTO-FIRE ON TARGET${tog('autofire')}
`, `
TOUCH: AIM ASSIST${tog('assist')}
`, ].join(''); for (const [id, k, f] of [['st-fov', 'fov', (v) => `${v}°`], ['st-sens', 'sens', (v) => Number(v).toFixed(2)], ['st-ads', 'adsSens', (v) => Number(v).toFixed(2)], ['st-tsens', 'tsens', (v) => Number(v).toFixed(2)]]) { $(id).oninput = (e) => { S[k] = Number(e.target.value); $(`${id}-v`).textContent = f(S[k]); save(); }; } for (const b of $('st-body').querySelectorAll('[data-t]')) b.onclick = () => { S[b.dataset.t] = !S[b.dataset.t]; save(); SFX.ui(); renderSettings(); }; } function openSettings() { renderSettings(); $('settings').hidden = false; } $('m-set').onclick = () => openSettings(); $('p-set').onclick = () => openSettings(); $('st-done').onclick = () => { $('settings').hidden = true; }; // ------------------------------------------------------------------ buying: web checkout or, inside Telegram, Stars let tgInfo = {}; function openFull(from) { if (TGM.app()) TGM.sheet('turfbreak-full', { perks: FULL_PERKS, onPaid: claimTG }); else FP.open(from); } function openSkins(from) { if (TGM.app()) TGM.sheet('turfbreak-skins', { perks: ['6 operator looks with their own visor glow', 'Matching sleeves, weapon glow and tracers in first person', 'Cosmetic only: same maps, same scores'], onPaid: claimTG }); else skins.open(from); } async function claimTG() { tgInfo = { ...tgInfo, ...(await TGM.status()) }; const codes = tgInfo.codes || {}; if (codes['turfbreak-full'] && !FP.ownsGame()) await FP.unlockGame(codes['turfbreak-full']); if (codes['turfbreak-skins'] && !skins.owned()) await skins.unlock(codes['turfbreak-skins']); renderMenu(); } $('pass-btn').onclick = () => openFull('header'); $('skins-btn').onclick = () => openSkins('under'); $('tg-invite').onclick = () => { if (tgInfo.invite) TGM.invite(tgInfo.invite); }; FP.onChange(() => renderMenu()); skins.onChange(() => renderMenu()); // ------------------------------------------------------------------ menu function renderMenu() { if (!world || !SKINS.length) return; const c = contract(chal ? chal.d : DAY); const pro = FP.isPro(); $('m-contract').innerHTML = `CONTRACT #${c.day}${c.day === TODAY ? ' · TODAY' : ''}${MAPS[c.map].name} · ${MAPS[c.map].area} · same map for everyone todayOBJECTIVE: ${esc(c.text)}`; const ch = $('m-chal'); if (chal) { ch.hidden = false; ch.textContent = `${chal.n} SCORED ${fmt(chal.s)} (${chal.k} ELIMS) ON CONTRACT #${chal.d} · BEAT IT`; } else ch.hidden = true; $('m-show').innerHTML = `SHOWDOWN 3v3${pro ? '' : 'FULL GAME'}`; $('m-weap').innerHTML = WEAPON_IDS.map((id) => { const w = WEAPONS[id]; const bars = (v) => [0, 1, 2, 3, 4].map((i) => ``).join(''); const dmg = Math.round(Math.min(5, (w.dmg * w.pellets / w.cd) / 60)), rng2 = Math.round(w.range / 10); return ``; }).join(''); $('m-perk').innerHTML = PERK_IDS.map((id) => ``).join(''); const own = skins.owned(); $('m-skin').innerHTML = SKINS.map((k, i) => ``).join(''); $('m-name').value = S.name; $('m-lo').innerHTML = `LOADOUT ${WEAPONS[S.weapon].short} + P9 · ${PERKS[S.perk].name} · ${(SKINS[S.skin] || SKINS[0]).name.toUpperCase()}${$('m-load').hidden ? 'EDIT' : 'DONE'}`; $('m-vet').classList.toggle('on', S.vet && pro); $('m-vet').classList.toggle('lock', !pro); $('m-map').textContent = `MAP: ${S.map === 'daily' || !pro ? 'RANDOM' : MAPS[S.map].name}`; $('m-map').classList.toggle('lock', !pro); $('m-stats').textContent = `${S.name} · ${S.plays} matches · ${S.kills} eliminations${S.streak ? ` · ${S.streak}-day contract streak` : ''}`; $('tg-row').hidden = !(TGM.app() && tgInfo.invite); for (const b of $('m-weap').querySelectorAll('[data-w]')) b.onclick = () => { S.weapon = b.dataset.w; save(); SFX.ui(); renderMenu(); }; for (const b of $('m-perk').querySelectorAll('[data-p]')) b.onclick = () => { S.perk = b.dataset.p; save(); SFX.ui(); renderMenu(); }; for (const b of $('m-skin').querySelectorAll('[data-s]')) b.onclick = () => { const i = Number(b.dataset.s); if (i > 0 && !skins.owned()) { openSkins('menu'); return; } S.skin = i; save(); SFX.ui(); renderMenu(); }; updUnder(); } $('m-play').onclick = () => startMatch('daily'); $('m-lo').onclick = () => { const l = $('m-load'); l.hidden = !l.hidden; $('m-lo').setAttribute('aria-expanded', String(!l.hidden)); SFX.ui(); renderMenu(); }; $('m-quick').onclick = () => startMatch('quick'); $('m-show').onclick = () => startMatch('showdown'); $('m-vet').onclick = () => { if (!FP.isPro()) { openFull('veteran'); return; } S.vet = !S.vet; save(); renderMenu(); }; $('m-map').onclick = () => { if (!FP.isPro()) { openFull('maps'); return; } const order = ['daily', ...MAP_IDS]; S.map = order[(order.indexOf(S.map) + 1) % order.length]; save(); renderMenu(); }; $('m-name').addEventListener('input', (e) => { S.name = e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '').slice(0, 10) || S.name; save(); }); function updUnder() { $('streak').textContent = S.streak || 0; $('best').textContent = S.best[DAY] ? fmt(S.best[DAY]) : '-'; $('plays').textContent = S.plays; } // ------------------------------------------------------------------ test + trailer hooks window.__turf = { ready: false, restartMs: 0, get state() { return state; }, G: () => G, attract: () => attract, world: () => world, look: () => ({ ...look }), lock: () => ({ ...lockInfo, locked: locked() }), settings: () => ({ fov: S.fov, sens: S.sens, adsSens: S.adsSens, invert: S.invert, autofire: S.autofire, assist: S.assist, bob: S.bob, mouseK: MOUSE_K, skill: G ? G.skill : botSkill(), plays: S.plays }), howto: () => howtoOn, start: (m = 'daily') => startMatch(m), auto: (on = true) => { if (G) { G.you.bot = on; autoCam = null; } }, advance: (n = 1) => { for (let i = 0; i < n; i++) frame(1 / 30); }, fast: (sec) => { const n = Math.round(sec * 60); for (let i = 0; i < n && !G.over; i++) { SIM.step(G, G.you.bot ? {} : {}); } world.stage.parts.clear(); seenT = -99; autoCam = null; }, stats: () => world.stats(), showCaption: (h) => { $('cap').innerHTML = h; }, endCard: (h) => { const e = $('endcard'); e.innerHTML = h; e.hidden = !h; if (h) { $('cine').hidden = true; $('hud').hidden = true; } }, hideUi: (on) => { document.body.classList.toggle('noui', on); for (const id of ['menu']) $(id).style.opacity = on ? 0 : ''; }, skip: () => skipCine(), full: (on = true) => { document.body.classList.toggle('capfull', on); fit(); }, callStreak: (id) => { const y = G.you; let best = null, bd = 40; for (const o of G.ents) if (o.team !== y.team && o.alive) { const d = Math.hypot(o.x - y.x, o.z - y.z); if (d < bd && d > 8) { bd = d; best = o; } } if (best) { y.yaw = Math.atan2(best.z - y.z, best.x - y.x); look.yaw = y.yaw; if (autoCam) autoCam.yaw = y.yaw; } y.owned.push(id); SIM.useStreak(G, y, id); }, menuOff: (on = true) => { $('menu').style.visibility = on ? 'hidden' : ''; }, face: (maxD = 40) => { const y = G.you; const eye = SIM.eyeOf(y); let best = null, bd = maxD; for (const o of G.ents) if (o.team !== y.team && o.alive && SIM.canSee(G, y, o)) { const d = Math.hypot(o.x - y.x, o.z - y.z); if (d < bd) { bd = d; best = o; } } if (!best) return null; look.yaw = Math.atan2(best.z - y.z, best.x - y.x); look.pitch = Math.atan2(best.y + 1.2 - eye[1], bd); y.yaw = look.yaw; y.pitch = look.pitch; autoCam = null; return { d: bd, id: best.id }; }, aimOn: () => { const y = G.you; const t = SIM.aimTarget(G, { ...y, yaw: look.yaw, pitch: look.pitch }); return t ? { d: +t.dist.toFixed(1), head: t.head } : null; }, setLook: (yaw, pitch) => { look.yaw = yaw; look.pitch = pitch; autoCam = null; }, zoom: () => {}, killcam: () => { if (G && G.you.killer != null) { G.you.alive = false; G.you.respawn = 3; startKillcam(); } return state; }, finish: () => { if (G) { G.left = 1 / 60; G.target = Math.min(G.target, Math.max(G.score[0], G.score[1]) + 0.01); } }, over: () => showOver(), }; boot().catch((e) => { console.error(e); $('loading').querySelector('.hint').textContent = 'Could not start the 3D city. Reload the page to try again.'; });