<!-- RUBY-BACKUP · part 226 -->
TS.toString()); }catch(e){}
    try{
      var owned = JSON.parse(localStorage.getItem('ruby-products')||'[]');
      owned.push(p.name);
      localStorage.setItem('ruby-products', JSON.stringify(owned));
    }catch(e){}
    unlockAchievement('Bought '+p.name);
    codeStatus('purchased: '+p.name+'! Remaining: '+CREDITS+' credits');
    var d = document.getElementById('product-panel');
    if(d) d.remove();
  }else{
    codeStatus('not enough credits — need '+p.cost+', have '+CREDITS);
  }
}
/* init */
loadAchievements(); loadCredits(); loadInfluence();
function buildPurposefulCity(){
  if(typeof scene==='undefined' || typeof mePos==='undefined') return;
  /* start with the ancestral city */
  buildAncestralCity();
  /* assign purposes to downtown towers */
  var purposeIdx = 0;
  CITY_TOWERS.forEach(function(tower, i){
    /* every 4th tower gets a public purpose */
    if(i % 3 === 0){
      var purpose = BUILDING_PURPOSES[purposeIdx % BUILDING_PURPOSES.length];
      assignBuildingPurpose(tower, purpose, 0, 0);
      purposeIdx++;
    }
  });
  /* ensure fire and police are present */
  var hasFire = false, hasPolice = false;
  PURPOSE_BUILDINGS.forEach(function(b){
    if(b.userData.purpose==='fire') hasFire = true;
    if(b.userData.purpose==='police') hasPolice = true;
  });
  codeStatus('city with purpose — shops, businesses, fire, police. All visible. No cars.');
}
function listPurposes(){
  var counts = {};
  CITY_TOWERS.forEach(function(t){
    if(t.userData.purpose){
      counts[t.userData.purpose] = (counts[t.userData.purpose]||0)+1;
    }
  });
  var parts = [];
  for(var k in counts) parts.push(k+': '+counts[k]);
  codeStatus(parts.length ? 'purposes: '+parts.join(', ') : 'no purposes assigned yet');
}

/* WILD TERRAIN — islands, volcanoes, mountains, cliffs with trails */
var TERRAIN_GROUP = null;
function buildVolcano(x, z, size){
  var g = new THREE.Group();
  /* cone */
  var cone = new THREE.Mesh(
    new THREE.ConeGeometry(size, size*1.5, 16),
    new THREE.MeshStandardMaterial({color: 0x4a3a2a, roughness: 0.9})
  );
  cone.position.y = size*0.75;
  g.add(cone);
  /* crater — glowing */
  var crater = new THREE.Mesh(
    new THREE.CylinderGeometry(size*0.25, size*0.3, 0.3, 12),
    new THREE.MeshStandardMaterial({color: 0xff4400, emissive: 0xff4400, emissiveIntensity: 2})
  );
  crater.position.y = size*1.5;
  g.add(crater);
  /* lava glow */
  var lava = new THREE.Mesh(
    new THREE.SphereGeometry(size*0.2, 12, 12),
    new THREE.MeshBasicMaterial({color: 0xff6600})
  );
  lava.position.y = size*1.55;
  lava.scale.y = 0.3;
  g.add(lava);
  /* smoke */
  for(var i=0; i<5; i++){
    var puff = new THREE.Mesh(
      new THREE.SphereGeometry(0.5+i*0.2, 8, 8),
      new THREE.MeshStandardMaterial({color: 0x888888, transparent: true, opacity: 0.4})
    );
    puff.position.set((Math.random()-0.5)*2, size*1.8+i*0.8, (Math.random()-0.5)*2);
    g.add(puff);
  }
  g.position.set(x*0.13, 0, z*0.13);
  return g;
}
function buildMountainRange(x, z, length, height){
  var g = new THREE.Group();
  /* long ridge */
  for(var i=0; i<length; i++){
    var peak = new THREE.Mesh(
      new THREE.ConeGeometry(2+Math.random()*1.5, height*(0.7+Math.random()*0.6), 7),
      new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.35, 0.2, 0.25+Math.random()*0.15),
        roughness: 0.95, flatShading: true
      })
    );
    peak.position.set(i*3.5 - length*1.75, height*0.4, (Math.random()-0.5)*4);
    peak.rotation.y = Math.random()*Math.PI;
    g.add(peak);
    /* snow cap */
    if(height > 4){
      var snow = new THREE.Mesh(
        new THREE.ConeGeometry(0.8, 1.2, 7),
        new THREE.MeshStandardMaterial({color: 0xffffff, roughness: 0.6, flatShading: true})
      );
      snow.position.copy(peak.position);
      snow.position.y += height*0.45;
      g.add(snow);
    }
  }
  g.position.set(x*0.13, 0, z*0.13);
  return g;
}
function buildRoughTerrain(x, z, size){
  var g = new THREE.Group();
  /* jagged rocks */
  for(var i=0; i<30; i++){
    var rock = new THREE.Mesh(
      new THREE.DodecahedronGeometry(0.3+Math.random()*0.8, 0),
      new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.08, 0.2, 0.2+Math.random()*0.2),
        roughness: 1, flatShading: true
      })
    );
    rock.position.set(
      (Math.random()-0.5)*size,
      Math.random()*1.5,
      (Math.random()-0.5)*size
    );
    rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI);
    g.add(rock);
  }
  g.position.set(x*0.13, 0, z*0.13);
  return g;
}
function buildCliffTrail(x, z, height){
  var g = new THREE.Group();
  /* cliff face */
  var cliff = new THREE.Mesh(
    new THREE.BoxGeometry(4, height, 1),
    new THREE.MeshStandardMaterial({color: 0x5a4a3a, roughness: 0.95, flatShading: true})
  );
  cliff.position.y = height/2;
  g.add(cliff);
  /* trail — zigzag path down */
  var trailMat = new THREE.MeshStandardMateria