Dice World — Config, scene, slab, lighting

// dc-scene.js — Dice World: Config, constants, THREE.js scene, slab, lighting // ca-012 | load_order 100 | world: dice

(function() { if (typeof THREE === 'undefined') { console.warn('dc-scene: THREE.js not loaded'); return; }

window.DC = window.DC || {}; var DC = window.DC;

// ── Config ── DC.CFG = { physics: { gravity: -130, bounce: 0.22, linear_damping: 0.05, angular_damping: 0.08, settle_speed: 0.08, settle_spin: 0.15, launch_y: [6, 3], launch_xz: 8, spin: 18, face_torque: 120.0, align_threshold: 0.9995, ground_angular_mult: 5.0, ground_linear_mult: 8.0, dashpot_strength: 8.0 }, field: { size: 12 }, camera: { position: [0, 10, 14], fov: 60, orbit_damping: 0.08, max_polar: 0.48, min_distance: 5, max_distance: 30 }, cards: { mode: 'each' } }; DC.cc = function(path, fallback) { var keys = path.split('.'), v = DC.CFG; for (var i = 0; i < keys.length; i++) { if (v == null) return fallback; v = v[keys[i]]; } return v != null ? v : fallback; };

// ── Scale bands ── DC.SCALE_BANDS = ['Planck','Subatomic','Nuclear','Atomic','Molecular','Cellular','Human','Geographic','Planetary','Stellar','Galactic','Cosmic']; DC.BAND_COLORS = { 'Planck':0x707070,'Subatomic':0x707070,'Nuclear':0x707070,'Atomic':0xA7A7A7, 'Molecular':0xE1E1E1,'Cellular':0xE1E1E1,'Human':0xE1E1E1,'Geographic':0xC3C3C3, 'Planetary':0xA7A7A7,'Stellar':0x8B8B8B,'Galactic':0x8B8B8B,'Cosmic':0xA7A7A7 };

// ── Subu colors ── DC.SUBU_COLORS = { past: { base: 0x565656, light: 0xA7A7A7, accent: 0xE1E1E1 }, present: { base: 0xE1E1E1, light: 0xE1E1E1, accent: 0xFFFFFF }, future: { base: 0x707070, light: 0xA7A7A7, accent: 0xC3C3C3 } };

// ── HexCube level colors ── DC.HC_LEVEL_COLORS = { array: 0xffffff, hc: 0x707070, cube: 0x8B8B8B, cluster: 0xA7A7A7, set: 0x8B8B8B, node: 0x8B8B8B }; DC.HC_ELEMENTS = ['\u25BC', '\u25B3', '\u25B2']; DC.HC_ELEM_COLORS = [0x8B8B8B, 0xcccccc, 0x8B8B8B];

// ── Utility functions ── DC.fixNormals = function(sd) { for (var fi = 0; fi < sd.face_indices.length; fi++) { var face = sd.face_indices[fi]; var v0 = sd.vertices[face[0]], v1 = sd.vertices[face[1]], v2 = sd.vertices[face[2]]; var cx = 0, cy = 0, cz = 0; for (var vi = 0; vi < face.length; vi++) { cx += sd.vertices[face[vi]][0]; cy += sd.vertices[face[vi]][1]; cz += sd.vertices[face[vi]][2]; } cx /= face.length; cy /= face.length; cz /= face.length; var e1x = v1[0]-v0[0], e1y = v1[1]-v0[1], e1z = v1[2]-v0[2]; var e2x = v2[0]-v0[0], e2y = v2[1]-v0[1], e2z = v2[2]-v0[2]; var nx = e1y*e2z - e1z*e2y, ny = e1z*e2x - e1x*e2z, nz = e1x*e2y - e1y*e2x; if (nx*cx + ny*cy + nz*cz < 0) { nx = -nx; ny = -ny; nz = -nz; } var len = Math.sqrt(nx*nx + ny*ny + nz*nz); if (len > 0) sd.normals[fi] = [nx/len, ny/len, nz/len]; } return sd; };

DC.fetchSolid = function(p, q) { return fetch('/api/platonic/solid/' + p + '/' + q) .then(function(r) { return r.json(); }) .then(function(sd) { return sd.error ? sd : DC.fixNormals(sd); }); };

DC.dimColor = function(hex, factor) { var r = (hex >> 16) & 0xFF, g = (hex >> 8) & 0xFF, b = hex & 0xFF; return ((r * factor | 0) << 16) | ((g * factor | 0) << 8) | (b * factor | 0); };

DC.solidForCount = function(n) { if (n <= 4) return [3,3]; if (n <= 6) return [4,3]; if (n <= 8) return [3,4]; if (n <= 12) return [5,3]; return [3,5]; };

DC.deckBaseName = function(onyx) { return onyx.replace(/\s*\(\d+\/\d+\)$/, ''); };

DC.groupDecks = function(deckList) { var groups = {}, order = []; for (var i = 0; i < deckList.length; i++) { var name = DC.deckBaseName(deckList[i].onyx); if (!groups[name]) { groups[name] = []; order.push(name); } groups[name].push(deckList[i]); } return { groups: groups, order: order }; };

// ── Deck type config ── DC.HOME_DECK_TYPES = { 'playing': true, 'tarot-major': true, 'tarot-minor': true }; DC.DECK_TYPE_LABELS = { 'playing': 'Playing Cards', 'tarot-major': 'Major Arcana', 'tarot-minor': 'Minor Arcana' }; DC.DECK_TYPE_COLORS = { 'playing': 0x707070, 'tarot-major': 0x565656, 'tarot-minor': 0x707070 };

// ── Glyph / Gem / Face colors ── DC.GLYPH_MODES = [ { key: 'off', label: 'Glyphs Off', resolve: null }, { key: 'onyx', label: 'Onyx Initial', resolve: function(f) { var o = f && f.onyx; return o && o !== 'empty' ? o.charAt(0).toUpperCase() : ''; } }, { key: 'number', label: 'Face Number', resolve: function(f) { return f && f.face_number ? '' + f.face_number : ''; } }, { key: 'suit', label: 'Suit Symbol', resolve: function(f) { var r = f && f.ruby; return r ? (r.suit_symbol || r.rank_symbol || '') : ''; } }, { key: 'element', label: 'Element', resolve: function(f) { var r = f && f.ruby; if (!r || !r.element) return ''; var m = {fire:'\uD83D\uDD25',water:'\uD83D\uDCA7',earth:'\uD83C\uDF0D',air:'\uD83D\uDCA8'}; return m[r.element] || r.element.charAt(0).toUpperCase(); } }, { key: 'rank', label: 'Rank', resolve: function(f) { var r = f && f.ruby; return r && r.rank_name ? r.rank_name.charAt(0) : ''; } } ];

DC.GEM_NAMES = ['onyx','sapphire','pearl','ruby','topaz','amethyst','obsidian','diamond','emerald','citrine']; DC.GEM_COLORS = { onyx:0x222222, sapphire:0x565656, pearl:0xE1E1E1, ruby:0x707070, topaz:0xA7A7A7, amethyst:0x707070, obsidian:0x3E3E3E, diamond:0xE1E1E1, emerald:0x8B8B8B, citrine:0xA7A7A7 }; DC.GEM_LABELS = { onyx:'WHO', sapphire:'WHAT kind', pearl:'WHAT it says', ruby:'HOW tagged', topaz:'HOW MUCH', amethyst:'WHO sees', obsidian:'WHERE connects', diamond:'WHO made', emerald:'WHEN', citrine:'WHERE lives' };

DC.FACE_COLORS = [ 0x8B8B8B,0x8B8B8B,0xC3C3C3,0xC3C3C3,0x8B8B8B,0xC3C3C3,0x707070,0xA7A7A7, 0x8B8B8B,0xC3C3C3,0x8B8B8B,0xC3C3C3,0x707070,0x8B8B8B,0x565656,0x707070, 0x8B8B8B,0x707070,0xaaaaaa,0x666666 ];

// ── Phase constants ── DC.PHASE_HOME = 'home'; DC.PHASE_BAND = 'band'; DC.PHASE_SUBU = 'subu'; DC.PHASE_HEXCUBE = 'hexcube'; DC.PHASE_GEM = 'gem'; DC.PHASE_DECK = 'deck';

// ── Mutable phase state ── DC.phase = DC.PHASE_HOME; DC.currentBand = null; DC.subuStack = []; DC.hexcubeStack = []; DC.gemReturnPhase = null; DC.alignResult = [-1, -1];

// ── Dice set toggle ── DC.DICE_SETS = ['gnosis','grimoire','fid','hexcube','subu','decks','alignment','pulse']; DC.DICE_SET_LABELS = { gnosis:'Gnosis', grimoire:'Grimoire', fid:'FIDs', hexcube:'HexCube', subu:'Subu', decks:'Decks', alignment:'Alignment', pulse:'Pulse' }; DC.diceSetVisible = {}; for (var dsi = 0; dsi < DC.DICE_SETS.length; dsi++) DC.diceSetVisible[DC.DICE_SETS[dsi]] = true;

// ── Character alignment ── DC.ALIGN_AXIS = [ { label: 'Order', values: ['Lawful','Neutral','Chaotic'], colors: [0x707070,0x888888,0x707070] }, { label: 'Moral', values: ['Good','Neutral','Evil'], colors: [0xA7A7A7,0xC3C3C3,0x707070] } ];

// ── Dice array ── DC.dice = [];

// ── Scene ── var container = document.getElementById('world-canvas'); if (!container) { container = document.createElement('div'); container.id = 'world-canvas'; container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;'; document.body.insertBefore(container, document.body.firstChild); } container.style.display = 'block'; DC.container = container;

DC.scene = new THREE.Scene(); DC.scene.background = new THREE.Color(0x121212);

DC.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200); DC.camera.position.set(0, 10, 14); DC.camera.lookAt(0, 0, 0);

DC.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); DC.renderer.setSize(window.innerWidth, window.innerHeight); DC.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); DC.renderer.shadowMap.enabled = true; DC.renderer.shadowMap.type = THREE.PCFSoftShadowMap; container.innerHTML = ''; container.appendChild(DC.renderer.domElement);

DC.controls = null; if (THREE.OrbitControls) { DC.controls = new THREE.OrbitControls(DC.camera, DC.renderer.domElement); DC.controls.enableDamping = true; DC.controls.dampingFactor = 0.08; DC.controls.target.set(0, 1, 0); DC.controls.maxPolarAngle = Math.PI * 0.48; DC.controls.minDistance = 5; DC.controls.maxDistance = 30; }

DC.raycaster = new THREE.Raycaster(); DC.mouse = new THREE.Vector2();

window.addEventListener('resize', function() { DC.camera.aspect = window.innerWidth / window.innerHeight; DC.camera.updateProjectionMatrix(); DC.renderer.setSize(window.innerWidth, window.innerHeight); });

// ── Slab (polished obsidian disc) ── var SLAB_RADIUS = 14, SLAB_THICK = 0.5; var slabGeo = new THREE.CylinderGeometry(SLAB_RADIUS, SLAB_RADIUS, SLAB_THICK, 64); var slabMat = new THREE.MeshPhysicalMaterial({ color: 0x121212, roughness: 0.15, metalness: 0.4, clearcoat: 0.8, clearcoatRoughness: 0.1, envMapIntensity: 2.0 }); DC.slab = new THREE.Mesh(slabGeo, slabMat); DC.slab.position.y = -SLAB_THICK / 2; DC.slab.receiveShadow = true; DC.scene.add(DC.slab);

var ringGeo = new THREE.TorusGeometry(SLAB_RADIUS, 0.05, 8, 96); var ringMat = new THREE.MeshBasicMaterial({ color: 0x3E3E3E, transparent: true, opacity: 0.5 }); DC.ring = new THREE.Mesh(ringGeo, ringMat); DC.ring.rotation.x = -Math.PI / 2; DC.ring.position.y = 0.001; DC.scene.add(DC.ring);

// ── Lighting ── DC.ambient = new THREE.AmbientLight(0x3E3E3E, 0.6); DC.scene.add(DC.ambient); DC.dirLight = new THREE.DirectionalLight(0xFFFFFF, 1.0); DC.dirLight.position.set(5, 10, 5); DC.dirLight.castShadow = true; DC.dirLight.shadow.mapSize.width = 1024; DC.dirLight.shadow.mapSize.height = 1024; DC.scene.add(DC.dirLight); DC.pointLight = new THREE.PointLight(0x8B8B8B, 0.5, 30); DC.pointLight.position.set(-3, 6, -3); DC.scene.add(DC.pointLight);

// ── Environment map ── DC.renderer.toneMapping = THREE.ACESFilmicToneMapping; DC.renderer.toneMappingExposure = 1.2; var pmrem = new THREE.PMREMGenerator(DC.renderer); pmrem.compileEquirectangularShader(); var envScene = new THREE.Scene(); var envLight1 = new THREE.HemisphereLight(0x707070, 0x121212, 2.0); envScene.add(envLight1); var envLight2 = new THREE.PointLight(0xFFFFFF, 8, 0); envLight2.position.set(5, 8, 3); envScene.add(envLight2); var envLight3 = new THREE.PointLight(0x8B8B8B, 4, 0); envLight3.position.set(-4, 6, -5); envScene.add(envLight3); var envRT = pmrem.fromScene(envScene, 0.04); DC.scene.environment = envRT.texture;

// ── Phase label overlay ── DC.bandLabel = document.createElement('div'); DC.bandLabel.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);' + 'display:none;pointer-events:none;z-index:200;font:bold 28px "hanken-grotesk",sans-serif;' + 'text-shadow:0 2px 8px #000;letter-spacing:2px;transition:opacity 0.5s;'; document.body.appendChild(DC.bandLabel);

console.log('dc-scene: Dice World scene initialized'); })();