Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into add-team-attribute-to…
Browse files Browse the repository at this point in the history
…-entities
  • Loading branch information
Nico-Sanchez committed Dec 2, 2024
2 parents d4db338 + 6a699ce commit 5520788
Show file tree
Hide file tree
Showing 19 changed files with 840 additions and 1,143 deletions.
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
elixir 1.16.3-otp-26
erlang 26.2.5.5
1 change: 1 addition & 0 deletions apps/arena/lib/arena/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Arena.Application do
Arena.Matchmaking.GameLauncher,
Arena.Matchmaking.PairMode,
Arena.Matchmaking.QuickGameMode,
Arena.Matchmaking.DeathmatchMode,
Arena.GameBountiesFetcher,
Arena.GameTracker,
Arena.Authentication.GatewaySigner,
Expand Down
16 changes: 15 additions & 1 deletion apps/arena/lib/arena/game/player.ex
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,13 @@ defmodule Arena.Game.Player do
end

execution_duration = calculate_duration(skill, player.position, skill_direction, auto_aim?)
Process.send_after(self(), {:block_actions, player.id, false}, execution_duration)

# For dash and leaps, we rely the unblock action message to their stop action callbacks
is_dash_or_leap? = Enum.any?(skill.mechanics, fn mechanic -> mechanic.type in ["leap", "dash"] end)

unless is_dash_or_leap? do
Process.send_after(self(), {:block_actions, player.id, false}, execution_duration)
end

if skill.block_movement do
send(self(), {:block_movement, player.id, true})
Expand Down Expand Up @@ -450,6 +456,14 @@ defmodule Arena.Game.Player do
end)
end

def respawn_player(player, position) do
aditional_info = player.aditional_info |> Map.put(:health, player.aditional_info.base_health)

player
|> Map.put(:aditional_info, aditional_info)
|> Map.put(:position, position)
end

####################
# Internal helpers #
####################
Expand Down
11 changes: 11 additions & 0 deletions apps/arena/lib/arena/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ defmodule Arena.GameSocketHandler do
end
end

def websocket_info({:respawn_player, player_id}, state) do
state =
if state.player_id == player_id do
state |> Map.put(:enable, true) |> Map.put(:player_alive, true)
else
state
end

{:ok, state}
end

@impl true
def websocket_info({:block_actions, player_id, value}, state) do
if state.player_id == player_id do
Expand Down
95 changes: 92 additions & 3 deletions apps/arena/lib/arena/game_updater.ex
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ defmodule Arena.GameUpdater do
|> activate_trap_mechanics()
# Obstacles
|> handle_obstacles_transitions()
# Deathmatch
|> add_players_to_respawn_queue(state.game_config)
|> respawn_players(state.game_config)

{:ok, state_diff} = diff(state.last_broadcasted_game_state, game_state)

Expand Down Expand Up @@ -311,7 +314,12 @@ defmodule Arena.GameUpdater do
Process.send_after(self(), :match_timeout, state.game_config.game.match_timeout_ms)

send(self(), :natural_healing)
send(self(), {:end_game_check, Map.keys(state.game_state.players)})

if state.game_config.game.game_mode != :DEATHMATCH do
send(self(), {:end_game_check, Map.keys(state.game_state.players)})
else
Process.send_after(self(), :deathmatch_end_game_check, state.game_config.game.match_duration)
end

unless state.game_config.game.bots_enabled do
toggle_bots(self())
Expand All @@ -320,6 +328,36 @@ defmodule Arena.GameUpdater do
{:noreply, put_in(state, [:game_state, :status], :RUNNING)}
end

def handle_info(:deathmatch_end_game_check, state) do
players =
state.game_state.players
|> Enum.map(fn {player_id, player} ->
%{kills: kills} = GameTracker.get_player_result(player_id)
{player_id, player, kills}
end)
|> Enum.sort_by(fn {_player_id, _player, kills} -> kills end, :desc)

{winner_id, winner, _kills} = Enum.at(players, 0)

state =
state
|> put_in([:game_state, :status], :ENDED)
|> update_in([:game_state], fn game_state ->
players
|> Enum.reduce(game_state, fn {player_id, _player, _kills}, game_state_acc ->
put_player_position(game_state_acc, player_id)
end)
end)

PubSub.broadcast(Arena.PubSub, state.game_state.game_id, :end_game_state)
broadcast_game_ended(winner, state.game_state)
GameTracker.finish_tracking(self(), winner_id)

Process.send_after(self(), :game_ended, state.game_config.game.shutdown_game_wait_ms)

{:noreply, state}
end

def handle_info({:end_game_check, last_players_ids}, state) do
case check_game_ended(state.game_state.players, last_players_ids) do
{:ongoing, players_ids} ->
Expand Down Expand Up @@ -377,6 +415,9 @@ defmodule Arena.GameUpdater do
Map.get(state.game_state.players, player_id)
|> Player.reset_forced_movement(previous_speed)

# Dash finished, we unblock the actions.
broadcast_player_block_actions(state.game_state.game_id, player_id, false)

state = put_in(state, [:game_state, :players, player_id], player)
{:noreply, state}
end
Expand All @@ -390,6 +431,9 @@ defmodule Arena.GameUpdater do
put_in(state.game_state, [:players, player_id], player)
|> Skill.do_mechanic(player, on_arrival_mechanic, %{skill_direction: player.direction})

# Leap finished, we unblock the actions.
broadcast_player_block_actions(state.game_state.game_id, player_id, false)

{:noreply, %{state | game_state: game_state}}
end

Expand Down Expand Up @@ -747,6 +791,10 @@ defmodule Arena.GameUpdater do
PubSub.broadcast(Arena.PubSub, state.game_id, {:game_finished, encoded_state})
end

defp broadcast_player_respawn(game_id, player_id) do
PubSub.broadcast(Arena.PubSub, game_id, {:respawn_player, player_id})
end

defp complete_entities(nil, _), do: []

defp complete_entities(entities, category) do
Expand Down Expand Up @@ -809,7 +857,7 @@ defmodule Arena.GameUpdater do
square_wall: config.map.square_wall,
zone: %{
radius: config.map.radius - 5000,
should_start?: config.game.zone_enabled,
should_start?: if(config.game.game_mode == :DEATHMATCH, do: false, else: config.game.zone_enabled),
started: false,
enabled: false,
shrinking: false,
Expand All @@ -820,7 +868,8 @@ defmodule Arena.GameUpdater do
status: :PREPARING,
start_game_timestamp: initial_timestamp + config.game.start_game_time_ms + config.game.bounty_pick_time_ms,
positions: %{},
traps: %{}
traps: %{},
respawn_queue: %{}
}
end

Expand Down Expand Up @@ -1885,6 +1934,46 @@ defmodule Arena.GameUpdater do
end
end

defp add_players_to_respawn_queue(game_state, %{game: %{game_mode: :DEATHMATCH}} = game_config) do
now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

respawn_queue =
Enum.reduce(game_state.players, game_state.respawn_queue, fn {player_id, player}, respawn_queue ->
if Map.has_key?(respawn_queue, player_id) || Player.alive?(player) do
respawn_queue
else
Map.put(respawn_queue, player_id, now + game_config.game.respawn_time)
end
end)

Map.put(game_state, :respawn_queue, respawn_queue)
end

defp add_players_to_respawn_queue(game_state, _game_config), do: game_state

defp respawn_players(game_state, %{game: %{game_mode: :DEATHMATCH}} = game_config) do
now = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

players_to_respawn =
game_state.respawn_queue
|> Enum.filter(fn {_player_id, time} ->
time < now
end)

{game_state, respawn_queue} =
Enum.reduce(players_to_respawn, {game_state, game_state.respawn_queue}, fn {player_id, _time},
{game_state, respawn_queue} ->
new_position = Enum.random(game_config.map.initial_positions)
player = Map.get(game_state.players, player_id) |> Player.respawn_player(new_position)
broadcast_player_respawn(game_state.game_id, player_id)
{put_in(game_state, [:players, player_id], player), Map.delete(respawn_queue, player_id)}
end)

Map.put(game_state, :respawn_queue, respawn_queue)
end

defp respawn_players(game_state, _game_config), do: game_state

##########################
# End Helpers
##########################
Expand Down
1 change: 1 addition & 0 deletions apps/arena/lib/arena/matchmaking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ defmodule Arena.Matchmaking do
def get_queue("battle-royale"), do: Arena.Matchmaking.GameLauncher
def get_queue("pair"), do: Arena.Matchmaking.PairMode
def get_queue("quick-game"), do: Arena.Matchmaking.QuickGameMode
def get_queue("deathmatch"), do: Arena.Matchmaking.DeathmatchMode
def get_queue(:undefined), do: Arena.Matchmaking.GameLauncher
end
146 changes: 146 additions & 0 deletions apps/arena/lib/arena/matchmaking/deathmatch_mode.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
defmodule Arena.Matchmaking.DeathmatchMode do
@moduledoc false
alias Arena.Utils
alias Ecto.UUID

use GenServer

# 3 Mins
# TODO: add this to the configurator https://github.com/lambdaclass/mirra_backend/issues/985
@match_duration 180_000
@respawn_time 5000

# API
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

def join(client_id, character_name, player_name) do
GenServer.call(__MODULE__, {:join, client_id, character_name, player_name})
end

def leave(client_id) do
GenServer.call(__MODULE__, {:leave, client_id})
end

# Callbacks
@impl true
def init(_) do
Process.send_after(self(), :launch_game?, 300)
{:ok, %{clients: [], batch_start_at: 0}}
end

@impl true
def handle_call({:join, client_id, character_name, player_name}, {from_pid, _}, %{clients: clients} = state) do
batch_start_at = maybe_make_batch_start_at(state.clients, state.batch_start_at)

client = %{
client_id: client_id,
character_name: character_name,
name: player_name,
from_pid: from_pid,
type: :human
}

{:reply, :ok,
%{
state
| batch_start_at: batch_start_at,
clients: clients ++ [client]
}}
end

def handle_call({:leave, client_id}, _, state) do
clients = Enum.reject(state.clients, fn %{client_id: id} -> id == client_id end)
{:reply, :ok, %{state | clients: clients}}
end

@impl true
def handle_info(:launch_game?, %{clients: clients} = state) do
Process.send_after(self(), :launch_game?, 300)
diff = System.monotonic_time(:millisecond) - state.batch_start_at

if length(clients) >= Application.get_env(:arena, :players_needed_in_match) or
(diff >= Utils.start_timeout_ms() and length(clients) > 0) do
send(self(), :start_game)
end

{:noreply, state}
end

def handle_info(:start_game, state) do
{game_clients, remaining_clients} = Enum.split(state.clients, Application.get_env(:arena, :players_needed_in_match))
create_game_for_clients(game_clients)

{:noreply, %{state | clients: remaining_clients}}
end

def handle_info({:spawn_bot_for_player, bot_client, game_id}, state) do
spawn(fn ->
Finch.build(:get, Utils.get_bot_connection_url(game_id, bot_client))
|> Finch.request(Arena.Finch)
end)

{:noreply, state}
end

defp maybe_make_batch_start_at([], _) do
System.monotonic_time(:millisecond)
end

defp maybe_make_batch_start_at([_ | _], batch_start_at) do
batch_start_at
end

defp spawn_bot_for_player(bot_clients, game_id) do
Enum.each(bot_clients, fn %{client_id: bot_client_id} ->
send(self(), {:spawn_bot_for_player, bot_client_id, game_id})
end)
end

defp get_bot_clients(missing_clients) do
characters =
Arena.Configuration.get_game_config()
|> Map.get(:characters)
|> Enum.filter(fn character -> character.active end)

bot_names = Utils.list_bot_names(missing_clients)

Enum.map(1..missing_clients//1, fn i ->
client_id = UUID.generate()

%{client_id: client_id, character_name: Enum.random(characters).name, name: Enum.at(bot_names, i - 1), type: :bot}
end)
end

# Receives a list of clients.
# Fills the given list with bots clients, creates a game and tells every client to join that game.
defp create_game_for_clients(clients, game_params \\ %{}) do
game_params =
Map.merge(game_params, %{
game_mode: :DEATHMATCH,
match_duration: @match_duration,
respawn_time: @respawn_time
})

# We spawn bots only if there is one player
bot_clients =
case Enum.count(clients) do
1 -> get_bot_clients(Application.get_env(:arena, :players_needed_in_match) - Enum.count(clients))
_ -> []
end

players = Utils.assign_teams_to_players(clients ++ bot_clients, :solo)

{:ok, game_pid} = GenServer.start(Arena.GameUpdater, %{players: players, game_params: game_params})

game_id = game_pid |> :erlang.term_to_binary() |> Base58.encode()

spawn_bot_for_player(bot_clients, game_id)

Enum.each(clients, fn %{from_pid: from_pid} ->
Process.send(from_pid, {:join_game, game_id}, [])
Process.send(from_pid, :leave_waiting_game, [])
end)
end
end
2 changes: 2 additions & 0 deletions apps/arena/lib/arena/matchmaking/game_launcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ defmodule Arena.Matchmaking.GameLauncher do
# Receives a list of clients.
# Fills the given list with bots clients, creates a game and tells every client to join that game.
def create_game_for_clients(clients, game_params \\ %{}) do
game_params = Map.put(game_params, :game_mode, :BATTLE)

# We spawn bots only if there is one player
bot_clients =
case Enum.count(clients) do
Expand Down
Loading

0 comments on commit 5520788

Please sign in to comment.