

world_elements <- {}


class WorldElement
{
  topo=null
  geo=null
  tags=null
  weight=10

  constructor(params)
  {
    if (params) foreach (n,v in params)
      if (n in this) this[n]=v
  }

  function addToWGen(wg, prefix)
  {
    if (topo) wg.addElem(topo, prefix+"Topo")

    if (geo)
    {
      wg.addGeo(geo)
      wg.addElem(geo, prefix+"Geo")
    }

    wg.addElem(this, prefix+"WE")
  }

  function placeObjects() {}

  function _newmember(index, value, attrs, isstatic)
  {
    if (index=="tags")
    {
      if (typeof value=="string") value=split(value, " \t\r\n")
      if (typeof value!="array") value=[value]

      this.tags<-{}
      foreach (t in value)
      {
        this.tags[t]<-true
        if (!(t in world_elements)) world_elements[t]<-[]
        world_elements[t].append(this)
      }
    }
    else
      this[index]<-value
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


function check_world_elem_match(e, tags)
{
  local matched=0

  foreach (n,t in tags)
  {
    if (t=="required")
    {
      if (!(n in e.tags)) return 0;
      ++matched
    }
    else if (t=="negative")
    {
      if (n in e.tags) return 0;
    }
    else
    {
      if (n in e.tags) ++matched
    }
  }

  return matched;
}


function gather_world_elem_tags(a, tags)
{
  if (a==null) return;

  if (typeof a=="string")
  {
    a=split(a, " \t\r\n")
    if (a.len()==1)
    {
      a=a[0]
      if (a[0]=='!')
      {
        local n=a.slice(1)
        if (n in tags && tags[n]!="optional") return;
        tags[n]<-"required"
      }
      else if (a[0]=='-')
      {
        local n=a.slice(1)
        if (n in tags && tags[n]!="optional") return;
        tags[n]<-"negative"
      }
      else tags[a]<-"optional"
      return;
    }
  }

  if (typeof a=="array")
  {
    foreach (s in a) gather_world_elem_tags(s, tags)
  }
  else
  {
    foreach (n,t in a)
    {
      if (n in tags)
      {
        if (tags[n]=="optional" && t!="optional") tags[n]=t
      }
      else tags[n]<-t
    }
  }
}


function _WELEMS(...)
{
  // gather tags from params
  local tags={}
  gather_world_elem_tags(vargv, tags)

  // gather elements by tag, count max matched tags
  local elems=[]
  local maxMatched=0

  foreach (name,tp in tags)
  {
    if (!(name in world_elements))
    {
      if (tp=="required") break;
      continue;
    }

    foreach (e in world_elements[name])
    {
      if (e.weight<=0) continue;

      local ok=true
      foreach (el in elems)
        if (el==e) { ok=false; break; }
      if (!ok) continue;

      local matched=check_world_elem_match(e, tags)
      if (matched<=0 || matched<maxMatched) continue;

      if (matched>maxMatched)
      {
        maxMatched=matched
        elems.clear()
      }

      elems.append(e)
    }
  }

  return elems;
}


function _WELEM(...)
{
  local tags={}
  gather_world_elem_tags(vargv, tags)

  local elems=_WELEMS(tags)

  if (0)
  {
    local s=""
    foreach (n,t in tags)
    {
      if (s!="") s+=" "
      if (t=="required") s+="!"
      else if (t=="negative") s+="-"
      else if (t=="optional") {}
      else s+="<"+t+">"
      s+=n
    }
    print(format("!!! request \"%s\"\n", s));
    print("found:");
    foreach (e in elems) print(" "+get_global_name(e));
    print("\n");
  }

  // fail if found nothing
  if (elems.len()<1)
  {
    local s=""
    foreach (n,t in tags)
    {
      if (s!="") s+=" "
      if (t=="required") s+="!"
      else if (t=="negative") s+="-"
      else if (t=="optional") {}
      else s+="<"+t+">"
      s+=n
    }
    print(format("Error: no world elements for request \"%s\"\n", s));
    return null;
  }

  // select random element, respecting weights
  local sum=0
  foreach (e in elems) sum+=e.weight

  local r=frnd()*sum
  local a=0
  foreach (e in elems)
  {
    a+=e.weight
    if (r<=a) return e;
  }
  return elems[elems.len()-1];
}


function WELEM(...)
{
  return _WELEM(vargv, wgen.worldTags);
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


function create_stone_circle(cx, cz, rad)
{
  local cpos=vec4f(cx, 0, cz, 1)
  local num=(rad*(1.5+srnd()*0.1)).tointeger()
  local ofs=frnd()
  for (local i=0; i<num; ++i)
  {
    local p=cpos+make_dir_xz((i+srnd()*0.2)/num+ofs)*(rad*(1+srnd()*0.04))
    Stone1StaticObject(getx(p), getz(p)).makeSpecial()
  }
}


class StoneCircle extends WorldElement
{
  tags="PlayerTrigger Circle stone"

  rad=null
  avgh=0
  onEvent=null
  offEvent=null

  sensor=null

  constructor(params=null)
  {
    base.constructor(params)

    if (rad==null) rad=rndf(5, 15)

    topo=TopoCircle(rad*1.2)
    geo=GeoCircle(topo, rad*1.2, { avgh=avgh, priority=1000 })
  }

  function placeObjects()
  {
    create_stone_circle(geo.posX(), geo.posZ(), rad)
    if (rad>7) create_stone_circle(geo.posX(), geo.posZ(), rad-3)

    sensor=Sensor(vec4f(geo.posX(), avgh, geo.posZ(), 1), rad)
    sensor.setOnEvent(onEvent)
    sensor.setOffEvent(offEvent)

    mark_circle(geo.posX(), geo.posZ(), rad, MARK_PLACE, 0)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class PlayerStart extends WorldElement
{
  tags="PlayerStart"

  rad=null
  avgh=0

  constructor(params=null)
  {
    base.constructor(params)

    if (rad==null) rad=rndf(5, 10)

    topo=TopoCircle(rad)
    geo=GeoCircle(topo, rad, { avgh=avgh, priority=1000 })
  }

  function placeObjects()
  {
    set_player_pos(vec4f(geo.posX(), 0, geo.posZ(), 1))
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class StoneCupCircle extends WorldElement
{
  tags="MiniPuzzle Circle stone"

  rad=null
  number=null
  avgh=0
  event=null
  once=true

  trigger=null

  constructor(params=null)
  {
    base.constructor(params)

    if (rad==null) rad=rndf(8, 15)

    if (number==null)
    {
      number=(rad*(0.4+srnd()*0.05)).tointeger()
      if (number<3) number=3
    }

    topo=TopoCircle(rad*1.2)
    geo=GeoCircle(topo, rad*1.2, { avgh=avgh, priority=1000 })
  }

  function placeObjects()
  {
    local objs=[]

    local cpos=vec4f(geo.posX(), avgh, geo.posZ(), 1)
    local ofs=frnd()
    for (local i=0; i<number; ++i)
    {
      local p = cpos + make_dir_xz((i+srnd()*0.1)/number+ofs) * (rad*(0.75+srnd()*0.04));
      local o=StoneCup1Object(getx(p), getz(p))
      o.makeSpecial()
      objs.append(o)
    }

    trigger=StoneCupCircleTrigger(objs, event, { once=once })

    mark_circle(geo.posX(), geo.posZ(), rad, MARK_PLACE, 0)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class TreeCircle extends WorldElement
{
  tags="MiniPuzzle Circle tree"

  rad=null
  number=null
  avgh=0
  event=null
  once=true

  trigger=null

  constructor(params=null)
  {
    base.constructor(params)

    if (rad==null) rad=rndf(15, 20)

    if (number==null)
    {
      number=(rad*(0.4+srnd()*0.05)).tointeger()
      if (number<3) number=3
    }

    topo=TopoCircle(rad*1.2)
    geo=GeoCircle(topo, rad*1.2, { avgh=avgh, priority=1000 })
  }

  function placeObjects()
  {
    local objs=[]

    local cpos=vec4f(geo.posX(), avgh, geo.posZ(), 1)
    local ofs=frnd()
    for (local i=0; i<number; ++i)
    {
      local p = cpos + make_dir_xz((i+srnd()*0.1)/number+ofs) * (rad*(0.75+srnd()*0.04));
      local o=TreeObject(getx(p), getz(p),
        get_pal_color(cur_world_pal, "leaves"),
        get_pal_color(cur_world_pal, "trunk"))
      o.setFoliage(false)
      o.makeSpecial()
      objs.append(o)
    }

    trigger=TreeCircleTrigger(objs, event, { once=once })

    mark_circle(geo.posX(), geo.posZ(), rad, MARK_PLACE, 0)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class MenhirCircle extends WorldElement
{
  tags="PlayerTrigger Circle stone megalith"

  rad=null
  number=null
  avgh=0
  onEvent=null
  offEvent=null

  sensor=null

  constructor(params=null)
  {
    base.constructor(params)

    if (rad==null) rad=rndf(8, 20)

    if (number==null)
    {
      number=(rad*(0.5+srnd()*0.1)).tointeger()
      if (number<3) number=3
    }

    topo=TopoCircle(rad*1.2)
    geo=GeoCircle(topo, rad*1.2, { avgh=avgh, priority=1000 })
  }

  function placeObjects()
  {
    local cpos=vec4f(geo.posX(), avgh, geo.posZ(), 1)
    local ofs=frnd()
    for (local i=0; i<number; ++i)
    {
      local p = cpos + make_dir_xz((i+srnd()*0.1)/number+ofs) * (rad*(1+srnd()*0.04));
      (frnd()<0.5?Menhir1StaticObject:Menhir2StaticObject)(getx(p), getz(p)).makeSpecial()
    }

    sensor=Sensor(cpos, rad)
    sensor.setOnEvent(onEvent)
    sensor.setOffEvent(offEvent)

    mark_circle(geo.posX(), geo.posZ(), rad, MARK_PLACE, 0)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class SingleMenhir extends WorldElement
{
  tags="PlayerTrigger stone megalith"
  weight=3

  rad=null
  avgh=0
  onEvent=null
  offEvent=null

  sensor=null

  constructor(params=null)
  {
    base.constructor(params)

    if (rad==null) rad=rndf(5, 10)

    topo=TopoCircle(rad)
    geo=GeoCircle(topo, rad, { avgh=avgh, priority=1000 })
  }

  function placeObjects()
  {
    local cpos=vec4f(geo.posX(), avgh, geo.posZ(), 1);
    (frnd()<0.5?Menhir1StaticObject:Menhir2StaticObject)(getx(cpos), getz(cpos)).makeSpecial()

    sensor=Sensor(cpos, rad)
    sensor.setOnEvent(onEvent)
    sensor.setOffEvent(offEvent)

    mark_circle(geo.posX(), geo.posZ(), rad, MARK_PLACE, 0)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class BonusMana extends WorldElement
{
  tags="Bonus"

  event=null
  targetTopo=null
  amount=2

  listener=null

  function placeObjects()
  {
    listener=SpawnManaListener(event, { amount=amount, topo=targetTopo })
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class BonusSpell extends WorldElement
{
  tags="Bonus BonusItem"

  event=null
  targetTopo=null
  spellId=null

  listener=null

  function placeObjects()
  {
    if (spellId==null)
    {
      local list=[ID_MeteorSpell, ID_WaterSourceSpell, ID_FaultSpell]
      if (DEMO) list.extend([ID_HillSpell, ID_PitSpell])
      else list.extend([ID_HealingSpell, ID_LightningSpell, ID_ThunderstormSpell,
        ID_GuardTowerSpell, ID_VolcanoSpell])

      local filtered=[]
      foreach (s in list) if (!player_has_spell(s)) filtered.append(s)

      if (filtered.len()==0) filtered=list

      spellId=filtered[irnd()%filtered.len()]
    }

    listener=SpawnSpellListener(event, spellId, { topo=targetTopo })
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class BonusPlace extends WorldElement
{
  tags="BonusPlace"

  event=null
  trigger=null
  bonus=null

  constructor(params=null)
  {
    base.constructor(params)

    bonus=WELEM("!Bonus")

    trigger=WELEM("PlayerTrigger MiniPuzzle", "BonusItem" in bonus.tags ? "Circle":null)
      ({ event=event, onEvent=event })

    bonus=bonus({ targetTopo=trigger.topo, event=event })
  }

  function addToWGen(wg, prefix)
  {
    trigger.addToWGen(wg, prefix)
    bonus  .addToWGen(wg, prefix)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class ObjectsFiller extends WorldElement
{
  tags="Filler"

  targetTopo=null
  targetMaskName="mask"

  function placeObjects()
  {
    local mask=null
    if (targetTopo) mask=targetTopo[targetMaskName].shiftedCopy(targetTopo.ox, targetTopo.oz)

    if (mask==null) return;

    local area=mask.area()
    local num=round(area/200.0)
    if (num<=0) return;

    // print(format("!!! area %d, about to place %d objects\n", area, num))

    mask.placePoints(num, function(x, z)
    {
      if (frnd()<0.5)
      {
        local o=TreeObject(x, z,
          get_pal_color(cur_world_pal, "leaves"),
          get_pal_color(cur_world_pal, "trunk"))
      }
      else
      {
        Stone1StaticObject(x, z)
      }
    })
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class ForestArea extends WorldElement
{
  tags="Area tree"

  constructor(params=null)
  {
    base.constructor(params)

    topo=TopoIsland(TopoCircle(rndf(10, 30)), { extraSteps=15 })
  }

  function placeObjects()
  {
    local mask=topo.mask.shiftedCopy(topo.ox, topo.oz)

    for (local iter=4; iter>0; --iter) mask=mask.erode()

    local area=mask.area()
    local num=round(area/100.0)
    if (num<=0) return;

    mask.placePoints(num, function(x, z)
    {
      local o=TreeObject(x, z,
        get_pal_color(cur_world_pal, "leaves"),
        get_pal_color(cur_world_pal, "trunk"))
      o.setFoliage(true)
    })
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class StoneArea extends WorldElement
{
  tags="Area stone"

  constructor(params=null)
  {
    base.constructor(params)

    topo=TopoIsland(TopoCircle(rndf(10, 30)), { extraSteps=15 })
  }

  function placeObjects()
  {
    local mask=topo.mask.shiftedCopy(topo.ox, topo.oz)

    for (local iter=4; iter>0; --iter) mask=mask.erode()

    local area=mask.area()
    local num=round(area/30.0)
    if (num<=0) return;

    mask.placePoints(num, function(x, z)
    {
      Stone1StaticObject(x+srnd()*0.5, z+srnd()*0.5)
    })
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class BarrenArea extends WorldElement
{
  tags="Area barren"

  constructor(params=null)
  {
    base.constructor(params)

    topo=TopoIsland(TopoCircle(rndf(10, 30)), { extraSteps=15 })
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class SpawnMonstersInArea extends WorldElement
{
  tags="MonsterSpawner"

  event=null

  trigger=null
  area=null
  listener=null

  constructor(params=null)
  {
    base.constructor(params)

    trigger=WELEM("!PlayerTrigger")({ onEvent=event })
    area=WELEM("!Area")()
  }

  function addToWGen(wg, prefix)
  {
    trigger.addToWGen(wg, prefix)
    area   .addToWGen(wg, prefix)
    wg.addElem(this, prefix+"WE")
  }

  function placeObjects()
  {
    listener=get_monster_spawn_listener()(event, { topo=area.topo })
    TriggerSoundListener(event)
  }
}


//ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ//


class SpawnMonstersAround extends WorldElement
{
  tags="MonsterSpawner"

  event=null
  extraSteps=10

  trigger=null
  listener=null

  constructor(params=null)
  {
    base.constructor(params)

    trigger=WELEM("!PlayerTrigger")({ onEvent=event })

    topo=TopoIsland(trigger.topo, { extraSteps=extraSteps })
    geo=trigger.geo
  }

  function placeObjects()
  {
    trigger.placeObjects()
    listener=get_monster_spawn_listener()(event, { topo=topo, maskName="maskAround" })
    TriggerSoundListener(event)
  }
}
