@minicode-dev/chat 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{BattleshipGameCard-BFqA0cjT.js → BattleshipGameCard-iRk8d4vR.js} +2 -2
- package/dist/chunks/{BattleshipGameCard-BFqA0cjT.js.map → BattleshipGameCard-iRk8d4vR.js.map} +1 -1
- package/dist/chunks/{ChatContext-dty_KPDX.js → ChatContext-DjIEifGY.js} +2 -2
- package/dist/chunks/{ChatContext-dty_KPDX.js.map → ChatContext-DjIEifGY.js.map} +1 -1
- package/dist/chunks/{TanksGameCard-D0n-lzTs.js → TanksGameCard-B47ZQcSr.js} +4 -4
- package/dist/chunks/{TanksGameCard-D0n-lzTs.js.map → TanksGameCard-B47ZQcSr.js.map} +1 -1
- package/dist/chunks/{TanksLiveGameCard-BmNRXP66.js → TanksLiveGameCard-BxQGR8P9.js} +264 -216
- package/dist/chunks/TanksLiveGameCard-BxQGR8P9.js.map +1 -0
- package/dist/chunks/{WebRtcLab-B7xBlJy1.js → WebRtcLab-lijo_Rta.js} +2 -2
- package/dist/chunks/{WebRtcLab-B7xBlJy1.js.map → WebRtcLab-lijo_Rta.js.map} +1 -1
- package/dist/chunks/{connect4Sounds-BN7d_TM2.js → connect4Sounds-Cv0q31VW.js} +527 -517
- package/dist/chunks/connect4Sounds-Cv0q31VW.js.map +1 -0
- package/dist/chunks/{createLiveScene-CtYPcOPo.js → createLiveScene-GkalLsqm.js} +2 -2
- package/dist/chunks/{createLiveScene-CtYPcOPo.js.map → createLiveScene-GkalLsqm.js.map} +1 -1
- package/dist/chunks/{createPeerLink-qei05vpV.js → createPeerLink-CL7VZuHS.js} +5 -3
- package/dist/chunks/createPeerLink-CL7VZuHS.js.map +1 -0
- package/dist/chunks/{tanksSounds-C99OD2xD.js → tanksSounds-CUNDIOal.js} +3 -3
- package/dist/chunks/{tanksSounds-C99OD2xD.js.map → tanksSounds-CUNDIOal.js.map} +1 -1
- package/dist/index.js +1533 -1533
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunks/TanksLiveGameCard-BmNRXP66.js.map +0 -1
- package/dist/chunks/connect4Sounds-BN7d_TM2.js.map +0 -1
- package/dist/chunks/createPeerLink-qei05vpV.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s } from "./TanksLiveGameCard-
|
|
1
|
+
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s } from "./TanksLiveGameCard-BxQGR8P9.js";
|
|
2
2
|
import { _ as c, a as l, c as u, d, f, h as p, i as m, l as ee, m as te, n as h, o as ne, p as re, r as ie, s as ae, t as oe, u as se } from "./fx-BHsl4S_J.js";
|
|
3
3
|
import { ACESFilmicToneMapping as ce, DirectionalLight as le, DynamicDrawUsage as ue, Fog as de, Group as fe, HemisphereLight as pe, InstancedMesh as me, Matrix4 as he, MeshBasicMaterial as ge, MeshLambertMaterial as _e, MeshStandardMaterial as ve, PCFSoftShadowMap as ye, PerspectiveCamera as be, Plane as xe, PlaneGeometry as Se, Raycaster as Ce, Scene as we, Vector2 as Te, Vector3 as g, WebGLRenderer as Ee, sRGBEncoding as De } from "three";
|
|
4
4
|
// src/games/tanks-live/scene/createLiveScene.js
|
|
@@ -361,4 +361,4 @@ function y(y, { arena: Fe, lowPower: b = !1, reducedMotion: x = !1, onEvent: Ie
|
|
|
361
361
|
}
|
|
362
362
|
export { y as createLiveScene, y as default };
|
|
363
363
|
|
|
364
|
-
//# sourceMappingURL=createLiveScene-
|
|
364
|
+
//# sourceMappingURL=createLiveScene-GkalLsqm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createLiveScene-CtYPcOPo.js","names":["damageLevel","tankById","arenaObstacles","wrapAngle","RAD_TO_DEG","eventKind","arenaBounds","rng","createTankShared","createHills","lin","createTankGeometries","createSky","dampK","WHEELS","createDecor","clamp","createTank","SKY","createFx","createTerrain","MAX_TANKS","MAX_SHELLS","MARGIN","CELL","MAX_DT","SUN_DIR","Vector3","COLORS","FIREWORK","makeHeights","seed","p","h","n","cell","size","iz","ix","x","z","createLiveScene","container","emit","quality","lowPower","bounds","arena","ox","oz","disposed","dpr","renderer","WebGLRenderer","sRGBEncoding","ACESFilmicToneMapping","PCFSoftShadowMap","canvas","width","height","contextLost","onContextLost","scene","Scene","Fog","camera","PerspectiveCamera","world","Group","hemi","HemisphereLight","sun","DirectionalLight","sc","center","sky","hills","terrain","grid","decor","obstacles","decorList","rockList","o","r","count","i","side","t","inset","borderRocks","step","edge","len","x1","x0","z1","z0","bx","bz","borderDecor","fx","tankGeos","tankShared","MeshLambertMaterial","MeshStandardMaterial","wheels","InstancedMesh","DynamicDrawUsage","blobTex","blobTexture","blobGeo","PlaneGeometry","blobMat","MeshBasicMaterial","blobs","tanks","v1","v2","mtx","Matrix4","scl","ensureTank","key","rt","entry","removeTank","color","index","blowTurret","s","animate","a","DEG","updateBlown","b","dt","g","shellSlots","freeSlots","syncShells","seen","list","id","y","terrainBase","slot","cam","portrait","updateFov","aspect","shake","reducedMotion","amount","updateCamera","focus","back","up","ahead","diff","dx","dz","k","celebrate","handleEvent","kind","shooterOf","lastStats","render","dtMs","state","wheelCount","blobCount","hullYaw","fwd","turn","moved","level","w","info","raycaster","Raycaster","ndc","Vector2","ground","Plane","hit","screenToGround","nx","ny","resize","ro"],"sources":["../../src/games/tanks-live/scene/createLiveScene.js"],"sourcesContent":["// TANQUES LIVE 3D — real-time arena scene (three >=0.134 <0.138). Framework-agnostic.\n//\n// createLiveScene(container, { arena, lowPower, reducedMotion, onEvent }) → {\n// render(renderState, dtMs), screenToGround(nx, ny), resize(), getStats(), dispose()\n// }\n// Reuses the turn-based Tanks look (terrain, tank model, decor, FX) through their modules.\n// The host drives every frame (no internal loop): render() draws once. World meters 1:1;\n// render-state yaw is radians, forward = (cos, sin) on x/z.\nimport {\n ACESFilmicToneMapping,\n DirectionalLight,\n DynamicDrawUsage,\n Fog,\n Group,\n HemisphereLight,\n InstancedMesh,\n Matrix4,\n MeshBasicMaterial,\n MeshLambertMaterial,\n MeshStandardMaterial,\n PCFSoftShadowMap,\n PerspectiveCamera,\n Plane,\n PlaneGeometry,\n Raycaster,\n Scene,\n sRGBEncoding,\n Vector2,\n Vector3,\n WebGLRenderer,\n} from 'three'\nimport { createTerrain, createSky, createHills, SKY } from '../../tanks/scene/terrain.js'\nimport { createDecor } from '../../tanks/scene/decor.js'\nimport { createTank, createTankGeometries, createTankShared, WHEELS } from '../../tanks/scene/tank.js'\nimport { createFx } from '../../tanks/scene/fx.js'\nimport { blobTexture, clamp, dampK, DEG, lin, rng } from '../../tanks/scene/util.js'\nimport { arenaBounds, arenaObstacles, damageLevel, eventKind, RAD_TO_DEG, shooterOf, tankById, wrapAngle } from './liveView.js'\n\nconst MAX_TANKS = 4\nconst MAX_SHELLS = 8 // fx projectile pool\nconst MARGIN = 14 // decorative ground around the arena\nconst CELL = 2\nconst MAX_DT = 1 / 15\nconst SUN_DIR = new Vector3(0.62, 0.42, 0.66).normalize()\nconst COLORS = { green: 'green', red: 'red' }\nconst FIREWORK = { green: lin('#22c55e'), red: lin('#ef4444'), gold: lin('#fbbf24') }\n\n// Gentle rolling ground: tanks drive on a 2D plane, so relief stays under ~1 m.\nfunction makeHeights(n, size, seed) {\n const r = rng(seed)\n const p = [r() * 6.28, r() * 6.28, r() * 6.28, r() * 6.28]\n const h = new Float32Array(n * n)\n const cell = size / (n - 1)\n for (let iz = 0; iz < n; iz++) {\n for (let ix = 0; ix < n; ix++) {\n const x = ix * cell\n const z = iz * cell\n h[iz * n + ix] = 2 +\n 0.45 * Math.sin(x * 0.07 + p[0]) * Math.cos(z * 0.06 + p[1]) +\n 0.3 * Math.sin(x * 0.16 + z * 0.11 + p[2]) +\n 0.18 * Math.cos(x * 0.23 - z * 0.19 + p[3])\n }\n }\n return h\n}\n\nexport function createLiveScene(container, { arena, lowPower = false, reducedMotion = false, onEvent } = {}) {\n if (!container) throw new Error('createLiveScene: container element required')\n const emit = (e) => {\n try { onEvent?.(e) } catch { /* listener errors must never break rendering */ }\n }\n const quality = lowPower ? 'low' : 'high'\n const bounds = arenaBounds(arena)\n const size = Math.ceil((Math.max(bounds.width, bounds.depth) + MARGIN * 2) / CELL) * CELL\n // logical (arena) → scene coordinates\n const ox = MARGIN - bounds.minX\n const oz = MARGIN - bounds.minZ\n let disposed = false\n\n // ------------------------------------------------------------------ renderer\n const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1\n const renderer = new WebGLRenderer({ antialias: !lowPower || dpr < 2, alpha: false, stencil: false, powerPreference: 'high-performance' })\n renderer.outputEncoding = sRGBEncoding\n renderer.toneMapping = ACESFilmicToneMapping\n renderer.toneMappingExposure = 1.05\n renderer.shadowMap.enabled = !lowPower\n renderer.shadowMap.type = PCFSoftShadowMap\n renderer.setPixelRatio(Math.min(dpr, lowPower ? 1.25 : 1.75))\n renderer.setClearColor(SKY.horizon, 1)\n const canvas = renderer.domElement\n canvas.style.display = 'block'\n canvas.style.width = '100%'\n canvas.style.height = '100%'\n canvas.style.outline = 'none'\n container.appendChild(canvas)\n let width = Math.max(1, container.clientWidth)\n let height = Math.max(1, container.clientHeight)\n renderer.setSize(width, height, false)\n\n let contextLost = false\n const onContextLost = (e) => {\n e?.preventDefault?.()\n contextLost = true\n emit({ type: 'context_lost' })\n }\n canvas.addEventListener('webglcontextlost', onContextLost)\n\n // ------------------------------------------------------------------ scene\n const scene = new Scene()\n scene.fog = new Fog(SKY.fog.clone(), 70, 320)\n const camera = new PerspectiveCamera(55, width / height, 0.5, 1400)\n const world = new Group()\n scene.add(world)\n\n const hemi = new HemisphereLight(lin('#ffd2b0'), lin('#33402f'), lowPower ? 0.8 : 0.6)\n const sun = new DirectionalLight(lin('#ffbe7a'), 1.9)\n if (!lowPower) {\n sun.castShadow = true\n sun.shadow.mapSize.set(1024, 1024)\n sun.shadow.bias = -0.0006\n sun.shadow.normalBias = 0.04\n const sc = sun.shadow.camera\n sc.left = -42\n sc.right = 42\n sc.top = 42\n sc.bottom = -42\n sc.near = 20\n sc.far = 320\n sc.updateProjectionMatrix()\n }\n scene.add(hemi, sun, sun.target)\n\n const center = new Vector3(size / 2, 0, size / 2)\n const sky = createSky(SUN_DIR)\n scene.add(sky.mesh)\n const hills = createHills(center, SUN_DIR)\n scene.add(hills.mesh)\n\n const terrain = createTerrain()\n const grid = size / CELL + 1\n terrain.setHeights({ heights: makeHeights(grid, size, Number(arena?.seed) || 7), grid, size })\n world.add(terrain.mesh)\n\n // Obstacles: trees block as trees, everything else as rocks; plus decorative trees outside the arena.\n const decor = createDecor()\n decor.setQuality(quality)\n decor.setShadows(!lowPower)\n const obstacles = arenaObstacles(arena)\n const decorList = []\n const rockList = []\n for (const o of obstacles) {\n const x = o.x + ox\n const z = o.z + oz\n if (o.kind === 'tree') decorList.push({ kind: 'tree', x, z, scale: clamp(o.r / 1.3, 0.7, 2.2) })\n else rockList.push({ kind: 'rock', x, z, w: o.r * 2, d: o.r * 2, h: o.r * 1.2 })\n }\n {\n const r = rng(31)\n const count = lowPower ? 40 : 90\n for (let i = 0; i < count; i++) {\n const side = i % 4\n const t = r() * size\n const inset = 2 + r() * (MARGIN - 5)\n const x = side === 0 ? inset : side === 1 ? size - inset : t\n const z = side === 2 ? inset : side === 3 ? size - inset : t\n decorList.push({ kind: 'tree', x, z, scale: 0.8 + r() * 0.7 })\n }\n }\n decor.build(world, { decor: decorList, obstacles: rockList }, terrain)\n\n // Arena border: a low line of stones marking the playable area.\n const borderRocks = []\n {\n const r = rng(53)\n const step = lowPower ? 7 : 4.5\n const edge = (x0, z0, x1, z1) => {\n const len = Math.hypot(x1 - x0, z1 - z0)\n const n = Math.max(1, Math.round(len / step))\n for (let i = 0; i <= n; i++) {\n const t = i / n\n borderRocks.push({ kind: 'rock', x: x0 + (x1 - x0) * t, z: z0 + (z1 - z0) * t, scale: 0.45 + r() * 0.35 })\n }\n }\n const a = MARGIN - 1\n const bx = MARGIN + bounds.width + 1\n const bz = MARGIN + bounds.depth + 1\n edge(a, a, bx, a)\n edge(bx, a, bx, bz)\n edge(bx, bz, a, bz)\n edge(a, bz, a, a)\n }\n const borderDecor = createDecor()\n borderDecor.setQuality(quality)\n borderDecor.build(world, { decor: borderRocks, obstacles: [] }, terrain)\n\n const fx = createFx(scene, { quality, reducedMotion, heightAt: terrain.heightAt })\n\n // ------------------------------------------------------------------ tanks\n const tankGeos = createTankGeometries()\n const tankShared = createTankShared()\n tankShared.wheelMat = lowPower\n ? new MeshLambertMaterial({ vertexColors: true })\n : new MeshStandardMaterial({ vertexColors: true, roughness: 0.7, metalness: 0.25 })\n const wheels = new InstancedMesh(tankGeos.wheel, tankShared.wheelMat, MAX_TANKS * WHEELS.length)\n wheels.instanceMatrix.setUsage(DynamicDrawUsage)\n wheels.frustumCulled = false\n wheels.count = 0\n world.add(wheels)\n\n const blobTex = blobTexture(64)\n const blobGeo = new PlaneGeometry(1, 1)\n blobGeo.rotateX(-Math.PI / 2)\n const blobMat = new MeshBasicMaterial({ map: blobTex, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 })\n const blobs = new InstancedMesh(blobGeo, blobMat, MAX_TANKS)\n blobs.frustumCulled = false\n blobs.count = 0\n blobs.renderOrder = 2\n world.add(blobs)\n\n const tanks = new Map() // id → { t, alive, last:{x,z}, dust, exhaust }\n const v1 = new Vector3()\n const v2 = new Vector3()\n const mtx = new Matrix4()\n const scl = new Vector3()\n\n function ensureTank(rt, index) {\n const key = String(rt.id)\n let entry = tanks.get(key)\n if (entry && entry.alive === false && rt.alive !== false) {\n // revived (rematch): rebuild cleanly\n removeTank(key)\n entry = null\n }\n if (entry) return entry\n const color = COLORS[rt.color] || (index === 0 ? 'green' : 'red')\n const t = createTank(tankGeos, tankShared, { id: key, color, quality })\n t.setShadows(!lowPower)\n world.add(t.root)\n t.state.pitch = 4\n entry = { t, color, alive: true, last: null, dustAcc: 0, exhaustAcc: 0, seen: true }\n tanks.set(key, entry)\n return entry\n }\n\n function removeTank(key) {\n const entry = tanks.get(key)\n if (!entry) return\n if (entry.t.state.blown) entry.t.turret.removeFromParent()\n entry.t.dispose()\n tanks.delete(key)\n }\n\n function blowTurret(t, animate) {\n const s = t.state\n if (s.blown) return\n world.attach(t.turret)\n if (!animate) {\n const a = (s.hullYaw + 120) * DEG\n const x = s.x + Math.cos(a) * 4.5\n const z = s.z + Math.sin(a) * 4.5\n t.turret.position.set(x, terrain.heightAt(x, z) + 0.2, z)\n t.turret.rotation.set(0.5, a, 2.6)\n s.blown = { p: t.turret.position.clone(), v: new Vector3(), w: new Vector3(), landed: true }\n return\n }\n const a = Math.random() * Math.PI * 2\n s.blown = {\n p: t.turret.position.clone(),\n v: new Vector3(Math.cos(a) * 3.5, 15, Math.sin(a) * 3.5),\n w: new Vector3((Math.random() - 0.5) * 6, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 6),\n landed: false,\n }\n }\n\n function updateBlown(t, dt) {\n const b = t.state.blown\n if (!b || b.landed) return\n b.v.y -= 20 * dt\n b.p.addScaledVector(b.v, dt)\n t.turret.rotation.x += b.w.x * dt\n t.turret.rotation.y += b.w.y * dt\n t.turret.rotation.z += b.w.z * dt\n const g = terrain.heightAt(b.p.x, b.p.z) + 0.25\n if (b.p.y < g && b.v.y < 0) {\n b.p.y = g\n if (b.v.y < -5) {\n b.v.y *= -0.25\n b.v.x *= 0.5\n b.v.z *= 0.5\n b.w.multiplyScalar(0.4)\n fx.dust(b.p.x, g, b.p.z, 2)\n } else b.landed = true\n }\n t.turret.position.copy(b.p)\n }\n\n // ------------------------------------------------------------------ shells\n const shellSlots = new Map() // shell id → fx slot\n const freeSlots = Array.from({ length: MAX_SHELLS }, (_, i) => i)\n\n function syncShells(list) {\n const seen = new Set()\n for (const s of list || []) {\n const id = String(s.id)\n seen.add(id)\n const x = s.x + ox\n const z = s.z + oz\n const y = Number.isFinite(s.y) ? s.y + terrainBase : terrain.heightAt(x, z) + 1.8\n let slot = shellSlots.get(id)\n if (slot == null) {\n if (!freeSlots.length) continue\n slot = freeSlots.pop()\n shellSlots.set(id, slot)\n fx.projShow(slot, x, y, z, false)\n } else fx.projMove(slot, x, y, z)\n }\n for (const [id, slot] of shellSlots) {\n if (seen.has(id)) continue\n fx.projHide(slot)\n shellSlots.delete(id)\n freeSlots.push(slot)\n }\n }\n\n // Shell heights in the render state are relative to a flat ground at 0; the terrain sits ~2 m up.\n const terrainBase = 2\n\n // ------------------------------------------------------------------ camera\n const cam = { pos: new Vector3(), target: new Vector3(), yaw: 0, snap: true, shake: 0, orbit: 0 }\n let portrait = false\n function updateFov() {\n const aspect = width / height\n portrait = aspect < 0.85\n camera.fov = portrait ? 68 : aspect < 1.25 ? 60 : 52\n camera.aspect = aspect\n camera.updateProjectionMatrix()\n }\n updateFov()\n\n function shake(amount) {\n if (reducedMotion) return\n cam.shake = Math.max(cam.shake, amount)\n }\n\n function updateCamera(dt, focus) {\n if (!focus) {\n cam.pos.set(center.x, 70, center.z + size * 0.55)\n cam.target.set(center.x, 2, center.z)\n } else {\n const s = focus.t.state\n const back = portrait ? 19 : 15\n const up = portrait ? 15 : 10.5\n const ahead = portrait ? 14 : 12\n if (focus.alive) {\n let diff = wrapAngle(s.hullYaw * DEG - cam.yaw)\n cam.yaw += diff * (cam.snap ? 1 : dampK(2.6, dt))\n const dx = Math.cos(cam.yaw)\n const dz = Math.sin(cam.yaw)\n v1.set(s.x - dx * back, s.y + up, s.z - dz * back)\n v2.set(s.x + dx * ahead, s.y + 1, s.z + dz * ahead)\n } else {\n // own wreck: slow orbit\n cam.orbit += dt * (reducedMotion ? 0 : 0.3)\n v1.set(s.x + Math.cos(cam.orbit) * 20, s.y + 9, s.z + Math.sin(cam.orbit) * 20)\n v2.set(s.x, s.y + 2, s.z)\n }\n const g = terrain.heightAt(v1.x, v1.z) + 2.5\n if (v1.y < g) v1.y = g\n if (cam.snap) {\n cam.pos.copy(v1)\n cam.target.copy(v2)\n cam.orbit = Math.atan2(cam.pos.z - s.z, cam.pos.x - s.x)\n } else {\n cam.pos.lerp(v1, dampK(6, dt))\n cam.target.lerp(v2, dampK(8, dt))\n }\n cam.snap = false\n }\n camera.position.copy(cam.pos)\n if (cam.shake > 0.002) {\n const k = cam.shake\n camera.position.x += (Math.random() - 0.5) * k\n camera.position.y += (Math.random() - 0.5) * k\n camera.position.z += (Math.random() - 0.5) * k\n cam.shake *= Math.exp(-7 * dt)\n } else cam.shake = 0\n camera.lookAt(cam.target)\n if (!lowPower) {\n sun.target.position.copy(cam.target)\n sun.position.copy(cam.target).addScaledVector(SUN_DIR, 200)\n }\n }\n\n // ------------------------------------------------------------------ events\n let celebrate = null\n function handleEvent(e, list) {\n const kind = eventKind(e)\n if (kind === 'fire') {\n const rt = shooterOf(e, list)\n const entry = rt ? tanks.get(String(rt.id)) : null\n if (!entry) return\n entry.t.state.recoil = 1\n entry.t.muzzleWorld(v1)\n entry.t.gunDirWorld(v2)\n fx.muzzle(v1, v2)\n if (rt.self) shake(0.18)\n } else if (kind === 'impact') {\n if (!Number.isFinite(e.x) || !Number.isFinite(e.z)) return\n const x = e.x + ox\n const z = e.z + oz\n const y = terrain.heightAt(x, z)\n fx.explosion(x, y, z, 3, e.kind === 'obstacle' ? 'obstacle' : 'terrain')\n terrain.crater(x, z, 2.2, 0)\n decor.blast(terrain, x, z, 2.5)\n } else if (kind === 'hit') {\n const rt = tankById(list, e.target)\n const entry = rt ? tanks.get(String(rt.id)) : null\n const x = Number.isFinite(e.x) ? e.x + ox : entry?.t.state.x\n const z = Number.isFinite(e.z) ? e.z + oz : entry?.t.state.z\n if (x == null) return\n const y = (entry ? entry.t.state.y : terrain.heightAt(x, z)) + 1.6\n fx.hitSparks(x, y, z)\n fx.explosion(x, y, z, 2.6, 'tank')\n if (entry) entry.t.state.hitFlash = 1\n shake(rt?.self ? 0.9 : 0.25)\n } else if (kind === 'destroyed') {\n const rt = tankById(list, e.target)\n const entry = rt ? tanks.get(String(rt.id)) : null\n if (!entry) return\n const s = entry.t.state\n fx.bigExplosion(s.x, s.y, s.z)\n decor.blast(terrain, s.x, s.z, 6)\n terrain.crater(s.x, s.z, 4, 0)\n entry.t.setCharred(true)\n blowTurret(entry.t, !reducedMotion)\n entry.alive = false\n shake(rt.self ? 1.4 : 0.6)\n } else if (kind === 'over') {\n celebrate = { id: e.winner != null ? String(e.winner) : null, t: 0, next: 0.2 }\n }\n }\n\n // ------------------------------------------------------------------ frame\n let lastStats = { tanks: 0, shells: 0, particles: 0, calls: 0, triangles: 0 }\n let focus = null\n\n function render(state, dtMs = 16) {\n if (disposed || contextLost) return\n const dt = clamp((Number(dtMs) || 16) / 1000, 0.001, MAX_DT)\n const list = (state && state.tanks) || []\n\n // tanks\n for (const entry of tanks.values()) entry.seen = false\n let wheelCount = 0\n let blobCount = 0\n focus = null\n list.forEach((rt, index) => {\n if (!Number.isFinite(rt.x) || !Number.isFinite(rt.z)) return\n const entry = ensureTank(rt, index)\n entry.seen = true\n const t = entry.t\n const s = t.state\n const x = rt.x + ox\n const z = rt.z + oz\n const hullYaw = (rt.yaw ?? 0) * RAD_TO_DEG\n if (entry.last) {\n const dx = x - entry.last.x\n const dz = z - entry.last.z\n const fwd = dx * Math.cos(hullYaw * DEG) + dz * Math.sin(hullYaw * DEG)\n const turn = wrapAngle((hullYaw - entry.last.yaw) * DEG) * 1.4\n if (Math.abs(fwd) > 1e-4 || Math.abs(turn) > 1e-4) t.scrollTracks(Math.abs(fwd) > 1e-4 ? fwd : turn)\n const moved = Math.hypot(dx, dz) / dt\n if (moved > 1 && entry.alive) {\n entry.dustAcc += dt * moved * (lowPower ? 0.5 : 1)\n while (entry.dustAcc > 1.4) {\n entry.dustAcc -= 1.4\n t.localToWorld(v1, -2.6, 0, (Math.random() - 0.5) * 2.6)\n fx.dust(v1.x, terrain.heightAt(v1.x, v1.z), v1.z, 0.9)\n }\n }\n }\n entry.last = { x, z, yaw: hullYaw }\n if (entry.alive && rt.alive !== false) {\n entry.exhaustAcc += dt * (lowPower ? 3 : 6)\n while (entry.exhaustAcc > 1) {\n entry.exhaustAcc -= 1\n t.localToWorld(v1, -2.9, 1.3, 0.8)\n fx.exhaust(v1.x, v1.y, v1.z)\n }\n }\n s.x = x\n s.z = z\n s.y = terrain.heightAt(x, z)\n s.hullYaw = hullYaw\n s.yaw = (rt.turretYaw ?? rt.yaw ?? 0) * RAD_TO_DEG\n s.hp = rt.hp ?? s.hp\n if (rt.alive === false && entry.alive) {\n // dead without a destroyed event (late join / resync): show the wreck as is\n entry.alive = false\n t.setCharred(true)\n blowTurret(t, false)\n }\n if (s.recoil > 0) s.recoil = Math.max(0, s.recoil - dt * 4)\n if (s.hitFlash > 0) {\n s.hitFlash = Math.max(0, s.hitFlash - dt * 3)\n t.setFlash(s.hitFlash)\n }\n t.place(terrain, dt)\n updateBlown(t, dt)\n const level = damageLevel(rt.hp ?? 100, entry.alive)\n if (level > 0) {\n t.localToWorld(v1, -0.6, 2.2, 0)\n fx.emitDamage(v1.x, v1.y, v1.z, dt, level, s)\n }\n if (wheelCount + WHEELS.length <= MAX_TANKS * WHEELS.length) {\n t.writeWheels(wheels, wheelCount)\n wheelCount += WHEELS.length\n }\n if (lowPower && blobCount < MAX_TANKS) {\n scl.set(8, 1, 6)\n mtx.makeRotationY(-hullYaw * DEG).scale(scl).setPosition(x, s.y + 0.05, z)\n blobs.setMatrixAt(blobCount++, mtx)\n }\n if (rt.self) focus = entry\n })\n for (const [key, entry] of tanks) if (!entry.seen) removeTank(key)\n wheels.count = wheelCount\n wheels.instanceMatrix.needsUpdate = true\n blobs.count = blobCount\n if (blobCount) blobs.instanceMatrix.needsUpdate = true\n if (!focus && list.length) focus = tanks.get(String(list[0].id)) || null\n\n // events after tank poses so muzzle positions are current\n for (const e of (state && state.events) || []) handleEvent(e, list)\n syncShells(state && state.shells)\n\n if (celebrate) {\n celebrate.t += dt\n const w = celebrate.id != null ? tanks.get(celebrate.id) : null\n if (w && celebrate.t < 4 && celebrate.t >= celebrate.next && !reducedMotion) {\n celebrate.next += 0.45\n const s = w.t.state\n fx.firework(s.x + (Math.random() - 0.5) * 8, s.y + 9 + Math.random() * 4, s.z + (Math.random() - 0.5) * 8,\n Math.random() < 0.5 ? FIREWORK[w.color] || FIREWORK.gold : FIREWORK.gold)\n }\n if (celebrate.t > 4) celebrate = null\n }\n\n updateCamera(dt, focus)\n fx.update(dt, camera, renderer, false)\n renderer.render(scene, camera)\n const info = renderer.info.render\n lastStats = { tanks: tanks.size, shells: shellSlots.size, particles: fx.particleCount, calls: info.calls, triangles: info.triangles }\n }\n\n // ------------------------------------------------------------------ picking\n const raycaster = new Raycaster()\n const ndc = new Vector2()\n const ground = new Plane(new Vector3(0, 1, 0), 0)\n const hit = new Vector3()\n /** Normalized device coords (-1..1, y up) → arena ground point { x, z } or null. */\n function screenToGround(nx, ny) {\n if (disposed) return null\n ndc.set(nx, ny)\n raycaster.setFromCamera(ndc, camera)\n const y = focus ? focus.t.state.y + 1 : terrainBase\n ground.constant = -y\n if (!raycaster.ray.intersectPlane(ground, hit)) return null\n return { x: hit.x - ox, z: hit.z - oz }\n }\n\n // ------------------------------------------------------------------ resize\n function resize() {\n if (disposed) return\n const w = container.clientWidth\n const h = container.clientHeight\n if (!w || !h) return\n width = w\n height = h\n renderer.setSize(w, h, false)\n updateFov()\n }\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null\n if (ro) ro.observe(container)\n else if (typeof window !== 'undefined') window.addEventListener('resize', resize)\n\n return {\n kind: '3d',\n render,\n screenToGround,\n resize,\n getStats() {\n return { ...lastStats }\n },\n dispose() {\n if (disposed) return\n disposed = true\n if (ro) ro.disconnect()\n else if (typeof window !== 'undefined') window.removeEventListener('resize', resize)\n canvas.removeEventListener('webglcontextlost', onContextLost)\n for (const key of Array.from(tanks.keys())) removeTank(key)\n wheels.dispose()\n tankShared.wheelMat.dispose()\n tankShared.dispose()\n tankGeos.dispose()\n blobs.dispose()\n blobGeo.dispose()\n blobMat.dispose()\n blobTex.dispose()\n fx.dispose()\n decor.dispose()\n borderDecor.dispose()\n terrain.dispose()\n sky.dispose()\n hills.dispose()\n if (sun.shadow && sun.shadow.map) sun.shadow.map.dispose()\n scene.clear()\n renderer.dispose()\n renderer.forceContextLoss()\n if (canvas.parentNode) canvas.parentNode.removeChild(canvas)\n },\n }\n}\n\nexport default createLiveScene\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createLiveScene-GkalLsqm.js","names":["damageLevel","tankById","arenaObstacles","wrapAngle","RAD_TO_DEG","eventKind","arenaBounds","rng","createTankShared","createHills","lin","createTankGeometries","createSky","dampK","WHEELS","createDecor","clamp","createTank","SKY","createFx","createTerrain","MAX_TANKS","MAX_SHELLS","MARGIN","CELL","MAX_DT","SUN_DIR","Vector3","COLORS","FIREWORK","makeHeights","seed","p","h","n","cell","size","iz","ix","x","z","createLiveScene","container","emit","quality","lowPower","bounds","arena","ox","oz","disposed","dpr","renderer","WebGLRenderer","sRGBEncoding","ACESFilmicToneMapping","PCFSoftShadowMap","canvas","width","height","contextLost","onContextLost","scene","Scene","Fog","camera","PerspectiveCamera","world","Group","hemi","HemisphereLight","sun","DirectionalLight","sc","center","sky","hills","terrain","grid","decor","obstacles","decorList","rockList","o","r","count","i","side","t","inset","borderRocks","step","edge","len","x1","x0","z1","z0","bx","bz","borderDecor","fx","tankGeos","tankShared","MeshLambertMaterial","MeshStandardMaterial","wheels","InstancedMesh","DynamicDrawUsage","blobTex","blobTexture","blobGeo","PlaneGeometry","blobMat","MeshBasicMaterial","blobs","tanks","v1","v2","mtx","Matrix4","scl","ensureTank","key","rt","entry","removeTank","color","index","blowTurret","s","animate","a","DEG","updateBlown","b","dt","g","shellSlots","freeSlots","syncShells","seen","list","id","y","terrainBase","slot","cam","portrait","updateFov","aspect","shake","reducedMotion","amount","updateCamera","focus","back","up","ahead","diff","dx","dz","k","celebrate","handleEvent","kind","shooterOf","lastStats","render","dtMs","state","wheelCount","blobCount","hullYaw","fwd","turn","moved","level","w","info","raycaster","Raycaster","ndc","Vector2","ground","Plane","hit","screenToGround","nx","ny","resize","ro"],"sources":["../../src/games/tanks-live/scene/createLiveScene.js"],"sourcesContent":["// TANQUES LIVE 3D — real-time arena scene (three >=0.134 <0.138). Framework-agnostic.\n//\n// createLiveScene(container, { arena, lowPower, reducedMotion, onEvent }) → {\n// render(renderState, dtMs), screenToGround(nx, ny), resize(), getStats(), dispose()\n// }\n// Reuses the turn-based Tanks look (terrain, tank model, decor, FX) through their modules.\n// The host drives every frame (no internal loop): render() draws once. World meters 1:1;\n// render-state yaw is radians, forward = (cos, sin) on x/z.\nimport {\n ACESFilmicToneMapping,\n DirectionalLight,\n DynamicDrawUsage,\n Fog,\n Group,\n HemisphereLight,\n InstancedMesh,\n Matrix4,\n MeshBasicMaterial,\n MeshLambertMaterial,\n MeshStandardMaterial,\n PCFSoftShadowMap,\n PerspectiveCamera,\n Plane,\n PlaneGeometry,\n Raycaster,\n Scene,\n sRGBEncoding,\n Vector2,\n Vector3,\n WebGLRenderer,\n} from 'three'\nimport { createTerrain, createSky, createHills, SKY } from '../../tanks/scene/terrain.js'\nimport { createDecor } from '../../tanks/scene/decor.js'\nimport { createTank, createTankGeometries, createTankShared, WHEELS } from '../../tanks/scene/tank.js'\nimport { createFx } from '../../tanks/scene/fx.js'\nimport { blobTexture, clamp, dampK, DEG, lin, rng } from '../../tanks/scene/util.js'\nimport { arenaBounds, arenaObstacles, damageLevel, eventKind, RAD_TO_DEG, shooterOf, tankById, wrapAngle } from './liveView.js'\n\nconst MAX_TANKS = 4\nconst MAX_SHELLS = 8 // fx projectile pool\nconst MARGIN = 14 // decorative ground around the arena\nconst CELL = 2\nconst MAX_DT = 1 / 15\nconst SUN_DIR = new Vector3(0.62, 0.42, 0.66).normalize()\nconst COLORS = { green: 'green', red: 'red' }\nconst FIREWORK = { green: lin('#22c55e'), red: lin('#ef4444'), gold: lin('#fbbf24') }\n\n// Gentle rolling ground: tanks drive on a 2D plane, so relief stays under ~1 m.\nfunction makeHeights(n, size, seed) {\n const r = rng(seed)\n const p = [r() * 6.28, r() * 6.28, r() * 6.28, r() * 6.28]\n const h = new Float32Array(n * n)\n const cell = size / (n - 1)\n for (let iz = 0; iz < n; iz++) {\n for (let ix = 0; ix < n; ix++) {\n const x = ix * cell\n const z = iz * cell\n h[iz * n + ix] = 2 +\n 0.45 * Math.sin(x * 0.07 + p[0]) * Math.cos(z * 0.06 + p[1]) +\n 0.3 * Math.sin(x * 0.16 + z * 0.11 + p[2]) +\n 0.18 * Math.cos(x * 0.23 - z * 0.19 + p[3])\n }\n }\n return h\n}\n\nexport function createLiveScene(container, { arena, lowPower = false, reducedMotion = false, onEvent } = {}) {\n if (!container) throw new Error('createLiveScene: container element required')\n const emit = (e) => {\n try { onEvent?.(e) } catch { /* listener errors must never break rendering */ }\n }\n const quality = lowPower ? 'low' : 'high'\n const bounds = arenaBounds(arena)\n const size = Math.ceil((Math.max(bounds.width, bounds.depth) + MARGIN * 2) / CELL) * CELL\n // logical (arena) → scene coordinates\n const ox = MARGIN - bounds.minX\n const oz = MARGIN - bounds.minZ\n let disposed = false\n\n // ------------------------------------------------------------------ renderer\n const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1\n const renderer = new WebGLRenderer({ antialias: !lowPower || dpr < 2, alpha: false, stencil: false, powerPreference: 'high-performance' })\n renderer.outputEncoding = sRGBEncoding\n renderer.toneMapping = ACESFilmicToneMapping\n renderer.toneMappingExposure = 1.05\n renderer.shadowMap.enabled = !lowPower\n renderer.shadowMap.type = PCFSoftShadowMap\n renderer.setPixelRatio(Math.min(dpr, lowPower ? 1.25 : 1.75))\n renderer.setClearColor(SKY.horizon, 1)\n const canvas = renderer.domElement\n canvas.style.display = 'block'\n canvas.style.width = '100%'\n canvas.style.height = '100%'\n canvas.style.outline = 'none'\n container.appendChild(canvas)\n let width = Math.max(1, container.clientWidth)\n let height = Math.max(1, container.clientHeight)\n renderer.setSize(width, height, false)\n\n let contextLost = false\n const onContextLost = (e) => {\n e?.preventDefault?.()\n contextLost = true\n emit({ type: 'context_lost' })\n }\n canvas.addEventListener('webglcontextlost', onContextLost)\n\n // ------------------------------------------------------------------ scene\n const scene = new Scene()\n scene.fog = new Fog(SKY.fog.clone(), 70, 320)\n const camera = new PerspectiveCamera(55, width / height, 0.5, 1400)\n const world = new Group()\n scene.add(world)\n\n const hemi = new HemisphereLight(lin('#ffd2b0'), lin('#33402f'), lowPower ? 0.8 : 0.6)\n const sun = new DirectionalLight(lin('#ffbe7a'), 1.9)\n if (!lowPower) {\n sun.castShadow = true\n sun.shadow.mapSize.set(1024, 1024)\n sun.shadow.bias = -0.0006\n sun.shadow.normalBias = 0.04\n const sc = sun.shadow.camera\n sc.left = -42\n sc.right = 42\n sc.top = 42\n sc.bottom = -42\n sc.near = 20\n sc.far = 320\n sc.updateProjectionMatrix()\n }\n scene.add(hemi, sun, sun.target)\n\n const center = new Vector3(size / 2, 0, size / 2)\n const sky = createSky(SUN_DIR)\n scene.add(sky.mesh)\n const hills = createHills(center, SUN_DIR)\n scene.add(hills.mesh)\n\n const terrain = createTerrain()\n const grid = size / CELL + 1\n terrain.setHeights({ heights: makeHeights(grid, size, Number(arena?.seed) || 7), grid, size })\n world.add(terrain.mesh)\n\n // Obstacles: trees block as trees, everything else as rocks; plus decorative trees outside the arena.\n const decor = createDecor()\n decor.setQuality(quality)\n decor.setShadows(!lowPower)\n const obstacles = arenaObstacles(arena)\n const decorList = []\n const rockList = []\n for (const o of obstacles) {\n const x = o.x + ox\n const z = o.z + oz\n if (o.kind === 'tree') decorList.push({ kind: 'tree', x, z, scale: clamp(o.r / 1.3, 0.7, 2.2) })\n else rockList.push({ kind: 'rock', x, z, w: o.r * 2, d: o.r * 2, h: o.r * 1.2 })\n }\n {\n const r = rng(31)\n const count = lowPower ? 40 : 90\n for (let i = 0; i < count; i++) {\n const side = i % 4\n const t = r() * size\n const inset = 2 + r() * (MARGIN - 5)\n const x = side === 0 ? inset : side === 1 ? size - inset : t\n const z = side === 2 ? inset : side === 3 ? size - inset : t\n decorList.push({ kind: 'tree', x, z, scale: 0.8 + r() * 0.7 })\n }\n }\n decor.build(world, { decor: decorList, obstacles: rockList }, terrain)\n\n // Arena border: a low line of stones marking the playable area.\n const borderRocks = []\n {\n const r = rng(53)\n const step = lowPower ? 7 : 4.5\n const edge = (x0, z0, x1, z1) => {\n const len = Math.hypot(x1 - x0, z1 - z0)\n const n = Math.max(1, Math.round(len / step))\n for (let i = 0; i <= n; i++) {\n const t = i / n\n borderRocks.push({ kind: 'rock', x: x0 + (x1 - x0) * t, z: z0 + (z1 - z0) * t, scale: 0.45 + r() * 0.35 })\n }\n }\n const a = MARGIN - 1\n const bx = MARGIN + bounds.width + 1\n const bz = MARGIN + bounds.depth + 1\n edge(a, a, bx, a)\n edge(bx, a, bx, bz)\n edge(bx, bz, a, bz)\n edge(a, bz, a, a)\n }\n const borderDecor = createDecor()\n borderDecor.setQuality(quality)\n borderDecor.build(world, { decor: borderRocks, obstacles: [] }, terrain)\n\n const fx = createFx(scene, { quality, reducedMotion, heightAt: terrain.heightAt })\n\n // ------------------------------------------------------------------ tanks\n const tankGeos = createTankGeometries()\n const tankShared = createTankShared()\n tankShared.wheelMat = lowPower\n ? new MeshLambertMaterial({ vertexColors: true })\n : new MeshStandardMaterial({ vertexColors: true, roughness: 0.7, metalness: 0.25 })\n const wheels = new InstancedMesh(tankGeos.wheel, tankShared.wheelMat, MAX_TANKS * WHEELS.length)\n wheels.instanceMatrix.setUsage(DynamicDrawUsage)\n wheels.frustumCulled = false\n wheels.count = 0\n world.add(wheels)\n\n const blobTex = blobTexture(64)\n const blobGeo = new PlaneGeometry(1, 1)\n blobGeo.rotateX(-Math.PI / 2)\n const blobMat = new MeshBasicMaterial({ map: blobTex, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 })\n const blobs = new InstancedMesh(blobGeo, blobMat, MAX_TANKS)\n blobs.frustumCulled = false\n blobs.count = 0\n blobs.renderOrder = 2\n world.add(blobs)\n\n const tanks = new Map() // id → { t, alive, last:{x,z}, dust, exhaust }\n const v1 = new Vector3()\n const v2 = new Vector3()\n const mtx = new Matrix4()\n const scl = new Vector3()\n\n function ensureTank(rt, index) {\n const key = String(rt.id)\n let entry = tanks.get(key)\n if (entry && entry.alive === false && rt.alive !== false) {\n // revived (rematch): rebuild cleanly\n removeTank(key)\n entry = null\n }\n if (entry) return entry\n const color = COLORS[rt.color] || (index === 0 ? 'green' : 'red')\n const t = createTank(tankGeos, tankShared, { id: key, color, quality })\n t.setShadows(!lowPower)\n world.add(t.root)\n t.state.pitch = 4\n entry = { t, color, alive: true, last: null, dustAcc: 0, exhaustAcc: 0, seen: true }\n tanks.set(key, entry)\n return entry\n }\n\n function removeTank(key) {\n const entry = tanks.get(key)\n if (!entry) return\n if (entry.t.state.blown) entry.t.turret.removeFromParent()\n entry.t.dispose()\n tanks.delete(key)\n }\n\n function blowTurret(t, animate) {\n const s = t.state\n if (s.blown) return\n world.attach(t.turret)\n if (!animate) {\n const a = (s.hullYaw + 120) * DEG\n const x = s.x + Math.cos(a) * 4.5\n const z = s.z + Math.sin(a) * 4.5\n t.turret.position.set(x, terrain.heightAt(x, z) + 0.2, z)\n t.turret.rotation.set(0.5, a, 2.6)\n s.blown = { p: t.turret.position.clone(), v: new Vector3(), w: new Vector3(), landed: true }\n return\n }\n const a = Math.random() * Math.PI * 2\n s.blown = {\n p: t.turret.position.clone(),\n v: new Vector3(Math.cos(a) * 3.5, 15, Math.sin(a) * 3.5),\n w: new Vector3((Math.random() - 0.5) * 6, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 6),\n landed: false,\n }\n }\n\n function updateBlown(t, dt) {\n const b = t.state.blown\n if (!b || b.landed) return\n b.v.y -= 20 * dt\n b.p.addScaledVector(b.v, dt)\n t.turret.rotation.x += b.w.x * dt\n t.turret.rotation.y += b.w.y * dt\n t.turret.rotation.z += b.w.z * dt\n const g = terrain.heightAt(b.p.x, b.p.z) + 0.25\n if (b.p.y < g && b.v.y < 0) {\n b.p.y = g\n if (b.v.y < -5) {\n b.v.y *= -0.25\n b.v.x *= 0.5\n b.v.z *= 0.5\n b.w.multiplyScalar(0.4)\n fx.dust(b.p.x, g, b.p.z, 2)\n } else b.landed = true\n }\n t.turret.position.copy(b.p)\n }\n\n // ------------------------------------------------------------------ shells\n const shellSlots = new Map() // shell id → fx slot\n const freeSlots = Array.from({ length: MAX_SHELLS }, (_, i) => i)\n\n function syncShells(list) {\n const seen = new Set()\n for (const s of list || []) {\n const id = String(s.id)\n seen.add(id)\n const x = s.x + ox\n const z = s.z + oz\n const y = Number.isFinite(s.y) ? s.y + terrainBase : terrain.heightAt(x, z) + 1.8\n let slot = shellSlots.get(id)\n if (slot == null) {\n if (!freeSlots.length) continue\n slot = freeSlots.pop()\n shellSlots.set(id, slot)\n fx.projShow(slot, x, y, z, false)\n } else fx.projMove(slot, x, y, z)\n }\n for (const [id, slot] of shellSlots) {\n if (seen.has(id)) continue\n fx.projHide(slot)\n shellSlots.delete(id)\n freeSlots.push(slot)\n }\n }\n\n // Shell heights in the render state are relative to a flat ground at 0; the terrain sits ~2 m up.\n const terrainBase = 2\n\n // ------------------------------------------------------------------ camera\n const cam = { pos: new Vector3(), target: new Vector3(), yaw: 0, snap: true, shake: 0, orbit: 0 }\n let portrait = false\n function updateFov() {\n const aspect = width / height\n portrait = aspect < 0.85\n camera.fov = portrait ? 68 : aspect < 1.25 ? 60 : 52\n camera.aspect = aspect\n camera.updateProjectionMatrix()\n }\n updateFov()\n\n function shake(amount) {\n if (reducedMotion) return\n cam.shake = Math.max(cam.shake, amount)\n }\n\n function updateCamera(dt, focus) {\n if (!focus) {\n cam.pos.set(center.x, 70, center.z + size * 0.55)\n cam.target.set(center.x, 2, center.z)\n } else {\n const s = focus.t.state\n const back = portrait ? 19 : 15\n const up = portrait ? 15 : 10.5\n const ahead = portrait ? 14 : 12\n if (focus.alive) {\n let diff = wrapAngle(s.hullYaw * DEG - cam.yaw)\n cam.yaw += diff * (cam.snap ? 1 : dampK(2.6, dt))\n const dx = Math.cos(cam.yaw)\n const dz = Math.sin(cam.yaw)\n v1.set(s.x - dx * back, s.y + up, s.z - dz * back)\n v2.set(s.x + dx * ahead, s.y + 1, s.z + dz * ahead)\n } else {\n // own wreck: slow orbit\n cam.orbit += dt * (reducedMotion ? 0 : 0.3)\n v1.set(s.x + Math.cos(cam.orbit) * 20, s.y + 9, s.z + Math.sin(cam.orbit) * 20)\n v2.set(s.x, s.y + 2, s.z)\n }\n const g = terrain.heightAt(v1.x, v1.z) + 2.5\n if (v1.y < g) v1.y = g\n if (cam.snap) {\n cam.pos.copy(v1)\n cam.target.copy(v2)\n cam.orbit = Math.atan2(cam.pos.z - s.z, cam.pos.x - s.x)\n } else {\n cam.pos.lerp(v1, dampK(6, dt))\n cam.target.lerp(v2, dampK(8, dt))\n }\n cam.snap = false\n }\n camera.position.copy(cam.pos)\n if (cam.shake > 0.002) {\n const k = cam.shake\n camera.position.x += (Math.random() - 0.5) * k\n camera.position.y += (Math.random() - 0.5) * k\n camera.position.z += (Math.random() - 0.5) * k\n cam.shake *= Math.exp(-7 * dt)\n } else cam.shake = 0\n camera.lookAt(cam.target)\n if (!lowPower) {\n sun.target.position.copy(cam.target)\n sun.position.copy(cam.target).addScaledVector(SUN_DIR, 200)\n }\n }\n\n // ------------------------------------------------------------------ events\n let celebrate = null\n function handleEvent(e, list) {\n const kind = eventKind(e)\n if (kind === 'fire') {\n const rt = shooterOf(e, list)\n const entry = rt ? tanks.get(String(rt.id)) : null\n if (!entry) return\n entry.t.state.recoil = 1\n entry.t.muzzleWorld(v1)\n entry.t.gunDirWorld(v2)\n fx.muzzle(v1, v2)\n if (rt.self) shake(0.18)\n } else if (kind === 'impact') {\n if (!Number.isFinite(e.x) || !Number.isFinite(e.z)) return\n const x = e.x + ox\n const z = e.z + oz\n const y = terrain.heightAt(x, z)\n fx.explosion(x, y, z, 3, e.kind === 'obstacle' ? 'obstacle' : 'terrain')\n terrain.crater(x, z, 2.2, 0)\n decor.blast(terrain, x, z, 2.5)\n } else if (kind === 'hit') {\n const rt = tankById(list, e.target)\n const entry = rt ? tanks.get(String(rt.id)) : null\n const x = Number.isFinite(e.x) ? e.x + ox : entry?.t.state.x\n const z = Number.isFinite(e.z) ? e.z + oz : entry?.t.state.z\n if (x == null) return\n const y = (entry ? entry.t.state.y : terrain.heightAt(x, z)) + 1.6\n fx.hitSparks(x, y, z)\n fx.explosion(x, y, z, 2.6, 'tank')\n if (entry) entry.t.state.hitFlash = 1\n shake(rt?.self ? 0.9 : 0.25)\n } else if (kind === 'destroyed') {\n const rt = tankById(list, e.target)\n const entry = rt ? tanks.get(String(rt.id)) : null\n if (!entry) return\n const s = entry.t.state\n fx.bigExplosion(s.x, s.y, s.z)\n decor.blast(terrain, s.x, s.z, 6)\n terrain.crater(s.x, s.z, 4, 0)\n entry.t.setCharred(true)\n blowTurret(entry.t, !reducedMotion)\n entry.alive = false\n shake(rt.self ? 1.4 : 0.6)\n } else if (kind === 'over') {\n celebrate = { id: e.winner != null ? String(e.winner) : null, t: 0, next: 0.2 }\n }\n }\n\n // ------------------------------------------------------------------ frame\n let lastStats = { tanks: 0, shells: 0, particles: 0, calls: 0, triangles: 0 }\n let focus = null\n\n function render(state, dtMs = 16) {\n if (disposed || contextLost) return\n const dt = clamp((Number(dtMs) || 16) / 1000, 0.001, MAX_DT)\n const list = (state && state.tanks) || []\n\n // tanks\n for (const entry of tanks.values()) entry.seen = false\n let wheelCount = 0\n let blobCount = 0\n focus = null\n list.forEach((rt, index) => {\n if (!Number.isFinite(rt.x) || !Number.isFinite(rt.z)) return\n const entry = ensureTank(rt, index)\n entry.seen = true\n const t = entry.t\n const s = t.state\n const x = rt.x + ox\n const z = rt.z + oz\n const hullYaw = (rt.yaw ?? 0) * RAD_TO_DEG\n if (entry.last) {\n const dx = x - entry.last.x\n const dz = z - entry.last.z\n const fwd = dx * Math.cos(hullYaw * DEG) + dz * Math.sin(hullYaw * DEG)\n const turn = wrapAngle((hullYaw - entry.last.yaw) * DEG) * 1.4\n if (Math.abs(fwd) > 1e-4 || Math.abs(turn) > 1e-4) t.scrollTracks(Math.abs(fwd) > 1e-4 ? fwd : turn)\n const moved = Math.hypot(dx, dz) / dt\n if (moved > 1 && entry.alive) {\n entry.dustAcc += dt * moved * (lowPower ? 0.5 : 1)\n while (entry.dustAcc > 1.4) {\n entry.dustAcc -= 1.4\n t.localToWorld(v1, -2.6, 0, (Math.random() - 0.5) * 2.6)\n fx.dust(v1.x, terrain.heightAt(v1.x, v1.z), v1.z, 0.9)\n }\n }\n }\n entry.last = { x, z, yaw: hullYaw }\n if (entry.alive && rt.alive !== false) {\n entry.exhaustAcc += dt * (lowPower ? 3 : 6)\n while (entry.exhaustAcc > 1) {\n entry.exhaustAcc -= 1\n t.localToWorld(v1, -2.9, 1.3, 0.8)\n fx.exhaust(v1.x, v1.y, v1.z)\n }\n }\n s.x = x\n s.z = z\n s.y = terrain.heightAt(x, z)\n s.hullYaw = hullYaw\n s.yaw = (rt.turretYaw ?? rt.yaw ?? 0) * RAD_TO_DEG\n s.hp = rt.hp ?? s.hp\n if (rt.alive === false && entry.alive) {\n // dead without a destroyed event (late join / resync): show the wreck as is\n entry.alive = false\n t.setCharred(true)\n blowTurret(t, false)\n }\n if (s.recoil > 0) s.recoil = Math.max(0, s.recoil - dt * 4)\n if (s.hitFlash > 0) {\n s.hitFlash = Math.max(0, s.hitFlash - dt * 3)\n t.setFlash(s.hitFlash)\n }\n t.place(terrain, dt)\n updateBlown(t, dt)\n const level = damageLevel(rt.hp ?? 100, entry.alive)\n if (level > 0) {\n t.localToWorld(v1, -0.6, 2.2, 0)\n fx.emitDamage(v1.x, v1.y, v1.z, dt, level, s)\n }\n if (wheelCount + WHEELS.length <= MAX_TANKS * WHEELS.length) {\n t.writeWheels(wheels, wheelCount)\n wheelCount += WHEELS.length\n }\n if (lowPower && blobCount < MAX_TANKS) {\n scl.set(8, 1, 6)\n mtx.makeRotationY(-hullYaw * DEG).scale(scl).setPosition(x, s.y + 0.05, z)\n blobs.setMatrixAt(blobCount++, mtx)\n }\n if (rt.self) focus = entry\n })\n for (const [key, entry] of tanks) if (!entry.seen) removeTank(key)\n wheels.count = wheelCount\n wheels.instanceMatrix.needsUpdate = true\n blobs.count = blobCount\n if (blobCount) blobs.instanceMatrix.needsUpdate = true\n if (!focus && list.length) focus = tanks.get(String(list[0].id)) || null\n\n // events after tank poses so muzzle positions are current\n for (const e of (state && state.events) || []) handleEvent(e, list)\n syncShells(state && state.shells)\n\n if (celebrate) {\n celebrate.t += dt\n const w = celebrate.id != null ? tanks.get(celebrate.id) : null\n if (w && celebrate.t < 4 && celebrate.t >= celebrate.next && !reducedMotion) {\n celebrate.next += 0.45\n const s = w.t.state\n fx.firework(s.x + (Math.random() - 0.5) * 8, s.y + 9 + Math.random() * 4, s.z + (Math.random() - 0.5) * 8,\n Math.random() < 0.5 ? FIREWORK[w.color] || FIREWORK.gold : FIREWORK.gold)\n }\n if (celebrate.t > 4) celebrate = null\n }\n\n updateCamera(dt, focus)\n fx.update(dt, camera, renderer, false)\n renderer.render(scene, camera)\n const info = renderer.info.render\n lastStats = { tanks: tanks.size, shells: shellSlots.size, particles: fx.particleCount, calls: info.calls, triangles: info.triangles }\n }\n\n // ------------------------------------------------------------------ picking\n const raycaster = new Raycaster()\n const ndc = new Vector2()\n const ground = new Plane(new Vector3(0, 1, 0), 0)\n const hit = new Vector3()\n /** Normalized device coords (-1..1, y up) → arena ground point { x, z } or null. */\n function screenToGround(nx, ny) {\n if (disposed) return null\n ndc.set(nx, ny)\n raycaster.setFromCamera(ndc, camera)\n const y = focus ? focus.t.state.y + 1 : terrainBase\n ground.constant = -y\n if (!raycaster.ray.intersectPlane(ground, hit)) return null\n return { x: hit.x - ox, z: hit.z - oz }\n }\n\n // ------------------------------------------------------------------ resize\n function resize() {\n if (disposed) return\n const w = container.clientWidth\n const h = container.clientHeight\n if (!w || !h) return\n width = w\n height = h\n renderer.setSize(w, h, false)\n updateFov()\n }\n const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null\n if (ro) ro.observe(container)\n else if (typeof window !== 'undefined') window.addEventListener('resize', resize)\n\n return {\n kind: '3d',\n render,\n screenToGround,\n resize,\n getStats() {\n return { ...lastStats }\n },\n dispose() {\n if (disposed) return\n disposed = true\n if (ro) ro.disconnect()\n else if (typeof window !== 'undefined') window.removeEventListener('resize', resize)\n canvas.removeEventListener('webglcontextlost', onContextLost)\n for (const key of Array.from(tanks.keys())) removeTank(key)\n wheels.dispose()\n tankShared.wheelMat.dispose()\n tankShared.dispose()\n tankGeos.dispose()\n blobs.dispose()\n blobGeo.dispose()\n blobMat.dispose()\n blobTex.dispose()\n fx.dispose()\n decor.dispose()\n borderDecor.dispose()\n terrain.dispose()\n sky.dispose()\n hills.dispose()\n if (sun.shadow && sun.shadow.map) sun.shadow.map.dispose()\n scene.clear()\n renderer.dispose()\n renderer.forceContextLoss()\n if (canvas.parentNode) canvas.parentNode.removeChild(canvas)\n },\n }\n}\n\nexport default createLiveScene\n"],"mappings":"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"}
|
|
@@ -89,6 +89,7 @@ function x({ signaling: e, selfId: t, peerId: n, roomId: r = null, tabId: i = u(
|
|
|
89
89
|
signals: {
|
|
90
90
|
sent: 0,
|
|
91
91
|
received: 0,
|
|
92
|
+
sendFailures: 0,
|
|
92
93
|
retransmits: 0,
|
|
93
94
|
duplicates: 0,
|
|
94
95
|
ignored: 0,
|
|
@@ -110,10 +111,11 @@ function x({ signaling: e, selfId: t, peerId: n, roomId: r = null, tabId: i = u(
|
|
|
110
111
|
}), F = (e, t = null) => {
|
|
111
112
|
M.phase = e, M.error = t, w(`fase: ${e}${t ? ` (${t})` : ""}`), N();
|
|
112
113
|
}, I = (t) => {
|
|
114
|
+
let n = () => (E || (M.signals.sendFailures += 1, N()), !1);
|
|
113
115
|
try {
|
|
114
|
-
return Promise.resolve(e.send(t)).then(() => !
|
|
116
|
+
return Promise.resolve(e.send(t)).then((e) => e !== !1 || n(), n);
|
|
115
117
|
} catch {
|
|
116
|
-
return Promise.resolve(
|
|
118
|
+
return Promise.resolve(n());
|
|
117
119
|
}
|
|
118
120
|
}, L = (e, t, n, { to: r = e?.peerTab ?? null, reliable: a = !0, gen: o = e?.gen ?? 0, sid: s = e?.sid } = {}) => {
|
|
119
121
|
let l = u(), d = c({
|
|
@@ -475,4 +477,4 @@ function x({ signaling: e, selfId: t, peerId: n, roomId: r = null, tabId: i = u(
|
|
|
475
477
|
}
|
|
476
478
|
export { l as a, e as i, x as n, c as o, t as r, a as s, d as t };
|
|
477
479
|
|
|
478
|
-
//# sourceMappingURL=createPeerLink-
|
|
480
|
+
//# sourceMappingURL=createPeerLink-CL7VZuHS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createPeerLink-CL7VZuHS.js","names":["WEBRTC_PREFIX","MAX_SIGNAL_LENGTH","SIGNAL_TYPES","ID","MAX_CANDIDATES","isWebrtcMessage","text","validData","d","t","c","isValidSignal","signal","encodeSignal","decodeSignal","newSignalId","DEFAULT_ICE_SERVERS","RETRY_MS","MAX_TRIES","HELLO_EVERY_MS","HELLO_GIVE_UP_MS","ICE_BATCH_MS","DISCONNECTED_GRACE_MS","CONNECT_TIMEOUT_MS","PING_EVERY_MS","HEARTBEAT_TIMEOUT_MS","MAX_RESTARTS","MAX_RECONNECTS","MAX_BUFFERED","MAX_SEEN","emptyMoveStats","ROUTE_LABELS","createPeerLink","signaling","selfId","peerId","listening","listen","destroyed","session","reconnects","reconnectTimer","endedSid","seen","state","emit","onChange","snapshot","setPhase","phase","error","log","rawSend","failed","result","sendSignal","s","mid","tabId","fail","reliable","entry","retry","remember","key","unsubscribe","frame","roomId","newSession","teardown","bye","timer","channel","scheduleReconnect","autoReconnect","reason","delay","call","wasCaller","bindState","createPc","RTCPeerConnectionImpl","pc","candidate","flushLocalIce","onIceState","onChannelOpen","lose","onMoveMessage","event","onEventMessage","batch","i","addRemoteIce","candidates","flushPendingIce","ready","item","makeOffer","offer","iceRestart","restartIce","ice","refreshStats","ping","tick","sendRaw","message","data","onEvent","packet","seq","payload","m","onMove","expected","report","byId","pair","local","remote","relay","type","onAck","onHello","glare","onReady","onOffer","answer","onAnswer","onIce","onBye","hello","value","sent"],"sources":["../../src/net/webrtc/protocol.js","../../src/net/webrtc/createPeerLink.js"],"sourcesContent":["// WebRTC signaling frames carried inside ordinary CHAT_MESSAGE text, the same idea\n// as LIRA_GAME_V1:. The chat backend only relays them; the sender is the\n// server-stamped user_id of the envelope, never a field of the payload.\n//\n// LIRA_WEBRTC_V1:{\"v\":1,\"t\":\"offer\",\"sid\":\"…\",\"gen\":0,\"mid\":\"…\",\"from\":\"tab\",\"to\":\"tab\",\"d\":{…}}\n//\n// t: hello (call, to = null) | ready (reply to hello) | offer | answer | ice | ack | bye\n// sid: session id chosen by the caller; gen: negotiation round (ICE restarts);\n// mid: message id for ACK/dedupe; from/to: tab ids, so several tabs of one user\n// and the server echo to the sender are told apart; at: sender clock (ms), only\n// compared with earlier frames of the same tab to spot late, stale calls.\n\nexport const WEBRTC_PREFIX = 'LIRA_WEBRTC_V1:';\n// Backend limit for CHAT_MESSAGE.message (Django audit). Frames above it are refused locally.\nexport const MAX_SIGNAL_LENGTH = 10000;\nexport const SIGNAL_TYPES = ['hello', 'ready', 'offer', 'answer', 'ice', 'ack', 'bye'];\n\nconst ID = /^[A-Za-z0-9_-]{4,64}$/;\nconst MAX_CANDIDATES = 32;\n\nexport const isWebrtcMessage = (text) => typeof text === 'string' && text.startsWith(WEBRTC_PREFIX);\n\nconst validData = (t, d) => {\n if (d == null) return ['hello', 'ready', 'ack', 'bye'].includes(t);\n if (typeof d !== 'object' || Array.isArray(d)) return false;\n switch (t) {\n case 'offer':\n case 'answer':\n return typeof d.sdp === 'string' && d.sdp.length > 0;\n case 'ice':\n return Array.isArray(d.c) && d.c.length <= MAX_CANDIDATES\n && d.c.every((c) => c === null || (c && typeof c.candidate === 'string'));\n case 'ack':\n return typeof d.of === 'string' && ID.test(d.of);\n default:\n return true;\n }\n};\n\nexport function isValidSignal(signal) {\n return Boolean(signal) && typeof signal === 'object'\n && signal.v === 1\n && SIGNAL_TYPES.includes(signal.t)\n && typeof signal.sid === 'string' && ID.test(signal.sid)\n && typeof signal.mid === 'string' && ID.test(signal.mid)\n && typeof signal.from === 'string' && ID.test(signal.from)\n && (signal.to == null || (typeof signal.to === 'string' && ID.test(signal.to)))\n && Number.isInteger(signal.gen ?? 0) && (signal.gen ?? 0) >= 0\n && (signal.at == null || Number.isFinite(signal.at))\n && validData(signal.t, signal.d);\n}\n\n/** Returns the frame text, or null when it would exceed MAX_SIGNAL_LENGTH. */\nexport function encodeSignal(signal) {\n const text = WEBRTC_PREFIX + JSON.stringify({ v: 1, ...signal });\n return text.length <= MAX_SIGNAL_LENGTH ? text : null;\n}\n\nexport function decodeSignal(text) {\n if (!isWebrtcMessage(text) || text.length > MAX_SIGNAL_LENGTH) return null;\n try {\n const signal = JSON.parse(text.slice(WEBRTC_PREFIX.length));\n return isValidSignal(signal) ? signal : null;\n } catch {\n return null;\n }\n}\n\nexport const newSignalId = () => {\n try {\n if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 20);\n } catch { /* fallback below */ }\n return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;\n};\n\n// Normalizes a chat frame (flat or nested in data/payload) to { userId, roomId, text }.\nexport function readChatFrame(frame) {\n if (!frame || typeof frame !== 'object') return null;\n const nested = frame.data ?? frame.payload;\n const body = nested && typeof nested === 'object' ? { ...nested, ...frame } : frame;\n const text = frame.message ?? frame.text ?? nested?.message ?? nested?.text;\n if (typeof text !== 'string') return null;\n return { userId: String(body.user_id ?? ''), roomId: String(body.room_id ?? ''), text };\n}\n","// One WebRTC link between two chat users, signaled through the chat itself.\n//\n// Signaling: LIRA_WEBRTC_V1: frames (protocol.js) sent as ordinary CHAT_MESSAGE\n// text in the users' private room. The relay echoes frames to every tab of both\n// users, may duplicate them after a reconnect and gives no delivery guarantee, so:\n// - frames carry tab ids (from/to): own echoes and other tabs are ignored;\n// - every frame except hello/ack is ACKed and retransmitted until ACKed;\n// - duplicates are dropped by message id; ICE that arrives before its\n// description waits in a queue; stale sessions/generations are ignored.\n// Session: caller sends hello (repeated until answered); the first tab of the\n// peer that answers ready is bound, gets the offer and the others a bye.\n// Simultaneous calls resolve by tab id (the lower one keeps calling).\n//\n// Data: two negotiated DataChannels (no ondatachannel race):\n// move id 0, unordered, maxRetransmits 0 — latest position wins;\n// events id 1, ordered and reliable — pings, hits, game events.\n// Recovery: ICE 'failed' (or 'disconnected' for 3 s) triggers an ICE restart by\n// the caller; no traffic for HEARTBEAT_TIMEOUT_MS ends the session and the caller\n// starts a new one (autoReconnect, with backoff).\n\nimport { encodeSignal, decodeSignal, isWebrtcMessage, newSignalId } from './protocol.js';\n\nexport const DEFAULT_ICE_SERVERS = [{ urls: ['stun:stun.l.google.com:19302', 'stun:stun.cloudflare.com:3478'] }];\n\nconst RETRY_MS = 1200;\nconst MAX_TRIES = 6;\nconst HELLO_EVERY_MS = 2000;\nconst HELLO_GIVE_UP_MS = 30000;\nconst ICE_BATCH_MS = 80;\nconst DISCONNECTED_GRACE_MS = 3000;\nconst CONNECT_TIMEOUT_MS = 20000;\nconst PING_EVERY_MS = 1000;\nconst HEARTBEAT_TIMEOUT_MS = 10000;\nconst MAX_RESTARTS = 2;\nconst MAX_RECONNECTS = 5;\nconst MAX_BUFFERED = 16 * 1024;\nconst MAX_SEEN = 500;\n\nconst emptyMoveStats = () => ({ sent: 0, dropped: 0, received: 0, firstSeq: null, maxSeq: 0, outOfOrder: 0, lossPct: 0, hzIn: 0, hzOut: 0, windowIn: 0, windowOut: 0 });\n\nconst ROUTE_LABELS = { host: 'directa (red local)', srflx: 'directa (NAT vía STUN)', prflx: 'directa (NAT)', relay: 'TURN (relay)' };\n\nexport function createPeerLink({\n signaling,\n selfId,\n peerId,\n roomId = null,\n tabId = newSignalId(),\n iceServers = DEFAULT_ICE_SERVERS,\n iceTransportPolicy = 'all',\n autoReconnect = true,\n listen = true,\n RTCPeerConnectionImpl = globalThis.RTCPeerConnection,\n onChange = () => {},\n onMove = () => {},\n onEvent = () => {},\n log = () => {},\n} = {}) {\n if (!signaling?.send || !signaling?.subscribe) throw new Error('createPeerLink: signaling { send, subscribe } requerido');\n selfId = String(selfId);\n peerId = String(peerId);\n\n let listening = listen;\n let destroyed = false;\n let session = null;\n let reconnects = 0;\n let reconnectTimer = null;\n let endedSid = null; // last session that ended without the peer's bye\n const seen = new Set();\n const state = {\n phase: 'idle', role: null, sid: null, gen: 0, tabId, peerTab: null,\n ice: 'new', connection: 'new', signalingState: 'stable', gathering: 'new',\n route: null, rtt: null, statsRtt: null, reconnects: 0, restarts: 0, error: null,\n move: emptyMoveStats(),\n signals: { sent: 0, received: 0, sendFailures: 0, retransmits: 0, duplicates: 0, ignored: 0, invalid: 0, tooLarge: 0, maxLength: 0 },\n events: { sent: 0, received: 0 },\n };\n\n const emit = () => { if (!destroyed) onChange(snapshot()); };\n const snapshot = () => ({ ...state, move: { ...state.move }, signals: { ...state.signals }, events: { ...state.events } });\n const setPhase = (phase, error = null) => {\n state.phase = phase;\n state.error = error;\n log(`fase: ${phase}${error ? ` (${error})` : ''}`);\n emit();\n };\n\n // ---- signaling --------------------------------------------------------\n\n const rawSend = (text) => {\n const failed = () => {\n if (!destroyed) { state.signals.sendFailures += 1; emit(); }\n return false;\n };\n try {\n return Promise.resolve(signaling.send(text)).then((result) => result === false ? failed() : true, failed);\n } catch {\n return Promise.resolve(failed());\n }\n };\n\n const sendSignal = (s, t, d, { to = s?.peerTab ?? null, reliable = true, gen = s?.gen ?? 0, sid = s?.sid } = {}) => {\n const mid = newSignalId();\n const text = encodeSignal({ t, sid, gen, mid, at: Date.now(), from: tabId, to, d: d ?? undefined });\n if (!text) {\n state.signals.tooLarge += 1;\n log(`señal ${t} supera el límite y no se envía`);\n if (s) fail(s, 'signal_too_large');\n return null;\n }\n state.signals.sent += 1;\n state.signals.maxLength = Math.max(state.signals.maxLength, text.length);\n void rawSend(text);\n if (reliable && s) {\n const entry = { text, tries: 1, timer: null };\n const retry = () => {\n if (s.closed || !s.outbox.has(mid)) return;\n if (entry.tries >= MAX_TRIES) {\n s.outbox.delete(mid);\n log(`señal ${t} sin ACK tras ${MAX_TRIES} intentos`);\n if (t !== 'bye') fail(s, 'signal_timeout');\n return;\n }\n entry.tries += 1;\n state.signals.retransmits += 1;\n void rawSend(text);\n entry.timer = setTimeout(retry, RETRY_MS);\n };\n entry.timer = setTimeout(retry, RETRY_MS);\n s.outbox.set(mid, entry);\n }\n return mid;\n };\n\n const remember = (key) => {\n if (seen.has(key)) return false;\n seen.add(key);\n if (seen.size > MAX_SEEN) seen.delete(seen.values().next().value);\n return true;\n };\n\n const handleFrame = (frame) => {\n if (destroyed || !frame || !isWebrtcMessage(frame.text)) return;\n if (roomId != null && frame.roomId && String(frame.roomId) !== String(roomId)) return;\n const signal = decodeSignal(frame.text);\n if (!signal) { state.signals.invalid += 1; return; }\n // Own echoes, own other tabs and third parties never drive this link.\n if (String(frame.userId) !== peerId || signal.from === tabId || (signal.to && signal.to !== tabId)) {\n state.signals.ignored += 1;\n return;\n }\n state.signals.received += 1;\n if (signal.t !== 'hello' && signal.t !== 'ack') {\n // ACK even duplicates: the first ACK may be the one that got lost.\n sendSignal(null, 'ack', { of: signal.mid }, { to: signal.from, reliable: false, sid: signal.sid, gen: signal.gen });\n }\n if (!remember(`${signal.from}:${signal.mid}`)) { state.signals.duplicates += 1; emit(); return; }\n switch (signal.t) {\n case 'ack': onAck(signal); break;\n case 'hello': onHello(signal); break;\n case 'ready': onReady(signal); break;\n case 'offer': void onOffer(signal); break;\n case 'answer': void onAnswer(signal); break;\n case 'ice': onIce(signal); break;\n case 'bye': onBye(signal); break;\n default: break;\n }\n // After dispatch, so the frame that binds the peer tab (hello/ready) counts too.\n if (session && signal.sid === session.sid && signal.from === session.peerTab && signal.at) {\n session.peerAt = Math.max(session.peerAt, signal.at);\n }\n };\n\n const unsubscribe = signaling.subscribe(handleFrame);\n\n // ---- session lifecycle ------------------------------------------------\n\n const newSession = (role, sid = newSignalId(), peerTab = null) => ({\n role, sid, peerTab, gen: 0, pc: null, move: null, events: null, closed: false,\n outbox: new Map(), pendingIce: [], localIce: [], iceTimer: null, helloTimer: null, helloStarted: Date.now(),\n disconnectTimer: null, connectTimer: null, pingTimer: null, statsTimer: null, tickTimer: null,\n lastSeen: Date.now(), pingSeq: 0, moveSeq: 0, restarts: 0, open: 0, peerAt: 0,\n });\n\n const teardown = (s, { bye = false } = {}) => {\n if (!s || s.closed) return;\n if (bye && s.peerTab) sendSignal(s, 'bye', null, { reliable: false });\n s.closed = true;\n [s.iceTimer, s.disconnectTimer, s.connectTimer].forEach((timer) => clearTimeout(timer));\n [s.helloTimer, s.pingTimer, s.statsTimer, s.tickTimer].forEach((timer) => clearInterval(timer));\n s.outbox.forEach((entry) => clearTimeout(entry.timer));\n s.outbox.clear();\n [s.move, s.events].forEach((channel) => {\n if (!channel) return;\n channel.onopen = channel.onclose = channel.onmessage = channel.onerror = null;\n try { channel.close(); } catch { /* already closed */ }\n });\n if (s.pc) {\n s.pc.onicecandidate = s.pc.oniceconnectionstatechange = s.pc.onconnectionstatechange = null;\n s.pc.onsignalingstatechange = s.pc.onicegatheringstatechange = null;\n try { s.pc.close(); } catch { /* already closed */ }\n }\n if (session === s) session = null;\n };\n\n const scheduleReconnect = (reason) => {\n if (destroyed || !autoReconnect || reconnectTimer) return;\n if (reconnects >= MAX_RECONNECTS) { setPhase('failed', `${reason}; sin más reintentos`); return; }\n const delay = Math.min(8000, 1000 * 2 ** reconnects);\n reconnects += 1;\n state.reconnects = reconnects;\n setPhase('reconnecting', reason);\n reconnectTimer = setTimeout(() => { reconnectTimer = null; if (!destroyed && !session) call(); }, delay);\n };\n\n const fail = (s, reason) => {\n if (s.closed) return;\n const wasCaller = s.role === 'caller';\n endedSid = s.sid;\n teardown(s, { bye: true });\n if (wasCaller) scheduleReconnect(reason);\n else setPhase(listening ? 'listening' : 'idle', reason);\n };\n\n const bindState = (s) => {\n state.role = s.role;\n state.sid = s.sid;\n state.gen = s.gen;\n state.peerTab = s.peerTab;\n state.ice = 'new';\n state.connection = 'new';\n state.signalingState = 'stable';\n state.gathering = 'new';\n state.route = null;\n state.rtt = null;\n state.statsRtt = null;\n state.restarts = 0;\n state.move = emptyMoveStats();\n };\n\n // ---- peer connection --------------------------------------------------\n\n const createPc = (s) => {\n if (!RTCPeerConnectionImpl) { fail(s, 'webrtc_unavailable'); return null; }\n const pc = new RTCPeerConnectionImpl({ iceServers, iceTransportPolicy });\n s.pc = pc;\n pc.onicecandidate = ({ candidate }) => {\n if (s.closed) return;\n s.localIce.push(candidate ? candidate.toJSON?.() ?? candidate : null);\n if (!s.iceTimer) s.iceTimer = setTimeout(() => flushLocalIce(s), ICE_BATCH_MS);\n };\n pc.onicegatheringstatechange = () => { state.gathering = pc.iceGatheringState; emit(); };\n pc.onsignalingstatechange = () => { state.signalingState = pc.signalingState; emit(); };\n pc.onconnectionstatechange = () => { state.connection = pc.connectionState; emit(); };\n pc.oniceconnectionstatechange = () => onIceState(s);\n s.move = pc.createDataChannel('move', { negotiated: true, id: 0, ordered: false, maxRetransmits: 0 });\n s.events = pc.createDataChannel('events', { negotiated: true, id: 1, ordered: true });\n [s.move, s.events].forEach((channel) => {\n channel.onopen = () => onChannelOpen(s);\n channel.onclose = () => { if (!s.closed && state.phase === 'connected') lose(s, 'canal cerrado'); };\n });\n s.move.onmessage = (event) => onMoveMessage(s, event.data);\n s.events.onmessage = (event) => onEventMessage(s, event.data);\n s.connectTimer = setTimeout(() => { if (state.phase !== 'connected') fail(s, 'connect_timeout'); }, CONNECT_TIMEOUT_MS);\n return pc;\n };\n\n const flushLocalIce = (s) => {\n s.iceTimer = null;\n if (s.closed || !s.localIce.length) return;\n const batch = s.localIce.splice(0, s.localIce.length);\n for (let i = 0; i < batch.length; i += 16) sendSignal(s, 'ice', { c: batch.slice(i, i + 16) });\n };\n\n const addRemoteIce = async (s, candidates) => {\n for (const candidate of candidates) {\n try {\n if (candidate) await s.pc.addIceCandidate(candidate);\n } catch (error) {\n log(`ICE remoto rechazado: ${error?.message ?? error}`);\n }\n }\n };\n\n const flushPendingIce = (s) => {\n const ready = s.pendingIce.filter((item) => item.gen === s.gen);\n s.pendingIce = s.pendingIce.filter((item) => item.gen > s.gen);\n ready.forEach((item) => void addRemoteIce(s, item.c));\n };\n\n const makeOffer = async (s, iceRestart = false) => {\n try {\n const offer = await s.pc.createOffer(iceRestart ? { iceRestart: true } : undefined);\n if (s.closed) return;\n await s.pc.setLocalDescription(offer);\n if (s.closed) return;\n sendSignal(s, 'offer', { sdp: s.pc.localDescription.sdp });\n } catch (error) {\n fail(s, `offer: ${error?.message ?? error}`);\n }\n };\n\n const restartIce = (s) => {\n if (s.closed || s.role !== 'caller') return;\n if (s.restarts >= MAX_RESTARTS) { lose(s, 'ICE no se recupera'); return; }\n s.restarts += 1;\n state.restarts = s.restarts;\n s.gen += 1;\n state.gen = s.gen;\n setPhase('restarting');\n void makeOffer(s, true);\n };\n\n const onIceState = (s) => {\n if (s.closed) return;\n const ice = s.pc.iceConnectionState;\n state.ice = ice;\n clearTimeout(s.disconnectTimer);\n if (ice === 'connected' || ice === 'completed') {\n if (state.phase === 'restarting') setPhase('connected');\n void refreshStats(s);\n } else if (ice === 'disconnected') {\n s.disconnectTimer = setTimeout(() => restartIce(s), DISCONNECTED_GRACE_MS);\n } else if (ice === 'failed') {\n restartIce(s);\n }\n emit();\n };\n\n const onChannelOpen = (s) => {\n s.open += 1;\n if (s.open < 2 || s.closed) return;\n clearTimeout(s.connectTimer);\n reconnects = 0;\n s.lastSeen = Date.now();\n s.pingTimer = setInterval(() => ping(s), PING_EVERY_MS);\n s.statsTimer = setInterval(() => void refreshStats(s), 2000);\n s.tickTimer = setInterval(() => tick(s), 1000);\n setPhase('connected');\n void refreshStats(s);\n };\n\n // A session that stops hearing the peer is over; the caller starts a new one.\n const lose = (s, reason) => fail(s, reason);\n\n // ---- data -------------------------------------------------------------\n\n const ping = (s) => {\n if (Date.now() - s.lastSeen > HEARTBEAT_TIMEOUT_MS) { lose(s, 'sin latido del rival'); return; }\n sendRaw(s.events, JSON.stringify({ k: 'ping', n: ++s.pingSeq, ts: performance.now() }));\n };\n\n const sendRaw = (channel, text) => {\n if (channel?.readyState !== 'open') return false;\n try { channel.send(text); return true; } catch { return false; }\n };\n\n const onEventMessage = (s, data) => {\n s.lastSeen = Date.now();\n let message;\n try { message = JSON.parse(data); } catch { return; }\n if (message?.k === 'ping') sendRaw(s.events, JSON.stringify({ k: 'pong', ts: message.ts }));\n else if (message?.k === 'pong') { state.rtt = Math.round((performance.now() - message.ts) * 10) / 10; }\n else if (message?.k === 'app') { state.events.received += 1; onEvent(message.d); }\n };\n\n const onMoveMessage = (s, data) => {\n s.lastSeen = Date.now();\n let packet;\n try { packet = JSON.parse(data); } catch { return; }\n if (!Array.isArray(packet) || !Number.isInteger(packet[0])) return;\n const [seq, , payload] = packet;\n const m = state.move;\n m.received += 1;\n m.windowIn += 1;\n if (m.firstSeq == null) m.firstSeq = seq;\n if (seq < m.maxSeq) { m.outOfOrder += 1; return; } // stale position: counted, not applied\n m.maxSeq = seq;\n onMove(payload, { seq });\n };\n\n const tick = (s) => {\n const m = state.move;\n m.hzIn = m.windowIn;\n m.hzOut = m.windowOut;\n m.windowIn = 0;\n m.windowOut = 0;\n const expected = m.firstSeq == null ? 0 : m.maxSeq - m.firstSeq + 1;\n m.lossPct = expected > 0 ? Math.max(0, Math.round((1 - m.received / expected) * 1000) / 10) : 0;\n if (!s.closed) emit();\n };\n\n const refreshStats = async (s) => {\n if (s.closed || !s.pc?.getStats) return;\n try {\n const report = await s.pc.getStats();\n const byId = new Map();\n report.forEach((entry) => byId.set(entry.id, entry));\n let pair = null;\n report.forEach((entry) => {\n if (entry.type === 'transport' && entry.selectedCandidatePairId) pair = byId.get(entry.selectedCandidatePairId);\n });\n if (!pair) report.forEach((entry) => { if (entry.type === 'candidate-pair' && (entry.selected || entry.nominated) && entry.state === 'succeeded') pair = entry; });\n if (!pair) return;\n const local = byId.get(pair.localCandidateId);\n const remote = byId.get(pair.remoteCandidateId);\n const relay = local?.candidateType === 'relay' || remote?.candidateType === 'relay';\n const type = relay ? 'relay' : (local?.candidateType ?? 'host');\n state.route = {\n local: local?.candidateType ?? null,\n remote: remote?.candidateType ?? null,\n protocol: local?.protocol ?? null,\n relay,\n label: ROUTE_LABELS[type] ?? type,\n };\n if (typeof pair.currentRoundTripTime === 'number') state.statsRtt = Math.round(pair.currentRoundTripTime * 10000) / 10;\n emit();\n } catch { /* stats are optional */ }\n };\n\n // ---- signal handlers ----------------------------------------------------\n\n const onAck = (signal) => {\n const entry = session?.outbox.get(signal.d.of);\n if (!entry) return;\n clearTimeout(entry.timer);\n session.outbox.delete(signal.d.of);\n };\n\n const onHello = (signal) => {\n if (session && session.sid === signal.sid) return;\n // A late hello from a call the bound tab already dropped (glare, reorder).\n if (session && session.peerTab === signal.from && (signal.at ?? 0) <= session.peerAt) return;\n const glare = session?.role === 'caller' && !session.peerTab;\n // Both called at once: the lower tab id keeps calling, the other answers.\n if (glare && tabId < signal.from) return;\n if (!listening && !glare) { state.signals.ignored += 1; return; }\n // Otherwise the peer started over (reload, lost session): the new call replaces ours.\n if (session) teardown(session, { bye: !glare });\n const s = newSession('answerer', signal.sid, signal.from);\n session = s;\n bindState(s);\n if (!createPc(s)) return;\n sendSignal(s, 'ready', null);\n setPhase('negotiating');\n };\n\n const onReady = (signal) => {\n const s = session;\n if (!s || s.role !== 'caller' || s.sid !== signal.sid) return;\n if (s.peerTab && s.peerTab !== signal.from) {\n // Another tab of the peer answered later: release it.\n sendSignal(s, 'bye', null, { to: signal.from, reliable: false });\n return;\n }\n if (s.peerTab) return;\n s.peerTab = signal.from;\n state.peerTab = signal.from;\n clearInterval(s.helloTimer);\n if (!createPc(s)) return;\n setPhase('negotiating');\n void makeOffer(s);\n };\n\n const onOffer = async (signal) => {\n const s = session;\n if (!s || s.role !== 'answerer' || s.sid !== signal.sid || s.peerTab !== signal.from || signal.gen < s.gen) return;\n s.gen = signal.gen;\n state.gen = s.gen;\n try {\n await s.pc.setRemoteDescription({ type: 'offer', sdp: signal.d.sdp });\n if (s.closed) return;\n flushPendingIce(s);\n const answer = await s.pc.createAnswer();\n if (s.closed) return;\n await s.pc.setLocalDescription(answer);\n if (s.closed) return;\n sendSignal(s, 'answer', { sdp: s.pc.localDescription.sdp });\n } catch (error) {\n fail(s, `answer: ${error?.message ?? error}`);\n }\n };\n\n const onAnswer = async (signal) => {\n const s = session;\n if (!s || s.role !== 'caller' || s.sid !== signal.sid || s.peerTab !== signal.from || signal.gen !== s.gen) return;\n if (s.pc.signalingState !== 'have-local-offer') return;\n try {\n await s.pc.setRemoteDescription({ type: 'answer', sdp: signal.d.sdp });\n if (s.closed) return;\n flushPendingIce(s);\n // An ICE restart over a working path may never leave 'connected'.\n if (state.phase === 'restarting' && ['connected', 'completed'].includes(s.pc.iceConnectionState)) setPhase('connected');\n } catch (error) {\n fail(s, `answer remota: ${error?.message ?? error}`);\n }\n };\n\n const onIce = (signal) => {\n const s = session;\n if (!s || s.sid !== signal.sid || s.peerTab !== signal.from || signal.gen < s.gen) return;\n if (signal.gen > s.gen || !s.pc?.remoteDescription) {\n s.pendingIce.push({ gen: signal.gen, c: signal.d.c }); // arrived before its offer/answer\n return;\n }\n void addRemoteIce(s, signal.d.c);\n };\n\n const onBye = (signal) => {\n // The channel may close before the bye arrives through the chat: the peer left\n // on purpose, so the reconnection that started meanwhile is cancelled.\n if (signal.sid === endedSid && (reconnectTimer || (session?.role === 'caller' && !session.peerTab))) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n reconnects = 0;\n if (session) teardown(session);\n setPhase(listening ? 'listening' : 'idle', 'el rival cerró la conexión');\n return;\n }\n const s = session;\n if (!s || s.sid !== signal.sid) return;\n if (s.peerTab && s.peerTab !== signal.from) return;\n teardown(s);\n setPhase(listening ? 'listening' : 'idle', 'el rival cerró la conexión');\n };\n\n // ---- public API -------------------------------------------------------\n\n function call() {\n if (destroyed) return;\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n if (session) teardown(session, { bye: true });\n const s = newSession('caller');\n session = s;\n bindState(s);\n setPhase('calling');\n const hello = () => {\n if (s.closed || s.peerTab) return;\n if (Date.now() - s.helloStarted > HELLO_GIVE_UP_MS) {\n teardown(s);\n setPhase('failed', 'el rival no respondió');\n return;\n }\n sendSignal(s, 'hello', null, { to: null, reliable: false });\n };\n hello();\n s.helloTimer = setInterval(hello, HELLO_EVERY_MS);\n }\n\n if (listening) setPhase('listening');\n\n return {\n tabId,\n /** Starts a new session as caller (the peer must be listening). */\n connect() { reconnects = 0; call(); },\n /** Accept (true) or ignore (false) incoming calls. */\n setListening(value) {\n listening = Boolean(value);\n if (!session) setPhase(listening ? 'listening' : 'idle');\n },\n /** Latest-wins position update over the unreliable channel. */\n sendMove(payload) {\n const s = session;\n if (!s || s.move?.readyState !== 'open') return false;\n if (s.move.bufferedAmount > MAX_BUFFERED) { state.move.dropped += 1; return false; }\n const sent = sendRaw(s.move, JSON.stringify([++s.moveSeq, Date.now(), payload]));\n if (sent) { state.move.sent += 1; state.move.windowOut += 1; }\n return sent;\n },\n /** Reliable, ordered game event. */\n sendEvent(payload) {\n const sent = sendRaw(session?.events, JSON.stringify({ k: 'app', d: payload }));\n if (sent) state.events.sent += 1;\n return sent;\n },\n /** Forces an ICE restart (caller) — for tests. */\n restartIce() { if (session) restartIce(session); },\n getState: snapshot,\n /** Ends the session (bye to the peer) but keeps listening. */\n hangup() {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n if (session) teardown(session, { bye: true });\n setPhase(listening ? 'listening' : 'idle');\n },\n /** Full cleanup: session, timers and the signaling subscription. */\n destroy() {\n if (destroyed) return;\n clearTimeout(reconnectTimer);\n if (session) teardown(session, { bye: true });\n destroyed = true;\n unsubscribe?.();\n },\n };\n}\n\nexport default createPeerLink;\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as e, s as t, t as n } from "./connect4Sounds-
|
|
2
|
-
import { i as r, o as i } from "./ChatContext-
|
|
1
|
+
import { a as e, s as t, t as n } from "./connect4Sounds-Cv0q31VW.js";
|
|
2
|
+
import { i as r, o as i } from "./ChatContext-DjIEifGY.js";
|
|
3
3
|
// src/games/tanks/sound/cues.js
|
|
4
4
|
var a = [
|
|
5
5
|
"engine_loop",
|
|
@@ -174,4 +174,4 @@ function H(e, t) {
|
|
|
174
174
|
}
|
|
175
175
|
export { z as a, M as i, V as n, F as o, H as r, E as t };
|
|
176
176
|
|
|
177
|
-
//# sourceMappingURL=tanksSounds-
|
|
177
|
+
//# sourceMappingURL=tanksSounds-CUNDIOal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tanksSounds-C99OD2xD.js","names":["createSoundManager","chatSoundUrl","connect4Sounds","isSoundEnabled","subscribeSoundPreference","TANKS_LOOPS","play","loopStart","loopStop","IMPACT_SOUNDS","sceneEventToCues","event","name","outcomeToCues","result","TANKS_SOUND_NAMES","LEVELS","DELIVERED_TANKS_SOUNDS","RETRIGGER_MS","LOOP_RAMPS","MP3_LOOP_TRIM","YIELDS_TO","supportsOgg","files","fileFor","extension","configured","format","configureTanksSounds","overrides","available","file","manager","getTanksSoundManager","hidden","canPlay","wantedLoops","syncLoops","allowed","started","startTanksSounds","lastPlayed","suppressedUntil","suppressTanksCue","ms","playTanksCue","key","now","played","startTanksLoop","stopTanksLoop","stopAllTanksLoops","names","runCues","cues","action","handleSceneEvent","suffix","playOutcome"],"sources":["../../src/games/tanks/sound/cues.js","../../src/games/tanks/sound/tanksSounds.js"],"sourcesContent":["// Pure sound choreography for Tanks (no audio, no DOM): maps scene events to\n// one-shot plays and loop start/stop actions.\n\nexport const TANKS_LOOPS = ['engine_loop', 'tracks_loop', 'turret_loop'];\n\nconst play = (name) => ({ name, action: 'play' });\nconst loopStart = (name) => ({ name, action: 'loop_start' });\nconst loopStop = (name) => ({ name, action: 'loop_stop' });\n\nconst IMPACT_SOUNDS = { terrain: 'terrain_impact', obstacle: 'terrain_impact', ground: 'terrain_impact' };\n\n/** Scene onEvent ({ type, ... } or a bare type string) → ordered cues. */\nexport function sceneEventToCues(event) {\n const type = typeof event === 'string' ? event : event?.type;\n switch (type) {\n case 'engine_start': return [play('engine_start')];\n case 'move_start': return [loopStart('engine_loop'), loopStart('tracks_loop')];\n case 'move_end': return [loopStop('tracks_loop'), loopStop('engine_loop')];\n case 'turret_start': return [loopStart('turret_loop')];\n case 'turret_end': return [loopStop('turret_loop')];\n case 'fire': return [play('cannon_fire')];\n case 'projectile_start': return [play('projectile_whistle')];\n case 'impact': {\n const name = IMPACT_SOUNDS[event?.kind];\n return name ? [play(name)] : [];\n }\n case 'tank_hit': return [play('tank_hit')];\n case 'big_explosion': return [play('big_explosion')];\n case 'destroyed': return [play('tank_destroyed')];\n default: return [];\n }\n}\n\n/** 'victory' | 'defeat' → cues; anything else (draw, unknown) stays silent. */\nexport function outcomeToCues(result) {\n if (result === 'victory' || result === 'defeat') return [...TANKS_LOOPS.map(loopStop), play(result)];\n return [];\n}\n","// Tanks (3D) sound layer.\n//\n// Expected files (host serves them from <assets>/sounds/tanks/, i.e. the\n// package's assets/sounds/tanks/), each as .ogg and .mp3: OGG Vorbis is used where\n// the browser supports it, MP3 otherwise. Only names listed in `available` are\n// fetched, so sounds not delivered (the optional UI cues) cost no requests and never error.\n// Format: 44.1 kHz, mono. True peak <= -1 dBTP; loops cut at zero crossings so\n// they repeat without clicks.\n//\n// name duration loudness\n// engine_start 1.5-2.5 s -20 LUFS\n// engine_loop 2-4 s (loop) -24 LUFS\n// tracks_loop 1-2 s (loop) -24 LUFS\n// turret_loop 1-2 s (loop) -26 LUFS\n// cannon_fire 1.5-2.5 s -16 LUFS\n// projectile_whistle 1.5-3 s -22 LUFS\n// terrain_impact 1-2 s -17 LUFS\n// tank_hit 1-2 s -16 LUFS\n// big_explosion 2.5-4 s -14 LUFS\n// tank_destroyed 3-5 s -18 LUFS\n// victory 3-4 s -18 LUFS\n// defeat 2.5-4 s -19 LUFS\n// optional: turn_start, your_turn, timer_warning (short UI cues, ~-22 LUFS)\n//\n// Gating: the games' shared mute/volume (connect4Sounds) applies through the\n// manager; the GLOBAL chat mute (isSoundEnabled) and a hidden page silence\n// every play and stop all loops.\n\nimport { createSoundManager } from '../../../audio/createSoundManager.js';\nimport { connect4Sounds } from '../../connect4/connect4Sounds.js';\nimport { chatSoundUrl } from '../../../assets/chatAssets.js';\nimport { isSoundEnabled, subscribeSoundPreference } from '../../../notifications/chatSounds.js';\nimport { outcomeToCues, sceneEventToCues, TANKS_LOOPS } from './cues.js';\n\nexport { outcomeToCues, sceneEventToCues } from './cues.js';\n\nexport const TANKS_SOUND_NAMES = [\n 'engine_start', 'engine_loop', 'tracks_loop', 'turret_loop', 'cannon_fire', 'projectile_whistle',\n 'terrain_impact', 'tank_hit', 'big_explosion', 'tank_destroyed', 'victory', 'defeat',\n 'turn_start', 'your_turn', 'timer_warning',\n];\n\n// Relative mix, set from the delivered files' measured loudness (blasts ~-16 LUFS,\n// impacts/stingers ~-20, loops -23..-25) so weapons lead and the engine sits under them.\nconst LEVELS = {\n engine_start: 0.6, engine_loop: 0.45, tracks_loop: 0.5, turret_loop: 0.5, cannon_fire: 0.8,\n projectile_whistle: 0.6, terrain_impact: 0.85, tank_hit: 0.85, big_explosion: 0.9, tank_destroyed: 1,\n victory: 0.8, defeat: 0.8, turn_start: 0.35, your_turn: 0.4, timer_warning: 0.35,\n};\n\n// Sounds shipped in assets/sounds/tanks/ (the UI cues are optional and not delivered).\nexport const DELIVERED_TANKS_SOUNDS = [\n 'engine_start', 'engine_loop', 'tracks_loop', 'turret_loop', 'cannon_fire', 'projectile_whistle',\n 'terrain_impact', 'tank_hit', 'big_explosion', 'tank_destroyed', 'victory', 'defeat',\n];\n// The same sound within this window (cluster bomblets, impact + blast in one frame) plays once.\nconst RETRIGGER_MS = 120;\n// Short ramps so loops follow the animation without clicks (defaults are ambient-slow).\nconst LOOP_RAMPS = {\n engine_loop: { fadeIn: 0.5, fadeOut: 0.35 },\n tracks_loop: { fadeIn: 0.12, fadeOut: 0.2 },\n turret_loop: { fadeIn: 0.06, fadeOut: 0.12 },\n};\n// MP3 decoders pad both ends; trimming that keeps the Safari loops gapless.\nconst MP3_LOOP_TRIM = 0.03;\n// tank_destroyed is the cinematic destruction sound: big_explosion yields to it.\nconst YIELDS_TO = { tank_destroyed: { name: 'big_explosion', ms: 1500 } };\n\nconst supportsOgg = () => {\n try {\n return typeof Audio !== 'undefined' && new Audio().canPlayType('audio/ogg; codecs=\"vorbis\"') !== '';\n } catch {\n return false;\n }\n};\n\n// Shared by reference with the manager, so configure() still applies to files\n// not fetched yet.\nconst files = {};\nconst fileFor = (name, extension) => `${name}.${String(extension).replace(/^\\./, '')}`;\n\nlet configured = false;\nlet format = 'ogg';\n/**\n * Call before first use to change the format ('auto' | 'ogg' | 'mp3'), the delivered\n * names, or override files ({ name: 'file.ext' | null }).\n */\nexport function configureTanksSounds({ extension = 'auto', available = DELIVERED_TANKS_SOUNDS, files: overrides } = {}) {\n configured = true;\n format = extension === 'auto' ? (supportsOgg() ? 'ogg' : 'mp3') : extension;\n TANKS_SOUND_NAMES.forEach((name) => {\n if (available.includes(name)) files[name] = fileFor(name, format);\n else delete files[name];\n });\n Object.entries(overrides || {}).forEach(([name, file]) => {\n if (file) files[name] = String(file);\n else delete files[name];\n });\n}\n\nlet manager = null;\nexport function getTanksSoundManager() {\n if (!configured) configureTanksSounds();\n if (!manager) {\n manager = createSoundManager({\n id: 'tanks',\n baseUrl: () => chatSoundUrl('tanks'), // resolved lazily at fetch time\n settingsFrom: connect4Sounds, // one mute/volume preference for every chat game\n sounds: files,\n levels: LEVELS,\n });\n }\n return manager;\n}\n\nconst hidden = () => typeof document !== 'undefined' && document.hidden;\nconst canPlay = () => isSoundEnabled() && !hidden();\n\n// Loops the game currently wants; they only sound while canPlay().\nconst wantedLoops = new Set();\nconst syncLoops = () => {\n if (!manager) return;\n const allowed = canPlay();\n new Set([...TANKS_LOOPS, ...wantedLoops]).forEach((name) => {\n if (allowed && wantedLoops.has(name)) manager.startLoop(name, { ...LOOP_RAMPS[name], trim: format === 'mp3' ? MP3_LOOP_TRIM : 0 });\n else manager.stopLoop(name);\n });\n};\n\nlet started = false;\nlet unsubscribePreference = null;\n\n/** Idempotent: starts the manager, preloads files and links mute/visibility. */\nexport function startTanksSounds() {\n const sounds = getTanksSoundManager();\n sounds.preload();\n if (started) return;\n started = true;\n unsubscribePreference = subscribeSoundPreference(syncLoops);\n if (typeof document !== 'undefined') document.addEventListener('visibilitychange', syncLoops);\n}\n\n/** Stops loops and unlinks listeners (the manager restarts on next use). */\nexport function disposeTanksSounds() {\n wantedLoops.clear();\n syncLoops();\n if (unsubscribePreference) { unsubscribePreference(); unsubscribePreference = null; }\n if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', syncLoops);\n started = false;\n manager?.dispose();\n}\n\nconst lastPlayed = new Map();\nconst suppressedUntil = new Map();\n\n/** Silences `name` for `ms` (e.g. a turn whose destruction already has its own sound). */\nexport function suppressTanksCue(name, ms) {\n suppressedUntil.set(name, Math.max(suppressedUntil.get(name) ?? 0, Date.now() + ms));\n}\n\n/** One-shot cue, deduplicated by key when given. Returns true when scheduled. */\nexport function playTanksCue(name, key) {\n if (!configured) configureTanksSounds();\n if (Date.now() < (suppressedUntil.get(name) ?? 0)) return false;\n if (!canPlay()) {\n // Keep keyed cues from replaying once the page/sound comes back.\n if (key != null) getTanksSoundManager().markPlayed(name, key);\n return false;\n }\n const now = Date.now();\n if (now - (lastPlayed.get(name) ?? 0) < RETRIGGER_MS) return false;\n const played = getTanksSoundManager().play(name, key != null ? { key } : undefined);\n if (played) {\n lastPlayed.set(name, now);\n if (YIELDS_TO[name]) suppressTanksCue(YIELDS_TO[name].name, YIELDS_TO[name].ms);\n }\n return played;\n}\n\nexport function startTanksLoop(name) {\n wantedLoops.add(name);\n getTanksSoundManager();\n syncLoops();\n}\n\nexport function stopTanksLoop(name) {\n wantedLoops.delete(name);\n manager?.stopLoop(name);\n}\n\nexport function stopAllTanksLoops() {\n const names = [...TANKS_LOOPS, ...wantedLoops];\n wantedLoops.clear();\n names.forEach((name) => manager?.stopLoop(name));\n}\n\nconst runCues = (cues, key) => cues.forEach(({ name, action }) => {\n if (action === 'loop_start') startTanksLoop(name);\n else if (action === 'loop_stop') stopTanksLoop(name);\n else playTanksCue(name, key != null ? `${key}:${name}` : undefined);\n});\n\n/** Maps a scene onEvent ({ type, kind?, tankId? }) to sounds. `key` dedupes one-shots (e.g. turn id). */\nexport function handleSceneEvent(event, key) {\n const suffix = event?.tankId != null ? `:${event.tankId}` : '';\n runCues(sceneEventToCues(event), key != null ? `${key}${suffix}` : undefined);\n}\n\n/** 'victory' | 'defeat': stops every loop and plays the outcome once per key. */\nexport function playOutcome(result, key) {\n runCues(outcomeToCues(result), key);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tanksSounds-CUNDIOal.js","names":["createSoundManager","chatSoundUrl","connect4Sounds","isSoundEnabled","subscribeSoundPreference","TANKS_LOOPS","play","loopStart","loopStop","IMPACT_SOUNDS","sceneEventToCues","event","name","outcomeToCues","result","TANKS_SOUND_NAMES","LEVELS","DELIVERED_TANKS_SOUNDS","RETRIGGER_MS","LOOP_RAMPS","MP3_LOOP_TRIM","YIELDS_TO","supportsOgg","files","fileFor","extension","configured","format","configureTanksSounds","overrides","available","file","manager","getTanksSoundManager","hidden","canPlay","wantedLoops","syncLoops","allowed","started","startTanksSounds","lastPlayed","suppressedUntil","suppressTanksCue","ms","playTanksCue","key","now","played","startTanksLoop","stopTanksLoop","stopAllTanksLoops","names","runCues","cues","action","handleSceneEvent","suffix","playOutcome"],"sources":["../../src/games/tanks/sound/cues.js","../../src/games/tanks/sound/tanksSounds.js"],"sourcesContent":["// Pure sound choreography for Tanks (no audio, no DOM): maps scene events to\n// one-shot plays and loop start/stop actions.\n\nexport const TANKS_LOOPS = ['engine_loop', 'tracks_loop', 'turret_loop'];\n\nconst play = (name) => ({ name, action: 'play' });\nconst loopStart = (name) => ({ name, action: 'loop_start' });\nconst loopStop = (name) => ({ name, action: 'loop_stop' });\n\nconst IMPACT_SOUNDS = { terrain: 'terrain_impact', obstacle: 'terrain_impact', ground: 'terrain_impact' };\n\n/** Scene onEvent ({ type, ... } or a bare type string) → ordered cues. */\nexport function sceneEventToCues(event) {\n const type = typeof event === 'string' ? event : event?.type;\n switch (type) {\n case 'engine_start': return [play('engine_start')];\n case 'move_start': return [loopStart('engine_loop'), loopStart('tracks_loop')];\n case 'move_end': return [loopStop('tracks_loop'), loopStop('engine_loop')];\n case 'turret_start': return [loopStart('turret_loop')];\n case 'turret_end': return [loopStop('turret_loop')];\n case 'fire': return [play('cannon_fire')];\n case 'projectile_start': return [play('projectile_whistle')];\n case 'impact': {\n const name = IMPACT_SOUNDS[event?.kind];\n return name ? [play(name)] : [];\n }\n case 'tank_hit': return [play('tank_hit')];\n case 'big_explosion': return [play('big_explosion')];\n case 'destroyed': return [play('tank_destroyed')];\n default: return [];\n }\n}\n\n/** 'victory' | 'defeat' → cues; anything else (draw, unknown) stays silent. */\nexport function outcomeToCues(result) {\n if (result === 'victory' || result === 'defeat') return [...TANKS_LOOPS.map(loopStop), play(result)];\n return [];\n}\n","// Tanks (3D) sound layer.\n//\n// Expected files (host serves them from <assets>/sounds/tanks/, i.e. the\n// package's assets/sounds/tanks/), each as .ogg and .mp3: OGG Vorbis is used where\n// the browser supports it, MP3 otherwise. Only names listed in `available` are\n// fetched, so sounds not delivered (the optional UI cues) cost no requests and never error.\n// Format: 44.1 kHz, mono. True peak <= -1 dBTP; loops cut at zero crossings so\n// they repeat without clicks.\n//\n// name duration loudness\n// engine_start 1.5-2.5 s -20 LUFS\n// engine_loop 2-4 s (loop) -24 LUFS\n// tracks_loop 1-2 s (loop) -24 LUFS\n// turret_loop 1-2 s (loop) -26 LUFS\n// cannon_fire 1.5-2.5 s -16 LUFS\n// projectile_whistle 1.5-3 s -22 LUFS\n// terrain_impact 1-2 s -17 LUFS\n// tank_hit 1-2 s -16 LUFS\n// big_explosion 2.5-4 s -14 LUFS\n// tank_destroyed 3-5 s -18 LUFS\n// victory 3-4 s -18 LUFS\n// defeat 2.5-4 s -19 LUFS\n// optional: turn_start, your_turn, timer_warning (short UI cues, ~-22 LUFS)\n//\n// Gating: the games' shared mute/volume (connect4Sounds) applies through the\n// manager; the GLOBAL chat mute (isSoundEnabled) and a hidden page silence\n// every play and stop all loops.\n\nimport { createSoundManager } from '../../../audio/createSoundManager.js';\nimport { connect4Sounds } from '../../connect4/connect4Sounds.js';\nimport { chatSoundUrl } from '../../../assets/chatAssets.js';\nimport { isSoundEnabled, subscribeSoundPreference } from '../../../notifications/chatSounds.js';\nimport { outcomeToCues, sceneEventToCues, TANKS_LOOPS } from './cues.js';\n\nexport { outcomeToCues, sceneEventToCues } from './cues.js';\n\nexport const TANKS_SOUND_NAMES = [\n 'engine_start', 'engine_loop', 'tracks_loop', 'turret_loop', 'cannon_fire', 'projectile_whistle',\n 'terrain_impact', 'tank_hit', 'big_explosion', 'tank_destroyed', 'victory', 'defeat',\n 'turn_start', 'your_turn', 'timer_warning',\n];\n\n// Relative mix, set from the delivered files' measured loudness (blasts ~-16 LUFS,\n// impacts/stingers ~-20, loops -23..-25) so weapons lead and the engine sits under them.\nconst LEVELS = {\n engine_start: 0.6, engine_loop: 0.45, tracks_loop: 0.5, turret_loop: 0.5, cannon_fire: 0.8,\n projectile_whistle: 0.6, terrain_impact: 0.85, tank_hit: 0.85, big_explosion: 0.9, tank_destroyed: 1,\n victory: 0.8, defeat: 0.8, turn_start: 0.35, your_turn: 0.4, timer_warning: 0.35,\n};\n\n// Sounds shipped in assets/sounds/tanks/ (the UI cues are optional and not delivered).\nexport const DELIVERED_TANKS_SOUNDS = [\n 'engine_start', 'engine_loop', 'tracks_loop', 'turret_loop', 'cannon_fire', 'projectile_whistle',\n 'terrain_impact', 'tank_hit', 'big_explosion', 'tank_destroyed', 'victory', 'defeat',\n];\n// The same sound within this window (cluster bomblets, impact + blast in one frame) plays once.\nconst RETRIGGER_MS = 120;\n// Short ramps so loops follow the animation without clicks (defaults are ambient-slow).\nconst LOOP_RAMPS = {\n engine_loop: { fadeIn: 0.5, fadeOut: 0.35 },\n tracks_loop: { fadeIn: 0.12, fadeOut: 0.2 },\n turret_loop: { fadeIn: 0.06, fadeOut: 0.12 },\n};\n// MP3 decoders pad both ends; trimming that keeps the Safari loops gapless.\nconst MP3_LOOP_TRIM = 0.03;\n// tank_destroyed is the cinematic destruction sound: big_explosion yields to it.\nconst YIELDS_TO = { tank_destroyed: { name: 'big_explosion', ms: 1500 } };\n\nconst supportsOgg = () => {\n try {\n return typeof Audio !== 'undefined' && new Audio().canPlayType('audio/ogg; codecs=\"vorbis\"') !== '';\n } catch {\n return false;\n }\n};\n\n// Shared by reference with the manager, so configure() still applies to files\n// not fetched yet.\nconst files = {};\nconst fileFor = (name, extension) => `${name}.${String(extension).replace(/^\\./, '')}`;\n\nlet configured = false;\nlet format = 'ogg';\n/**\n * Call before first use to change the format ('auto' | 'ogg' | 'mp3'), the delivered\n * names, or override files ({ name: 'file.ext' | null }).\n */\nexport function configureTanksSounds({ extension = 'auto', available = DELIVERED_TANKS_SOUNDS, files: overrides } = {}) {\n configured = true;\n format = extension === 'auto' ? (supportsOgg() ? 'ogg' : 'mp3') : extension;\n TANKS_SOUND_NAMES.forEach((name) => {\n if (available.includes(name)) files[name] = fileFor(name, format);\n else delete files[name];\n });\n Object.entries(overrides || {}).forEach(([name, file]) => {\n if (file) files[name] = String(file);\n else delete files[name];\n });\n}\n\nlet manager = null;\nexport function getTanksSoundManager() {\n if (!configured) configureTanksSounds();\n if (!manager) {\n manager = createSoundManager({\n id: 'tanks',\n baseUrl: () => chatSoundUrl('tanks'), // resolved lazily at fetch time\n settingsFrom: connect4Sounds, // one mute/volume preference for every chat game\n sounds: files,\n levels: LEVELS,\n });\n }\n return manager;\n}\n\nconst hidden = () => typeof document !== 'undefined' && document.hidden;\nconst canPlay = () => isSoundEnabled() && !hidden();\n\n// Loops the game currently wants; they only sound while canPlay().\nconst wantedLoops = new Set();\nconst syncLoops = () => {\n if (!manager) return;\n const allowed = canPlay();\n new Set([...TANKS_LOOPS, ...wantedLoops]).forEach((name) => {\n if (allowed && wantedLoops.has(name)) manager.startLoop(name, { ...LOOP_RAMPS[name], trim: format === 'mp3' ? MP3_LOOP_TRIM : 0 });\n else manager.stopLoop(name);\n });\n};\n\nlet started = false;\nlet unsubscribePreference = null;\n\n/** Idempotent: starts the manager, preloads files and links mute/visibility. */\nexport function startTanksSounds() {\n const sounds = getTanksSoundManager();\n sounds.preload();\n if (started) return;\n started = true;\n unsubscribePreference = subscribeSoundPreference(syncLoops);\n if (typeof document !== 'undefined') document.addEventListener('visibilitychange', syncLoops);\n}\n\n/** Stops loops and unlinks listeners (the manager restarts on next use). */\nexport function disposeTanksSounds() {\n wantedLoops.clear();\n syncLoops();\n if (unsubscribePreference) { unsubscribePreference(); unsubscribePreference = null; }\n if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', syncLoops);\n started = false;\n manager?.dispose();\n}\n\nconst lastPlayed = new Map();\nconst suppressedUntil = new Map();\n\n/** Silences `name` for `ms` (e.g. a turn whose destruction already has its own sound). */\nexport function suppressTanksCue(name, ms) {\n suppressedUntil.set(name, Math.max(suppressedUntil.get(name) ?? 0, Date.now() + ms));\n}\n\n/** One-shot cue, deduplicated by key when given. Returns true when scheduled. */\nexport function playTanksCue(name, key) {\n if (!configured) configureTanksSounds();\n if (Date.now() < (suppressedUntil.get(name) ?? 0)) return false;\n if (!canPlay()) {\n // Keep keyed cues from replaying once the page/sound comes back.\n if (key != null) getTanksSoundManager().markPlayed(name, key);\n return false;\n }\n const now = Date.now();\n if (now - (lastPlayed.get(name) ?? 0) < RETRIGGER_MS) return false;\n const played = getTanksSoundManager().play(name, key != null ? { key } : undefined);\n if (played) {\n lastPlayed.set(name, now);\n if (YIELDS_TO[name]) suppressTanksCue(YIELDS_TO[name].name, YIELDS_TO[name].ms);\n }\n return played;\n}\n\nexport function startTanksLoop(name) {\n wantedLoops.add(name);\n getTanksSoundManager();\n syncLoops();\n}\n\nexport function stopTanksLoop(name) {\n wantedLoops.delete(name);\n manager?.stopLoop(name);\n}\n\nexport function stopAllTanksLoops() {\n const names = [...TANKS_LOOPS, ...wantedLoops];\n wantedLoops.clear();\n names.forEach((name) => manager?.stopLoop(name));\n}\n\nconst runCues = (cues, key) => cues.forEach(({ name, action }) => {\n if (action === 'loop_start') startTanksLoop(name);\n else if (action === 'loop_stop') stopTanksLoop(name);\n else playTanksCue(name, key != null ? `${key}:${name}` : undefined);\n});\n\n/** Maps a scene onEvent ({ type, kind?, tankId? }) to sounds. `key` dedupes one-shots (e.g. turn id). */\nexport function handleSceneEvent(event, key) {\n const suffix = event?.tankId != null ? `:${event.tankId}` : '';\n runCues(sceneEventToCues(event), key != null ? `${key}${suffix}` : undefined);\n}\n\n/** 'victory' | 'defeat': stops every loop and plays the outcome once per key. */\nexport function playOutcome(result, key) {\n runCues(outcomeToCues(result), key);\n}\n"],"mappings":"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"}
|