/* ============================================================
   Wildlife Kingdom — js/sim.js
   The Game: time · money · research · missions · rating ·
   guest spawning · staff dispatch · placement rules · save/load
   ============================================================ */
(function(){
'use strict';
const WK = window.WK;
const D = WK.Data;

const SAVE_KEY='wildlifeKingdom.save.v1';

class Game{
  constructor(){
    const g=D.GRID;
    this.world=new WK.World(g.COLS,g.ROWS);
    this.money=D.START_MONEY;
    this.rp=0;
    this.researchTier=1;
    this.day=1;
    this.minutes=8*60;
    this.speed=1;
    this.animals=[];
    this.guests=[];
    this.staff=[];
    this.particles=new WK.Particles();
    this.stats={paths:0,fences:0,adopted:0,guestsTotal:0,guestsToday:0,revenueTotal:0,revenueToday:0,expensesTotal:0,sold:0};
    this.missionIdx=0;
    this.rating=1;
    this.ratingParts={};
    this.spawnAcc=0;
    this.sfx=(n)=>{ if(WK.AudioSys&&WK.AudioSys.enabled) WK.AudioSys.sfx(n); };
    this._scoreCache={ver:-1,map:new Map()};
    this._poiVer=-1;
    this.pois=[];
    this.exitTile=null;
    this.crowdStress=0;
    this.selected=null;
    this.timeAbs=0;
  }

  /* ---------------- helpers ---------------- */
  get maxRegion(){
    this.world.ensureRegions();
    let m=0;
    for(const r of this.world.regions) if(r.openEdges===0&&!r.hasPath) m=Math.max(m,r.area);
    return m;
  }
  speciesCount(){ return new Set(this.animals.map(a=>a.species)).size; }
  notify(msg,type){ if(this.ui&&this.ui.toast) this.ui.toast(msg,type); }
  earnMoney(n,src){
    this.money+=n;
    this.stats.revenueTotal+=n; this.stats.revenueToday+=n;
    if(this.ui&&this.ui.flashStat) this.ui.flashStat('money');
  }
  spendMoney(n){
    if(this.money<n){ this.notify('Not enough coins!','bad'); this.sfx('error'); return false; }
    this.money-=n; return true;
  }

  /* ---------------- habitat scoring ---------------- */
  habitatScoreFor(regionId,species){
    const ver=this.world.ver||0;
    if(this._scoreCache.ver!==ver){ this._scoreCache.ver=ver; this._scoreCache.map.clear(); }
    const key=regionId+':'+species;
    if(this._scoreCache.map.has(key)) return this._scoreCache.map.get(key);
    const reg=this.world.regions.find(r=>r.id===regionId);
    let score=8;
    if(reg){
      const sp=D.SPECIES[species];
      const area=reg.area;
      let bio=0, enrich=0, water=0;
      for(const k of reg.tiles){
        const gid=WK.GIDS[this.world.ground[k]];
        bio+= (sp.biome[gid]!=null)? sp.biome[gid] : 0.25;
        const o=this.world.obj[k];
        if(o){ enrich+=D.NATURE[WK.NIDS[o-1]].enrich||0; }
        if(D.GROUND[gid].water) water++;
      }
      bio/=area;
      const areaF=WK.clamp(area/Math.max(6,sp.minArea),0,1);
      const enr=WK.clamp(enrich/Math.max(0.5,sp.enrichNeed||0.5),0,1);
      score = WK.clamp(bio*52 + areaF*22 + enr*16 + (sp.swim?(water>0?10:(bio>0.75?6:0)):4), 5, 100);
    }
    this._scoreCache.map.set(key,score);
    return score;
  }
  habitatScore(animal){
    const s=this.habitatScoreFor(animal.regionId,animal.species);
    // crowd of same species penalty (max 4 comfortable per species)
    const same=this.animals.filter(a=>a.regionId===animal.regionId&&a.species===animal.species).length;
    return WK.clamp(s-(same>4?(same-4)*4:0),5,100);
  }
  validHabitatAt(x,y){
    this.world.ensureRegions();
    const rid=this.world.regionOf(x,y);
    if(rid<0) return {error:'Can\u2019t place that here.'};
    const reg=this.world.regions.find(r=>r.id===rid);
    if(!reg) return {error:'No region here.'};
    if(reg.hasPath) return {error:'Paths run through this area — habitats must be fence-only.'};
    if(reg.openEdges>0) return {error:'Not enclosed! Fully surround the area with fences.'};
    return {region:reg};
  }

  /* ---------------- placement operations ---------------- */
  tryGround(x,y,gid){
    const def=D.GROUND[gid];
    if(def.tier>this.researchTier) return false;
    const cur=this.world.gid(x,y);
    if(cur===gid) return false;
    if(!this.spendMoney(def.cost)) return false;
    if(this.world.setGround(x,y,gid)){ this.sfx('dig'); return true; }
    this.money+=def.cost; return false;
  }
  tryPath(x,y,pid){
    const def=D.PATH[pid];
    if(def.tier>this.researchTier) return false;
    if(this.world.path[this.world.idx(x,y)]) { // replace path: charge diff
      const cur=this.world.pid(x,y);
      if(cur===pid) return false;
      const diff=Math.max(0,def.cost-D.PATH[cur].cost);
      if(diff&&!this.spendMoney(diff)) return false;
      this.world.setPath(x,y,pid); this.sfx('place');
      this.stats.paths++; return true;
    }
    if(!this.spendMoney(def.cost)) return false;
    if(this.world.setPath(x,y,pid)){ this.sfx('place'); this.stats.paths++; return true; }
    this.money+=def.cost; return false;
  }
  tryNature(x,y,nid){
    const def=D.NATURE[nid];
    if(def.tier>this.researchTier) return false;
    if(this.world.oid(x,y)) { this.tryBulldozeObject(x,y,true); }
    if(!this.spendMoney(def.cost)) return false;
    if(this.world.setObj(x,y,nid,(Math.random()*3)|0)){ this.sfx('place'); return true; }
    this.money+=def.cost; return false;
  }
  tryFence(x,y,edge,kindName){
    const kind= kindName==='gate'?3: kindName==='hedge'?2:1;
    const def=D.FENCE[kind];
    if(def.tier>this.researchTier) return false;
    const cur= edge==='S'?this.world.fenceS[this.world.idx(x,y)]:this.world.fenceE[this.world.idx(x,y)];
    if(cur===kind) return false;
    if(!this.spendMoney(def.cost)) return false;
    this.world.setFence(x,y,edge,kind);
    this.sfx('place'); this.stats.fences++;
    return true;
  }
  tryBulldoze(x,y){
    const b=this.world.buildingAt(x,y);
    if(b){ return this.demolishBuilding(b); }
    if(this.world.obj[this.world.idx(x,y)]) return this.tryBulldozeObject(x,y);
    if(this.world.path[this.world.idx(x,y)]){
      this.world.setPath(x,y,null); this.sfx('trash'); return true;
    }
    return false;
  }
  tryBulldozeObject(x,y,silent){
    if(!this.world.obj[this.world.idx(x,y)]) return false;
    this.money+=Math.round(D.NATURE[this.world.oid(x,y)].cost*0.4);
    this.world.setObj(x,y,null);
    if(!silent) this.sfx('trash');
    return true;
  }
  tryBulldozeEdge(x,y,edge){
    const cur= edge==='S'?this.world.fenceS[this.world.idx(x,y)]:this.world.fenceE[this.world.idx(x,y)];
    if(!cur) return false;
    this.money+=Math.round(D.FENCE[cur].cost*0.4);
    this.world.setFence(x,y,edge,0);
    this.sfx('trash');
    return true;
  }
  tryBuilding(type,x,y){
    const def=D.BUILDING[type];
    if(def.tier>this.researchTier){ this.notify('Locked — unlock more research tiers.','bad'); return false; }
    if(!this.world.canPlaceBuilding(x,y,def.w,def.h)){ this.notify('Space is blocked — clear the area first.','bad'); this.sfx('error'); return false; }
    if(!this.spendMoney(def.cost)) return false;
    const b=this.world.placeBuilding(type,x,y);
    if(!b){ this.money+=def.cost; return false; }
    this.sfx('place');
    if(def.staff==='keeper'){ this.addStaff('keeper',b); this.notify('A keeper joined your staff!','good'); }
    if(def.staff==='vet'){ this.addStaff('vet',b); this.notify('A vet joined your staff!','good'); }
    this.refreshPois();
    return true;
  }
  demolishBuilding(b){
    const def=D.BUILDING[b.type];
    this.money+=Math.round(def.cost*0.5);
    if(def.staff){
      const idx=this.staff.findIndex(s=>s.role===def.staff&&!s._homeRemoved);
      if(idx>=0){ this.staff.splice(idx,1); this.notify(def.staff==='keeper'?'A keeper left your staff.':'A vet left your staff.','bad'); }
    }
    this.world.removeBuildingById(b.id);
    this.sfx('trash');
    this.refreshPois();
    if(this.ui&&this.ui.hideSelection) this.ui.hideSelection();
    return true;
  }
  addStaff(role,b){
    const d=this.world.doorTile(b);
    this.staff.push(new WK.Staff(role,d[0],d[1]));
  }

  tryAdopt(species,x,y){
    const sp=D.SPECIES[species];
    if(sp.tier>this.researchTier){ this.notify('Locked — research more tiers first.','bad'); return false; }
    const chk=this.validHabitatAt(x,y);
    if(chk.error){ this.notify(chk.error,'bad'); this.sfx('error'); return false; }
    const reg=chk.region;
    if(reg.area<sp.minArea){
      this.notify(sp.name+' needs a habitat of at least '+sp.minArea+' tiles.','bad');
      this.sfx('error'); return false;
    }
    const score=this.habitatScoreFor(reg.id,species);
    if(score<50){
      this.notify('Terrain unsuitable for a '+sp.name+'! Match its favorite biomes (needs '+Math.round(score)+'%, want 50%+).','bad');
      this.sfx('error'); return false;
    }
    if(!this.spendMoney(sp.cost)) return false;
    // drop the animal onto a passable nearby tile inside region
    let px=x,py=y;
    const g0=D.GROUND[this.world.gid(x,y)];
    if((g0.water&&!sp.swim)||(this.world.oid(x,y)&&D.NATURE[this.world.oid(x,y)].block)){
      const alt=this.world.randomRegionTile(reg.id,sp,Math.random);
      if(alt){px=alt[0];py=alt[1];}
    }
    const a=new WK.Animal(species,px+0.5-0.5,py+0.5-0.5,reg.id);
    a.x=px; a.y=py;
    this.animals.push(a);
    this.stats.adopted++;
    this.sfx('chirp');
    this.notify(a.name+' the '+sp.name+' joins the zoo!','good');
    this.particles.hearts(px,py);
    return true;
  }
  sellAnimal(a){
    const i=this.animals.indexOf(a);
    if(i<0)return;
    this.animals.splice(i,1);
    const refund=Math.round(D.SPECIES[a.species].cost*0.45);
    this.money+=refund;
    this.stats.sold++;
    this.notify(a.name+' found a new home (+'+WK.fmtMoney(refund)+').','good');
    this.sfx('coin');
    if(this.ui&&this.ui.hideSelection) this.ui.hideSelection();
  }

  /* ---------------- jobs dispatcher ---------------- */
  requestJob(role){
    if(role==='keeper'){
      let best=null;
      for(const a of this.animals){
        if(a._taken||a.eatT>0||a.sick) continue;
        if(a.hunger>62 && (!best||a.hunger>best.hunger)) best=a;
      }
      if(best){ best._taken=true; return best; }
    }else{
      for(const a of this.animals){
        if(a.sick&&!a._taken){ a._taken=true; return a; }
      }
    }
    return null;
  }
  abandonJob(role,a){ a._taken=false; }

  /* ---------------- POIs ---------------- */
  refreshPois(){
    this._poiVer=-1;
  }
  rebuildPois(){
    this.world.ensureRegions();
    const pois=[];
    const regionAppeal=new Map();
    for(const a of this.animals){
      const cur=regionAppeal.get(a.regionId)||{appeal:0,q:0,n:0};
      cur.appeal=Math.max(cur.appeal,D.SPECIES[a.species].appeal);
      cur.q+=a.happiness/100; cur.n++;
      regionAppeal.set(a.regionId,cur);
    }
    for(const [rid,info] of regionAppeal){
      const vps=this.world.viewpoints(rid);
      const q=info.q/info.n;
      for(const vp of vps.slice(0,10)){
        pois.push({type:'view',x:vp[0],y:vp[1],regionId:rid,appeal:info.appeal,quality:q});
      }
    }
    for(const b of this.world.buildings){
      const def=D.BUILDING[b.type];
      const d=this.world.doorTile(b);
      if(def.entrance) continue;
      if(def.restroom){}
      pois.push({
        type: def.income>0?'shop':(b.type==='restroom'?'restroom':'visit'),
        btype:b.type, x:d[0], y:d[1], income:def.income, joy:def.joy,
      });
      if(b.type==='restroom'){ pois.pop(); pois.push({type:'restroom',btype:b.type,x:d[0],y:d[1]}); }
    }
    this.pois=pois;
    // exits
    const ent=this.world.buildings.filter(b=>D.BUILDING[b.type].entrance);
    this.exitTile = ent.length ? this.world.doorTile(ent[0]) : null;
    this.entrances=ent.length;
    this.entranceCap = ent.reduce((s,b)=>s+(D.BUILDING[b.type].big?90:45),0);
    this._poiVer=this.world.ver||0;
  }

  /* ---------------- rating ---------------- */
  recomputeRating(){
    const P={};
    P.welfare = this.animals.length ? this.animals.reduce((s,a)=>s+a.happiness,0)/(this.animals.length*100) : 0.55;
    P.variety = WK.clamp(this.speciesCount()/10,0,1);
    P.joy = this.guests.length ? this.guests.reduce((s,g)=>s+g.joy,0)/(this.guests.length*100) : (this.ratingParts.joy!=null?this.ratingParts.joy:0.5);
    const guests=this.guests.length;
    const restrooms=this.world.buildings.filter(b=>b.type==='restroom').length;
    const food=this.world.buildings.filter(b=>['burger','icecream','restaurant'].includes(b.type)).length;
    const benches=this.countObj('bench');
    P.facilities = WK.clamp(
      0.1 + 0.45*Math.min(restrooms/Math.max(1,guests/28),1) +
      0.25*Math.min(food/Math.max(1,guests/45),1) +
      0.2*Math.min(benches/Math.max(1,guests/14),1),
    0,1);
    P.decor = WK.clamp(this.countNature()/55,0,1);
    P.exhibits = WK.clamp(this.world.buildings.filter(b=>D.BUILDING[b.type].exhibit).length/3,0,1);
    this.ratingParts=P;
    this.rating = WK.clamp((P.welfare*1.5 + P.variety*1.2 + P.joy*1.0 + P.facilities*0.8 + P.decor*0.4 + P.exhibits*0.6)/5.5*5, 0.4, 5);
  }
  countObj(id){
    let n=0; const arr=this.world.obj;
    for(let i=0;i<arr.length;i++) if(arr[i]===WK.NIDS.indexOf(id)+1) n++;
    return n;
  }
  countNature(){
    let n=0; const arr=this.world.obj;
    for(let i=0;i<arr.length;i++) if(arr[i]) n++;
    return n;
  }

  /* ---------------- main tick ---------------- */
  tick(dt){
    this.timeAbs+=dt;
    const spd=this.speed;
    const sdt=dt*spd;
    if(sdt>0){
      this.minutes+=sdt*4;
      while(this.minutes>=1440){ this.minutes-=1440; this.newDay(); }
      // world version → caches
      if((this.world.ver||0)!==this._poiVer) this.rebuildPois();
      // research
      const centers=this.world.buildings.filter(b=>b.type==='research').length;
      this.rp += sdt*(0.003 + centers*(5/60));
      // entities
      for(let i=this.animals.length-1;i>=0;i--){
        const a=this.animals[i];
        a.update(sdt,this);
        // sickness rolls
        if(!a.sick){
          const p=a.happiness<45?0.0011:0.00013;
          if(Math.random()<p*sdt*4){ a.sick=true; a._taken=false;
            this.notify(a.name+' the '+D.SPECIES[a.species].name+' feels sick! Build a Vet Clinic.','bad'); this.sfx('error'); }
        }else{
          a._sickT=(a._sickT||0)+sdt;
          if(a._sickT>150){ a.sick=false; a._sickT=0; this.notify(a.name+' recovered on their own.','good'); }
        }
        if(a.eatT<=0&&a.state!=='eat'&&!a.sick){ /* _taken cleared on feeding */ }
      }
      // escape check (throttled)
      this._escT=(this._escT||0)+sdt;
      if(this._escT>2){
        this._escT=0;
        for(const a of this.animals){
          const reg=this.world.regions.find(r=>r.id===a.regionId);
          if(!reg||(reg.openEdges>0)||reg.hasPath){
            a.happiness=WK.clamp(a.happiness-sdt*8,5,100);
            if(!a._escWarned){ a._escWarned=true;
              this.notify(a.name+'\u2019s habitat is not secure! Check fences & paths.','bad'); }
          }else a._escWarned=false;
        }
      }
      // guests
      this.crowdStress = this.guests.length > 25 + this.world.buildings.filter(b=>b.type==='restroom').length*15 ? 0.06 : 0;
      const cap=Math.min(D.CAPS.guests, 12 + (this.entranceCap||0) + Math.round(this.rating*8));
      const rate=(0.05 + this.rating*0.09);
      this.spawnAcc+=sdt*rate;
      if(this.spawnAcc>=1){
        this.spawnAcc-=1;
        if(this.guests.length<cap && this.exitTile){
          const e=this.exitTile;
          const gs=new WK.Guest(this,e[0]+0.5,e[1]+0.5);
          this.guests.push(gs);
          const fee=8+Math.round(this.rating*4);
          this.earnMoney(fee,'ticket');
          this.stats.guestsTotal++; this.stats.guestsToday++;
          if(this.guests.length%10===0) this.coinFloat(e[0],e[1]);
        }
      }
      for(let i=this.guests.length-1;i>=0;i--){
        const g=this.guests[i];
        g.update(sdt);
        if(g.done){
          if(g.joy>85) this.ratingParts.joy=Math.min(1,(this.ratingParts.joy==null?0.6:this.ratingParts.joy)+0.01);
          if(g.joy<28) this.ratingParts.joy=Math.max(0.1,(this.ratingParts.joy==null?0.5:this.ratingParts.joy)-0.02);
          this.guests.splice(i,1);
        }
      }
      for(const s of this.staff) s.update(sdt,this);
      this.particles.update(sdt);
      // rating throttle
      this._rateT=(this._rateT||0)+dt;
      if(this._rateT>1){ this._rateT=0; this.recomputeRating(); }
      // missions
      this._misT=(this._misT||0)+dt;
      if(this._misT>0.8){
        this._misT=0;
        this.checkMission();
      }
    }
  }
  coinFloat(x,y){ this.particles.coins(x,y); }
  coinBurst(x,y){ this.particles.coins(x,y); }
  puffHeart(x,y){ this.particles.hearts(x,y); }

  newDay(){
    this.day++;
    let wages=0;
    wages += this.staff.filter(s=>s.role==='keeper').length*70;
    wages += this.staff.filter(s=>s.role==='vet').length*90;
    let upkeep=0;
    for(const b of this.world.buildings) upkeep+=Math.round(D.BUILDING[b.type].cost*0.006);
    let animalUpkeep=0;
    for(const a of this.animals) animalUpkeep+=D.SPECIES[a.species].upkeep;
    const total=wages+upkeep+animalUpkeep;
    this.money-=total;
    this.stats.expensesTotal+=total;
    const profit=this.stats.revenueToday-total;
    this.notify('Day '+this.day+' — wages '+WK.fmtMoney(wages)+' · upkeep '+WK.fmtMoney(upkeep+animalUpkeep)+(profit>=0?' · profit '+WK.fmtMoney(profit):' · loss '+WK.fmtMoney(-profit)), profit>=0?'good':'bad');
    this.stats.revenueToday=0;
    this.stats.guestsToday=0;
    this.sfx(profit>=0?'coin':'error');
  }

  /* ---------------- missions & research ---------------- */
  checkMission(){
    const m=D.MISSIONS[this.missionIdx];
    if(!m) return;
    if(m.check(this)){
      let msg='Mission complete: '+m.label+'!';
      if(typeof m.reward==='number'){ this.money+=m.reward; msg+=' +'+WK.fmtMoney(m.reward); }
      else{
        if(m.reward.cash){ this.money+=m.reward.cash; msg+=' +'+WK.fmtMoney(m.reward.cash); }
        if(m.reward.rp){ this.rp+=m.reward.rp; msg+=' +'+m.reward.rp+' RP'; }
      }
      this.notify(msg,'gold');
      this.sfx('unlock');
      this.missionIdx++;
    }
  }
  researchCost(){ 
    const next=D.RESEARCH_TIERS.find(t=>t.tier===this.researchTier+1);
    return next?next.cost:null;
  }
  buyResearch(){
    const cost=this.researchCost();
    if(cost==null) return false;
    if(this.rp<cost){ this.notify('Need '+Math.ceil(cost-this.rp)+' more RP.','bad'); this.sfx('error'); return false; }
    this.rp-=cost;
    this.researchTier++;
    this.notify('Research unlocked: '+D.RESEARCH_TIERS.find(t=>t.tier===this.researchTier).label+'!','gold');
    this.sfx('unlock');
    return true;
  }

  /* ---------------- persistence ---------------- */
  save(){
    try{
      const data={
        v:1, money:this.money, rp:this.rp, researchTier:this.researchTier,
        day:this.day, minutes:this.minutes, missionIdx:this.missionIdx,
        stats:this.stats, ratingJoy:this.ratingParts.joy,
        world:this.world.serialize(),
        animals:this.animals.map(a=>({s:a.species,n:a.name,x:a.x,y:a.y,h:a.hunger,p:a.happiness,k:a.sick,r:a.regionId})),
        staff:this.staff.map(s=>({r:s.role,x:s.x,y:s.y})),
      };
      localStorage.setItem(SAVE_KEY,JSON.stringify(data));
      return true;
    }catch(e){ return false; }
  }
  static hasSave(){ try{ return !!localStorage.getItem(SAVE_KEY); }catch(e){ return false; } }
  static load(){
    try{
      const raw=localStorage.getItem(SAVE_KEY);
      if(!raw) return null;
      const d=JSON.parse(raw);
      const g=new Game();
      g.world.deserialize(d.world);
      g.money=d.money; g.rp=d.rp; g.researchTier=d.researchTier;
      g.day=d.day; g.minutes=d.minutes; g.missionIdx=d.missionIdx;
      Object.assign(g.stats,d.stats||{});
      g.ratingParts.joy=d.ratingJoy;
      for(const a of d.animals||[]){
        const an=new WK.Animal(a.s,a.x,a.y,a.r||0);
        an.name=a.n; an.hunger=a.h; an.happiness=a.p; an.sick=!!a.k;
        g.animals.push(an);
      }
      for(const s of d.staff||[]) g.staff.push(new WK.Staff(s.r,s.x,s.y));
      g.recomputeRating();
      g.rebuildPois();
      return g;
    }catch(e){ return null; }
  }
  static clearSave(){ try{ localStorage.removeItem(SAVE_KEY); }catch(e){} }
}

/* starter zoo: cozy beginning — entrance, plaza path, some greenery */
Game.buildStarter = function(game){
  const w=game.world;
  const cx=(w.cols/2)|0, sy=w.rows-6;
  const b=w.placeBuilding('entrance_s',cx-1,sy);
  // plaza
  for(let x=cx-4;x<=cx+5;x++){
    w.setGround(x,sy+1,'grass'); w.setPath(x,sy+1,'gravel');
    w.setGround(x,sy+2,'grass'); w.setPath(x,sy+2,'gravel');
  }
  for(let y=sy-8;y<=sy+2;y++){ w.setGround(cx+2,y,'grass'); w.setPath(cx+2,y,'gravel'); }
  // greenery
  const deco=[[cx-6,sy-2,'tree'],[cx+7,sy-1,'tree'],[cx-5,sy+2,'bush'],[cx+6,sy+2,'bush'],
              [cx-3,sy-4,'flower'],[cx+3,sy-4,'flower'],[cx-8,sy,'tree'],[cx+9,sy,'tree'],
              [cx-7,sy+3,'bench'],[cx+8,sy+3,'bench'],[cx-2,sy-5,'rock']];
  for(const [x,y,o] of deco){ if(w.inBounds(x,y)&&w.occ[w.idx(x,y)]<0) w.setObj(x,y,o,(Math.random()*3)|0); }
  game.refreshPois();
  return b;
};

/* demo zoo for the menu hero scene */
Game.buildDemo = function(game){
  const w=game.world;
  const C=w.cols, Rws=w.rows;
  const cx=(C/2)|0;
  const rnd=WK.mulberry32(20240001);
  // grand entrance south
  w.placeBuilding('entrance_g',cx-2,Rws-5);
  for(let x=cx-6;x<=cx+7;x++){ w.setPath(x,Rws-4,'stonep'); w.setPath(x,Rws-3,'stonep'); }
  // ring promenade
  const yN=8, yS=Rws-6, xW=6, xE=C-7;
  for(let x=xW;x<=xE;x++){ w.setPath(x,yN,'stonep'); w.setPath(x,yS,'stonep'); }
  for(let y=yN;y<=yS;y++){ w.setPath(xW,y,'stonep'); w.setPath(xE,y,'stonep'); }
  for(let y=yS;y<=Rws-3;y++) w.setPath(cx,y,'stonep');
  // helper: rectangular fenced habitat with terrain + animals + gate on given side path
  function habitat(x0,y0,x1,y1,grounds,species,waterRect){
    for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++) w.setGround(x,y,grounds[(rnd()*grounds.length)|0]);
    if(waterRect){
      for(let y=waterRect[1];y<=waterRect[3];y++)for(let x=waterRect[0];x<=waterRect[2];x++) w.setGround(x,y, waterRect[4]||'water');
    }
    for(let x=x0;x<=x1;x++){ w.setFence(x,y0-1,'S',1); w.setFence(x,y1,'S',1); }
    for(let y=y0;y<=y1;y++){ w.setFence(x0-1,y,'E',1); w.setFence(x1,y,'E',1); }
    // gate on south side middle connecting to nearest path below
    const gx=((x0+x1)/2)|0;
    w.setFence(gx,y1,'S',3);
    const rid=w.regionOf(((x0+x1)/2)|0,((y0+y1)/2)|0);
    w.computeRegions();
    for(const sp of species){
      const t=w.randomRegionTile(rid,D.SPECIES[sp],rnd)||[x0+1,y0+1];
      const a=new WK.Animal(sp,t[0],t[1],rid);
      game.animals.push(a);
    }
    // interior decor
    for(let i=0;i<Math.max(2,((x1-x0)*(y1-y0))/18|0);i++){
      const rx=x0+1+((rnd()*(x1-x0-1))|0), ry=y0+1+((rnd()*(y1-y0-1))|0);
      const o=grounds.includes('snow')?'rock':(grounds.includes('sand')?'palm':(species.includes('panda')?'bamboo':'tree'));
      if(!w.path[w.idx(rx,ry)]) w.setObj(rx,ry,o,0);
    }
  }
  // savanna center-left: lions+zebra+giraffe
  habitat(9,11,17,17,['grass','grass','sand'],['lion','zebra','giraffe']);
  // elephants right of it
  habitat(19,11,27,17,['grass','dirt'],['elephant','elephant']);
  // arctic NW corner
  habitat(9,20,16,26,['snow','ice'],['penguin','penguin','polarbear'],[11,22,14,25,'water']);
  // wetland SW
  habitat(19,21,26,27,['sand','grass'],['flamingo','flamingo'],[21,23,24,25,'water']);
  // jungle SE quadrant
  habitat(29,20,38,28,['dirt','grass'],['tiger','gorilla','monkey']);
  for(let i=0;i<8;i++){ const rx=30+((rnd()*8)|0), ry=21+((rnd()*7)|0); w.setObj(rx,ry,'bamboo',0); }
  // panda grove NE
  habitat(29,10,36,16,['grass','dirt'],['panda','panda']);
  for(let i=0;i<6;i++){ const rx=30+((rnd()*6)|0), ry=11+((rnd()*5)|0); w.setObj(rx,ry,'bamboo',0); }
  // croc lagoon far east
  habitat(39,12,C-2,18,['sand'],['crocodile'],[40,14,C-3,17,'deep']);

  w.computeRegions();

  // facilities along paths
  const put=(t,x,y)=>{ w.canPlaceBuilding(x,y,D.BUILDING[t].w,D.BUILDING[t].h)&&w.placeBuilding(t,x,y); };
  put('aquarium',31,29); put('restaurant',8,29); put('gift',13,29); put('icecream',18,30);
  put('burger',24,30); put('restroom',28,29); put('restroom',7,19); put('aviary',40,20);
  put('reptile',40,29); put('visitor',20,4); put('research',27,3); put('vet',13,3); put('hut',37,3);
  // street life
  for(let i=0;i<40;i++){
    const x=xW+((rnd()*(xE-xW))|0), y=rnd()<0.5?yN:yS;
    const o=['tree','bush','flower','lamp','bench'][(rnd()*5)|0];
    if(w.inBounds(x,y)&&!w.path[w.idx(x,y)]) w.setObj(x,y,o,(rnd()*3)|0);
  }
  for(let i=0;i<26;i++){
    const ang=rnd()*Math.PI*2, rr=6+rnd()*14;
    const x=cx+Math.cos(ang)*rr|0, y=((yN+yS)/2+Math.sin(ang)*rr*0.7)|0;
    if(w.inBounds(x,y)&&w.occ[w.idx(x,y)]<0&&!w.path[w.idx(x,y)]) w.setObj(x,y,['tree','tree','bush','flower'][(rnd()*4)|0],(rnd()*3)|0);
  }
  w.computeRegions();
  game.rebuildPois();
  game.recomputeRating();
};

Game.SAVE_KEY=SAVE_KEY;
WK.Game = Game;
})();
