local P=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\065\098\105\108\105\116\121")local E=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\099\117\115\116\111\109\095\101\118\101\110\116\115")local C=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\068\097\109\097\103\101\073\110\115\116\097\110\099\101")local u=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\085\110\105\116")local F="\067\111\117\108\100 \110\111\116 \102\105\110\100 \097\098\105\108\105\116\121 \111\114 \105\116\101\109 \110\097\109\101\100 \'\037\113\' \111\110 \117\110\105\116 \'\037\115\'"function GameMode.OnGameRulesStateChange(C,P,E)C:d("\079\110\071\097\109\101\082\117\108\101\115\083\116\097\116\101\067\104\097\110\103\101",{state=P,payload=E})end function GameMode.OnConnectFull(C,P,E)C:d("\079\110\067\111\110\110\101\099\116\070\117\108\108",{player=P:GetPlayerID()})C.users[E]=P C.players[P:GetPlayerID()]=P end function GameMode.OnFirstPlayerLoaded(P)P:d("\079\110\070\105\114\115\116\080\108\097\121\101\114\076\111\097\100\101\100")end function GameMode.OnAllPlayersLoaded(P)P:d("\079\110\065\108\108\080\108\097\121\101\114\115\076\111\097\100\101\100")end function GameMode.OnHeroInGame(C,P)C:d("\079\110\072\101\114\111\073\110\071\097\109\101",{hero=P:GetUnitName()})local u=P:GetPlayerID()local F=PlayerResource:GetPlayer(u)local Z={id=P:GetHeroID();name=P:GetUnitName(),variant=P:GetHeroFacetID()}E.SendPlayer(E.EVENT_PLAYER_HERO_IN_GAME,F,Z)end function GameMode.OnGameInProgress(P)P:d("\079\110\071\097\109\101\073\110\080\114\111\103\114\101\115\115")end function GameMode.OnEntityHurt(F,E)F:d("\079\110\069\110\116\105\116\121\072\117\114\116",{payload=E})local Z=u(EntIndexToHScript(E.entindex_killed))local r if E.entindex_attacker~=nil then r=u(EntIndexToHScript(E.entindex_attacker))end local s if E.entindex_inflictor~=nil and E.damagebits==0 then s=P(EntIndexToHScript(E.entindex_inflictor))end local G=C(Z,E.damage,r,s)F.combos:OnEntityHurt(G)end function GameMode.OnItemPurchased(E,P)E:d("\079\110\073\116\101\109\080\117\114\099\104\097\115\101\100",{payload=P})local C=PlayerResource:GetPlayer(P.PlayerID)E.combos:OnItemPurchased(C,{item=P.itemname,cost=P.itemcost})end function GameMode.OnAbilityCastBegins(E,P)E:d("\079\110\065\098\105\108\105\116\121\067\097\115\116\066\101\103\105\110\115",{payload=P})end function GameMode.OnAbilityUsed(C,E)C:d("\079\110\065\098\105\108\105\116\121\085\115\101\100",{payload=E})local Z=PlayerResource:GetPlayer(E.PlayerID)local r=u(EntIndexToHScript(E.caster_entindex))local s=r:FindAbilityOrItem(E.abilityname)if s==nil then local P=F:format(E.abilityname,r.name)error(P)end local G=P(s)C.combos:OnAbilityUsed(Z,r,G)end function GameMode.OnNonPlayerUsedAbility(E,P)E:d("\079\110\078\111\110\080\108\097\121\101\114\085\115\101\100\065\098\105\108\105\116\121",{payload=P})end function GameMode.OnAbilityChannelFinished(E,P)E:d("\079\110\065\098\105\108\105\116\121\067\104\097\110\110\101\108\070\105\110\105\115\104\101\100",{payload=P})end function GameMode.OnEntityKilled(u,P,E,C)u:d("\079\110\069\110\116\105\116\121\075\105\108\108\101\100",{killed=P:GetName();attacker=E and E:GetName(),inflictor=C and C:GetName()})end
--- Game Events Listeners
-- @submodule invokation.GameMode
local Ability = require("invokation.dota2.Ability")
local CustomEvents = require("invokation.dota2.custom_events")
local DamageInstance = require("invokation.dota2.DamageInstance")
local Unit = require("invokation.dota2.Unit")

local ERRF_ABILITY_OR_ITEM_NOT_FOUND = "Could not find ability or item named '%q' on unit '%s'"

--- Game Events Listeners
-- @section game_events

--- The overall game state has changeds.
-- @tparam string state
-- @tparam table payload
function GameMode:OnGameRulesStateChange(state, payload)
  self:d("OnGameRulesStateChange", { state = state, payload = payload })
end

--- Called once when the player fully connects and becomes "Ready" during
-- loading.
-- @tparam CDOTAPlayer player
function GameMode:OnConnectFull(player, user_id)
  self:d("OnConnectFull", { player = player:GetPlayerID() })

  self.users[user_id] = player
  self.players[player:GetPlayerID()] = player
end

--- Called once and only once as soon as the first player (almost certain to be
-- the server in local lobbies) loads in.
--
-- It can be used to initialize state that isn't initializeable in @{Activate}
-- but needs to be done before everyone loads in.
function GameMode:OnFirstPlayerLoaded()
  self:d("OnFirstPlayerLoaded")
end

--- Called once and only once after all players have loaded into the game,
-- right as the hero selection time begins.
--
-- It can be used to initialize non-hero player state or adjust the hero
-- selection (i.e. force random etc)
function GameMode:OnAllPlayersLoaded()
  self:d("OnAllPlayersLoaded")
end

--- Called once and only once for every player when they spawn into the game
-- for the first time.
--
-- It is also called if the player's hero is replaced with a new hero for any
-- reason. This function is useful for initializing heroes, such as adding
-- levels, changing the starting gold, removing/adding abilities, adding
-- physics, etc.
--
-- @tparam CDOTA_BaseNPC_Hero hero
function GameMode:OnHeroInGame(hero)
  self:d("OnHeroInGame", { hero = hero:GetUnitName() })

  local player_id = hero:GetPlayerID()
  local player = PlayerResource:GetPlayer(player_id)
  local payload = {
    id = hero:GetHeroID(),
    name = hero:GetUnitName(),
    variant = hero:GetHeroFacetID(),
  }

  CustomEvents.SendPlayer(CustomEvents.EVENT_PLAYER_HERO_IN_GAME, player, payload)
end

--- Called once and only once when the game completely begins (about 0:00 on the clock).
function GameMode:OnGameInProgress()
  self:d("OnGameInProgress")
end

--- An entity has been hurt.
-- @tparam table payload
-- @tparam number payload.damage Damage amount
-- @tparam int payload.entindex_killed Victim (unit) entity index
-- @tparam[opt] int payload.entindex_attacker Attacker (unit) entity index
-- @tparam[opt] int payload.entindex_inflictor Inflictor (item, ability, etc) entity index
function GameMode:OnEntityHurt(payload)
  self:d("OnEntityHurt", { payload = payload })

  local victim = Unit(EntIndexToHScript(payload.entindex_killed))

  local attacker
  if payload.entindex_attacker ~= nil then
    attacker = Unit(EntIndexToHScript(payload.entindex_attacker))
  end

  local inflictor
  -- FIXME: This is a hack to fix an issue when an entity is killed with `ForceKill`,
  -- FIXME: which will trigger this event. I'm not sure which enum `damagebits` is using,
  -- FIXME: but regular damage seems to always set it to 0 while `ForceKill` sets it to 4096
  if payload.entindex_inflictor ~= nil and payload.damagebits == 0 then
    inflictor = Ability(EntIndexToHScript(payload.entindex_inflictor))
  end

  local damage = DamageInstance(victim, payload.damage, attacker, inflictor)

  self.combos:OnEntityHurt(damage)
end

--- An item was purchased by a player.
-- @tparam table payload
-- @tparam int payload.PlayerID Player id
-- @tparam string payload.itemname Item name
-- @tparam number payload.itemcost Item cost
function GameMode:OnItemPurchased(payload)
  self:d("OnItemPurchased", { payload = payload })

  local player = PlayerResource:GetPlayer(payload.PlayerID)

  self.combos:OnItemPurchased(player, { item = payload.itemname, cost = payload.itemcost })
end

--- Called whenever an ability begins its PhaseStart phase (but before it is actually cast).
-- @tparam table payload
-- @tparam int payload.PlayerID Player id
-- @tparam string payload.abilityname Ability name
function GameMode:OnAbilityCastBegins(payload)
  self:d("OnAbilityCastBegins", { payload = payload })
end

--- An ability was used by a player (including items).
-- @tparam table payload
-- @tparam int payload.PlayerID Player id
-- @tparam int payload.caster_entindex Caster (unit) entity index
-- @tparam string payload.abilityname Ability name
function GameMode:OnAbilityUsed(payload)
  self:d("OnAbilityUsed", { payload = payload })

  local player = PlayerResource:GetPlayer(payload.PlayerID)
  local caster = Unit(EntIndexToHScript(payload.caster_entindex))
  local abilityEnt = caster:FindAbilityOrItem(payload.abilityname)

  if abilityEnt == nil then
    local err = ERRF_ABILITY_OR_ITEM_NOT_FOUND:format(payload.abilityname, caster.name)
    error(err)
  end

  local ability = Ability(abilityEnt)

  self.combos:OnAbilityUsed(player, caster, ability)
end

--- A non-player entity (necro-book, chen creep, etc) used an ability.
-- @tparam table payload
-- @tparam int payload.caster_entindex Caster (unit) entity index
-- @tparam string payload.abilityname Ability name
function GameMode:OnNonPlayerUsedAbility(payload)
  self:d("OnNonPlayerUsedAbility", { payload = payload })
end

--- A channelled ability finished by either completing or being interrupted.
-- @tparam table payload
-- @tparam string payload.abilityname Ability name
-- @tparam int payload.interrupted `1` if ability was interrupted
function GameMode:OnAbilityChannelFinished(payload)
  self:d("OnAbilityChannelFinished", { payload = payload })
end

--- An entity died.
-- @tparam CDOTA_BaseNPC killed Killed unit
-- @tparam[opt] CDOTA_BaseNPC attacker Attacker unit
-- @tparam[opt] CDOTABaseAbility inflictor Inflictor ability
function GameMode:OnEntityKilled(killed, attacker, inflictor)
  self:d("OnEntityKilled", {
    killed = killed:GetName(),
    attacker = attacker and attacker:GetName(),
    inflictor = inflictor and inflictor:GetName(),
  })
end
