--[========================================================================[--

Game logic for Thrust II Reloaded.

Copyright © 2015-2018 Pedro Gimeno Fortea

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--]========================================================================]--

local game = {}

local la,le,lfs,lf,lg,li,lj,lk,lm,lmo,lp,ls,lsys,lth,lt,lw = require'ns'()
local json = require '3rdparty.dkjson'

local garbagetimer = 0
local collision_canvas, no_collision
local enemies, ship, cable, orbs, counters
local tileset, tilecoll, spriteset, spritecoll, crashset, shotimg
local tilebatch, orbbatch
local tilequads, spritequads, crashquads
local orb_sprite
local lastxtile, lastytile
local fineangle
local was_turning, shooting, extending, thrusting
local thrustvol = 0

-- cached namespaces
local map, enemytypes, decoys, targets, respawn, agents, keys

local tgt_tile_start = 122
local tgt_frames = 2 -- how many frames in animation
local tgt_tile_current = tgt_tile_start
local tgt_fps = 8/25
local tgt_timer = 0
local tgt_index -- index of current target in the targets table
-- squared velocity threshold for being able to drop an orb into the target
-- (can't drop if the orb crosses the target too fast)
local tgt_vel_threshold2 = 10000 -- 100 px/s

local cable_maxlen = 80
local cable_maxlen2 = cable_maxlen * cable_maxlen
local cable_extend_speed = 5
local max_shoot_radius = 224
local shoot_radius = 0
local total_shoot_time = 4

local latchsnd, orbdangersnd, crashsnd, cablesnd, enemykillsnd
local getagentsnd, shootsnd, orbpickupsnd, orbdropsnd, thrustsnd, music

local DIV = love_version >= 11000000 and 255 or 1

function game.load()
  -- Game viewport
  game.vx, game.vy, game.vw, game.vh = 0, 0, main.ww, main.wh

  -- Cache some namespaces
  map, enemytypes, decoys, targets, respawn, agents, keys = main.map, main.enemytypes,
    main.decoys, main.targets, main.respawn, main.agents, main.keys
  tileset = lg.newImage("img/Tiles32x32.png")
  tilecoll = lg.newImage("img/Tiles32x32_collision.png")
  spriteset = lg.newImage("img/Sprites32x32.png")
  spritecoll = lg.newImage("img/Sprites32x32_collision.png")
  crashset = lg.newImage("img/Explosion32x32.png")
  shotimg = lg.newImage("img/Shot.png")
  -- Don't use nearest for the shot, as it's blurred to start with.
  shotimg:setFilter("linear", "linear")
  local tx, ty = tileset:getDimensions()
  local sx, sy = spriteset:getDimensions()

  -- Tiles are easy
  tilequads = {}  -- used for both normal tiles and collision tiles
                  -- which means the images must have the same size
                  -- (same applies to spritequads below)
  for y = 0, (ty-1)/32 do
    for x = 0, (tx-1)/32 do
      tilequads[#tilequads + 1] = lg.newQuad(x*32, y*32, 32, 32, tx, ty)
    end
  end

  -- Sprites
  spritequads = {}

  -- Ship quads
  for y = 0, 1 do
    for x = 0, 15 do
      spritequads[#spritequads + 1] = lg.newQuad(x*32, y*32, 32, 32, sx, sy)
    end
  end

  -- Enemies quads
  -- Assumes that if there is a higher sprite in a row, then the space
  -- in the next rows until completing that height is empty.
  -- E.g. if X is 32x32 (1x1 cells) and Y is 64x64 (2x2 cells):
  --       X YY YY X X    correct
  --         YY YY
  --
  --       X YY YY X X    incorrect - don't reuse the empty spaces
  --       X YY YY        (our algorithm isn't that clever)

  local idx = 32 -- 32x32 cell number - we start after the ship sprites
  local height = 1 -- height of the highest cell in the row
  for k, v in ipairs(enemytypes) do
    -- Cache the starting quad. The animation spans nframes from here.
    if v.nframes >= 1 then v.initsprite = #spritequads + 1 end
    for enemynum = 1, v.nframes do
      spritequads[#spritequads + 1] =
         lg.newQuad(idx%16*32, (idx-idx%16)/16*32, v.width*32, v.height*32, sx, sy)
      if v.height > height then height = v.height end
      idx = idx + v.width
      if idx % 16 == 0 then
        idx = idx + 32 * (height-1) -- skip row height
        height = 1
      end
    end
  end

  -- Finally, the orb
  orb_sprite = #spritequads + 1
  spritequads[orb_sprite] =
    lg.newQuad(idx%16*32, (idx-idx%16)/16*32, 32, 32, sx, sy)
  -- and its glow
  idx = idx + 1
  spritequads[orb_sprite+1] =
    lg.newQuad(idx%16*32, (idx-idx%16)/16*32, 32, 32, sx, sy)

  -- Ship crash animation
  sx, sy = crashset:getDimensions()
  crashquads = {}

  for idx = 0, 31 do
    crashquads[idx+1] = lg.newQuad(idx%4*32, (idx-idx%4)/4*32, 32, 32, sx, sy)
  end

  tilebatch = lg.newSpriteBatch(tileset,
                -- In 1D, a 33-pixel window can see up to two 32-pixel tiles
                -- simultaneously. One needs a 34-pixel window to be able
                -- to see three. So in 1D it would be: floor((widht+62)/32)
                -- which equals 2 for width=33 and 3 for width=34. In 2D the
                -- natural extension is the following.
                math.floor((game.vw + 62)*(game.vh + 62)/(32*32))
                -- For 640x480, that's 336 tiles. Less than the default 1000,
                -- so quite bearable.
              )

  collision_canvas = lg.newCanvas(32, 64) -- enough for the ship and orb sprite
  -- 0.10.0 compatibility
  if collision_canvas.newImageData then
    no_collision = string.rep("\0", #collision_canvas:newImageData():getString())
  else
    no_collision = string.rep("\0", #collision_canvas:getImageData():getString())
  end

  -- How many orbs we're going to draw max. It's pretty static, it only changes
  -- when an orb is picked up.
  orbbatch = lg.newSpriteBatch(spriteset, #main.orbs + #main.decoys)

  -- Load sounds
  latchsnd = la.newSource("snd/latch.wav", "static")
  latchsnd:setVolume(0.4) -- make it subtler
  getagentsnd = la.newSource("snd/LoadAgent.wav", "static")
  shootsnd = la.newSource("snd/AgentShot.wav", "static")
  orbdangersnd = la.newSource("snd/orbdanger.wav", "static")
  orbdangersnd:setLooping(true)
  orbpickupsnd = la.newSource("snd/click_one_22khz.wav", "static")
  orbdropsnd = la.newSource("snd/click_two_22khz.wav", "static")
  thrustsnd = la.newSource("snd/jet_lp.ogg", "static")
  thrustsnd:setLooping(true)
  -- Crash sound
  crashsnd = la.newSource("snd/Grenade-SoundBible.com-1777900486.ogg", "static")
  -- Enemy killed
  enemykillsnd = la.newSource("snd/supertank_plazma_fireball_22khz.mp3", "static")
  -- Make a duplicate so we can ply two at the same time
  -- Only 0.9.1+ has Source:clone(); 0.9.0 needs to load a new instance.
  local enemykillsnd2 = enemykillsnd.clone and enemykillsnd:clone()
      or la.newSource("snd/supertank_plazma_fireball_22khz.mp3", "static")
  -- Turn it into a table
  enemykillsnd = { enemykillsnd, enemykillsnd2 }
  -- Cable extend/retract sound
  cablesnd = la.newSource("snd/Cable_Sound.wav", "static")
  -- Music
  music = la.newSource("snd/Thrust II - in game - Loop 909924-6942804.ogg", "stream")
  music:setVolume(0.2)
end

local indent_true = {indent=true}
function game.savegame()
  main.savedelay = 2.5
  local f = json.encode(game.state, indent_true)
  lfs.write("saved.txt", f, #f)
end

function game.activate()
  if cable.m ~= 0 then
    orbdangersnd:setVolume(0)
    orbdangersnd:play()
  end
  if main.music then
    music:play()
  end
  thrustvol = 0
  thrustsnd:setVolume(0)
  thrustsnd:play()
end

function game.deactivate()
  orbdangersnd:stop()
  thrustsnd:stop()
  music:stop()
end

function game.pause(pause)
  if pause then
    orbdangersnd:stop()
    music:pause()
    thrustsnd:setVolume(0)
    thrustvol = 0
  else
    if cable.m ~= 0 then
      orbdangersnd:play()
    end
    if main.music then
      music:play()
    end
    thrusting = lk.isDown(keys.thrust)
    if thrusting then
      thrustsnd:setVolume(1)
      thrustvol = 1
    end
  end
end


local function new_or_load_game(load)
  -- restore saved game or start new game

  if load and main.isFile("saved.txt") then
    local f, s = lfs.read("saved.txt")
    local tmp, err
    game.state, tmp, err = json.decode(f, 1, json.null, nil)
    if err then
      game.state = false
    else
      -- validate
      if #game.state.orbs > #main.orbs
        or #game.state.enemies > #main.enemies
        or game.state.counters.shields > 99
      then game.state = false end
    end

  else
    game.state = false
  end

  if not game.state then
    -- New game - copy initial state
    game.state = { enemies = main.deepcopy(main.enemies),
                   dyingenemies = {},
                   ship = { angle = 0 },
                   orbs = main.deepcopy(main.orbs),
                   cable = { length = 0, m = 0, latched = false },
                   counters = { shields = 10, score = 0, respawn = 1, clock = 0,
                                deadtimer = false, },
                 }
    game.state.ship.x = main.respawn[game.state.counters.respawn].x
    game.state.ship.y = main.respawn[game.state.counters.respawn].y
    game.state.ship.oldx = game.state.ship.x -- initialize integrator with vel 0
    game.state.ship.oldy = game.state.ship.y
    game.state.cable.x = game.state.ship.x
    game.state.cable.y = game.state.ship.y
    game.state.cable.oldx = game.state.ship.x
    game.state.cable.oldy = game.state.ship.y

    -- Can't find a good place for this at the moment.
    game.state.counters.shields = game.state.counters.shields - 1
  end

  -- Compatibility fixup
  if not game.state.dyingenemies then
    game.state.dyingenemies = {}
  end
  if game.state.counters.agent and not game.state.counters.shoot_agent then
    game.state.counters.shoot_agent = game.state.counters.agent
  end

  -- shortcuts
  enemies, ship, cable, orbs, counters =
    game.state.enemies, game.state.ship, game.state.cable, game.state.orbs,
    game.state.counters

  -- Clean up to start again
  orbbatch:clear()

  -- Add decoys
  for k, v in ipairs(decoys) do
    orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
  end

  -- Add normal orbs
  for k, v in ipairs(orbs) do
    v.sprite = orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
  end

  -- Update map with current progress
  tgt_index = #orbs + (cable.m ~= 0 and 1 or 0)
  for k, v in ipairs(targets) do
    if k <= tgt_index then
      map[v.y*128+v.x+1] = v.empty
    else
      map[v.y*128+v.x+1] = v.tile
    end
  end

  -- force refresh
  lastxtile = false

  -- assume no key pressed
  was_turning = false
  shooting = false
  extending = false

  -- initialize first frame
  game.update(0)
end


local function update_ship(dt)
  if dt == 0 then return end

  --[[ update angle ]]

  local angvel = 16

  local kleft, kright = lk.isDown(keys.left), lk.isDown(keys.right)
  if kleft and not kright then
    if was_turning then
      fineangle = (fineangle - angvel*dt) % 32
      ship.angle = math.ceil(fineangle) % 32
    else
      was_turning = true -- sharp reaction upon pressing
      fineangle = (ship.angle - 1) % 32
      ship.angle = fineangle
    end
  elseif kright and not kleft then
    if was_turning then
      fineangle = (fineangle + angvel*dt) % 32
      ship.angle = math.floor(fineangle)
    else
      was_turning = true
      fineangle = (ship.angle + 1) % 32
      ship.angle = fineangle
    end
  else
    was_turning = false
    fineangle = ship.angle
  end

  --[[ update ship (Verlet integrator) ]]

  -- flight parameters
  local thrust = 600
  local gravity = 45
  local dragx = 0.2
  local dragy = 0.166
  local shipmass = 1

  local dt2 = dt*dt

  local shipforcex, shipforcey = 0, gravity * shipmass

  local was_thrusting = thrusting
  thrusting = lk.isDown(keys.thrust)
  if thrusting then
    thrustsnd:setVolume(1)
    thrustvol = 1
    shipforcex = shipforcex + math.sin(fineangle*math.pi/16) * thrust
    shipforcey = shipforcey - math.cos(fineangle*math.pi/16) * thrust
  --else
    -- sound stopping handled by game.update, to ramp down volume
  end

  local orbaccelx = 0
  local orbaccely = 0
  local newcablex
  local newcabley
  local newshipx = ship.x
  local newshipy = ship.y
  local orbmass

  orbmass = cable.m * shipmass -- orb mass is how heavier it is than the ship
  newcablex = cable.x
  newcabley = cable.y
  orbaccelx = 0
  orbaccely = gravity

  newcablex = newcablex + (cable.x - cable.oldx) * (1-dragx*dt) + orbaccelx * dt2
  newcabley = newcabley + (cable.y - cable.oldy) * (1-dragy*dt) + orbaccely * dt2

  local accelx = shipforcex / shipmass
  local accely = shipforcey / shipmass

  newshipx = newshipx + (newshipx - ship.oldx) * (1-dragx*dt) + accelx * dt2
  newshipy = newshipy + (newshipy - ship.oldy) * (1-dragy*dt) + accely * dt2

  -- Apply length constraint
  local cablex = newshipx - newcablex
  local cabley = newshipy - newcabley
  local actuallen = math.sqrt(cablex*cablex + cabley*cabley)
  if actuallen < 0.00001 then actuallen = 0.00001 end -- should never happen
  -- the distance to adjust for is cable_maxlen - actuallen
  -- ours is a rope, not a stick - remove this condition if original behavior wanted
  if actuallen > cable_maxlen then
    newshipx = newshipx + (cable_maxlen - actuallen) * cablex / actuallen
              * (orbmass / (orbmass + shipmass))
    newshipy = newshipy + (cable_maxlen - actuallen) * cabley / actuallen
              * (orbmass / (orbmass + shipmass))
    newcablex = newcablex + (cable_maxlen - actuallen) * cablex / actuallen
              * -(shipmass / (orbmass + shipmass))
    newcabley = newcabley + (cable_maxlen - actuallen) * cabley / actuallen
              * -(shipmass / (orbmass + shipmass))
  end

  -- "Scroll" orb position
  cable.oldx = cable.x
  cable.oldy = cable.y
  cable.x = newcablex
  cable.y = newcabley

  -- "Scroll" ship position
  ship.oldx = ship.x
  ship.oldy = ship.y
  ship.x = newshipx
  ship.y = newshipy

  -- Normalize if both orb and ship are equal modulo 4096
  if not cable or math.floor(ship.x / 4096) == math.floor(cable.x / 4096) then
    local k = math.floor(ship.x / 4096) * 4096
    ship.x = ship.x - k
    ship.oldx = ship.oldx - k
    if cable then
      cable.x = cable.x - k
      cable.oldx = cable.oldx - k
    end
  end
end

function game.newgame()
  new_or_load_game(main.restore)
end

local function collided()
  -- Collision test - paint the collision tiles/sprites to a canvas
  -- and multiply by ship's collision sprite to see if there are
  -- common pixels (all zeros = no)
  lg.setCanvas(collision_canvas)

  -- Clear collision canvas
  if collision_canvas.clear then
    collision_canvas:clear()
  else
    lg.clear()
  end

  -- In order to only do getImageData once, our canvas is
  -- divided into two halves vertically. The top part is for
  -- the ship, the bottom part is for the orb.
  for i = 0, (cable.m ~= 0 and 32 or 0), 32 do -- Draw twice if an orb is carried, else once.

    lg.setScissor(0, i, 32, 32) -- select which half to be drawn
    local topleftx = i == 0 and math.floor(ship.x-15.5)%4096 or math.floor(cable.x-16)%4096
    local toplefty = i == 0 and math.floor(ship.y-15.5)      or math.floor(cable.y-16)

    -- Colision with tile
    local localx = -(topleftx%32)
    local localy = -(toplefty%32)
    local tilex = (topleftx+localx)/32
    local tiley = (toplefty+localy)/32
    if tiley > 62 then tiley = 62 end
    if tiley < 0 then tiley = 0 end

    lg.draw(tilecoll, tilequads[map[tilex%128 + tiley*128 + 1]],  localx, localy+i)
    if localx + 32 < 32 then
      lg.draw(tilecoll, tilequads[map[(tilex+1)%128 + tiley*128 + 1]], localx+32, localy+i)
    end
    if localy + 32 < 32 then
      lg.draw(tilecoll, tilequads[map[tilex%128 + (tiley+1)*128 + 1]], localx, localy+32+i)
    end
    if localx + 32 < 32 and localy + 32 < 32 then
      lg.draw(tilecoll, tilequads[map[(tilex+1)%128 + (tiley+1)*128 + 1]], localx+32, localy+32+i)
    end

    -- Collision with decoys
    local vx, vy
    for k, v in ipairs(decoys) do
      vx = v.x-16 - topleftx - (topleftx < 32 and v.x >= 4064 and 4096 or 0)
      vy = v.y-16 - toplefty
      if vx > -32 and vx < 32 and vy > -32 and vy < 32 then -- only draw if in range
        lg.draw(spritecoll, spritequads[orb_sprite], vx, vy + i)
      end
    end

    -- Same for orbs
    for k, v in ipairs(orbs) do
      vx = v.x-16 - topleftx - (topleftx < 32 and v.x >= 4064 and 4096 or 0)
      vy = v.y-16 - toplefty
      if vx > -32 and vx < 32 and vy > -32 and vy < 32 then -- only draw if in range
        lg.draw(spritecoll, spritequads[orb_sprite], vx, vy + i)
      end
    end

    if i ~= 0 then
      -- only collide the orb with the ship, not with itself
      -- they are guaranteed to be in the same multiple of 4096
      vx = math.floor(ship.x-15.5) - topleftx
      vy = math.floor(ship.y-15.5) - toplefty
      lg.draw(spritecoll, spritequads[ship.angle + 1], vx, vy + i)
    end

    -- Collision with enemies
    local et
    for k, v in ipairs(enemies) do
      et = enemytypes[v.type]
      vx = math.floor(v.x/2)*2 - topleftx - (topleftx < et.width*32 and v.x >= 4096-et.width*32 and 4096 or 0)
      vy = math.floor(v.y/2)*2 - toplefty
      if vx > -(et.width*32) and vx < 32 and vy > -(et.height*32) and vy < 32 then
        -- visible
        lg.draw(spriteset, spritequads[v.f], vx, vy + i)
      end
    end

    -- Draw ship/orb in multiplicative mode
    if love_version < 0010000 then
      lg.setBlendMode("multiplicative")
    else
      lg.setBlendMode("multiply", "premultiplied")
    end
    lg.draw(spritecoll, spritequads[i == 0 and ship.angle+1 or orb_sprite], 0, i)
    lg.setBlendMode("alpha", "alphamultiply") -- return blend mode to normal
  end

  lg.setScissor()
  lg.setCanvas()
  if collision_canvas.newImageData then
    return collision_canvas:newImageData():getString() ~= no_collision
  else
    return collision_canvas:getImageData():getString() ~= no_collision
  end
end


function game.update(dt)
  garbagetimer = garbagetimer + dt
  if garbagetimer >= 5 then
    -- playing with Canvas:getImageData tends to generate garbage that isn't
    -- collected, so we help Lua a bit here
    collectgarbage()
    garbagetimer = 0
  end

  if not counters.deadtimer and dt ~= 0 and collided() then
    -- Player died - start death timer
    counters.deadtimer = 2.5
    ship.crashframe = 1
    ship.crashinterval = 0
    thrusting = false
    crashsnd:play()
  end

  if not thrusting then
    thrustvol = (thrustvol * 0.33) - 0.00001
    if thrustvol < 0 then
      thrustvol = 0
    end
    thrustsnd:setVolume(thrustvol)
  end

  if counters.deadtimer then

    -- refresh ship crash animation
    if ship.crashframe then
      -- when crashframe exists, crashinterval must exist too
      ship.crashinterval = ship.crashinterval + dt
      if ship.crashinterval >= 0.1 then
        ship.crashinterval = ship.crashinterval - 0.1
        ship.crashframe = ship.crashframe + 1
        if ship.crashframe > 16 then
          ship.crashframe = nil
          ship.crashinterval = nil
        end
      end
    end

    counters.deadtimer = counters.deadtimer - dt
    if counters.deadtimer <= 0 then
      counters.deadtimer = false
      ship.crashframe = nil
      ship.crashinterval = nil
      -- Respawn point for the player
      ship.x = respawn[counters.respawn].x
      ship.y = respawn[counters.respawn].y
      ship.oldx = ship.x
      ship.oldy = ship.y
      ship.angle = 0
      was_turning = false
      if cable then
        cable.x = ship.x
        cable.y = ship.y + cable_maxlen
        cable.oldx = cable.x
        cable.oldy = cable.y
      end
      if counters.shields == 0 then
        main.activate(screens.gameover)
        return
      else
        counters.shields = counters.shields - 1
        screens.getready.fromstart = false
        main.activate(screens.getready)
        return
      end
    end
  end

  -- Update dead enemies' animations
  do
    local l = #game.state.dyingenemies
    for i = l, 1, -1 do
      local v = game.state.dyingenemies[i]
      v.t = v.t + dt
      if v.t >= 0.1 then
        v.t = v.t - 0.1
        v.f = v.f + 1
        if v.f > 8 then
          game.state.dyingenemies[i] = game.state.dyingenemies[l]
          game.state.dyingenemies[l] = nil
          l = l - 1
        end
      end
    end
  end

  if dt > 0.05 then dt = 0.05 end -- smoothen the movement

  -- read keys
  local was_shooting = shooting
  shooting = lk.isDown(keys.fire)
  local pickup = lk.isDown(keys.pickup)

  if not pickup and cable.latched then cable.latched = false end

  -- Update orb timer
  if counters.orbinterval and not counters.deadtimer then
    counters.orbinterval = counters.orbinterval + dt
    if counters.orbinterval >= 0.625 then
      counters.orbinterval = counters.orbinterval - 0.625
      counters.orbtimer = counters.orbtimer - 1
    end
    if counters.orbtimer <= 0 then
      -- Kaboom!
      main.activate(screens.orbexplode)
      return
    end
  end

  -- Check if an orb was picked up
  if cable.latched then
    cable.x = orbs[cable.latched].x
    cable.y = orbs[cable.latched].y
    cable.oldx = cable.x
    cable.oldy = cable.y
    if (orbs[cable.latched].x-ship.x)^2 + (orbs[cable.latched].y-ship.y)^2 >= cable_maxlen2 then
      -- Latched to orb
      cable.m = orbs[cable.latched].m
      orbpickupsnd:play()
      orbdangersnd:setVolume(0)
      orbdangersnd:play()

      -- Start the unstable orb timer
      counters.orbinterval = 0
      counters.orbtimer = 500

      -- Remove the orb. The orbs table is unsorted, so for performance, to
      -- avoid scrolling (if removing a middle element) or creating a hole
      -- (if setting it to nil), we move the last element to this place.
      --table.remove(orbs, cable.latched) -- works, but this is presumably faster:
      orbs[cable.latched] = orbs[#orbs]
      orbs[#orbs] = nil

      cable.oldx = cable.x
      cable.oldy = cable.y
      -- Sprites can't be deleted, so regenerate the batch.
      orbbatch:clear()

      -- Add decoys
      for k, v in ipairs(decoys) do
        orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
      end

      -- Add normal orbs
      for k, v in ipairs(orbs) do
        v.sprite = orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
      end

      cable.latched = false
    end
  end

  -- Update player
  if not counters.deadtimer then
    update_ship(dt)
  end

  -- Clamp vertically
  if ship.y < 64 then ship.y = 64 ship.oldy = 64 - dt*100 end -- make a bouncing effect
  if ship.y > 2038 then ship.y = 2038 end

  -- Assign respawn zone
  for k, v in ipairs(respawn) do
    if ship.x%4096 >= v.topleftx and ship.x%4096 < v.topleftx + v.w
       and ship.y >= v.toplefty and ship.y < v.toplefty + v.h
    then
      counters.respawn = k
      break
    end
  end

  -- Target animation timer
  tgt_timer = tgt_timer + dt
  if tgt_timer / tgt_fps >= 1 then
    tgt_timer = tgt_timer % tgt_fps
    tgt_tile_current = (tgt_tile_current - tgt_tile_start + 1) % tgt_frames + tgt_tile_start
  end

  -- Deposit orb in target
  local tgt = targets[tgt_index]
  if cable.m ~= 0 and tgt and dt > 0 then
    -- Check if orb is in target
    local orbvelx = (cable.x - cable.oldx) / dt
    local orbvely = (cable.y - cable.oldy) / dt
    if orbvelx*orbvelx + orbvely*orbvely < tgt_vel_threshold2 then
      if     cable.x >= tgt.x*32 and cable.x < tgt.x*32+32
         and cable.y >= tgt.y*32 and cable.y < tgt.y*32+32
      then
        orbdropsnd:play()
        counters.score = counters.score + counters.orbtimer * 10
        cable.m = 0
        map[tgt.y*128+tgt.x+1] = tgt.tile
        tgt_index = tgt_index - 1
        if tgt_index == 0 then
          -- Last target achieved!
          main.activate(screens.gamewon)
          return
        end
        -- Force refresh of map
        lastxtile = false

        -- extra shield every 4 orbs deposited
        if (#main.orbs - #orbs) % 4 == 0 then
          counters.shields = counters.shields + 1
        end

        counters.orbinterval = nil
        counters.orbtimer = nil
        orbdangersnd:stop()

      end
    end
  end

  -- Agent timer
  if counters.agent then
    counters.agentinterval = counters.agentinterval + dt
    if counters.agentinterval >= 0.625 then
      counters.agentinterval = counters.agentinterval - 0.625
      counters.agenttime = counters.agenttime - 1
      if counters.agenttime <= 0 then
        counters.agenttime = nil
        counters.agent = nil
        counters.agentinterval = nil
      end
    end
  end

  -- Check if agent picked up.
  if pickup and (not counters.agenttime or counters.agenttime < 290) then
    local x, y = ship.x % 4096, ship.y + 64
    local t
    if y >= 0 and  y < 2048 then
      t = agents[map[math.floor(x/32) + math.floor(y/32)*128 + 1]]
      if t then
        counters.agent = t
        counters.agenttime = 300
        counters.agentinterval = 0
        getagentsnd:play()
      end
    end
  end

  -- Shoot timer
  if counters.shoot_timer then
    counters.shoot_timer = counters.shoot_timer - dt
    if counters.shoot_timer <= 0 then
      counters.shoot_timer = nil
      counters.shoot_x = nil
      counters.shoot_y = nil
      counters.shoot_agent = nil
    end
  end

  -- Check if shooting
  if shooting and not was_shooting and counters.agent and not counters.shoot_timer and not counters.deadtimer then
    counters.shoot_timer = total_shoot_time
    counters.shoot_x = ship.x
    counters.shoot_y = ship.y
    counters.shoot_agent = counters.agent
    shootsnd:play()
  end

  if counters.shoot_timer then
    shoot_radius = counters.shoot_timer / total_shoot_time
    shoot_radius = (1 - shoot_radius ^ 5) * max_shoot_radius
    if counters.shoot_timer > 1.5 then -- past 2.5 seconds it's too diluted
      local x1, y1, x2, y2
      local radius2 = shoot_radius*shoot_radius*(0.85*0.85) -- (the .85 compensates the diffuse image radius)
      -- did it hit any enemies?
      local k, v, l
      k = 1
      l = #enemies
      while k <= l do -- iterate manually to safely delete elements
        v = enemies[k]
        if v.type == counters.shoot_agent then
          -- the centre of the bounding box must be within the radius
          -- (not too realistic, but a collision analysis for this sounds
          -- overkill)
          x1 = v.x - counters.shoot_x
          y1 = v.y - counters.shoot_y
          x2 = x1 + enemytypes[v.type].width*32
          y2 = y1 + enemytypes[v.type].height*32
          -- Consider wraparound if the distance is too large
          if x1 < -2048 then
              x1 = x1 + 4096
              x2 = x2 + 4096
          elseif x1 > 2048 then
              x1 = x1 - 4096
              x2 = x2 - 4096
          end
          x1 = (x1 + x2) * 0.5
          y1 = (y1 + y2) * 0.5
          x1 = x1*x1
          y1 = y1*y1
          if x1 + y1 < radius2 then
            -- Killed enemy
            counters.score = counters.score + counters.agenttime * 10
            enemies[k].f = 1 -- explosion frame
            enemies[k].t = 0 -- timer to advance frame
            game.state.dyingenemies[#game.state.dyingenemies + 1] = enemies[k]
            enemies[k] = enemies[l]
            enemies[l] = nil
            l = l - 1
            -- Play kill enemy sound
            if enemykillsnd[1]:isPlaying() then
              enemykillsnd[2]:play()
            else
              enemykillsnd[1]:play()
            end
          else
            k = k + 1
          end
        else
          k = k + 1
        end
      end
    end
  end

  if cable.m == 0 then
    local was_extending = extending
    extending = (pickup and 1 or -1)
    if cable.length > 0 or extending == 1 then
      cable.length = cable.length + extending * dt * cable_extend_speed
    else
      extending = false
    end
    if extending ~= was_extending and not counters.deadtimer then
      cablesnd:play()
    end
    if cable.length > 1 then cable.length = 1 end
    if cable.length <= 0 then
      cable.length = 0
      -- fixup position x and y
      cable.x = ship.x
      cable.y = ship.y + cable_maxlen
      -- equate speed to ship's speed
      cable.oldx = ship.oldx - ship.x + cable.x
      cable.oldy = ship.oldy - ship.y + cable.y
    end
  end

  -- Check if cable reaches orb
  if cable.m == 0 and cable.length == 1 and not cable.latched then
    -- Window for consideration
    local sx1, sy1, sx2, sy2 = ship.x-cable_maxlen, ship.y-cable_maxlen,
                               ship.x+cable_maxlen, ship.y+cable_maxlen

    for k, v in ipairs(orbs) do
      if v.x >= sx1 and v.x <= sx2 and v.y >= sy1 and v.y <= sy2 then
        -- *Might* be in range - do the more expensive Euclidean check
        if (v.x-ship.x)^2 + (v.y-ship.y)^2 <= cable_maxlen2 then
          -- In range - latch to orb
          cable.latched = k
          cable.x = v.x
          cable.y = v.y
          latchsnd:play()
          break
        end
      end
    end

  end

  -- Update enemies' sprites and positions
  counters.clock = counters.clock + dt
  local t, tpos, et
  for k, v in ipairs(enemies) do
    -- Position
    tpos = (v.t + counters.clock) % v.period

    if tpos * 2 >= v.period then
      -- going back
      t = (v.period - tpos)*2 / v.period
    else
      t = tpos * 2 / v.period
    end

    -- Hack because our horizontal positions are broken
    local vx1
    if v.x0 == v.x1 then vx1 = v.x1 else vx1 = v.x1+2 end

    v.x = t < 0.5 and v.x0 + ( vx1 - v.x0) * t or  vx1 - ( vx1 - v.x0) * (1 - t)
    v.y = t < 0.5 and v.y0 + (v.y1 - v.y0) * t or v.y1 - (v.y1 - v.y0) * (1 - t)

    -- Frame
    et = enemytypes[v.type]
--    t = (v.frame + counters.clock * v.fps) % et.nframes
    if vx1 ~= v.x0 then
      t = (tpos/(v.period*16)*(vx1-v.x0)*v.fps) % et.nframes
    else
      t = (tpos/(v.period*16)*(v.y1-v.y0)*v.fps) % et.nframes
    end
    if et.pingpong then
      if tpos * 2 >= v.period then -- going backwards
        t = (et.nframes-t) % et.nframes
      end
    end
    v.f = et.initsprite + math.floor(t)
  end

  -- Debug string
  -- game.DEBUG=tostring(collided()).. " " .. 1/dt

  -- Loop music
  if music:isPlaying() then
      local pos = music:tell("samples")
      -- Loop it back to a sensible position
      if pos >= 6942804 then music:seek(pos - (6942804 - 909924), "samples") end
  end
end

function game.tiles_draw(x, y)
  -- Clamp coordinates to acceptable values
  --if y < 0 then y = 0 end
  --if y > 2048 - game.vh then y = 2048 - game.vh end
  x = x % 4096

  local xtile = math.floor(x/32)
  local ytile = math.floor(y/32)
  local xtiles = math.floor(game.vw+62)/32 -- max visible tiles
  local ytiles = math.floor(game.vh+62)/32
  if ytile + ytiles > 64 then
    -- clamp vertically
    ytiles = 64 - ytile
  end

  if xtile ~= lastxtile or ytile ~= lastytile then
    -- update required
    lastxtile, lastytile = xtile, ytile

    tilebatch:clear()
    for yt = ytile, ytile + ytiles - 1 do
      for xt = xtile, xtile + xtiles - 1 do
        tilebatch:add(tilequads[map[yt*128 + xt%128 + 1]],
                      (xt-xtile)*32, (yt-ytile)*32)
      end
    end
  end
  lg.draw(tilebatch, -(x%32), -(y%32))

end

function game.orbs_draw(x, y)
  x = x % 4096
  lg.draw(orbbatch, -x, -y)
  -- Draw 1 screen to the left and/or 1 screen to the right if necessary
  if x < 32 then
    lg.draw(orbbatch, -x-4096, -y)
  end
  if x + game.vh >= 4064 then
    lg.draw(orbbatch, -x+4096, -y)
  end
end


function game.draw()
  local vpx = math.floor(ship.x+0.5-game.vw/2)
  local vpy = math.floor(ship.y+0.5-game.vh/2)
  if vpy < 0 then vpy = 0 end
  if vpy > 2048-game.vh then vpy = 2048-game.vh end
  lg.setScissor(game.vx, game.vy, game.vw, game.vh)

  -- draw tiles
  game.tiles_draw(vpx, vpy)

  -- HACK: draw target in white first (we will colorize it later using multiplicative)
  local tgt = targets[tgt_index]
  if tgt then
    lg.draw(tileset, tilequads[75], tgt.x*32-vpx, tgt.y*32-vpy)
  end

  -- draw enemies
  local et
  for k, v in ipairs(enemies) do
    et = enemytypes[v.type]
    for x = v.x - 4096, vpx + game.vw, 4096 do
      if not (x >= vpx + game.vw
              or x+et.width*32 < vpx
              or v.y >= vpy + game.vh
              or v.y+et.height*32 < vpy)
      then
        -- visible
        lg.draw(spriteset, spritequads[v.f],
            (math.floor(x/2)*2 - vpx), (math.floor(v.y/2)*2 - vpy)
        )
      end
    end
  end

  -- Draw enemy crash animations
  for k, v in ipairs(game.state.dyingenemies) do
    lg.draw(crashset, crashquads[v.f], v.x - vpx, v.y - vpy)
    lg.draw(crashset, crashquads[v.f], v.x - 4096 - vpx, v.y - vpy)
  end

  -- draw cable line
  if (cable.m ~= 0 or cable.length ~= 0) and not counters.deadtimer then
    lg.setLineStyle("smooth")

    -- draw cable with length cable.length
    local cx = (cable.x-ship.x)*cable.length + ship.x
    local cy = (cable.y-ship.y)*cable.length + ship.y

    if not counters.deadtimer then
      lg.line(cx-vpx, cy-vpy, ship.x-vpx, ship.y-vpy)
    end
  end

  -- draw orbs/decoys
  game.orbs_draw(vpx, vpy)

  -- draw carried orb, if any
  if cable.m ~= 0 then
    -- draw orb
    if counters.deadtimer then
      if ship.crashframe then
        lg.draw(crashset, crashquads[ship.crashframe+16], cable.x-vpx-15, cable.y-vpy-18)
      end
    else
      local orbx = math.floor(cable.x-vpx-16)
      local orby = math.floor(cable.y-vpy-16)
      lg.draw(spriteset, spritequads[orb_sprite], orbx, orby)

      -- draw glow
      --lg.setBlendMode("additive")
      local orbtime = (500 - counters.orbtimer)*0.625 + counters.orbinterval
      local freq = .01/(1.01-orbtime*0.0032)
      if freq > 10 then freq = 10 end
      local amp = (1-math.cos(freq*orbtime))*freq*0.5
      lg.setColor(255/DIV,255/DIV,255/DIV, amp^0.7*(255/DIV))
      orbdangersnd:setVolume(amp^2)
      lg.draw(spriteset, spritequads[orb_sprite+1], orbx, orby)
      lg.setColor(255/DIV,255/DIV,255/DIV,255/DIV)
      lg.setBlendMode("alpha", "alphamultiply")
    end
  end

  -- draw ship
  if counters.deadtimer then
    if ship.crashframe then
      lg.draw(crashset, crashquads[ship.crashframe], ship.x-vpx-16, ship.y-vpy-16)
    end
  else
    lg.draw(spriteset, spritequads[ship.angle+1], math.floor(ship.x-vpx-15.5), math.floor(ship.y-vpy-15.5))
  end

  -- HACK: draw target in multiplicative mode (colorizes other sprites)
  if tgt then
    if love_version < 0010000 then
      lg.setBlendMode("multiplicative")
    else
      lg.setBlendMode("multiply", "premultiplied")
    end
    lg.draw(tileset, tilequads[tgt_tile_current], tgt.x*32-vpx, tgt.y*32-vpy)
    lg.setBlendMode("alpha", "alphamultiply")
  end

  -- draw shooting explosion
  if counters.shoot_timer then
    -- colorize explosion - red stays at 255,
    -- green diminishes slow and blue diminishes quick
    local red, green, blue
    local t = counters.shoot_timer/total_shoot_time -- from 1 to 0
    red = 255
    green = 255*t^1.5
    blue = 255*t^6
    lg.setColor(red/DIV, green/DIV, blue/DIV, 255/DIV*(counters.shoot_timer/total_shoot_time))
    local drawx = counters.shoot_x - vpx
    if drawx < -2048 then drawx = drawx + 4096 end
    if drawx > 2048 then drawx = drawx - 4096 end
    lg.draw(shotimg, drawx, counters.shoot_y-vpy, 0, shoot_radius/256, shoot_radius/256, 256, 256)
    lg.setColor(255/DIV, 255/DIV, 255/DIV, 255/DIV)
  end

  -- FIXME: draw current agent
  if counters.agent then
    for k, v in pairs(agents) do
      if v == counters.agent then
        lg.draw(tileset, tilequads[k], 0, main.wh-32)
        lg.print(counters.agenttime, 35, main.wh-24)
        local num = 0
        for i = 1, #game.state.enemies do
          if game.state.enemies[i].type == counters.agent then
            num = num + 1
          end
        end
        lg.print(num, 10, main.wh-48)
      end
    end
  end
  -- FIXME: draw current shields
  lg.print(string.format("%02d", counters.shields), 96, main.wh-24)

  -- FIXME: draw current orb timer
  if counters.orbtimer then
    lg.print(string.format("%03d", counters.orbtimer), 144, main.wh-24)
  end
  lg.print(string.format("%8d", counters.score), 160, main.wh - 24)

  lg.setScissor()


  --[[ debug
  lg.print(game.DEBUG, 0, 0)
  -- draw collision canvas
  lg.setColor(100/DIV,100/DIV,100/DIV)
  lg.rectangle("fill", 100, 100, 32, 64)
  lg.setColor(255/DIV,255/DIV,255/DIV)
  lg.draw(collision_canvas, 100, 100)
  ]]
end

function game.keypressed(k, r)
  if r then return end
  if k == "escape" then
    return main.dialog("EXIT TO MENU?", main.tomenu)
  end
  if k == "f10" then game.savegame() end
  if k == "f3" then
    new_or_load_game(true)
    screens.getready.fromstart = false
    return main.activate(screens.getready)
  end
end

function game.resize(neww, newh)
  game.vx = 0
  game.vy = 0
  game.vw = main.ww
  game.vh = main.wh
  tilebatch = lg.newSpriteBatch(tileset,
                -- In 1D, a 33-pixel window can see up to two 32-pixel tiles
                -- simultaneously. One needs a 34-pixel window to be able
                -- to see three. So in 1D it would be: floor((widht+62)/32)
                -- which equals 2 for width=33 and 3 for width=34. In 2D the
                -- natural extension is the following.
                math.floor((game.vw + 62)*(game.vh + 62)/(32*32))
                -- For 640x480, that's 336 tiles. Less than the default 1000,
                -- so quite bearable.
              )
  -- force refresh
  lastxtile = false
end

return game
