OverWorld ∅ Source

overworld.coffee
$ = document
 
###
substitute = ->
  source = arguments.shift()
  throw new TypeError "substitute called on null or undefined" if not source?
  replace = /%%/g
  dest = []
  tail = 0
  for arg in arguments
    break if (match = replace.exec source) is null
    dest.push source.substring(tail,match.index), arg
    tail = replace.lastIndex
  dest.join("") + source.substring(tail)
###
 
#random = Math.random
randomseed = (seed) ->
  m = Math.pow(2,48)
  x = (25214903917 * seed + 11) % m
  ->
    x = (25214903917 * x + 11) % m
    x / m
random = randomseed(Math.random()*Math.pow(2,48))
Rand = (x,y=0) -> y+Math.floor(random()*x)
RandF = (x=1) -> random()*x
Chance = (x) -> random()<x
Choose = (x) -> x[Math.floor(random()*x.length)]
 
class DieRoll
  constructor: (@dice,@sides,@add=0) ->
 
  roll: ->
    res = @add
    res += Rand(@sides,1) for [1..@dice]
    res
 
  describe: ->
    "#{@dice}d#{@sides}+#{@add}"
 
  thaco: ->
    Math.floor(100.0 * (@sides-(40-@add)) / @sides)
 
  inc_dice: (inc=1) -> @dice += inc
  inc_flat: (inc=1) -> @add += inc
 
 
class Terrain
  can_walk: true
  can_climb: false
  can_swim: false
  can_fly: true
  guard_chance: 0
  speed_cost: 1
  world_freq: 0
  heal_cost: 0
  type: "terr"
 
class ClearTerrain extends Terrain
 
class PlainTerrain extends ClearTerrain
  world_freq: 0.7
  type: "p"
 
class ForestTerrain extends ClearTerrain
  can_fly: false
  speed_cost: 43
  world_freq: 0.004
  type: "f"
 
class MountainTerrain extends Terrain
  can_walk: false
  can_climb: true
  world_freq: 0.055
  type: "m"
 
class WaterTerrain extends Terrain
  can_walk: false
  can_swim: true
  world_freq: 0.04
  type: "w"
 
class NamedTerrain extends Terrain
  constructor: ->
    @name = ""
 
class VillageTerrain extends NamedTerrain
  world_freq: 0.008
  guard_chance: 0.2
  heal_cost: 15
  type: "v"
 
class CityTerrain extends NamedTerrain
  world_freq: 0.004
  guard_chance: 0.5
  heal_cost: 5
  type: "c"
 
class RoadTerrain extends Terrain
  world_freq: 0.024
  type: "r"
 
class ShrineTerrain extends Terrain
  guard_chance: 99
  type: "s"
 
class CampTerrain extends Terrain
  constructor: (@monster) ->
 
  value: ->
    level = @monster::stat.level
    15+3*level*level
 
  state: (level) ->
    lvl_delta = level - 2*@monster::stat.level
    targ_cnt = Math.min(8, 3 + (@monster::stat.level+lvl_delta)//2)
    camp_state = switch
        when level > 4 + 3*@monster::stat.level then "dead"
        when lvl_delta > 0 then "charge"
        when level <= 2 then "asleep"
        else "awake"
    [camp_state, targ_cnt]
 
  chance: (cnt) ->
    lvl_delta = level - 2*@monster::stat.level
    targ_cnt = Math.min(8, 3 + (@monster::state.level+lvl_delta)//2)
    return false if cnt>=targ_cnt
    Chance 0.3+0.04*(targ_cnt-cnt)+0.06*lvl_delta
 
  type: "b"
 
class TempleTerrain extends Terrain
  type: "t"
 
 
class Loot
  type: "loot"
  guard_chance: 0.75
 
class GoldLoot extends Loot
  value: 0
 
class LittleLoot extends GoldLoot
  value: 1
  guard_chance: 0.15
  type: "g"
 
class LotsLoot extends GoldLoot
  value: 6
  type: "l"
 
class TonsLoot extends GoldLoot
  value: 17
  type: "t"
 
class EquipmentLoot extends Loot
  add_stat: (stat) ->
  sub_stat: (stat) ->
 
class ExoticLoot extends EquipmentLoot
  type: "e"
  add_stat: (stat) -> stat.armor += 10
  sub_stat: (stat) -> stat.armor -= 10
 
class HODLoot extends EquipmentLoot
  type: "d"
  add_stat: (stat) -> stat.damage.inc_flat 10
  sub_stat: (stat) ->stat.damage.inc_flat -10
 
class ShieldLoot extends EquipmentLoot
  type: "s"
  add_stat: (stat) -> stat.armor += 4
  sub_stat: (stat) -> stat.armor -= 4
 
class MasamuneLoot extends EquipmentLoot
  type: "m"
  add_stat: (stat) -> stat.damage.inc_flat 4
  sub_stat: (stat) ->stat.damage.inc_flat -4
 
class PotionLoot extends Loot
  type: "p"
 
 
class Treasure
  loot: [LittleLoot,LotsLoot,TonsLoot,ExoticLoot,HODLoot,ShieldLoot,MasamuneLoot,PotionLoot]
 
  gen: ->
    sum = 0
    val = RandF()
    for prob,i in @chance
      return @loot[i] if val<(sum += prob)
    null
 
class NoTreasure extends Treasure
  chance: [0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00]
 
class LameTreasure extends Treasure
  chance: [0.78,0.10,0.00,0.00,0.00,0.01,0.01,0.10]
 
class ModerateTreasure extends Treasure
  chance: [0.10,0.40,0.20,0.00,0.00,0.05,0.05,0.20]
 
class RichTreasure extends Treasure
  chance: [0.00,0.30,0.50,0.00,0.00,0.05,0.05,0.10]
 
class ArtifactTreasure extends Treasure
  chance: [0.00,0.00,0.00,0.00,0.10,0.30,0.30,0.30]
 
class SpawnedTreasure extends Treasure
  chance: [7/16,1/2,1/16,0,0,0,0,0]
 
class StartingTreasure extends Treasure
  chance: [7/8,15/128,1/128,0,0,0,0,0]
 
 
class Monster
  stat:
    tohit: new DieRoll(1,50,50)
    hitpoints: new DieRoll(9,10,10)
    damage: new DieRoll(9,10,10)
    armor: 0
    xp_val: 0
    level: 0
    view_radsq: 0
    treasure_chance: 0.0
    treasure_type: NoTreasure
    #is_wander: false
    #is_guard: false
    mobility: ["walk"]
 
  constructor: ->
    @hp = @stat.hitpoints.roll()
 
  destroy: (manager,world,player) ->
 
  can_move: (terr) ->
    for flag in @stat.mobility
      return true if terr['can_'+flag]
    false
 
  notify: (monster) ->
    false
 
  Move: (world,targ) ->
    {x:dx,y:dy} = world.Delta @loc,targ
    return true if dx is 0 and dy is 0
    dx = dx/Math.abs(dx) if dx isnt 0
    dy = dy/Math.abs(dy) if dy isnt 0
    if not world.monst_at(@loc.x+dx,@loc.y+dy) and @can_move world.terr_at(@loc.x+dx,@loc.y+dy)
      world.put_monst @,@loc.x+dx,@loc.y+dy
    else if dx isnt 0 and not world.monst_at(@loc.x,@loc.y+dy) and @can_move world.terr_at(@loc.x,@loc.y+dy)
      world.put_monst @,@loc.x,@loc.y+dy
    else if dy isnt 0 and not world.monst_at(@loc.x+dx,@loc.y) and @can_move world.terr_at(@loc.x+dx,@loc.y)
      world.put_monst @,@loc.x+dx,@loc.y
    else
      return false
    return true
 
  Combat: (monster) ->
    return "missed" if monster.Defend @
    damage = @Attack monster
    return "nodamage" unless damage > 0
    monster.Damage @,damage
 
  Defend: (monster) ->
    40 > (monster.stat.tohit.roll() - @stat.armor//2)
 
  Attack: (monster) ->
    @stat.damage.roll() - monster.stat.armor
 
  Damage: (monster,amount) ->
    return @Kill monster if amount >= @hp
    @hp -= amount
    amount
 
  Kill: (monster) ->
    @hp = 0
    monster.GiveXP @stat.xp_val if monster instanceof Player
    "itdied"
 
  Run: ->
 
  type: "monster"
 
class Player extends Monster
  type: "player"
  constructor: ->
    @level = 1
    @gp = @xp = 0
    @hp = @max_hp = Rand(6,5)
    @heal_timer = 0
    @shrine_time = 0
    @quest = null
    @questcount = 0
    @potions = 0
    @equipment = {}
    @exotic_loc = null
    @turns = 0
    @casting = false
    @stat = {   # overrides prototype
        damage: new DieRoll(2,4,1)
        tohit: new DieRoll(1,50,15)
        armor: 0
        mobility: ["walk"]
    }
 
  Move: (world,targ) ->
    {x:dx,y:dy} = world.Delta @loc,targ
    return true if targ.x is @loc.x and targ.y is @loc.y
    if not world.monst_at(targ.x,targ.y) and @can_move world.terr_at(targ.x,targ.y)
      world.put_monst @,targ.x,targ.y
      return true
    return false
 
  Kill: (monster) ->
    @hp = 0
    "youdied"
 
  Run: ->
    @turns++
    if @heal_timer != 0
      @heal_timer--
    else if @hp < @max_hp
      @heal_timer = 64
      @Heal 0.05
    return
 
  GiveHP: (hp) ->
    @hp = Math.min(@hp+hp, @max_hp)
 
  Heal: (pct) ->
    @GiveHP Math.max(@max_hp * pct, 1)
 
  CanHeal: ->
      @hp < @max_hp
 
  CanLevel: ->
      @level < @GetLevelFromXP()
 
  GetLevelFromXP: ->
    xp = @xp
    switch
      when xp >= 1404 then (Math.sqrt(216*xp-120935)+437)//54
      when xp >=  280 then (Math.sqrt(136*xp- 17055)+127)//34
      else                 (Math.sqrt( 64*xp+   576)-  8)//16
    #level = 1
    #txp = 16
    #while txp <= @xp
    #  txp += if level>14 then 10*(level-14)+9*(level-6)+8*level+16 else
    #         if level>6  then 9*(level-6)+8*level+16 else
    #                          8*level+16
    #  level++
    #level
 
  GiveLevel: ->
    new_hp = if @level<12 then Rand(6,2) else Rand(4,1)
    @level += 1
    if @max_hp < 222
      new_hp = 1 if @max_hp >= 200
      @max_hp += new_hp
      @hp += new_hp
    if @level <= 5
      @stat.damage.inc_dice()
      @stat.damage.inc_flat()
    else if @level <= 12
      @stat.damage.inc_dice()
    else if @level%2 == 1
      @stat.damage.inc_dice()
    if @level <= 6
      @stat.tohit.inc_dice(2)
      @stat.tohit.inc_flat()
    else if @stat.tohit.add < 30
      @stat.tohit.inc_flat()
    else if @stat.tohit.add < 35 and @level%2 == 1
      @stat.tohit.inc_flat()
    @level
 
  GiveXP: (inc) ->
    old = @GetLevelFromXP()
    @xp += inc
    old < @GetLevelFromXP()
 
  Praying: ->
    turns_needed = if @level > 8 then 16 else @level*2
    if ++@shrine_time > turns_needed
      @shrine_time = 0
      return true
    return false
 
  TakeLoot: (loot) ->
    @gp += loot.value
 
  TakeEquipment: (loot) ->
    return false if @equipment[loot.type]
    if loot instanceof ExoticLoot
      equip.sub_stat @stat for t,equip of @equipment when equip instanceof ShieldLoot
      loot.add_stat @stat
    else if loot instanceof ShieldLoot
      loot.add_stat @stat unless @equipment[ExoticLoot::type]
    else if loot instanceof HODLoot
      equip.sub_stat @stat for t,equip of @equipment when equip instanceof MasamuneLoot
      loot.add_stat @stat
    else if loot instanceof MasamuneLoot
      loot.add_stat @stat unless @equipment[HODLoot::type]
    @equipment[loot.type] = loot
    true
 
  FoundExotic: (loc) ->
    if @level>10 and @exotic_loc and @exotic_loc.x==loc.x and @exotic_loc.y==loc.y
      @exotic_loc = null
      return true
    return false
 
 
class CombatMonster extends Monster
 
  constructor: ->
    @is_attack = false
    @is_flee = false
    @is_guard = false
    @is_cloud = false
    @was_hit = false
    @was_damaged = false
    @ondeath = null
    @hp = @stat.hitpoints.roll()
    @targ = null
 
  Defend: (monster) ->
    @was_hit = true
    super
 
  camp: (x,y,hp_fac=1) ->
    @is_guard = @is_cloud = true
    @hp *= hp_fac
    @targ = x:x,y:y
 
  set_attack: (flag=true) ->
    @is_guard = false if flag
    @is_attack = flag
 
  set_flee: (flag=true) ->
    @is_attack = false if flag
    @is_flee = flag
 
  set_guard: (flag=true) ->
    @is_guard = flag
 
  clear_damage: ->
    @was_damaged = @was_hit = false
 
  notify: (monster) ->
    return false if @is_attack or @is_flee
    notify_chance = 
      if monster.type == @type
        if @is_cloud then 0.95 else 0.90
      else
        if @is_cloud or @is_guard then 0 else 0.60-0.15*(@stat.level-monster.stat.level)
    if Chance notify_chance then @set_attack true else false
 
  Run: (game,world,player) ->
    did_we_enrage = false
    player_delta = world.Delta @loc,player.loc
    player_dist =
      x: Math.abs(player_delta.x)
      y: Math.abs(player_delta.y)
    if @was_hit
      did_we_enrage = world.NotifyNearbyMonsters @,5
      if @is_cloud and @was_damaged and @dist_to_camp(world)>10
        @set_flee true
      else
        @set_attack true
      @clear_damage
    if @is_attack
      if player_dist.x<=1 and player_dist.y<=1
        game.RunCombat @,player
      else if not @is_guard
        if Chance 0.02*(player_dist.x+player_dist.y)-0.01
          @set_attack false
        else
          @Move world,player.loc
    #if @is_flee
    if @is_cloud and not (@is_attack or @is_guard)
      dist = @dist_to_camp world
      ret_chance = 0.10*dist+0.20
      if @is_flee
        if dist>4
          ret_chance = 1.0
        else
          @set_flee false
      @Move world, if Chance ret_chance then @targ else
        x:world.MAPX(@loc.x+Rand(9,-4))
        y:world.MAPY(@loc.y+Rand(9,-4))
    @do_wander world unless @is_attack or @is_flee or @is_guard
    #if @is_guard
    if did_we_enrage
      game.MonsterEnraged @
    else
      @LookFor game,player_delta if not (@is_guard or @is_attack or @is_cloud) and Chance 3/16
 
  dist_to_camp: (world) ->
    delta = world.Delta @loc,@targ
    Math.abs(delta.x)+Math.abs(delta.y)
 
  do_wander: (world) ->
    loc = world.NearbyTreasure(@) ?
      x: world.MAPX(@loc.x+Rand(9,-4))
      y: world.MAPY(@loc.y+Rand(9,-4))
    @Move world,loc
    @set_guard true if Chance (world.treas_at(loc.x,loc.y) ? world.terr_at(loc.x,loc.y)).guard_chance
 
  LookFor: (game,delta) ->
    # inverse square law
    net_rad = delta.x*delta.x+delta.y*delta.y
    if @stat.view_radsq>net_rad and
       Chance 1-net_rad/@stat.view_radsq
        @is_attack = true
        game.MonsterAlerted @
 
class GoblinMonster extends CombatMonster
  type: "gb"
  stat:
    tohit: new DieRoll(1,50,2)
    hitpoints: new DieRoll(1,6,1)
    damage: new DieRoll(1,4,0)
    armor: 0
    xp_val: 3
    level: 1
    view_rad: 8
    treasure_chance: 0.25
    treasure_type: LameTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
class HeadlessMonster extends CombatMonster
  type: "hd"
  stat:
    tohit: new DieRoll(1,50,8)
    hitpoints: new DieRoll(2,6,4)
    damage: new DieRoll(1,8,1)
    armor: 1
    xp_val: 7
    level: 2
    view_radsq: 4*4
    treasure_chance: 0.40
    treasure_type: ModerateTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
class BatMonster extends CombatMonster
  type: "bt"
  stat:
    tohit: new DieRoll(1,50,0)
    hitpoints: new DieRoll(1,4,0)
    damage: new DieRoll(1,3,0)
    armor: 0
    xp_val: 2
    level: 1
    view_radsq: 8*8
    treasure_chance: 0.0
    treasure_type: NoTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["fly"]
 
class PigMonster extends CombatMonster
  type: "pg"
  stat:
    tohit: new DieRoll(1,50,4)
    hitpoints: new DieRoll(4,5,5)
    damage: new DieRoll(2,3,0)
    armor: 4
    xp_val: 9
    level: 2
    view_radsq: 6*6
    treasure_chance: 0.20
    treasure_type: LameTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
class TrollMonster extends CombatMonster
  type: "tr"
  stat:
    tohit: new DieRoll(1,50,17)
    hitpoints: new DieRoll(3,8,4)
    damage: new DieRoll(2,8,2)
    armor: 2
    xp_val: 13
    level: 3
    view_radsq: 10*10
    treasure_chance: 0.50
    treasure_type: ModerateTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
class GhostMonster extends CombatMonster
  type: "gh"
  stat:
    tohit: new DieRoll(1,50,12)
    hitpoints: new DieRoll(2,8,8)
    damage: new DieRoll(3,8,1)
    armor: 0
    xp_val: 15
    level: 4
    view_radsq: 10*10
    treasure_chance: 0.25
    treasure_type: ModerateTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk","fly"]
 
class SquidMonster extends CombatMonster
  type: "sq"
  stat:
    tohit: new DieRoll(1,50,20)
    hitpoints: new DieRoll(2,8,1)
    damage: new DieRoll(3,8,1)
    armor: 1
    xp_val: 9
    level: 2
    view_radsq: 6*6
    treasure_chance: 0.15
    treasure_type: NoTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["swim"]
 
class DemonMonster extends CombatMonster
  type: "dm"
  stat:
    tohit: new DieRoll(1,50,25)
    hitpoints: new DieRoll(5,8,8)
    damage: new DieRoll(4,8,1)
    armor: 3
    xp_val: 28
    level: 6
    view_radsq: 12*12
    treasure_chance: 0.50
    treasure_type: RichTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk","climb"]
 
class DragonMonster extends CombatMonster
 
  do_wander: (world) ->
    loc =
      x: world.MAPX(@loc.x+Rand(9,-4))
      y: world.MAPY(@loc.y+Rand(9,-4))
    @Move world,loc
 
  type: "dg"
  stat:
    tohit: new DieRoll(1,60,30)
    hitpoints: new DieRoll(6,10,10)
    damage: new DieRoll(5,6,4)
    armor: 5
    xp_val: 75
    level: 99
    view_radsq: 18*18
    treasure_chance: 0.75
    treasure_type: ArtifactTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk","fly"]
 
class GazerMonster extends CombatMonster
  type: "gz"
  stat:
    tohit: new DieRoll(1,50,21)
    hitpoints: new DieRoll(6,6,1)
    damage: new DieRoll(4,4,1)
    armor: 2
    xp_val: 21
    level: 5
    view_radsq: 15*15
    treasure_chance: 0.15
    treasure_type: ModerateTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["fly"]
 
class ReaperMonster extends CombatMonster
  type: "rp"
  stat:
    tohit: new DieRoll(1,50,24)
    hitpoints: new DieRoll(5,8,8)
    damage: new DieRoll(3,12,2)
    armor: 4
    xp_val: 25
    level: 5
    view_radsq: 10*10
    treasure_chance: 0.25
    treasure_type: RichTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
class MimicMonster extends CombatMonster
 
  constructor: ->
    super
    @is_guard = true
 
  do_wander: (world) ->
 
  type: "mm"
  stat:
    tohit: new DieRoll(1,50,17)
    hitpoints: new DieRoll(4,8,4)
    damage: new DieRoll(4,3,0)
    armor: 3
    xp_val: 15
    level: 3
    view_radsq: 6*6
    treasure_chance: 0.50
    treasure_type: ModerateTreasure
    #is_wander: false
    #is_guard: true
    mobility: ["walk"]
 
class LurkerMonster extends CombatMonster
  type: "lk"
  stat:
    tohit: new DieRoll(1,50,17)
    hitpoints: new DieRoll(7,8,5)
    damage: new DieRoll(5,3,1)
    armor: 4
    xp_val: 19
    level: 4
    view_radsq: 6*6
    treasure_chance: 0.50
    treasure_type: ModerateTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
class SlimeMonster extends CombatMonster
  type: "sl"
  stat:
    tohit: new DieRoll(1,50,15)
    hitpoints: new DieRoll(1,3,1)
    damage: new DieRoll(1,2,1)
    armor: 0
    xp_val: 2
    level: 1
    view_radsq: 4*4
    treasure_chance: 0.0
    treasure_type: NoTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["walk"]
 
  destroy: (manager,world,player) ->
    if Chance(if player.level>20 then 40 else 20+player.level)
      # spawn two slime nearby... need to find 2 locations... one will be us
      loc = world.FindSurroundingGoodPlace @,@loc.x,@loc.y
      if loc
        world.put_monst_pt(manager.Gen(SlimeMonster),loc).set_attack()
        world.put_monst_pt(manager.Gen(SlimeMonster),@loc).set_attack()
 
class MongbatMonster extends CombatMonster
  type: "mb"
  stat:
    tohit: new DieRoll(1,50,11)
    hitpoints: new DieRoll(2,10,2)
    damage: new DieRoll(3,3,2)
    armor: 1
    xp_val: 9
    level: 3
    view_radsq: 12*12
    treasure_chance: 0.05
    treasure_type: LameTreasure
    #is_wander: true
    #is_guard: false
    mobility: ["fly"]
 
class TalkingMonster extends Monster
 
  Damage: (monster,amount) ->
    # never damage talking monsters, period...
    ""
 
class WisepersonMonster extends TalkingMonster
  type: "wp"
 
class SnickersMonster extends TalkingMonster
  type: "sn"
 
class LBMonster extends TalkingMonster
  type: "lb"
 
class MerchantMonster extends TalkingMonster
  type: "mc"
 
 
class MonsterManager
 
  types: [
      Player
      GoblinMonster
      HeadlessMonster
      BatMonster
      PigMonster
      TrollMonster
      GhostMonster
      SquidMonster
      DemonMonster
      DragonMonster
      GazerMonster
      ReaperMonster
      MimicMonster
      LurkerMonster
      SlimeMonster
      MongbatMonster
      WisepersonMonster
      SnickersMonster
      LBMonster
      MerchantMonster
  ]
 
  constructor: ->
    @monsters = {}
    @count = 0
 
  Remove: (monster,world,player) ->
    idx = @monsters[monster.type]?.indexOf(monster)
    return null if idx is -1
    @monsters[monster.type].splice(idx,1)
    world.pt(monster.loc).monster = null
    if monster instanceof CombatMonster and Chance monster.stat.treasure_chance
      treasure = monster.stat.treasure_type.gen()
      treasure = LotsLoot if player.equipment[treasure::type]
      world.put_treas_pt new treasure,monster.loc
    monster.destroy @,world,player
    @count-- if not (monster instanceof Player)
 
  Gen: (monster) ->
    list = @monsters[monster::type]
    list = @monsters[monster::type] = [] unless list
    list.push m = new monster
    @count++ if monster isnt Player
    m
 
  Count: (monster) ->
    @monster[monster::type].length
 
  Choose: (player) ->
    targ_lvl = player.level//2
    mgen = new DieRoll(6,2,-9+targ_lvl)
    roll = Math.min(Math.max(mgen.roll(),1),6)
    Choose (M for M in @types when M::stat.level is roll)
 
  Chance: ->
    Chance switch
        when @count < 30 then 1
        when @count < 50 then 1/32
        when @count < 100 then 1/64
 
  Camp: (camp,level,world) ->
    [camp_state,targ_cnt] = camp.state(level)
    {x:x, y:y} = camp.loc
    monster = world.monst_at x,y
    if monster is null
      monster = @Gen camp.monster
      monster.camp x,y,2.0
      monster.set_guard false if camp_state is "charge"
      world.put_monst monster,x,y
    else if monster instanceof camp.monster
      m.set_guard false if camp_state is "charge" and Chance 0.5
    return if camp_state is "asleep" or camp_state is "dead"
    cnt = world.CountMonsters camp.monster,x,y,7
    blt = 0
    lx = [-1,-1, 0, 1, 1, 1, 0,-1]
    ly = [ 0,-1,-1,-1, 0, 1, 1, 1]
    for j in [0...8]
      m = world.monst_at(x+lx[j],y+ly[j])
      if m is null
        if @Count camp.monster < 90 and camp.chance cnt+blt
          monster = @Gen camp.monster
          monster.camp world.MAPX(x+lx[j]),world.MAPY(y+ly[j]),1.4
          monster.set_guard false if camp_state is "charge"
          world.put_monst monster,x+lx[j],y+ly[j]
          blt++
      if m instanceof camp.monster
        m.set_guard false if camp_state is "charge" and Chance 0.5
 
  Run: (game,world,player) ->
    for M in @types
      continue unless @monsters[M::type]
      for monster in @monsters[M::type]
        monster.Run game,world,player
 
  Each: (monster, callback) ->
    @monsters[monster::type]?.forEach callback
 
MS_PER_VEL_MOVE = 400
gActions = []
class VelAction
  constructor: (dx,dy,steps) ->
    @delta = x:dx,y:dy
    @frames = steps
    @timeout = if steps? then steps*MS_PER_VEL_MOVE
    gActions.push @
 
  destroy: ->
    idx = gActions.indexOf @
    gActions[@type].splice(idx,1) if idx isnt -1
 
  Run: (frametime) ->
    if @timeout?
      if frametime >= @timeout
        @timeout = 0
        @loc = null
        return true
      next = (@timeout -= frametime)//MS_PER_VEL_MOVE
      if next != @frames
        @frames = next
        return true
    return false
 
 
class MapGenerator
  constructor: (@world) ->
 
  Clump: (terr,rad,xscale) ->
    ang = RandF(Math.PI)
    m0 = Math.cos(ang)
    m1 = Math.sin(ang)
    m2 = -m1
    m3 = m0
    xseed = Rand(@world.cols)
    yseed = Rand(@world.rows)
    for x in [-rad .. rad]
      for y in [-rad .. rad]
        vx = x+RandF()
        vy = y+RandF()
        nx = vx*m0 + vy*m1
        ny = vx*m2 + vy*m3
        nx /= xscale
        @world.put_terr new terr, xseed+x, yseed+y if nx*nx+ny*ny < rad*rad
 
  Circle: (terr,rad) ->
    @Clump terr,rad,1.0
 
  Linear: (terr,rad) ->
    @Clump terr,rad,RandF(1/3)
 
  Random: (terr,rad) ->
    (if Chance(0.5) then @Circle else @Linear).call @,terr,rad 
 
 
class Tile
  #width: 12
  #height: 12
 
class WorldMapTile extends Tile
  constructor: (x,y)->
    @terrain = new PlainTerrain
    @terrain.loc = x:x,y:y
    @treasure = null
    @monster = null
    @action = null
 
class World
  MAPX: (x) -> (x+@cols)%@cols
  MAPY: (y) -> (y+@rows)%@rows
 
  constructor: (@cols, @rows)->
    @map = (new WorldMapTile x,y for y in [1..@rows] for x in [1..@cols])
    @locations = {}
    gen = new MapGenerator @
    gen.Circle ForestTerrain, Rand(5,2) for [1..24]
    @FindAPlaceFor new ForestTerrain for [1..80]
    gen.Linear MountainTerrain, Rand(6,4) for [1..12]
    gen.Random WaterTerrain, Rand(3,3) for [1..3]
    for [1..3]
      {x:x,y:y} = @Find (x,y) => not (@terr_at(x,y) instanceof NamedTerrain)
      dx = Rand(3,-1)
      dy = Rand(3,-1)
      dx = 1 if dx==0 and dy==0
      @NameCity @put_terr(new CityTerrain,x,y), @RandomFantasyName(4,4)
      for [1..Rand(18,9)]
        x = @MAPX(x+dx)
        y = @MAPY(y+dy)
        @put_terr new RoadTerrain,x,y unless @terr_at(x,y) instanceof NamedTerrain
      x = @MAPX(x+dx)
      y = @MAPY(y+dy)
      while @terr_at(x,y) instanceof NamedTerrain
        x = @MAPX(x+dx)
        y = @MAPY(y+dy)
      @NameCity @put_terr(new CityTerrain,x,y), @RandomFantasyName(4,4)
    @NameCity @FindAPlaceFor(new VillageTerrain), @RandomFantasyName(2,3) for [1..8]
    @PlaceShrine MountainTerrain
    @PlaceShrine WaterTerrain
 
  dist_pt_to_pt: (la, lb) ->
    dx = Math.abs(la.x-lb.x)
    dy = Math.abs(la.y-lb.y)
    dx = @cols-dx if dx>@cols/2
    dy = @rows-dy if dy>@rows/2
    {x:dx, y:dy}
 
  at: (x,y) ->
    @map[@MAPX(x)][@MAPY(y)]
 
  pt: (loc) ->
    @at loc.x,loc.y
 
  terr_at: (x,y) ->
    @map[@MAPX(x)][@MAPY(y)].terrain
 
  terr_pt: (loc) ->
    @terr_at loc.x,loc.y
 
  monst_at: (x,y) ->
    @map[@MAPX(x)][@MAPY(y)].monster
 
  treas_at: (x,y) ->
    @map[@MAPX(x)][@MAPY(y)].treasure
 
  put_terr: (t,x,y) ->
    x = @MAPX(x)
    y = @MAPY(y)
    t.loc = x:x,y:y
    @map[x][y].terrain = t
 
  put_monst: (t,x,y) ->
    x = @MAPX(x)
    y = @MAPY(y)
    @map[t.loc.x][t.loc.y].monster = null if t.loc and @map[t.loc.x][t.loc.y].monster is t
    t.loc = x:x,y:y
    @map[x][y].monster = t
 
  put_monst_pt: (t,loc) ->
    @put_monst t,loc.x,loc.y
 
  put_treas: (t,x,y) ->
    x = @MAPX(x)
    y = @MAPY(y)
    t.loc = x:x,y:y
    @map[x][y].treasure = t
 
  put_treas_pt: (t,loc) ->
    @put_treas t,loc.x,loc.y
 
  add_action: (t,x,y,time) ->
    x = @MAPX(x)
    y = @MAPY(y)
    t.loc = x:x,y:y
    @map[x][y].action = t
 
  Delta: (l1, l2) ->
    x = l2.x-l1.x
    y = l2.y-l1.y
    hw = @cols//2
    hh = @rows//2
    x:if x<-hw then x+@cols else if x>hw then x-@cols else x,
    y:if y<-hh then y+@rows else if y>hh then y-@rows else y
 
  Each: (callback) ->
    for x in [0...@cols]
      for y in [0...@rows]
        callback(@at(x,y),x,y)
 
  Find: (check) ->
    x = Rand(@cols)
    y = Rand(@rows)
    for lx in [0...@cols]
      for ly in [0...@rows]
        mx = @MAPX(x+lx)
        my = @MAPY(y+ly)
        return x:mx, y:my if check mx,my
    null
 
  FindAPlaceFor: (terr) ->
    {x:x,y:y} = @Find (x,y) => @terr_at(x,y) instanceof ClearTerrain
    @put_terr terr,x,y
 
  FindClearRad: (rad, check) ->
    @Find (x,y) =>
      for i in [-rad..rad]
        for j in [-rad..rad]
          return false unless check x+i,y+j
      return true
 
  FindClearNearTerr: (terr, monster, plain_terr=ClearTerrain, screen=null) ->
    for lx in [0...@cols]
      for ly in [0...@rows]
        {x:x,y:y} = @FindSafeSquare monster, plain_terr, screen
        i = Rand(8)
        nx = [-1,-1, 0, 1, 1, 1, 0,-1]
        ny = [ 0,-1,-1,-1, 0, 1, 1, 1]
        for j in [0...8]
          return {x:x,y:y} if @terr_at(x+nx[i],y+ny[i]) instanceof terr
          i = (i+1)%8
    null
 
  FindSafeSquare: (monster,terr,screen) ->
    @Find (x,y) =>
      tile = @at x,y
      return (not terr or tile.terrain instanceof terr) and
             not tile.monster and
             monster.can_move(tile.terrain) and
             not screen?.visible x,y
 
  FindSurroundingGoodPlace: (monster, nx, ny) ->
    lx = [-1,-1, 0, 1, 1, 1, 0,-1]
    ly = [ 0,-1,-1,-1, 0, 1, 1, 1]
    Choose (x:@MAPX(lx[j]+nx),y:@MAPY(ly[j]+ny) for j in [0...8] when monster.can_move @terr_at(lx[j]+nx,ly[j]+ny))
 
  PlaceShrine: (terr) ->
    loc = @FindClearRad(2, (x,y)=>@terr_at(x,y) instanceof ClearTerrain) ?
          @FindClearRad(1, (x,y)=>@terr_at(x,y) instanceof ClearTerrain)
    return @FindAPlaceFor new ShrineTerrain unless loc
    @put_terr new terr,x,y for x in [loc.x-1 .. loc.x+1] for y in [loc.y-1 .. loc.y+1]
    [dx,dy] = Choose [
      [-1, 0]
      [ 0,-1]
      [ 1, 0]
      [ 0, 1]
    ]
    @put_terr new RoadTerrain, loc.x+dx,loc.y+dy
    @put_terr new RoadTerrain, loc.x+2*dx,loc.y+2*dy
    @put_terr new ShrineTerrain, loc.x,loc.y
 
  RandomFantasyName: (maxLen,minLen) ->
    consonant = "bcdfghjklmnpqrstvwxzzk"
    vowel = vowel1 = "aeiouyaey"
    vowel2 = vowel+"'"
    pick = (s) ->
      vowel = vowel1 if (c = s.charAt Rand(s.length)) == "'"
      c
    str = (pick(if i&1 then vowel else consonant) for i in [0...Rand(maxLen,minLen)])
    str[0] = str[0].toUpperCase()
    str.join ""
 
  NameCity: (terr,name) ->
    name = "New "+name if @locations[name]
    terr.name = name
    @locations[name] = terr
 
  CityNames: (skip="",filter=NamedTerrain) ->
    (name for name,terr of @locations when name isnt skip and terr instanceof filter)
 
  PlaceTreasure: (treasure,player,offscreen=true) ->
    loc = @FindSafeSquare player,null,if offscreen then @screen else null
    @put_treas_pt new treasure, loc
 
  # FIXME deprecated?
  CanEnter: (monster,x,y) ->
    tile = @at x,y
    not tile.monster and monster.can_move tile.terrain
 
  CountMonsters: (monster,x,y,rad) ->
    cnt = 0
    for i in [-rad..rad]
      for j in [-rad..rad]
        cnt++ if @monst_at x+i,y+j instanceof monster
    cnt
 
  NotifyNearbyMonsters: (us,rad) ->
    did_we_enrage = false
    for i in [-rad..rad]
      for j in [-rad..rad]
        monster = @monst_at us.loc.x+i,us.loc.y+j
        if monster and monster isnt us
          did_we_enrage = true if monster.notify us
    did_we_enrage
 
  NearbyTreasure: (monster,rad) ->
    for i in [-rad..rad]
      for j in [-rad..rad]
        tile = @at monster.loc.x+i,monster.loc.y+j
        if tile.treasure and monster.can_move tile.terrain
          return x:@MAPX(monster.loc.x+i), y:@MAPY(monster.loc.y+j)
    null
 
class OWGame
 
  constructor: (@screen) ->
    @status_lines = []
    @status_cur_line = 0
    @monsters = new MonsterManager
    @messageTime = 5000
 
  AddMessage: (msg) ->
    @status_lines.push msg
    @status_cur_line++
    @screen.timeout @messageTime, => @update_messages()
    @messageTime += 220
 
  update_messages: ->
      @status_cur_line--
      @screen.refresh()
 
  Look: (loc) ->
    @screen.do_look(false)
    tile = @world.pt loc
    dist = @world.dist_pt_to_pt loc,@player.loc
    if dist.x<2 and dist.y<2
      @world.put_treas_pt new ExoticLoot, loc if @player.FoundExotic loc
    if tile.monster is @player
      @AddMessage gOWString.ow_stats_intro
      if @player.quest
        @AddMessage gOWString.ow_stats_acquest.substitute @player.stat.armor,@player.quest
      else
        @AddMessage gOWString.ow_stats_ac.substitute @player.stat.armor
      @AddMessage gOWString.ow_stats_dmg.substitute @player.stat.damage.describe()
      @AddMessage gOWString.ow_stats_thaco.substitute @player.stat.tohit.thaco()
      @screen.refresh()
      return
    if tile.monster
      #if tile.monster instanceof DragonMonster and @player.dragons>=1
      @AddMessage gOWString.ow_id_monst.substitute gOWString["ow_monster_"+tile.monster.type]
    if tile.treasure
      @AddMessage if tile.treasure instanceof GoldLoot then gOWString.ow_id_loot.substitute tile.treasure.value else gOWString["ow_id_loot_"+tile.treasure.type]
    if tile.terrain instanceof CampTerrain or (tile.monster is null and tile.treasure is null)
      @AddMessage gOWString.ow_id_terr.substitute gOWString["ow_terrain_"+tile.terrain.type]
      @AddMessage gOWString.ow_id_city.substitute tile.terrain.name if tile.terrain instanceof NamedTerrain
      @AddMessage gOWString.ow_id_camp.substitute gOWString["ow_monster_"+tile.terrain.monster::type] if tile.terrain instanceof CampTerrain
    @screen.refresh()
 
  dump_map: ->
    for y in [0..@world.rows]
      line = ""
      for x in [0..@world.cols]
        tile = @world.at x,y
        code = switch tile.terrain.type
          when "f" then "+"
          when "m" then "^"
          when "w" then "~"
          when "v" then "*"
          when "c" then "$"
          when "r" then "#"
          when "s" then "T"
          when "p" then "w"
          when "t" then "&"
          else "."
        switch tile.treasure?.type
          when "loot" then code="0"
          when "l" then code="1"
          when "t" then code="2"
        code = "E" if @player.exotic_loc.x==x and @player.exotic_loc.y==y
        line += code
      console.log line
    console.log name for name in @world.CityNames()
 
  CreateTheGameSpace: ->
    @world = new World 64,64
    @player = @monsters.Gen Player
    @world.put_monst_pt @player,@world.FindSafeSquare @player
    @world.PlaceTreasure StartingTreasure::gen(),@player,false for [1..64]
    @treasureTime = 0
    #CreateMonsters()
    num_squid = 0
    for i in [1..120] when Chance(1/3)
      monster = @monsters.Choose @player
      if monster is SquidMonster
        num_squid++
      else
        @CreateAMonster monster
    #CreateTheDragon()
    loc = @world.FindClearRad(2,(x,y)=>@world.terr_at(x,y) instanceof MountainTerrain) or
          @world.FindClearRad(1,(x,y)=>@world.terr_at(x,y) instanceof MountainTerrain)
    return @CreateTheGameSpace unless loc
    @CreateAMonsterAt(DragonMonster,loc).set_guard true
    @CreateMonsterCamp GoblinMonster,PlainTerrain
    @CreateMonsterCamp GoblinMonster,PlainTerrain
    @campTime = -8
    @player.exotic_loc = @world.FindClearNearTerr WaterTerrain,@player
    @AddMessage gOWString.ow_entergame
 
  CreateAMonster: (type, screen=null) ->
    monster = @monsters.Gen type
    loc = @world.FindSafeSquare monster,null,screen
    @world.put_monst monster,loc.x,loc.y
 
  CreateAMonsterAt: (type, loc) ->
    monster = @monsters.Gen type
    @world.put_monst monster,loc.x,loc.y
 
  CreateMonsterCamp: (type, terr) ->
    monster = new type
    loc = @world.FindClearNearTerr(terr,monster,PlainTerrain,@screen) or
          @world.FindClearNearTerr(terr,monster,PlainTerrain) or
          @world.FindSafeSquare(monster,terr,@screen)
    return if not loc or @world.pt(loc).monster
    @world.put_terr new CampTerrain(type), loc.x,loc.y
    monster = @CreateAMonsterAt type,loc
    monster.camp loc.x,loc.y,2.0
    for [0...type::stat.level//3+2]
      nloc = @world.FindSurroundingGoodPlace monster,loc.x,loc.y
      @CreateAMonsterAt(type,nloc)?.camp(nloc.x,nloc.y,1.5) if nloc
 
  RunActions: (time) ->
 
 
  Draw: (time) ->
    @RunActions(time)
    @screen.refresh()
 
  MonsterEnraged: (monster) ->
    @monsterWasEnraged = true
 
  MonsterAlerted: (monster) ->
    @monsterWasAlerted = true
 
  RunSim: (time) ->
    @messageTime = 5000
    @monsterWasEnraged = false
    @monsterWasAlerted = false
    @monsters.Run @,@world,@player
    if @monsterWasEnraged
      # Did we enrage?
      @AddMessage gOWString.ow_enrage
    else if @monsterWasAlerted
      # Were we seen?
      @AddMessage gOWString.ow_wereseen
    # respawn monsters and treasure
    @CreateAMonster @monsters.Choose(@player),@screen if @monsters.Chance()
    # check monster camps
    if @player.turns - @campTime is 16
      @world.Each (tile) =>
        @monsters.Camp tile.terrain,@player.level,@world if tile.terrain instanceof CampTerrain
      @campTime = @player.turns
    # spawn new treasure every 32 turns
    @SpawnNewTreasure() if @player.turns - @treasureTime is 32
    # shrine setup
    terrain = @world.terr_pt @player.loc
    if terrain instanceof ShrineTerrain
      @RunShrine() if @player.CanLevel()
    else if @player.shrine_time > 0
      @AddMessage gOWString.ow_stoppray if @player.CanLevel()
      @player.shrine_time = 0
    # if im in a town or village, do healing, check quests
    if terrain instanceof VillageTerrain or terrain instanceof CityTerrain
      @check_healing_area terrain.heal_cost if @player.CanHeal()
      @DoQuest @world.pt @player.loc
    else if terrain instanceof CampTerrain
      @GivePlayerXP terrain.value()
      @AddMessage gOWString.ow_destroycamp
      @world.pt(@player.loc).terrain = new PlainTerrain
    else if terrain instanceof TempleTerrain
      @RunTemple() if false
    # crazy dragon fun
    #TODO
    # give more hp over time
    if @player.turns % 256 == 0
      @player.max_hp++ if @player.max_hp < 180
      @AddMessage gOWString.ow_moreturn
    @Draw time
 
  RunCombat: (attacker,defender) ->
    switch damage = attacker.Combat defender
      when "missed"
        @AddMessage gOWString.ow_missed.substitute gOWString["ow_monster_"+attacker.type]
      when "nodamage"
        @AddMessage gOWString.ow_nodamage.substitute gOWString["ow_monster_"+attacker.type]
      when "itdied"
        @AddMessage gOWString.ow_itdied.substitute gOWString["ow_monster_"+defender.type]
        @monsters.Remove defender,@world,@player
      when "youdied"
        @AddMessage gOWString.ow_youdied.substitute gOWString["ow_monster_"+attacker.type]
        @monsters.Remove @player,@world,@player
      else
        @AddMessage gOWString.ow_damage.substitute gOWString["ow_monster_"+attacker.type],damage
 
  check_healing_area: (cost) ->
    if @player.gp >= cost
      @player.Heal 0.10
      @player.gp -= cost
      @AddMessage gOWString.ow_payheal.substitute cost
    else
      @AddMessage gOWString.ow_nocash.substitute cost
 
  RunShrine: ->
    @AddMessage gOWString.ow_startpray if @player.shrine_time == 0
    @AddMessage gOWString.ow_keeppray if @player.shrine_time&1 == 1
    @GivePlayerLevel() if @player.Praying()
 
  RunTemple: ->
    @AddMessage gOWString.ow_temple
    @GivePlayerXP 222
 
  DoQuest: (tile) ->
    if @player.quest is null
      return unless tile.terrain instanceof CityTerrain
      name = Choose @world.CityNames tile.terrain.name, if Chance(0.25) then CityTerrain else VillageTerrain
      @player.quest = name
      @AddMessage gOWString.ow_getquest.substitute name
    else
      return unless @player.quest == tile.terrain.name
      @AddMessage gOWString.ow_doquest
      @GivePlayerXP Math.min(@player.questcount*(2+@player.level//10)+5, 20+2*@player.level)
      @player.quest = null
      @player.questcount++
 
  MovePlayer: (dir) ->
    targ =
      x: @player.loc.x+dir.x
      y: @player.loc.y+dir.y
    tile = @world.pt targ
    if tile.monster
      if tile.monster instanceof TalkingMonster
        tile.monster.TalkTo @player
      else if tile.monster isnt @player
        @RunCombat @player,tile.monster
    else
      if @player.Move @world,targ
        @HandleTreasure tile if tile.treasure
      else
        @AddMessage gOWString.ow_impass
 
  GivePlayerLevel: ->
    @player.GiveLevel()
    @AddMessage gOWString.ow_newlevel.substitute @player.level
    switch @player.level
    #  when 3, 9
    #    #CreateMonsterCamp TrollMonster,PlainTerrain
    #  when 4
    #    #CreateMonsterCamp HeadlessMonster,PlainTerrain
    #  when 5
    #    #CreateTalkingMonster MerchantMonster,RoadTerrain
    #  when 7
    #    #CreateTalkingMonster SnickersMonster,CityTerrain
    #  when 10
    #    #CreateTalkingMonster WisepersonMonster,WaterTerrain
      when 15
        @AddMessage gOWString.ow_dragon
        @monsters.Each DragonMonster, (dragon) -> dragon.set_attack()
    #  when 18, 21
    #    #CreateMonsterCamp DemonMonster,PlainTerrain
 
  GivePlayerXP: (xp) ->
    @AddMessage gOWString.ow_gainxp.substitute xp
    @AddMessage gOWString.ow_goshrine if @player.GiveXP xp
 
  SpawnNewTreasure: ->
    #sharp_die = new DieRoll(4,2)
    #if (roll = sharp_die.roll() - 4) >= 2
    #  roll -= 2
    #roll = 1 unless 0 <= roll <= 2
    @world.PlaceTreasure SpawnedTreasure::gen(),@player
    @treasureTime = @player.turns
 
  HandleTreasure: (tile) ->
    if tile.treasure instanceof PotionLoot
      @player.potions++
      @AddMessage gOWString.ow_find_loot_potion
    else if tile.treasure instanceof EquipmentLoot
      @AddMessage gOWString["ow_find_loot_"+tile.treasure.type] if @player.TakeEquipment tile.treasure
    else if tile.treasure instanceof GoldLoot
      @player.TakeLoot tile.treasure
      @AddMessage gOWString.ow_gotgold.substitute tile.treasure.value
    tile.treasure = null
 
 
class OWScreen
 
  constructor: (@cols,@rows,frame,inputClass,gameClass) ->
    (@container = $.createElement "div").setAttribute "class","ow-container"
    frame.appendChild @container
    (@grid = $.createElement "div").setAttribute "class","ow-grid"
    @container.appendChild @grid
    (@status = $.createElement "div").setAttribute "class","ow-status"
    @container.appendChild @status
    (@log = $.createElement "div").setAttribute "class","ow-status-log"
    @container.appendChild @log
 
    @status.innerHTML = """
    <div class="ow-player-stats">
    <p class="ow-playstat-health">8 / 8</p>
    <p class="ow-playstat-loot">0</p>
    <p class="ow-playstat-potion">0</p>
    <p class="ow-playstat-level">1</p>
    <p class="ow-playstat-exp">0</p>
    <p class="ow-playstat-look"></p>
    </div>
    <div class="ow-messages">
    </div>
    """
 
    @status.querySelector(".ow-messages").onclick = => @show_log()
    @log.onclick = => @hide_log()
    @status.querySelector(".ow-playstat-potion")?.onclick = => @do_potion()
    @status.querySelector(".ow-playstat-look")?.onclick = => @do_look(true)
 
    @simStart = Date.now()
    @simTime = 0
    @game = new gameClass @
    @game.CreateTheGameSpace()
 
    @mouseLook = false
    @input = new inputClass @grid
    @input.on "move", (k) =>
      @key k
 
    @input.on "click", (pt) =>
      @mouse pt
 
    @refresh()
 
  player_offset: ->
    x:(@cols+1)//2 - 1, y:(@rows+1)//2 - 1
 
  visible: (x,y) ->
    playerloc = @game.player.loc
    Math.abs(x-playerloc.x)<=@player_offset.x and Math.abs(y-playerloc.y)<=@player_offset.y
 
 
  update_sim: ->
    @simTime = Date.now() - @simStart
 
  run_sim: ->
    @game.RunSim @simTime
 
  refresh: ->
    window.requestAnimationFrame => @draw()
 
  draw: ->
    @clear @grid
    #{x:ox, y:oy} = @player_offset()
    {x:lx, y:ly} = @game.player.loc
    lx -= @player_offset().x
    ly -= @player_offset().y
    @draw_a_tile @game.world.at(fx+lx,fy+ly),fx,fy for fy in [0...@rows] for fx in [0...@cols]
    @draw_character_stats @game
    @draw_message_lines @game
 
  show_log: ->
    msgHtml = ("<p>"+msg+"</p>" for msg in @game.status_lines)
    @log.innerHTML = msgHtml.join "\n"
    @log.classList.add "ow-show"
    @log.lastChild.scrollIntoView()
 
  hide_log: ->
    @log.innerHTML = ""
    @log.classList.remove "ow-show"
 
  draw_message_lines: (game) ->
    num_msgs = game.status_lines.length
    cur = num_msgs - game.status_cur_line
    msgHtml = ("<p>"+game.status_lines[idx]+"</p>" for idx in [cur...num_msgs])
    @status.querySelector(".ow-messages")?.innerHTML = msgHtml.join "\n"
 
  draw_character_stats: (game) ->
    @status.querySelector(".ow-playstat-health")?.innerHTML = game.player.hp + " / " + game.player.max_hp
    @status.querySelector(".ow-playstat-potion")?.innerHTML = game.player.potions
    @status.querySelector(".ow-playstat-loot")?.innerHTML = game.player.gp
    @status.querySelector(".ow-playstat-exp")?.innerHTML = game.player.xp
    level = ""+game.player.level
    level += " / " + game.player.GetLevelFromXP() if game.player.CanLevel()
    @status.querySelector(".ow-playstat-level")?.innerHTML = level
 
  draw_a_tile: (tile,x,y) ->
    @set_position(wrapper = @create_a_tile(["ow-tile", "ow-tile-terr-"+tile.terrain.type]), x, y)
    wrapper.appendChild @create_a_tile(["ow-tile-inner", "ow-tile-monster-"+tile.monster.type]) if tile.monster
    wrapper.appendChild @create_a_tile(["ow-tile-inner", "ow-tile-loot-"+tile.treasure.type]) if tile.treasure and not tile.monster
    wrapper.appendChild @create_a_tile(["ow-tile-inner", "ow-tile-action-"+tile.action.type]) if tile.action
    @grid.appendChild wrapper
 
  create_a_tile: (classes) ->
    (tile = $.createElement "div").setAttribute "class",classes.join " "
    tile
 
  tile_size: ->
    x:@grid.clientWidth//@cols, y:@grid.clientHeight//@rows
 
  set_position: (tile,x,y) ->
    #tile.style.left = (x * Tile::width) + "px"
    #tile.style.top = (y * Tile::height) + "px"
    tile.classList.remove cl for cl in (cl for cl in tile.classList when cl.startsWith "ow-tile-position-")
    tile.classList.add "ow-tile-position-#{x}-#{y}"
 
  clear: (div) ->
    div.removeChild div.firstChild while div.firstChild
 
  tile_from_point: (pt) ->
    {x:tw, y:th} = @tile_size()
    {x:px, y:py} = @player_offset()
    x = pt.x // tw
    y = pt.y // th
    {x:@game.player.loc.x-px+x, y:@game.player.loc.y-py+y}
 
  delta_from_point: (pt) ->
    {x:tw, y:th} = @tile_size()
    {x:px, y:py} = @player_offset()
    dx = dy = 0
    x = pt.x - Math.floor(tw*(px+0.5))
    y = pt.y - Math.floor(th*(py+0.5))
    mx = Math.abs(x)
    my = Math.abs(y)
    dtx = if x>0 then 1 else -1
    dty = if y>0 then 1 else -1
    if mx<tw*0.5
      dy = dty unless my<th*0.5
    else
      slope = Math.abs(y/x)
      if slope>2.0 
          dy = dty
      else if slope<0.5
          dx = dtx
      else
          dx = dtx
          dy = dty
    x:dx,y:dy
 
  mouse: (pt) ->
    return @game.Look @tile_from_point pt if @mouseLook
    delta = @delta_from_point pt
    @update_sim()
    if @game.player.casting
      #@game.UpdateVelAction CombatMissileAct,@game.player.loc,delta
    else
      @game.MovePlayer delta
    @run_sim()
 
  key: (which) ->
      @update_sim()
      @game.MovePlayer switch which
        when 0 then {x: 0,y: 0}
        when 1 then {x: 1,y: 0}
        when 2 then {x: 1,y:-1}
        when 3 then {x: 0,y:-1}
        when 4 then {x:-1,y:-1}
        when 5 then {x:-1,y: 0}
        when 6 then {x:-1,y: 1}
        when 7 then {x: 0,y: 1}
        when 8 then {x: 1,y: 1}
      @run_sim()
 
  timeout: (time,cb) ->
    window.setTimeout cb,time
 
  do_potion: ->
    console.log "potion"
 
  do_look: (active) ->
    if @mouseLook = active
      @grid.classList.add "look"
    else
      @grid.classList.remove "look"
 
 
do ->
  window.OWGame = OWGame
  window.OWScreen = OWScreen
  window.OWInit = (width, height, container, inputClass, gameClass) ->
    window.requestAnimationFrame ->
        new OWScreen width,height,container,inputClass,OWGame
 
 
owstring.js
(function(window,undefined){
 
String.prototype.substitute = function() {
    var replace = /%%/g;
    var dest = [];
    var tail = 0;
    var argc = arguments.length;
    for (var i=0; i<argc; i++) {
      var match = replace.exec(this);
      if (match===null)
          break;
      dest.push(this.substring(tail,match.index), arguments[i])
      tail = replace.lastIndex;
    }
    return dest.join("") + this.substring(tail);
}
window.gOWString = {
g_newscore:"Score Number %%",
g_topscore:"A New High Score!",
g_youlose:"Score not in top 10.",
ow_damage:"%% did %% damage.",
ow_destroycamp:"You raze the monster's camp, and it will stop threatening the land.",
ow_doquest:"Thanks for the package.",
ow_dragon:"As you finish your prayer you hear the flapping of a Dragon's Wings.",
ow_dragondead:"As the dragon dies you hear a far off rumbling, and a dark dread covers the land.",
ow_enrage:"Nearby monsters are enraged!",
ow_entergame:"Entering OverWorld Null.",
ow_find_loot_e:"You dug up the exotic armor!",
ow_find_loot_d:"You found the Hoe of Destruction!",
ow_find_loot_s:"You found the magic shield.",
ow_find_loot_m:"You found the Legendary Magic Sword!",
ow_find_loot_p:"You take the healing potion.",
ow_gainxp:"You got %%xp.",
ow_gameover:"Game Over",
ow_get_exotic:"Your armor is improved.",
ow_getquest:"Deliver this to %%.",
ow_goshrine:"Pray at Shrine for level",
ow_gotgold:"Acquired %% gold.",
ow_hello:"Name? Job? Bye.",
ow_id_camp:"Looks like a %% lair",
ow_id_city:"You see %%.",
ow_id_drag_0:"The Ancient Mountain Dragon",
ow_id_drag_1:"The Ancient Water Dragon",
ow_id_drag_2:"The Ancient Forest Dragon",
ow_id_drag_3:"The Ancient Road Dragon",
ow_id_loot:"You see %%gp.",
ow_id_loot_e:"Exotic Armor",
ow_id_loot_d:"Hoe of Destruction",
ow_id_loot_s:"Magic Shield",
ow_id_loot_m:"Magic Sword",
ow_id_loot_p:"Healing Potion",
ow_id_monst:"It's a %%.",
ow_id_terr:"Terrain: %%.",
ow_impass:"Impassable",
ow_itdied:"A %% has died.",
ow_keeppray:"Ommmmmm...",
ow_lb_1:"Seals on the elemental temple are broken!  Repair the temple, then return to me.",
ow_lb_2:"4 Elemental Dragons have escaped!  You must slay them all to finally bring peace.",
ow_merchant_0:"Hey, you cant afford a potion!  I'm outta here...",
ow_merchant_1:"Talk to me again and I will sell you a potion for 50 gold.",
ow_merchant_2:"Here is your potion!  I am off to find foreign markets.",
ow_missed:"%% missed.",
ow_monster_player:"Player",
ow_monster_gb:"Goblin",
ow_monster_hd:"Headless",
ow_monster_bt:"Bat",
ow_monster_pg:"Swine",
ow_monster_tr:"Troll",
ow_monster_gh:"Ghost",
ow_monster_sq:"PondSquid",
ow_monster_dm:"Demon",
ow_monster_dg:"Dragon",
ow_monster_wp:"Wiseperson",
ow_monster_gz:"Gazer",
ow_monster_rp:"Reaper",
ow_monster_mm:"Mimic",
ow_monster_lk:"Lurker",
ow_monster_sl:"Slime",
ow_monster_mb:"Mongbat",
ow_monster_sn:"Snickers",
ow_monster_lb:" 'LB'",
ow_monster_mc:"Merchant",
ow_moreturn:"Another 256 turns.",
ow_newlevel:"Now level %%.",
ow_nocash:"You don't have %%gp.",
ow_nodamage:"%% did no damage.",
ow_nopotion:"You have no potions.",
ow_payheal:"You pay %%gp to heal.",
ow_potion_used:"You drank the potion.",
ow_restart:"Restarting...",
ow_resume:"Game Resuming...",
ow_score_long_lose:"Killed by a %%\n%% turns, level %%\n%%gp %%xp",
ow_score_long_win:"Solved OverWorld\n%% turns, level %%\n%%gp %%xp",
ow_score_lose:"level %%, %% turns",
ow_score_win:"won turn %% L%%",
ow_silent:"They are strangely silent.",
ow_snickers_0:"It's gonna bite you!  La la la....",
ow_snickers_10:"Even monsters don't have as much attention span as you do!  La la la...",
ow_snickers_11:"Fnord.",
ow_snickers_12:"Schmitty sez, 'Drag the Brick!'  La la la...",
ow_snickers_13:"I hear there is a secret basketball hidden in New Atlanta!  La la la...",
ow_snickers_14:"I hear there is not just one kind of Dragon. La la la...",
ow_snickers_15:"They called me mad at bard school!  La la la...",
ow_snickers_16:"I hear baking bread is very profitable. La la la...",
ow_snickers_17:"Some things can only be found if you go looking for them!  La la la...",
ow_snickers_18:"Villages are so poor, they can't even afford long names!  La la la...",
ow_snickers_19:"Health care is more affordable in the big city.  La la la....",
ow_snickers_1:"Shrines are very boring.   But there you'll practice warring!  La la la...",
ow_snickers_20:"Valor?  But I just met her!  La la la...",
ow_snickers_2:"I lost my legendary sword, have you seen it?  La la la...",
ow_snickers_3:"I hear exotic armor is made from the tears of a pond squid.  La la la ...",
ow_snickers_4:"If you get drunk on potions, you might salt the fries!  La la la...",
ow_snickers_5:"Consider a career in the lucative field of package delivery!  La la la...",
ow_snickers_6:"Name!  Job!  Bye!  La la la...",
ow_snickers_7:"Troll holes with whole rolls of trolls!  La la la ...",
ow_snickers_8:"Have you seen Professor Dog's tire?",
ow_snickers_9:"Monsters watch out for their own kind!  La la la...",
ow_spell_1:"Fireball",
ow_spell_2:"Magical Dart",
ow_startpray:"Begin praying...",
ow_stats_ac:"Def: %%     No Quest",
ow_stats_acquest:"Def:%%  Quest: %%",
//ow_stats_dmg:"You do %%d%%+%% damage.",
ow_stats_dmg:"You do %% damage.",
ow_stats_gp:"GP: %%",
ow_stats_hp:"Hp: %% / %%",
ow_stats_hp_short:"Hp:%%/%%",
ow_stats_intro:"You see the Player.",
ow_stats_lnorm:"Level: %%",
ow_stats_lplus:"Lvl: %% / %%",
ow_stats_thaco:"You hit %%% of the time.",
ow_stats_xp:"Xp: %%",
ow_stoppray:"Prayers abandoned.",
ow_temple:"You seal the temple, and realize the 4 Elemental Dragons are loose!",
ow_terrain_p:"Plains",
ow_terrain_f:"Forest",
ow_terrain_m:"Mountain",
ow_terrain_w:"Water",
ow_terrain_v:"Village",
ow_terrain_c:"City",
ow_terrain_r:"Road",
ow_terrain_s:"Shrine",
ow_terrain_b:"Monster camp.",
ow_terrain_t:"The Elemental Temple",
ow_tomb_1:" ____",
ow_tomb_2:"/     \\   {*}",
ow_tomb_3:"| R I P |    |",
ow_tomb_4:"|_____|__\\|/__",
ow_wereseen:"You were seen.",
ow_win:"You have slain all four elemental dragons!  You have brought peace to the land!",
ow_wiseman_0:"The Dragon will attack when you reach 12th level.",
ow_wiseman_1:"Pray at shrines for many turns to achieve your level.",
ow_wiseman_2:"Click Inspect on self to see combat stats.",
ow_wiseman_3:"Search near lakes for Exotic Armor.",
ow_wiseman_4:"There is a worse threat than one Dragon...",
ow_wiseman_5:"The trolls are building their forces...",
ow_wiseman_6:"Death and Victory are your only escape from OverWorld.",
ow_wiseman_7:"The OverWorld is hosed, come on down.",
ow_wiseman_bye:"Now, I must go.",
ow_youdied:"A %% has killed you.",
ow_youwon:"You Won"
};
 
})(window);