# this class encapsulates the Game and shows
# how to manage animations states. The components
# that control animation states are action, action_at, and Numeric.frame
class Game
  # expose player and enemies as public properties
  # from the Console you can see their values via $game.player and $game.enemies
  attr :player, :enemies

  # DragonRuby class macro that allows you to access inputs, outputs, state, etc
  # without passing args everywhere
  attr_gtk

  def initialize
    # when the game is constructed, create the player at the center of the screen
    @player = {
      speed: 3,
      x: 640,
      y: 360,
      w: 64,
      h: 64,
      x_dir: 1, # the direction the player is facing
      action: :idle, # set the player's current action to :idle
      action_at: 0,  # set the player's action timestamp to 0
      action_lookup: { # frame data for each action
        # when player is standing still
        idle: {
          path: "sprites/horizontal-stand.png",
          hold_for: 3,
          frame_count: 1,
          repeat: true
        },
        # when player is moving
        run: {
          path: "sprites/horizontal-run.png",
          hold_for: 3,
          frame_count: 6,
          repeat: true
        },
        # when player is attacking
        slash: {
          path: "sprites/horizontal-slash.png",
          hold_for: 3,
          frame_count: 5,
          repeat: false
        }
      }
    }

    # collection of enemies
    @enemies = []
  end

  def player_current_action_lookup
    @player.action_lookup[@player.action]
  end

  def player_frame
    # get the frame data for the current action the player is in
    action_lookup = player_current_action_lookup

    # Numeric.frame returns the following hash
    # For example, this would be the frame data for performing an attack
    # {
    #   frame_index: 3,
    #   frame_count: 5,
    #   frames_left: 2,
    #   started: true,
    #   completed: false,
    #   duration: 15,
    #   elapsed_time: 10,
    #   frame_elapsed_time: 1
    # }
    Numeric.frame(start_at: @player.action_at,
                  frame_count: action_lookup.frame_count,
                  hold_for: action_lookup.hold_for,
                  repeat: action_lookup.repeat)
  end

  # function adds an enemy to the enemies collection
  def add_enemy
    @enemies << {
      x: 1200 * rand,
      y: 600 * rand,
      w: 64,
      h: 64,
      anchor_x: 0.5,
      anchor_y: 0.5,
    }
  end

  # return the sprite to display based on the players current action
  def player_prefab
    # first get the action frame data for the player's current action
    # the lookup contains the sprite to display
    lookup = player_current_action_lookup

    # then get the frame information
    frame = player_frame

    {
      x: @player.x,
      y: @player.y,
      w: 128,
      h: 128,
      path: lookup.path, # lookup path
      tile_x: 128 * frame.frame_index, # the pngs are tile sheets, so we offset the tile_x by the frame index
      tile_y: 0,
      tile_w: 128,
      tile_h: 128,
      anchor_x: 0.5,
      anchor_y: 0.5,
      flip_horizontally: @player.x_dir == 1
    }
  end

  # return the representation of an enemy (int this case it's just a solid box)
  def enemy_prefab enemy
    { **enemy, path: :solid, r: 0, g: 0, b: 0, a: 128 }
  end

  # this represents the rectang for the player's sword
  def player_slash_rect
    {
      x: player.x + player.x_dir * 40,
      y: player.y,
      w: 40,
      h: 20,
      anchor_x: 0.5,
      anchor_y: 0.5,
      path: :solid,
      r: 0, g: 0, b: 0
    }
  end

  # slash is requested if controller's A is pressed,
  # J is pressed on the keyboard,
  # or enter is pressed on the keyboard
  def player_slash_requested?
    inputs.controller_one.key_down.a ||
    inputs.keyboard.key_down.j       ||
    inputs.keyboard.key_down.enter
  end

  # the slash of the player can damage an enemy
  # when the animation first transitions to frame index 2
  def player_slash_can_damage?
    @player.action == :slash &&
    player_frame.frame_index == 2 &&
    player_frame.frame_elapsed_time == 0
  end

  def player_action! action
    # set the player action and the timestamp for the action
    # if the player isn't already in that action
    return if @player.action == action
    @player.action = action
    @player.action_at = Kernel.tick_count
  end

  def tick
    # if no enemies exist in the enemies collection,
    # add an enemy at a random location
    add_enemy if @enemies.length == 0

    # if slash is requested, then put the player in the :slash action
    if player_slash_requested?
      player_action! :slash
    end

    # if :slash is completed, then move the player back to idle
    if @player.action == :slash
      if player_frame.completed
        player_action! :idle
      end
    else
      # get the directional vector for the player
      vec = inputs.directional_vector

      # if WASD/arrow keys/DPAD is being activated
      if vec
        # increment player's x by the vector x multiplied by speed
        @player.x += @player.speed * vec.x

        # increment player's y by the vector y multiplied by speed
        @player.y += @player.speed * vec.y

        # set the player's facing direction equal to vec.x's sign if vec.x is not zero
        if vec.x != 0
          @player.x_dir = vec.x.sign
        end

        # set the player action to run
        player_action! :run
      else
        # if no directional vector is being pressed then set the player to idle
        player_action! :idle
      end
    end

    # if the player can damage an enemy
    if player_slash_can_damage?
      # delete all enemies that intersect with the player's sword
      @enemies.reject! { |e| Geometry.intersect_rect? e, player_slash_rect }
    end

    outputs.watch "player action: #{@player.action}: #{@player.action_at}"
    outputs.watch "player frame data: #{pretty_format player_frame}"

    # render the player, they sword collision rect, and enemies
    outputs.primitives << player_slash_rect
    outputs.primitives << player_prefab
    outputs.primitives << @enemies.map { |e| enemy_prefab e }
  end
end

def boot args
  args.state = {}
end

def tick args
  # new up the game if it hasn't been initialized
  $game ||= Game.new
  # set args on the game
  $game.args = args
  # run tick
  $game.tick
end

# if reset is called, then set the game to nil so that it can be initialized again
def reset args
  $game = nil
end

GTK.reset