Dice World — Polyhedra, wisps, physics, input

// dc-physics.js — Dice World: Polyhedra, wisps, physics, hover/click/key, HUD // ca-014 | load_order 120 | world: dice

(function() { if (!window.DC) { console.warn('dc-physics: DC not initialized'); return; } var DC = window.DC;

// Local aliases for immutable scene objects var scene = DC.scene, camera = DC.camera, renderer = DC.renderer; var dice = DC.dice; // alias: never reassigned var FACE_COLORS = DC.FACE_COLORS; var GLYPH_MODES = DC.GLYPH_MODES; var GEM_NAMES = DC.GEM_NAMES; var PHASE_HOME = DC.PHASE_HOME, PHASE_GEM = DC.PHASE_GEM; var PHASE_BAND = DC.PHASE_BAND, PHASE_SUBU = DC.PHASE_SUBU; var PHASE_HEXCUBE = DC.PHASE_HEXCUBE, PHASE_DECK = DC.PHASE_DECK;

// Local mutable state (closures; dc-state accesses via exported functions) var hoveredDie = null, hoveredFaceIdx = -1; var lastPX = 0, lastPY = 0; var prevWispDie = null, prevWispFace = -1; var glyphModeIdx = 0; var glyphTexCache = {};

// Link lines — exported to DC so dc-cards' renderCardTray can read them var linkLines = []; DC.linkLines = linkLines; // same array reference DC.linksVisible = true; var LINK_RADIUS = 0.04, LINK_SEGMENTS = 5;

// ── Build polyhedron ── function buildPolyhedron(data, scale, faceColorOverrides) { scale = scale || 1.0; var geo = new THREE.BufferGeometry(); var positions = [], normals = [], colors = [], faceMap = []; for (var fi = 0; fi < data.face_indices.length; fi++) { var face = data.face_indices[fi]; var color; if (faceColorOverrides && faceColorOverrides[fi] != null) { color = new THREE.Color(faceColorOverrides[fi]); } else { color = new THREE.Color(FACE_COLORS[fi % FACE_COLORS.length]); } var normal = data.normals[fi]; for (var i = 1; i < face.length - 1; i++) { var tri = [face[0], face[i], face[i + 1]]; for (var t = 0; t < 3; t++) { var v = data.vertices[tri[t]]; positions.push(v[0]*scale, v[1]*scale, v[2]*scale); normals.push(normal[0], normal[1], normal[2]); colors.push(color.r, color.g, color.b); faceMap.push(fi); } } } geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); var mat = new THREE.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.05, metalness: 0.0, clearcoat: 1.0, clearcoatRoughness: 0.03, transparent: true, opacity: 0.85, flatShading: true, side: THREE.DoubleSide, envMapIntensity: 1.5 }); var mesh = new THREE.Mesh(geo, mat); mesh.castShadow = true; mesh.userData.faceMap = faceMap; return mesh; }

function buildWireframe(data, scale) { scale = scale || 1.0; var points = []; for (var e = 0; e < data.edges.length; e++) { var edge = data.edges[e]; var a = data.vertices[edge[0]], b = data.vertices[edge[1]]; points.push(new THREE.Vector3(a[0]*scale,a[1]*scale,a[2]*scale), new THREE.Vector3(b[0]*scale,b[1]*scale,b[2]*scale)); } return new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(points), new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.3 })); }

function computeFaceDistances(sd, scale) { var d = []; for (var fi = 0; fi < sd.face_indices.length; fi++) { var vi = sd.face_indices[fi][0], v = sd.vertices[vi], n = sd.normals[fi]; d.push(Math.abs(v[0]*n[0]+v[1]*n[1]+v[2]*n[2]) * scale); } return d; }

function getScaledVertices(sd, scale) { var v = []; for (var i = 0; i < sd.vertices.length; i++) v.push(new THREE.Vector3(sd.vertices[i][0]*scale, sd.vertices[i][1]*scale, sd.vertices[i][2]*scale)); return v; }

function getLowestVertexY(die) { var minY = Infinity; for (var i = 0; i < die.scaledVerts.length; i++) { var v = die.scaledVerts[i].clone().applyQuaternion(die.group.quaternion); if (die.group.position.y + v.y < minY) minY = die.group.position.y + v.y; } return minY; }

function getBoundingRadius(die) { if (die._boundingRadius != null) return die._boundingRadius; var maxR = 0; for (var i = 0; i < die.scaledVerts.length; i++) { var r = die.scaledVerts[i].length(); if (r > maxR) maxR = r; } die._boundingRadius = maxR; return maxR; }

function dieCollisions() { for (var i = 0; i < dice.length; i++) { var a = dice[i]; var ra = getBoundingRadius(a); for (var j = i + 1; j < dice.length; j++) { var b = dice[j]; if ((a.settled && !a.rolling) && (b.settled && !b.rolling)) continue; var rb = getBoundingRadius(b); var diff = new THREE.Vector3().subVectors(b.group.position, a.group.position); var dist = diff.length(); var minDist = ra + rb; if (dist < minDist && dist > 0.001) { var normal = diff.normalize(); var overlap = minDist - dist; a.group.position.addScaledVector(normal, -overlap * 0.5); b.group.position.addScaledVector(normal, overlap * 0.5); var relVel = new THREE.Vector3().subVectors(a.velocity, b.velocity); var vn = relVel.dot(normal); if (vn > 0) { var restitution = 0.5; a.velocity.addScaledVector(normal, -vn * restitution); b.velocity.addScaledVector(normal, vn * restitution); var angTransfer = 0.2; var cross = new THREE.Vector3().crossVectors(normal, relVel).multiplyScalar(angTransfer); a.angularVel.add(cross); b.angularVel.sub(cross); } if (b.settled) { b.settled = false; b.rolling = true; } } } } }

// ── Link lines ── function makeTubeMesh(color) { var geo = new THREE.CylinderGeometry(LINK_RADIUS, LINK_RADIUS, 1, LINK_SEGMENTS, 1, true); var mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.45, blending: THREE.AdditiveBlending, depthWrite: false }); return new THREE.Mesh(geo, mat); }

function addLink(parentDie, parentFaceIdx, childDie) { if (!parentDie || parentFaceIdx < 0) return; var color = getFaceColor(parentDie, parentFaceIdx); var tube = makeTubeMesh(color); tube.visible = DC.linksVisible; scene.add(tube); linkLines.push({ tube: tube, parent: parentDie, face: parentFaceIdx, child: childDie }); updateLink(linkLines[linkLines.length - 1]); }

function updateLink(lk) { var data = lk.parent.solid, face = data.face_indices[lk.face], s = lk.parent.dieScale || 1.0; var cx = 0, cy = 0, cz = 0; for (var i = 0; i < face.length; i++) { var v = data.vertices[face[i]]; cx += v[0]*s; cy += v[1]*s; cz += v[2]*s; } cx /= face.length; cy /= face.length; cz /= face.length; var local = new THREE.Vector3(cx, cy, cz); var world = local.applyMatrix4(lk.parent.group.matrixWorld); var childPos = lk.child.group.position; var mid = world.clone().add(childPos).multiplyScalar(0.5); var dir = new THREE.Vector3().subVectors(childPos, world); var len = dir.length(); if (len < 0.01) len = 0.01; lk.tube.position.copy(mid); lk.tube.scale.set(1, len, 1); lk.tube.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()); }

function tickLinks() { for (var i = 0; i < linkLines.length; i++) updateLink(linkLines[i]); } function clearLinks() { for (var i = 0; i < linkLines.length; i++) scene.remove(linkLines[i].tube); linkLines.length = 0; } function setLinksVisible(vis) { DC.linksVisible = vis; for (var i = 0; i < linkLines.length; i++) linkLines[i].tube.visible = vis; }

// ── Glyph Sprites ── function computeFaceCenter(solidData, fi, scale) { var face = solidData.face_indices[fi], cx = 0, cy = 0, cz = 0; for (var i = 0; i < face.length; i++) { var v = solidData.vertices[face[i]]; cx += v[0]; cy += v[1]; cz += v[2]; } var n = face.length; return new THREE.Vector3(cx/n * scale, cy/n * scale, cz/n * scale); }

function getGlyphTexture(ch) { if (glyphTexCache[ch]) return glyphTexCache[ch]; var sz = 128, canvas = document.createElement('canvas'); canvas.width = sz; canvas.height = sz; var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, sz, sz); ctx.font = 'bold 80px "hanken-grotesk",sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ffffff'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 6; ctx.fillText(ch, sz/2, sz/2); var tex = new THREE.CanvasTexture(canvas); tex.needsUpdate = true; glyphTexCache[ch] = tex; return tex; }

function addGlyphsToDie(die) { removeGlyphsFromDie(die); var mode = GLYPH_MODES[glyphModeIdx]; if (!mode.resolve) return; var sd = die.solid, s = die.dieScale || 1.0; var sprites = []; for (var fi = 0; fi < sd.face_indices.length; fi++) { var faceInfo = getFaceInfo(die, fi); var ch = mode.resolve(faceInfo); if (!ch) continue; var center = computeFaceCenter(sd, fi, s); var normal = new THREE.Vector3(sd.normals[fi][0], sd.normals[fi][1], sd.normals[fi][2]); var pos = center.clone().add(normal.clone().multiplyScalar(0.02 * s)); var tex = getGlyphTexture(ch); var mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: true, sizeAttenuation: true }); var sprite = new THREE.Sprite(mat); sprite.position.copy(pos); var glyphScale = s * 0.35; sprite.scale.set(glyphScale, glyphScale, 1); die.group.add(sprite); sprites.push(sprite); } die._glyphSprites = sprites; }

function removeGlyphsFromDie(die) { if (!die._glyphSprites) return; for (var i = 0; i < die._glyphSprites.length; i++) { die.group.remove(die._glyphSprites[i]); } die._glyphSprites = null; }

function refreshAllGlyphs() { for (var d = 0; d < dice.length; d++) addGlyphsToDie(dice[d]); }

// ── Create / clear dice ── function createDie(solidData, x, z, cubeInfo, faceColorOverrides, dieScale) { var scale = dieScale || 1.0; var mesh = buildPolyhedron(solidData, scale, faceColorOverrides); var wire = buildWireframe(solidData, scale); var faceDists = computeFaceDistances(solidData, scale); var scaledVerts = getScaledVertices(solidData, scale); var group = new THREE.Group(); group.add(mesh); group.add(wire); var baseY = (cubeInfo && cubeInfo.elevation != null) ? cubeInfo.elevation + faceDists[0] : faceDists[0] + 0.5; group.position.set(x, baseY, z); scene.add(group); var dieSet = (cubeInfo && cubeInfo.dieSet) || null; var isFloating = !!(cubeInfo && cubeInfo.elevation != null); var die = { group: group, mesh: mesh, wire: wire, solid: solidData, cube: cubeInfo || null, faceDists: faceDists, scaledVerts: scaledVerts, dieScale: scale, velocity: new THREE.Vector3(0,0,0), angularVel: isFloating ? new THREE.Vector3(0,(Math.random()-0.5)*0.05,0) : new THREE.Vector3((Math.random()-0.5)*0.2,(Math.random()-0.5)*0.2,(Math.random()-0.5)*0.2), rolling: false, settled: isFloating ? false : true, landedFace: -1, highlightMesh: null, isScaleCube: !!(cubeInfo && cubeInfo.ruby && cubeInfo.ruby.type === 'scale-index'), floating: isFloating, floatBaseY: isFloating ? baseY : 0, floatPhase: Math.random() * Math.PI * 2, dieSet: dieSet, faceWisps: {}, activeWisp: null }; if (dieSet && DC.diceSetVisible[dieSet] === false) group.visible = false; dice.push(die); var r1 = getBoundingRadius(die); for (var si = 0; si < dice.length - 1; si++) { var other = dice[si]; var r2 = getBoundingRadius(other); var diff = new THREE.Vector3().subVectors(die.group.position, other.group.position); var dist = diff.length(); var minDist = r1 + r2 + 0.3; if (dist < minDist && dist > 0.001) { diff.normalize().multiplyScalar(minDist - dist); die.group.position.add(diff); } } if (cubeInfo && cubeInfo.parentDie && cubeInfo.parentFace != null && cubeInfo.parentFace >= 0) { addLink(cubeInfo.parentDie, cubeInfo.parentFace, die); } if (glyphModeIdx > 0) addGlyphsToDie(die); return die; }

function clearDice() { for (var d = 0; d < dice.length; d++) { disposeWisps(dice[d]); scene.remove(dice[d].group); } dice.length = 0; hoveredDie = null; hoveredFaceIdx = -1; DC.hideAllCards(); clearLinks(); }

// ── Roll ── function rollDie(die, hitPoint) { DC.hideLandedCard(die); if (die.activeWisp) { die.activeWisp = null; } for (var wfi in die.faceWisps) { die.faceWisps[wfi].visible = false; die.faceWisps[wfi].userData.state = 'dormant'; } die.rolling = true; die.settled = false; die.landedFace = -1; var p = DC.cc('physics'); if (hitPoint) { var center = die.group.position.clone(); var pushDir = center.clone().sub(hitPoint).normalize(); var hForce = p.launch_xz * 1.2; die.velocity.set( pushDir.x * hForce + (Math.random()-0.5)*1.5, p.launch_y[0] + Math.random()*p.launch_y[1], pushDir.z * hForce + (Math.random()-0.5)*1.5 ); var spinAxis = new THREE.Vector3(-pushDir.z, 0, pushDir.x).normalize(); var spinMag = p.spin * (0.6 + Math.random()*0.4); die.angularVel.set( spinAxis.x * spinMag + (Math.random()-0.5)*p.spin*0.3, (Math.random()-0.5)*p.spin*0.5, spinAxis.z * spinMag + (Math.random()-0.5)*p.spin*0.3 ); } else { var lateralMag = p.launch_xz * (0.8 + Math.random() * 0.8); var lateralAngle = Math.random() * Math.PI * 2; die.velocity.set( Math.cos(lateralAngle) * lateralMag, p.launch_y[0] + Math.random() * p.launch_y[1], Math.sin(lateralAngle) * lateralMag ); die.angularVel.set((Math.random()-0.5)*p.spin*2, (Math.random()-0.5)*p.spin*2, (Math.random()-0.5)*p.spin*2); } unglowDie(die); clearBeam(die); }

// ── Face detection ── function getTopFace(die) { var up = new THREE.Vector3(0,1,0), best = 0, bestDot = -2; for (var i = 0; i < die.solid.normals.length; i++) { var n = new THREE.Vector3(die.solid.normals[i][0],die.solid.normals[i][1],die.solid.normals[i][2]); n.applyQuaternion(die.group.quaternion); var dot = n.dot(up); if (dot > bestDot) { bestDot = dot; best = i; } } return best; } function getBottomFace(die) { var down = new THREE.Vector3(0,-1,0), best = 0, bestDot = -2; for (var i = 0; i < die.solid.normals.length; i++) { var n = new THREE.Vector3(die.solid.normals[i][0],die.solid.normals[i][1],die.solid.normals[i][2]); n.applyQuaternion(die.group.quaternion); var dot = n.dot(down); if (dot > bestDot) { bestDot = dot; best = i; } } return best; } function computeFlatQuaternion(die, bfi) { var fn = new THREE.Vector3(die.solid.normals[bfi][0],die.solid.normals[bfi][1],die.solid.normals[bfi][2]); var wn = fn.clone().applyQuaternion(die.group.quaternion).normalize(); var c = new THREE.Quaternion().setFromUnitVectors(wn, new THREE.Vector3(0,-1,0)); return c.multiply(die.group.quaternion.clone()); }

// ── Beam ── function addBeam(die, fi, color) { clearBeam(die); var data = die.solid, face = data.face_indices[fi], s = die.dieScale || 1.0; var normal = data.normals[fi]; var p = face.length; var beamH = 5.0; var localUp = new THREE.Vector3(0, 1, 0).applyQuaternion(die.group.quaternion.clone().invert()); var baseVerts = []; for (var i = 0; i < p; i++) { var v = data.vertices[face[i]]; baseVerts.push(new THREE.Vector3(v[0]*s + normal[0]*0.03, v[1]*s + normal[1]*0.03, v[2]*s + normal[2]*0.03)); } var lift = localUp.clone().multiplyScalar(beamH); var topVerts = []; for (var i2 = 0; i2 < p; i2++) topVerts.push(baseVerts[i2].clone().add(lift)); var positions = [], colors = [], indices = []; var c3 = new THREE.Color(color || 0xE1E1E1); for (var bi = 0; bi < p; bi++) { positions.push(baseVerts[bi].x, baseVerts[bi].y, baseVerts[bi].z); colors.push(c3.r, c3.g, c3.b, 0.25); } for (var ti = 0; ti < p; ti++) { positions.push(topVerts[ti].x, topVerts[ti].y, topVerts[ti].z); colors.push(c3.r, c3.g, c3.b, 0.0); } for (var si = 0; si < p; si++) { var n1 = si, n2 = (si + 1) % p; indices.push(n1, n2, n1 + p); indices.push(n2, n2 + p, n1 + p); } for (var ci = 1; ci < p - 1; ci++) indices.push(p, p + ci, p + ci + 1); var geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 4)); geo.setIndex(indices); die.beamMesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ vertexColors: true, transparent: true, opacity: 1, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false })); die.beamAge = 0; die.beamP = p; die.group.add(die.beamMesh); } function clearBeam(die) { if (die.beamMesh) { die.group.remove(die.beamMesh); die.beamMesh = null; } die.beamAge = 0; } function tickBeams(dt) { for (var d = 0; d < dice.length; d++) { var die = dice[d]; if (!die.beamMesh) continue; die.beamAge += dt; var cols = die.beamMesh.geometry.attributes.color; var p = die.beamP || 3; var pulse = 0.22 + 0.08 * Math.sin(die.beamAge * 1.6); var fade = die.beamAge < 2.5 ? 1.0 : Math.max(0.1, 1.0 - (die.beamAge - 2.5) / 6); var baseA = pulse * fade; for (var i = 0; i < p; i++) cols.setW(i, baseA); for (var j = 0; j < p; j++) cols.setW(p + j, 0.0); cols.needsUpdate = true; } }

// ── Die glow ── function saveBaseColors(die) { if (die._baseColors) return; var cols = die.mesh.geometry.attributes.color; die._baseColors = new Float32Array(cols.array.length); die._baseColors.set(cols.array); } function glowDie(die, color, intensity) { if (!die.mesh || !die.mesh.material) return; saveBaseColors(die); var boost = intensity != null ? intensity : 0.4; var cols = die.mesh.geometry.attributes.color; var base = die._baseColors; var tmp = new THREE.Color(); var hsl = {}; for (var i = 0; i < base.length; i += 3) { tmp.setRGB(base[i], base[i+1], base[i+2]); tmp.getHSL(hsl); hsl.s = Math.min(hsl.s + boost * 0.5, 1); hsl.l = Math.min(hsl.l + boost * 0.15, 0.75); tmp.setHSL(hsl.h, hsl.s, hsl.l); cols.setXYZ(i / 3, tmp.r, tmp.g, tmp.b); } cols.needsUpdate = true; die.mesh.material.emissiveIntensity = 0; die.glowing = true; } function unglowDie(die) { if (!die.mesh || !die.mesh.material) return; if (die._baseColors) { var cols = die.mesh.geometry.attributes.color; cols.array.set(die._baseColors); cols.needsUpdate = true; } die.mesh.material.emissiveIntensity = 0; die.glowing = false; } function getFaceColor(die, fi) { if (!die.mesh) return 0xE1E1E1; var fMap = die.mesh.userData.faceMap; var src = die._baseColors || die.mesh.geometry.attributes.color.array; for (var i = 0; i < fMap.length; i++) { if (fMap[i] === fi) { return new THREE.Color(src[i*3], src[i*3+1], src[i*3+2]).getHex(); } } return 0xE1E1E1; } function settleGlow(die) { glowDie(die, null, 0.15); } function getFaceInfo(die, fi) { if (!die.cube || !die.cube.faces) return null; if (fi < 0 || fi >= die.cube.faces.length) return null; return die.cube.faces[fi]; }

// ── Face Wisps ── function createFaceWisp(color) { var c = color || 0xC3C3C3; var group = new THREE.Group(); var coreGeo = new THREE.IcosahedronGeometry(0.15, 1); var coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0 }); var core = new THREE.Mesh(coreGeo, coreMat); group.add(core); var shellGeo = new THREE.IcosahedronGeometry(0.22, 0); var shellMat = new THREE.MeshBasicMaterial({ color: c, transparent: true, opacity: 0, wireframe: true }); var shell = new THREE.Mesh(shellGeo, shellMat); group.add(shell); var light = new THREE.PointLight(c, 0, 3); group.add(light); group.visible = false; group.userData = { core: core, shell: shell, light: light, state: 'dormant' }; return group; }

function wispColorForFace(die, faceIdx) { var info = getFaceInfo(die, faceIdx); if (!info) return 0xC3C3C3; var gemColors = { onyx: 0x333333, sapphire: 0x707070, ruby: 0x707070, emerald: 0x8B8B8B, amethyst: 0x707070, diamond: 0xE1E1E1, pearl: 0xE1E1E1, obsidian: 0x121212, topaz: 0xA7A7A7, citrine: 0xE1E1E1 }; return gemColors[info.gem_name] || 0xC3C3C3; }

function getOrCreateWisp(die, faceIdx) { if (!die.faceWisps[faceIdx]) { var color = wispColorForFace(die, faceIdx); die.faceWisps[faceIdx] = createFaceWisp(color); scene.add(die.faceWisps[faceIdx]); } return die.faceWisps[faceIdx]; }

function faceWorldCenter(die, faceIdx) { var center = computeFaceCenter(die.solid, faceIdx, die.dieScale || 1.0); center.applyMatrix4(die.group.matrixWorld); return center; }

function faceWorldNormal(die, faceIdx) { var n = die.solid.normals[faceIdx]; return new THREE.Vector3(n[0], n[1], n[2]).applyQuaternion(die.group.quaternion).normalize(); }

function activateWispHover(die, faceIdx) { var wisp = getOrCreateWisp(die, faceIdx); var center = faceWorldCenter(die, faceIdx); var normal = faceWorldNormal(die, faceIdx); center.add(normal.clone().multiplyScalar(0.35)); wisp.position.copy(center); wisp.visible = true; wisp.userData.state = 'perched'; }

function deactivateWispHover(die, faceIdx) { var wisp = die.faceWisps[faceIdx]; if (!wisp) return; if (die.activeWisp && die.activeWisp.faceIdx === faceIdx) return; wisp.visible = false; wisp.userData.state = 'dormant'; wisp.userData.core.material.opacity = 0; wisp.userData.shell.material.opacity = 0; wisp.userData.light.intensity = 0; }

function faceHasChildren(die, faceIdx) { var fI = getFaceInfo(die, faceIdx); if (!fI) return false; return (fI.thing_count != null && fI.thing_count > 0) || (fI.subu_type === 'thing' && fI.id); }

function startWispEmerge(die, faceIdx) { if (die.activeWisp && die.activeWisp.state === 'anchored') { collapseWispCard(die); return; } if (die.activeWisp) return; var wisp = getOrCreateWisp(die, faceIdx); var center = faceWorldCenter(die, faceIdx); var normal = faceWorldNormal(die, faceIdx); var target = center.clone().add(normal.clone().multiplyScalar(3.0)); wisp.position.copy(center); wisp.visible = true; wisp.userData.state = 'emerging'; var isChildForm = faceHasChildren(die, faceIdx); die.activeWisp = { wisp: wisp, faceIdx: faceIdx, die: die, state: 'emerging', t: 0, duration: 0.4, startPos: center.clone(), endPos: target.clone(), normal: normal, cornerPos: null, cardEl: null, childDieForm: isChildForm, miniDie: null }; }

function conjureCard(aw) { if (aw.childDieForm) { conjureChildDie(aw); return; } var die = aw.die, faceIdx = aw.faceIdx; var fI = getFaceInfo(die, faceIdx); var title = fI && fI.onyx !== 'empty' ? (fI.title || fI.onyx) : ''; var subtitle = ''; if (fI && fI.subu_type === 'gem_face') { subtitle = (fI.gem_label || '') + '\n' + DC.formatGemValue(fI.gem_data); if (subtitle.length > 200) subtitle = subtitle.substring(0, 200) + '...'; } else if (fI && fI.thing_count != null) { subtitle = fI.thing_count + ' things'; } else if (fI && fI.preview) { subtitle = fI.preview.substring(0, 80); } var acts = DC.hoverActionsForFace(fI, die); DC.showLandedCard(die, faceIdx, title, subtitle, acts ? acts.actions : null, acts ? acts.handlers : null); var lc = DC.getLandedCard(die, faceIdx); if (lc) { aw.cardEl = lc.el; lc.el.style.opacity = '0'; lc.el.style.transition = 'opacity 0.3s ease-out'; requestAnimationFrame(function() { lc.el.style.opacity = '1'; }); } }

function conjureChildDie(aw) { var fI = getFaceInfo(aw.die, aw.faceIdx); var count = (fI && fI.thing_count) || 4; if (fI && fI.subu_type === 'thing') count = 4; var pq = DC.solidForCount(Math.max(count, 4)); var wispColor = wispColorForFace(aw.die, aw.faceIdx);

DC.fetchSolid(pq[0], pq[1]).then(function(sd) { if (sd.error || !aw.die.activeWisp || aw.die.activeWisp !== aw) return; var miniScale = 0.4; var miniGroup = new THREE.Group(); var geo = new THREE.BufferGeometry(); var positions = [], normals = [], colors = []; var holoColor = new THREE.Color(wispColor); for (var fi = 0; fi < sd.face_indices.length; fi++) { var face = sd.face_indices[fi]; var normal = sd.normals[fi]; var fColor = holoColor.clone(); fColor.offsetHSL(fi * 0.02, 0, (fi % 2) * 0.05 - 0.025); for (var i = 1; i < face.length - 1; i++) { var tri = [face[0], face[i], face[i + 1]]; for (var t = 0; t < 3; t++) { var v = sd.vertices[tri[t]]; positions.push(v[0]*miniScale, v[1]*miniScale, v[2]*miniScale); normals.push(normal[0], normal[1], normal[2]); colors.push(fColor.r, fColor.g, fColor.b); } } } geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); var mat = new THREE.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.1, metalness: 0.3, clearcoat: 1.0, clearcoatRoughness: 0.05, transparent: true, opacity: 0, flatShading: true, side: THREE.DoubleSide, envMapIntensity: 2.0, emissive: wispColor, emissiveIntensity: 0.15 }); var mesh = new THREE.Mesh(geo, mat); mesh.castShadow = false; miniGroup.add(mesh); var wirePoints = []; for (var e = 0; e < sd.edges.length; e++) { var edge = sd.edges[e]; var a = sd.vertices[edge[0]], b = sd.vertices[edge[1]]; wirePoints.push( new THREE.Vector3(a[0]*miniScale, a[1]*miniScale, a[2]*miniScale), new THREE.Vector3(b[0]*miniScale, b[1]*miniScale, b[2]*miniScale) ); } var wire = new THREE.LineSegments( new THREE.BufferGeometry().setFromPoints(wirePoints), new THREE.LineBasicMaterial({ color: wispColor, transparent: true, opacity: 0, linewidth: 2 }) ); miniGroup.add(wire); var glow = new THREE.PointLight(wispColor, 0, 3); miniGroup.add(glow); miniGroup.position.copy(aw.endPos); miniGroup.userData = { mesh: mesh, wire: wire, glow: glow, targetOpacity: 0.7, solid: sd, faceInfo: fI }; scene.add(miniGroup); aw.miniDie = miniGroup; var label = document.createElement('div'); label.style.cssText = 'position:fixed;pointer-events:none;font:bold 11px monospace;' + 'color:#' + holoColor.getHexString() + ';text-shadow:0 0 8px #' + holoColor.getHexString() + ';' + 'background:rgba(0,0,0,0.5);padding:2px 8px;border-radius:4px;z-index:100;' + 'transition:opacity 0.3s;opacity:0;white-space:nowrap;'; var labelText = fI ? (fI.title || fI.onyx || '') : ''; if (fI && fI.thing_count) labelText += ' \u00b7 ' + fI.thing_count; label.textContent = labelText; document.body.appendChild(label); aw.miniLabel = label; requestAnimationFrame(function() { label.style.opacity = '1'; }); }); }

function collapseWispCard(die) { var aw = die.activeWisp; if (!aw || (aw.state !== 'anchored' && aw.state !== 'conjuring')) return; if (aw.miniDie) { aw.miniDieCollapsing = true; if (aw.miniLabel) { aw.miniLabel.style.opacity = '0'; var lbl = aw.miniLabel; setTimeout(function() { if (lbl.parentNode) lbl.parentNode.removeChild(lbl); }, 300); aw.miniLabel = null; } } if (aw.cardEl) { aw.cardEl.style.opacity = '0'; var dieCopy = die; setTimeout(function() { DC.hideLandedCard(dieCopy); }, 300); } else if (!aw.miniDie) { DC.hideLandedCard(die); } aw.state = 'collapsing'; aw.t = 0; aw.duration = 0.4; }

function disposeWisps(die) { for (var fi in die.faceWisps) { var w = die.faceWisps[fi]; scene.remove(w); w.userData.core.geometry.dispose(); w.userData.core.material.dispose(); w.userData.shell.geometry.dispose(); w.userData.shell.material.dispose(); } if (die.activeWisp && die.activeWisp.miniDie) { scene.remove(die.activeWisp.miniDie); die.activeWisp.miniDie.userData.mesh.geometry.dispose(); die.activeWisp.miniDie.userData.mesh.material.dispose(); die.activeWisp.miniDie.userData.wire.geometry.dispose(); die.activeWisp.miniDie.userData.wire.material.dispose(); } if (die.activeWisp && die.activeWisp.miniLabel && die.activeWisp.miniLabel.parentNode) { die.activeWisp.miniLabel.parentNode.removeChild(die.activeWisp.miniLabel); } die.faceWisps = {}; die.activeWisp = null; }

function tickFaceWisps(dt) { for (var d = 0; d < dice.length; d++) { var die = dice[d]; for (var fi in die.faceWisps) { var w = die.faceWisps[fi]; var ud = w.userData; if (ud.state === 'perched') { var pulse = 0.6 + 0.4 * Math.sin(Date.now() * 0.004); ud.core.material.opacity = pulse; ud.shell.material.opacity = pulse * 0.6; ud.light.intensity = pulse * 0.8; ud.core.rotation.y += dt * 2; ud.shell.rotation.x += dt * 1.5; var fIdx = parseInt(fi); var fc = faceWorldCenter(die, fIdx); var fn = faceWorldNormal(die, fIdx); fc.add(fn.clone().multiplyScalar(0.35)); w.position.copy(fc); } } var aw = die.activeWisp; if (!aw) continue; aw.t += dt / aw.duration; if (aw.t > 1) aw.t = 1; if (aw.state === 'emerging') { var ease = 1 - Math.pow(1 - aw.t, 3); aw.wisp.position.lerpVectors(aw.startPos, aw.endPos, ease); aw.wisp.userData.core.material.opacity = ease; aw.wisp.userData.shell.material.opacity = ease * 0.5; aw.wisp.userData.light.intensity = ease * 0.8; aw.wisp.userData.core.rotation.y += dt * 4; if (aw.t >= 1) { aw.state = 'conjuring'; aw.t = 0; aw.duration = 0.3; conjureCard(aw); } } else if (aw.state === 'conjuring') { if (aw.childDieForm && aw.miniDie) { var ease2 = 1 - Math.pow(1 - aw.t, 2); var mud = aw.miniDie.userData; mud.mesh.material.opacity = ease2 * mud.targetOpacity; mud.wire.material.opacity = ease2 * 0.6; mud.glow.intensity = ease2 * 0.6; aw.miniDie.rotation.y += dt * 1.5; aw.miniDie.scale.setScalar(ease2); var vertPos = aw.endPos.clone(); vertPos.y += 0.5; aw.wisp.position.lerpVectors(aw.endPos, vertPos, ease2); if (aw.miniLabel) { var screenPos = aw.endPos.clone().project(camera); var sx = (screenPos.x * 0.5 + 0.5) * window.innerWidth; var sy = (-screenPos.y * 0.5 + 0.5) * window.innerHeight; aw.miniLabel.style.left = (sx + 30) + 'px'; aw.miniLabel.style.top = (sy - 10) + 'px'; } if (aw.t >= 1) { aw.state = 'anchored'; aw.cornerPos = vertPos; aw.wisp.userData.state = 'anchored'; } } else { var up = new THREE.Vector3(0, 1, 0); var side = aw.normal.clone().cross(up); if (side.length() < 0.01) side = aw.normal.clone().cross(new THREE.Vector3(1, 0, 0)); side.normalize().multiplyScalar(-0.5); side.y -= 0.5; var cornerPos = aw.endPos.clone().add(side); var ease2b = 1 - Math.pow(1 - aw.t, 2); aw.wisp.position.lerpVectors(aw.endPos, cornerPos, ease2b); if (aw.t >= 1) { aw.state = 'anchored'; aw.cornerPos = cornerPos; aw.wisp.userData.state = 'anchored'; } } } else if (aw.state === 'anchored' && aw.miniDie) { aw.miniDie.rotation.y += dt * 0.8; aw.miniDie.position.y = aw.endPos.y + Math.sin(Date.now() * 0.002) * 0.05; var orbitAngle = Date.now() * 0.001; var orbitR = 0.55; aw.wisp.position.set( aw.endPos.x + Math.cos(orbitAngle) * orbitR, aw.endPos.y + 0.5 + Math.sin(orbitAngle * 1.3) * 0.1, aw.endPos.z + Math.sin(orbitAngle) * orbitR ); if (aw.miniLabel) { var sp = aw.endPos.clone().project(camera); var lx = (sp.x * 0.5 + 0.5) * window.innerWidth; var ly = (-sp.y * 0.5 + 0.5) * window.innerHeight; aw.miniLabel.style.left = (lx + 30) + 'px'; aw.miniLabel.style.top = (ly - 10) + 'px'; } } else if (aw.state === 'collapsing') { var ease3 = 1 - Math.pow(1 - aw.t, 3); aw.wisp.position.lerpVectors(aw.cornerPos || aw.endPos, aw.startPos, ease3); aw.wisp.userData.core.material.opacity = 1 - ease3; aw.wisp.userData.shell.material.opacity = (1 - ease3) * 0.5; aw.wisp.userData.light.intensity = (1 - ease3) * 0.8; if (aw.miniDie) { var shrink = 1 - ease3; aw.miniDie.scale.setScalar(shrink); var mud2 = aw.miniDie.userData; mud2.mesh.material.opacity = shrink * mud2.targetOpacity; mud2.wire.material.opacity = shrink * 0.6; mud2.glow.intensity = shrink * 0.6; aw.miniDie.rotation.y += dt * 3; } if (aw.t >= 1) { aw.wisp.visible = false; aw.wisp.userData.state = 'dormant'; if (aw.miniDie) { scene.remove(aw.miniDie); aw.miniDie.userData.mesh.geometry.dispose(); aw.miniDie.userData.mesh.material.dispose(); aw.miniDie.userData.wire.geometry.dispose(); aw.miniDie.userData.wire.material.dispose(); aw.miniDie = null; } if (aw.miniLabel && aw.miniLabel.parentNode) { aw.miniLabel.parentNode.removeChild(aw.miniLabel); } die.activeWisp = null; } } } }

// ── Physics ── function physicsTick(dt) { var SUBSTEPS = 3; var subDt = dt / SUBSTEPS; for (var step = 0; step < SUBSTEPS; step++) physicsStep(subDt); }

function physicsStep(dt) { var p = DC.cc('physics'), fieldSize = DC.cc('field.size',12); var FT = DC.cc('physics.face_torque',30.0), AT = DC.cc('physics.align_threshold',0.9995); dieCollisions(); for (var d = 0; d < dice.length; d++) { var die = dice[d]; if (die.floating) { die.floatPhase += dt * 0.6; die.group.position.y = die.floatBaseY + Math.sin(die.floatPhase) * 0.3; var fAngle = die.angularVel.length() * dt; if (fAngle > 0.0001) die.group.rotateOnWorldAxis(die.angularVel.clone().normalize(), fAngle); continue; } if (die.settled && !die.rolling) continue; die.velocity.y += p.gravity * dt; die.group.position.add(die.velocity.clone().multiplyScalar(dt)); var angle = die.angularVel.length() * dt; if (angle > 0.0001) die.group.rotateOnWorldAxis(die.angularVel.clone().normalize(), angle); var GROUND_Y = 0.0; var lowestY = getLowestVertexY(die), onGround = false; if (lowestY < GROUND_Y) { die.group.position.y += (GROUND_Y - lowestY); die.velocity.y = Math.abs(die.velocity.y) * p.bounce; if (Math.abs(die.velocity.y) < 1.0) die.velocity.y = 0; onGround = true; } if (!onGround) onGround = getLowestVertexY(die) < GROUND_Y + 0.05; var baseDamp = onGround ? p.angular_damping * DC.cc('physics.ground_angular_mult',1.5) : p.angular_damping; var linFactor = onGround ? DC.cc('physics.ground_linear_mult', 1.0) : 1.0; var linDamp = Math.pow(1-p.linear_damping*linFactor, dt*60), angDamp = Math.pow(1-baseDamp, dt*60); die.velocity.x *= linDamp; die.velocity.z *= linDamp; die.angularVel.multiplyScalar(angDamp); if (onGround) { var speed = die.velocity.length(), spin = die.angularVel.length(); var bf = getBottomFace(die); var fn2 = new THREE.Vector3(die.solid.normals[bf][0],die.solid.normals[bf][1],die.solid.normals[bf][2]); var wn2 = fn2.clone().applyQuaternion(die.group.quaternion).normalize(); var dd = new THREE.Vector3(0,-1,0), alignment = wn2.dot(dd); var cross = new THREE.Vector3().crossVectors(wn2, dd), crossLen = cross.length(); if (crossLen > 0.0001 && spin < 1.0) { var ca = cross.normalize(); die.angularVel.add(ca.clone().multiplyScalar(FT * crossLen * dt)); var ala = die.angularVel.dot(ca); if (ala * crossLen < 0 || alignment > 0.90) { var df = ala * DC.cc('physics.dashpot_strength',3); die.angularVel.add(ca.clone().multiplyScalar(-df * dt)); } } var targetY = GROUND_Y + die.faceDists[bf]; die.group.position.y += (targetY - die.group.position.y) * Math.min(8*dt, 0.4); if (alignment > AT && spin < 0.3 && speed < 0.15) { die.group.quaternion.copy(computeFlatQuaternion(die, bf)); die.group.position.y = targetY; die.velocity.set(0,0,0); die.angularVel.set(0,0,0); die.rolling = false; die.settled = true; die.landedFace = getTopFace(die); die.sacredFace = bf; var beamColor = getFaceColor(die, die.landedFace); settleGlow(die); addBeam(die, die.landedFace, beamColor); var fi1 = getFaceInfo(die, die.landedFace), fi2 = getFaceInfo(die, die.sacredFace); window.dispatchEvent(new CustomEvent('dice-landed', { detail: { solid: die.solid.solid, face: die.landedFace, faces: die.solid.faces, sacredFace: die.sacredFace, cube: die.cube ? die.cube.onyx : null, card: fi1 ? fi1.title : null, sacredCard: fi2 ? fi2.title : null, isScaleCube: die.isScaleCube, die: die }})); recheckHover(); } } var maxVel = 50; if (die.velocity.length() > maxVel) die.velocity.setLength(maxVel); var bound = fieldSize - 1; if (Math.abs(die.group.position.x) > bound) { die.group.position.x = Math.sign(die.group.position.x)*bound; die.velocity.x *= -0.5; } if (Math.abs(die.group.position.z) > bound) { die.group.position.z = Math.sign(die.group.position.z)*bound; die.velocity.z *= -0.5; } } }

// ── Hover ── function recheckHover() { onPointerMove({ clientX: lastPX, clientY: lastPY }); } function onPointerMove(event) { lastPX = event.clientX; lastPY = event.clientY; var mouse = DC.mouse; var rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX-rect.left)/rect.width)*2-1; mouse.y = -((event.clientY-rect.top)/rect.height)*2+1; DC.raycaster.setFromCamera(mouse, camera); var found = false; for (var d = 0; d < dice.length; d++) { var die = dice[d]; if (die.rolling) continue; var hits = DC.raycaster.intersectObject(die.mesh); if (hits.length > 0) { var triIdx = hits[0].faceIndex; var faceIdx = die.mesh.userData.faceMap[triIdx * 3]; if (hoveredDie !== die || hoveredFaceIdx !== faceIdx) { if (prevWispDie && (prevWispDie !== die || prevWispFace !== faceIdx)) { deactivateWispHover(prevWispDie, prevWispFace); } if (hoveredDie && hoveredDie !== die) { if (hoveredDie.settled && hoveredDie.landedFace >= 0) settleGlow(hoveredDie); else unglowDie(hoveredDie); } hoveredDie = die; hoveredFaceIdx = faceIdx; glowDie(die, null, 0.5); if (die.settled) { activateWispHover(die, faceIdx); prevWispDie = die; prevWispFace = faceIdx; } } var fI = getFaceInfo(die, faceIdx); var hTitle = fI && fI.onyx !== 'empty' ? (fI.title || fI.onyx) : ''; var hSub = ''; if (fI && fI.subu_type === 'gem_face') { hSub = (fI.gem_label || '') + '\n' + DC.formatGemValue(fI.gem_data); if (hSub.length > 200) hSub = hSub.substring(0, 200) + '...'; } else if (fI && fI.thing_count != null) { hSub = fI.thing_count + ' things'; } else if (fI && fI.preview) { hSub = fI.preview.substring(0, 80); } var hoverActs = DC.hoverActionsForFace(fI, die); DC.showHoverCard(die, faceIdx, hTitle, hSub, hoverActs ? hoverActs.actions : null, hoverActs ? hoverActs.handlers : null); found = true; break; } } if (!found) { if (prevWispDie) { deactivateWispHover(prevWispDie, prevWispFace); prevWispDie = null; prevWispFace = -1; } if (hoveredDie) { if (hoveredDie.settled && hoveredDie.landedFace >= 0) settleGlow(hoveredDie); else unglowDie(hoveredDie); hoveredDie = null; hoveredFaceIdx = -1; } DC.hideHoverCard(); } } renderer.domElement.addEventListener('pointermove', onPointerMove);

// ── Click ── function onPointerDown(event) { var mouse = DC.mouse; var rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX-rect.left)/rect.width)*2-1; mouse.y = -((event.clientY-rect.top)/rect.height)*2+1; DC.raycaster.setFromCamera(mouse, camera); for (var md = 0; md < dice.length; md++) { var aw = dice[md].activeWisp; if (aw && aw.miniDie && aw.state === 'anchored') { var miniHits = DC.raycaster.intersectObject(aw.miniDie.userData.mesh); if (miniHits.length > 0) { var fI = getFaceInfo(aw.die, aw.faceIdx); var acts = DC.hoverActionsForFace(fI, aw.die); if (acts && acts.handlers && acts.handlers.enter) { collapseWispCard(aw.die); acts.handlers.enter(); } else if (acts && acts.handlers && acts.handlers.gaze) { collapseWispCard(aw.die); acts.handlers.gaze(); } return; } } } for (var d = 0; d < dice.length; d++) { var die = dice[d]; if (die.cube && die.cube.realmDie) continue; var hits = DC.raycaster.intersectObject(die.mesh); if (hits.length > 0) { var hitPoint = hits[0].point; rollDie(die, hitPoint); return; } } } renderer.domElement.addEventListener('pointerdown', onPointerDown);

// ── Key ── function isDiceWorldActive() { var c = document.getElementById('world-canvas'); return c && c.classList.contains('rb2-docked-world'); }

function onKeyDown(event) { if (!isDiceWorldActive()) return; if (event.code === 'Escape') { var collapsed = false; for (var wd = 0; wd < dice.length; wd++) { if (dice[wd].activeWisp && dice[wd].activeWisp.state === 'anchored') { collapseWispCard(dice[wd]); collapsed = true; } } if (collapsed) return; } if (event.code === 'Space') { event.preventDefault(); DC.hideAllCards(); DC.alignResult = [-1, -1]; for (var d = 0; d < dice.length; d++) { if (dice[d].cube && dice[d].cube.realmDie) continue; if (!dice[d].group.visible) continue; rollDie(dice[d]); } } if (event.code === 'KeyE') { if (DC.landedCards.length > 0) { DC.hideAllCards(); return; } if (DC.phase === PHASE_GEM) { if (DC.gemReturnPhase === PHASE_BAND) { DC.enterBandPhase(DC.currentBand); } else if (DC.gemReturnPhase === PHASE_SUBU && DC.subuStack.length > 0) { var prevG = DC.subuStack.pop(); DC.enterSubuLevel(prevG.level, prevG.items, prevG.label, prevG.color); } else { DC.enterHomePhase(); } return; } if (DC.phase === PHASE_DECK) { DC.enterHomePhase(); return; } if (DC.phase === PHASE_BAND || DC.phase === PHASE_SUBU || DC.phase === PHASE_HEXCUBE) { if (DC.phase === PHASE_SUBU && DC.subuStack.length > 1) { DC.subuStack.pop(); var prev = DC.subuStack.pop(); DC.enterSubuLevel(prev.level, prev.items, prev.label, prev.color); } else if (DC.phase === PHASE_HEXCUBE && DC.hexcubeStack.length > 1) { DC.hexcubeStack.pop(); var prev2 = DC.hexcubeStack.pop(); DC.enterHexcubeLevel(prev2.levelIdx, prev2.label, prev2.address); } else { DC.enterHomePhase(); } } } if (event.code === 'Enter') { var lcs = DC.landedCards; for (var lc = 0; lc < lcs.length; lc++) { if (lcs[lc].enterFn) { var fn = lcs[lc].enterFn; var lcDie = lcs[lc].die; DC.hideLandedCard(lcDie); fn(); return; } } } if (event.code === 'KeyG') { glyphModeIdx = (glyphModeIdx + 1) % GLYPH_MODES.length; refreshAllGlyphs(); DC.hud.textContent = GLYPH_MODES[glyphModeIdx].label; } if (event.code === 'KeyD') { var modes = ['each', 'all', 'hover', 'none']; var cur = DC.cc('cards.mode', 'each'); var idx = modes.indexOf(cur); var next = modes[(idx + 1) % modes.length]; DC.CFG.cards.mode = next; DC.hud.textContent = 'Card mode: ' + next; } if (event.code === 'KeyT' && DC.phase === PHASE_HOME) { var panel = document.getElementById('dice-set-panel'); if (panel) { panel.parentNode.removeChild(panel); } else { DC.buildSetToggleUI(); } } } window.addEventListener('keydown', onKeyDown);

// ── HUD ── DC.hud = document.createElement('div'); DC.hud.style.cssText = 'position:fixed;bottom:20px;left:20px;color:#8B8B8B;font:14px "hanken-grotesk",sans-serif;' + 'pointer-events:none;z-index:100;text-shadow:0 1px 3px #000;'; DC.hud.textContent = 'Loading dice...'; document.body.appendChild(DC.hud);

// ── Exports ── DC.buildPolyhedron = buildPolyhedron; DC.createDie = createDie; DC.clearDice = clearDice; DC.rollDie = rollDie; DC.getTopFace = getTopFace; DC.getBottomFace = getBottomFace; DC.getFaceInfo = getFaceInfo; DC.getFaceColor = getFaceColor; DC.addBeam = addBeam; DC.clearBeam = clearBeam; DC.tickBeams = tickBeams; DC.glowDie = glowDie; DC.unglowDie = unglowDie; DC.settleGlow = settleGlow; DC.faceWorldCenter = faceWorldCenter; DC.faceWorldNormal = faceWorldNormal; DC.activateWispHover = activateWispHover; DC.deactivateWispHover = deactivateWispHover; DC.disposeWisps = disposeWisps; DC.tickFaceWisps = tickFaceWisps; DC.startWispEmerge = startWispEmerge; DC.collapseWispCard = collapseWispCard; DC.physicsTick = physicsTick; DC.addLink = addLink; DC.clearLinks = clearLinks; DC.setLinksVisible = setLinksVisible; DC.tickLinks = tickLinks; DC.addGlyphsToDie = addGlyphsToDie; DC.refreshAllGlyphs = refreshAllGlyphs; DC.resetGlyphCache = function() { glyphTexCache = {}; if (glyphModeIdx > 0) { for (var d = 0; d < dice.length; d++) addGlyphsToDie(dice[d]); } }; DC.physicsCleanup = function() { renderer.domElement.removeEventListener('pointerdown', onPointerDown); renderer.domElement.removeEventListener('pointermove', onPointerMove); window.removeEventListener('keydown', onKeyDown); };

console.log('dc-physics: Physics & wisps initialized'); })();