# Contributors outside of DragonRuby who also hold Copyright:
# - Sujay Vadlakonda: https://github.com/sujayvadlakonda

# An example of some major components in a tower defence game
# The pathing of the tanks is determined by A* algorithm -- try editing the walls

# The turrets shoot bullets at the closest tank. The bullets are heat-seeking

def tick args
  GTK.reset if args.inputs.keyboard.key_down.r
  defaults args
  render args
  calc args
end

def defaults args
  args.outputs.background_color = wall_color
  args.state.grid_size = 5
  args.state.tile_size = 50
  args.state.grid_start ||= [0, 0]
  args.state.grid_goal  ||= [4, 4]

  # Try editing these walls to see the path change!
  args.state.walls ||= {
    [0, 4] => true,
    [1, 3] => true,
    [3, 1] => true,
    # [4, 0] => true,
  }

  args.state.a_star.frontier ||= []
  args.state.a_star.came_from ||= {}
  args.state.a_star.path ||= []

  args.state.tanks ||= []
  args.state.tank_spawn_period ||= 60
  args.state.tank_sprite_path ||= 'sprites/circle/white.png'
  args.state.tank_speed ||= 1

  args.state.turret_shoot_period = 10
  # Turrets can be entered as [x, y] but are immediately mapped to hashes
  # Walls are also added where the turrets are to prevent tanks from pathing over them
  args.state.turrets ||= [
    [2, 2]
  ].each { |turret| args.state.walls[turret] = true}.map do |x, y|
    {
      x: x * args.state.tile_size,
      y: y * args.state.tile_size,
      w: args.state.tile_size,
      h: args.state.tile_size,
      path: 'sprites/circle/gray.png',
      range: 100
    }
  end

  args.state.bullet_size ||= 25
  args.state.bullets ||= []
  args.state.bullet_path ||= 'sprites/circle/orange.png'
end

def render args
  render_grid args
  render_a_star args
  args.outputs.sprites << args.state.tanks
  args.outputs.sprites << args.state.turrets
  args.outputs.sprites << args.state.bullets
end

def render_grid args
  # Draw a square the size and color of the grid
  args.outputs.solids << {
    x: 0,
    y: 0,
    w: args.state.grid_size * args.state.tile_size,
    h: args.state.grid_size * args.state.tile_size,
  }.merge(grid_color)

  # Draw lines across the grid to show tiles
  (args.state.grid_size + 1).times do | value |
    render_horizontal_line(args, value)
    render_vertical_line(args, value)
  end

  # Render special tiles
  render_tile(args, args.state.grid_start, start_color)
  render_tile(args, args.state.grid_goal, goal_color)
  args.state.walls.keys.each { |wall| render_tile(args, wall, wall_color) }
end

def render_vertical_line args, x
  args.outputs.lines << {
    x: x * args.state.tile_size,
    y: 0,
    w: 0,
    h: args.state.grid_size * args.state.tile_size
  }
end

def render_horizontal_line args, y
  args.outputs.lines << {
    x: 0,
    y: y * args.state.tile_size,
    w: args.state.grid_size * args.state.tile_size,
    h: 0
  }
end

def render_tile args, tile, color
  args.outputs.solids << {
    x: tile.x * args.state.tile_size,
    y: tile.y * args.state.tile_size,
    w: args.state.tile_size,
    h: args.state.tile_size,
    r: color[0],
    g: color[1],
    b: color[2]
  }
end

def calc args
  calc_a_star args
  calc_tanks args
  calc_turrets args
  calc_bullets args
end

def calc_a_star args
  # Only does this one time
  return unless args.state.a_star.path.empty?

  # Start the search from the grid start
  args.state.a_star.frontier << args.state.grid_start
  args.state.a_star.came_from[args.state.grid_start] = nil

  # Until a path to the goal has been found or there are no more tiles to explore
  until (args.state.a_star.came_from.key?(args.state.grid_goal) || args.state.a_star.frontier.empty?)
    # For the first tile in the frontier
    tile_to_expand_from = args.state.a_star.frontier.shift
    # Add each of its neighbors to the frontier
    neighbors(args, tile_to_expand_from).each do |tile|
      args.state.a_star.frontier << tile
      args.state.a_star.came_from[tile] = tile_to_expand_from
    end
  end

  # Stop calculating a path if the goal was never reached
  return unless args.state.a_star.came_from.key? args.state.grid_goal

  # Fill path by tracing back from the goal
  current_cell = args.state.grid_goal
  while current_cell
    args.state.a_star.path.unshift current_cell
    current_cell = args.state.a_star.came_from[current_cell]
  end

  puts "The path has been calculated"
  puts args.state.a_star.path
end

def calc_tanks args
  spawn_tank args
  move_tanks args
end

def move_tanks args
  # Remove tanks that have reached the end of their path
  args.state.tanks.reject! { |tank| tank[:a_star].empty? }

  # Tanks have an array that has each tile it has to go to in order from a* path
  args.state.tanks.each do | tank |
    destination = tank[:a_star][0]
    # Move the tank towards the destination
    tank[:x] += copy_sign(args.state.tank_speed, ((destination.x * args.state.tile_size) - tank[:x]))
    tank[:y] += copy_sign(args.state.tank_speed, ((destination.y * args.state.tile_size) - tank[:y]))
    # If the tank has reached its destination
    if (destination.x * args.state.tile_size) == tank[:x] &&
        (destination.y * args.state.tile_size) == tank[:y]
      # Set the destination to the next point in the path
      tank[:a_star].shift
    end
  end
end

def calc_turrets args
  return unless Kernel.tick_count.mod_zero? args.state.turret_shoot_period
  args.state.turrets.each do | turret |
    # Finds the closest tank
    target = nil
    shortest_distance = turret[:range] + 1
    args.state.tanks.each do | tank |
      distance = distance_between(turret[:x], turret[:y], tank[:x], tank[:y])
      if distance < shortest_distance
        target = tank
        shortest_distance = distance
      end
    end
    # If there is a tank in range, fires a bullet
    if target
      args.state.bullets << {
        x: turret[:x],
        y: turret[:y],
        w: args.state.bullet_size,
        h: args.state.bullet_size,
        path: args.state.bullet_path,
        # Note that this makes it heat-seeking, because target is passed by reference
        # Could do target.clone to make the bullet go to where the tank initially was
        target: target
      }
    end
  end
end

def calc_bullets args
  # Bullets aim for the center of their targets
  args.state.bullets.each { |bullet| move bullet, center_of(bullet[:target])}
  args.state.bullets.reject! { |b| b.intersect_rect? b[:target] }
end

def center_of object
  object = object.clone
  object[:x] += 0.5
  object[:y] += 0.5
  object
end

def render_a_star args
  args.state.a_star.path.map do |tile|
    # Map each x, y coordinate to the center of the tile and scale up
    [(tile.x + 0.5) * args.state.tile_size, (tile.y + 0.5) * args.state.tile_size]
  end.inject do | point_a,  point_b |
    # Render the line between each point
    args.outputs.lines << [point_a.x, point_a.y, point_b.x, point_b.y, a_star_color]
    point_b
  end
end

# Moves object to target at speed
def move object, target, speed = 1
  if target.is_a? Hash
    object[:x] += copy_sign(speed, target[:x] - object[:x])
    object[:y] += copy_sign(speed, target[:y] - object[:y])
  else
    object[:x] += copy_sign(speed, target.x - object[:x])
    object[:y] += copy_sign(speed, target.y - object[:y])
  end
end


def distance_between a_x, a_y, b_x, b_y
  (((b_x - a_x) ** 2) + ((b_y - a_y) ** 2)) ** 0.5
end

def copy_sign value, sign
  return 0 if sign == 0
  return value if sign > 0
  -value
end

def spawn_tank args
  return unless Kernel.tick_count.mod_zero? args.state.tank_spawn_period
  args.state.tanks << {
    x: args.state.grid_start.x,
    y: args.state.grid_start.y,
    w: args.state.tile_size,
    h: args.state.tile_size,
    path: args.state.tank_sprite_path,
    a_star: args.state.a_star.path.clone
  }
end

def neighbors args, tile
  [[tile.x, tile.y - 1],
   [tile.x, tile.y + 1],
   [tile.x + 1, tile.y],
   [tile.x - 1, tile.y]].reject do |neighbor|
    args.state.a_star.came_from.key?(neighbor) || tile_out_of_bounds?(args, neighbor) ||
      args.state.walls.key?(neighbor)
  end
end

def tile_out_of_bounds? args, tile
  tile.x < 0 || tile.y < 0 || tile.x >= args.state.grid_size || tile.y >= args.state.grid_size
end

def grid_color
  { r: 133, g: 226, b: 144 }
end

def start_color
  [226, 144, 133]
end

def goal_color
  [226, 133, 144]
end

def wall_color
  [133, 144, 226]
end

def a_star_color
  [0, 0, 255]
end
