diff --git a/apps/arena/lib/arena/entities.ex b/apps/arena/lib/arena/entities.ex index 4922cdedd..6a979fa2b 100644 --- a/apps/arena/lib/arena/entities.ex +++ b/apps/arena/lib/arena/entities.ex @@ -6,7 +6,30 @@ defmodule Arena.Entities do alias Arena.Game.Player alias Arena.Game.Crate - def new_player(id, character_name, player_name, position, direction, config, now) do + @type new_player_params :: %{ + id: integer(), + team: integer(), + player_name: String.t(), + position: %{x: float(), y: float()}, + direction: %{x: float(), y: float()}, + character_name: String.t(), + config: map(), + now: integer() + } + + @spec new_player(new_player_params()) :: map() + def new_player(params) do + %{ + id: id, + player_name: player_name, + position: position, + direction: direction, + character_name: character_name, + config: config, + now: now, + team: team + } = params + character = Configuration.get_character_config(character_name, config) %{ @@ -21,6 +44,7 @@ defmodule Arena.Entities do direction: direction, is_moving: false, aditional_info: %{ + team: team, health: character.base_health, base_health: character.base_health, max_health: character.base_health, @@ -72,14 +96,26 @@ defmodule Arena.Entities do } end - def new_projectile( - id, - position, - direction, - owner_id, - skill_key, - config_params - ) do + @type new_projectile_params :: %{ + id: integer(), + owner: map(), + position: %{x: float(), y: float()}, + direction: %{x: float(), y: float()}, + skill_key: String.t(), + config_params: map() + } + + @spec new_projectile(new_projectile_params()) :: map() + def new_projectile(params) do + %{ + id: id, + owner: owner, + position: position, + direction: direction, + skill_key: skill_key, + config_params: config_params + } = params + %{ id: id, category: :projectile, @@ -94,7 +130,8 @@ defmodule Arena.Entities do aditional_info: %{ skill_key: skill_key, damage: config_params.damage, - owner_id: owner_id, + owner_id: owner.id, + owner_team: owner.aditional_info.team, status: :ACTIVE, remove_on_collision: config_params.remove_on_collision, on_explode_mechanics: Map.get(config_params, :on_explode_mechanics), @@ -178,7 +215,8 @@ defmodule Arena.Entities do is_moving: false, aditional_info: %{ effect_to_apply: pool_params.effect, - owner_id: pool_params.owner_id, + owner_id: pool_params.owner.id, + owner_team: pool_params.owner.aditional_info.team, effects: [], stat_multiplier: 0, duration_ms: duration_ms, @@ -237,6 +275,7 @@ defmodule Arena.Entities do time_until_transition: nil } } + |> Arena.Game.Obstacle.handle_transition_init() end def new_bush(id, position, radius, shape, vertices \\ []) do @@ -395,7 +434,8 @@ defmodule Arena.Entities do forced_movement: get_in(entity, [:aditional_info, :forced_movement]), bounty_completed: get_in(entity, [:aditional_info, :bounty_completed]), mana: get_in(entity, [:aditional_info, :mana]), - current_basic_animation: get_in(entity, [:aditional_info, :current_basic_animation]) + current_basic_animation: get_in(entity, [:aditional_info, :current_basic_animation]), + team: get_in(entity, [:aditional_info, :team]) }} end @@ -492,6 +532,30 @@ defmodule Arena.Entities do def alive?(%{category: :crate} = entity), do: Crate.alive?(entity) def alive?(%{category: :pool} = _entity), do: true + def filter_damageable(source, targets) do + Map.filter(targets, fn {_, target} -> can_damage?(source, target) end) + end + + def filter_targetable(source, targets) do + Map.filter(targets, fn {_, target} -> can_damage?(source, target) and visible?(source, target) end) + end + + defp visible?(%{category: :player} = source, target), do: Player.visible?(source, target) + defp visible?(%{category: _any} = _source, _target), do: true + + def can_damage?(source, target) do + alive?(target) and not same_team?(source, target) + end + + def same_team?(source, target) do + get_team(source) == get_team(target) + end + + defp get_team(%{category: :player} = entity), do: entity.aditional_info.team + defp get_team(%{category: :projectile} = entity), do: entity.aditional_info.owner_team + defp get_team(%{category: :pool} = entity), do: entity.aditional_info.owner_team + defp get_team(%{category: category} = _entity), do: category + def update_entity(%{category: :player} = entity, game_state) do put_in(game_state, [:players, entity.id], entity) end diff --git a/apps/arena/lib/arena/game/obstacle.ex b/apps/arena/lib/arena/game/obstacle.ex index 44c392907..96cd34c8b 100644 --- a/apps/arena/lib/arena/game/obstacle.ex +++ b/apps/arena/lib/arena/game/obstacle.ex @@ -19,7 +19,7 @@ defmodule Arena.Game.Obstacle do Map.filter(obstacles, fn {_obstacle_id, obstacle} -> obstacle.aditional_info.collide_with_projectiles end) end - def handle_transition_init(obstacle) do + def handle_transition_init(%{aditional_info: %{type: "dynamic"}} = obstacle) do now = DateTime.utc_now() |> DateTime.to_unix(:millisecond) current_status_params = @@ -34,6 +34,8 @@ defmodule Arena.Game.Obstacle do end) end + def handle_transition_init(obstacle), do: obstacle + def update_obstacle_transition_status(game_state, %{aditional_info: %{type: "dynamic"}} = obstacle) do now = DateTime.utc_now() |> DateTime.to_unix(:millisecond) diff --git a/apps/arena/lib/arena/game/player.ex b/apps/arena/lib/arena/game/player.ex index e836124e0..aeba14a91 100644 --- a/apps/arena/lib/arena/game/player.ex +++ b/apps/arena/lib/arena/game/player.ex @@ -3,7 +3,6 @@ defmodule Arena.Game.Player do Module for interacting with Player entity """ - alias Arena.Game.Crate alias Arena.GameUpdater alias Arena.GameTracker alias Arena.Utils @@ -90,8 +89,8 @@ defmodule Arena.Game.Player do Map.filter(players, fn {_, player} -> alive?(player) end) end - def targetable_players(current_player, players) do - Map.filter(players, fn {_, player} -> alive?(player) and visible?(current_player, player) end) + def same_team?(source, target) do + target.aditional_info.team != source.aditional_info.team end def stamina_full?(player) do @@ -225,10 +224,10 @@ defmodule Arena.Game.Player do {auto_aim?, skill_direction} = skill_params.target - |> Skill.maybe_auto_aim(skill, player, targetable_players(player, game_state.players)) + |> Skill.maybe_auto_aim(skill, player, game_state.players) |> case do {false, _} -> - Skill.maybe_auto_aim(skill_params.target, skill, player, Crate.alive_crates(game_state.crates)) + Skill.maybe_auto_aim(skill_params.target, skill, player, game_state.crates) auto_aim -> auto_aim @@ -382,8 +381,8 @@ defmodule Arena.Game.Player do end end - def visible?(current_player, candidate_player) do - candidate_player.id in current_player.aditional_info.visible_players + def visible?(source_player, target_player) do + target_player.id in source_player.aditional_info.visible_players end def remove_expired_effects(player) do diff --git a/apps/arena/lib/arena/game/skill.ex b/apps/arena/lib/arena/game/skill.ex index e22575d5a..f3ae124bd 100644 --- a/apps/arena/lib/arena/game/skill.ex +++ b/apps/arena/lib/arena/game/skill.ex @@ -131,18 +131,19 @@ defmodule Arena.Game.Skill do last_id = game_state.last_id + 1 projectile = - Entities.new_projectile( - last_id, - get_position_with_offset( - entity_player_owner.position, - entity_player_owner.direction, - repeated_shot.projectile_offset - ), - randomize_direction_in_angle(entity.direction, repeated_shot.angle), - entity_player_owner.id, - skill_params.skill_key, - repeated_shot - ) + Entities.new_projectile(%{ + id: last_id, + owner: entity_player_owner, + position: + get_position_with_offset( + entity_player_owner.position, + entity_player_owner.direction, + repeated_shot.projectile_offset + ), + direction: randomize_direction_in_angle(entity.direction, repeated_shot.angle), + skill_key: skill_params.skill_key, + config_params: repeated_shot + }) Process.send_after(self(), {:remove_projectile, projectile.id}, repeated_shot.duration_ms) @@ -164,18 +165,19 @@ defmodule Arena.Game.Skill do last_id = game_state_acc.last_id + 1 projectile = - Entities.new_projectile( - last_id, - get_position_with_offset( - entity_player_owner.position, - skill_direction, - multishot.projectile_offset - ), - direction, - entity_player_owner.id, - skill_params.skill_key, - multishot - ) + Entities.new_projectile(%{ + id: last_id, + owner: entity_player_owner, + position: + get_position_with_offset( + entity_player_owner.position, + skill_direction, + multishot.projectile_offset + ), + direction: direction, + skill_key: skill_params.skill_key, + config_params: multishot + }) Process.send_after(self(), {:remove_projectile, projectile.id}, multishot.duration_ms) @@ -210,18 +212,19 @@ defmodule Arena.Game.Skill do duration_ms = Physics.calculate_duration(entity.position, target_position, simple_shoot.speed, simple_shoot.range) projectile = - Entities.new_projectile( - last_id, - get_position_with_offset( - entity_player_owner.position, - direction, - simple_shoot.projectile_offset - ), - direction, - entity_player_owner.id, - skill_params.skill_key, - simple_shoot - ) + Entities.new_projectile(%{ + id: last_id, + owner: entity_player_owner, + position: + get_position_with_offset( + entity_player_owner.position, + direction, + simple_shoot.projectile_offset + ), + direction: direction, + skill_key: skill_params.skill_key, + config_params: simple_shoot + }) Process.send_after(self(), {:remove_projectile, projectile.id}, duration_ms) @@ -240,18 +243,19 @@ defmodule Arena.Game.Skill do entity_player_owner = get_entity_player_owner(game_state, entity) projectile = - Entities.new_projectile( - last_id, - get_position_with_offset( - entity_player_owner.position, - skill_direction, - simple_shoot.projectile_offset - ), - skill_direction, - entity_player_owner.id, - skill_params.skill_key, - simple_shoot - ) + Entities.new_projectile(%{ + id: last_id, + owner: entity_player_owner, + position: + get_position_with_offset( + entity_player_owner.position, + skill_direction, + simple_shoot.projectile_offset + ), + direction: skill_direction, + skill_key: skill_params.skill_key, + config_params: simple_shoot + }) Process.send_after(self(), {:remove_projectile, projectile.id}, simple_shoot.duration_ms) @@ -290,6 +294,7 @@ defmodule Arena.Game.Skill do def do_mechanic(game_state, %{category: :projectile} = entity, %{type: "spawn_pool"} = pool_params, skill_params) do last_id = game_state.last_id + 1 + entity_player_owner = get_entity_player_owner(game_state, entity) skill_direction = maybe_multiply_by_range(skill_params.skill_direction, skill_params.auto_aim?, pool_params.range) @@ -304,8 +309,8 @@ defmodule Arena.Game.Skill do Map.merge( %{ id: last_id, + owner: entity_player_owner, position: target_position, - owner_id: entity.aditional_info.owner_id, skill_key: entity.aditional_info.skill_key, status: :WAITING }, @@ -321,6 +326,7 @@ defmodule Arena.Game.Skill do def do_mechanic(game_state, player, %{type: "spawn_pool"} = pool_params, skill_params) do last_id = game_state.last_id + 1 + entity_player_owner = get_entity_player_owner(game_state, player) skill_direction = maybe_multiply_by_range(skill_params.skill_direction, skill_params.auto_aim?, pool_params.range) @@ -335,8 +341,8 @@ defmodule Arena.Game.Skill do Map.merge( %{ id: last_id, + owner: entity_player_owner, position: target_position, - owner_id: player.id, skill_key: skill_params.skill_key, status: :WAITING }, @@ -411,8 +417,10 @@ defmodule Arena.Game.Skill do def maybe_auto_aim(%{x: x, y: y}, skill, player, entities) when x == 0.0 and y == 0.0 do case skill.autoaim do true -> + targetable_entities = Entities.filter_targetable(player, entities) + {use_autoaim?, nearest_entity_position_in_range} = - Physics.nearest_entity_position_in_range(player, entities, skill.max_autoaim_range) + Physics.nearest_entity_position_in_range(player, targetable_entities, skill.max_autoaim_range) {use_autoaim?, nearest_entity_position_in_range |> maybe_normalize(not skill.can_pick_destination)} @@ -447,9 +455,9 @@ defmodule Arena.Game.Skill do }), do: get_in(game_state, [:players, owner_id]) - # Default to zone id + # Default to zone defp get_entity_player_owner(_game_state, _), - do: %{id: 9999} + do: %{id: 9999, aditional_info: %{team: :zone}} defp maybe_move_player(game_state, %{category: :player} = player, move_by) when not is_nil(move_by) do @@ -475,11 +483,8 @@ defmodule Arena.Game.Skill do defp apply_damage_and_effects_to_entities(game_state, player, skill_entity, damage, effect \\ nil) do # Players - alive_players = - Player.alive_players(game_state.players) - |> Map.filter(fn {_, alive_player} -> alive_player.id != player.id end) - - collided_players = Physics.check_collisions(skill_entity, alive_players) + damageable_players = Entities.filter_damageable(player, game_state.players) + collided_players = Physics.check_collisions(skill_entity, damageable_players) # Apply effects to players game_state = diff --git a/apps/arena/lib/arena/game_tracker.ex b/apps/arena/lib/arena/game_tracker.ex index 3d94c3ed2..ca98e3b12 100644 --- a/apps/arena/lib/arena/game_tracker.ex +++ b/apps/arena/lib/arena/game_tracker.ex @@ -16,8 +16,8 @@ defmodule Arena.GameTracker do GenServer.call(__MODULE__, {:start_tracking, match_id, client_to_player_map, players, human_clients}) end - def finish_tracking(match_pid, winner_id) do - GenServer.cast(__MODULE__, {:finish_tracking, match_pid, winner_id}) + def finish_tracking(match_pid, winner_ids) do + GenServer.cast(__MODULE__, {:finish_tracking, match_pid, winner_ids}) end @type player_id :: pos_integer() @@ -57,6 +57,7 @@ defmodule Arena.GameTracker do Map.new(players, fn {_, player} -> player_data = %{ id: player.id, + team: player.aditional_info.team, controller: if(Enum.member?(human_clients, player_to_client[player.id]), do: :human, else: :bot), character: player.aditional_info.character_name, kills: [], @@ -91,9 +92,9 @@ defmodule Arena.GameTracker do end @impl true - def handle_cast({:finish_tracking, match_pid, winner_id}, state) do + def handle_cast({:finish_tracking, match_pid, winner_team}, state) do match_data = get_in(state, [:matches, match_pid]) - results = generate_results(match_data, winner_id) + results = generate_results(match_data, winner_team) payload = Jason.encode!(%{results: results}) send_request("/curse/match/#{match_data.match_id}", payload) matches = Map.delete(state.matches, match_pid) @@ -155,10 +156,10 @@ defmodule Arena.GameTracker do put_in(data, [:players, player_id, :bounty_quest_id], bounty_quest_id) end - defp generate_results(match_data, winner_id) do + defp generate_results(match_data, winner_team) do Enum.filter(match_data.players, fn {_player_id, player_data} -> player_data.controller == :human end) |> Enum.map(fn {player_id, _player_data} -> - generate_player_result(match_data, player_id, winner_id) + generate_player_result(match_data, player_id, winner_team) end) end @@ -166,24 +167,26 @@ defmodule Arena.GameTracker do %{} end - defp generate_player_result(match_data, player_id, winner_id \\ nil) do + defp generate_player_result(match_data, player_id, winner_team \\ nil) do duration = System.monotonic_time(:millisecond) - match_data.start_at player_data = Map.get(match_data.players, player_id) + winner? = player_data.team == winner_team + %{ user_id: get_in(match_data, [:player_to_client, player_data.id]), ## TODO: way to track `abandon`, currently a bot taking over will endup with a result ## GameUpdater should send an event when the abandon happens to mark the player ## https://github.com/lambdaclass/mirra_backend/issues/601 - result: if(winner_id && player_data.id == winner_id, do: "win", else: "loss"), + result: if(winner?, do: "win", else: "loss"), kills: length(player_data.kills), ## TODO: this only works because you can only die once ## https://github.com/lambdaclass/mirra_backend/issues/601 deaths: if(player_data.death == nil, do: 0, else: 1), character: player_data.character, - position: get_player_match_place(player_data, winner_id, match_data), + position: get_player_match_place(player_data, winner?, match_data), damage_taken: player_data.damage_taken, damage_done: player_data.damage_done, health_healed: player_data.health_healed, @@ -212,8 +215,8 @@ defmodule Arena.GameTracker do end end - def get_player_match_place(%{id: winner_id}, winner_id, _match_data), do: 1 - def get_player_match_place(%{position: position}, _winner_id, _match_data), do: position + def get_player_match_place(_player_info, true = _winner?, _match_data), do: 1 + def get_player_match_place(%{position: position}, _winner?, _match_data), do: position # Default case for timeouts - def get_player_match_place(_player_info, _winner_id, match_data), do: match_data.position_on_death + def get_player_match_place(_player_info, _winner?, match_data), do: match_data.position_on_death end diff --git a/apps/arena/lib/arena/game_updater.ex b/apps/arena/lib/arena/game_updater.ex index a4305fa53..55679a3dd 100644 --- a/apps/arena/lib/arena/game_updater.ex +++ b/apps/arena/lib/arena/game_updater.ex @@ -59,12 +59,12 @@ defmodule Arena.GameUpdater do # END API ########################## - def init(%{clients: clients, bot_clients: bot_clients, game_params: game_params}) do + def init(%{players: players, game_params: game_params}) do game_id = self() |> :erlang.term_to_binary() |> Base58.encode() game_config = Configuration.get_game_config() game_config = Map.put(game_config, :game, Map.merge(game_config.game, game_params)) - game_state = new_game(game_id, clients ++ bot_clients, game_config) + game_state = new_game(game_id, players, game_config) match_id = Ecto.UUID.generate() send(self(), :update_game) @@ -77,8 +77,11 @@ defmodule Arena.GameUpdater do Process.send_after(self(), :game_start, game_config.game.start_game_time_ms) end - clients_ids = Enum.map(clients, fn {client_id, _, _, _} -> client_id end) - bot_clients_ids = Enum.map(bot_clients, fn {client_id, _, _, _} -> client_id end) + clients_ids = + Enum.filter(players, fn player -> player.type == :human end) |> Enum.map(fn player -> player.client_id end) + + bot_clients_ids = + Enum.filter(players, fn player -> player.type == :bot end) |> Enum.map(fn player -> player.client_id end) :ok = GameTracker.start_tracking(match_id, game_state.client_to_player_map, game_state.players, clients_ids) @@ -326,29 +329,38 @@ defmodule Arena.GameUpdater do end def handle_info(:deathmatch_end_game_check, state) do - players = + players_with_kills = state.game_state.players |> Enum.map(fn {player_id, player} -> %{kills: kills} = GameTracker.get_player_result(player_id) - {player_id, player, kills} + Map.put(player, :kills, kills) + end) + + {winner_team, _team_kills} = + players_with_kills + |> Enum.group_by(fn player -> player.aditional_info.team end) + |> Enum.map(fn {team, players} -> + team_kills = Enum.reduce(players, 0, fn player, acc -> player.kills + acc end) + {team, team_kills} end) - |> Enum.sort_by(fn {_player_id, _player, kills} -> kills end, :desc) + |> Enum.max_by(fn {_team, team_kills} -> team_kills end) - {winner_id, winner, _kills} = Enum.at(players, 0) + winners = + Enum.filter(state.game_state.players, fn {_player_id, player} -> player.aditional_info.team == winner_team end) 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) + players_with_kills + |> Enum.reduce(game_state, fn player, 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) + broadcast_game_ended(winners, state.game_state) + GameTracker.finish_tracking(self(), winner_team) Process.send_after(self(), :game_ended, state.game_config.game.shutdown_game_wait_ms) @@ -364,14 +376,17 @@ defmodule Arena.GameUpdater do state.game_config.game.end_game_interval_ms ) - {:ended, winner} -> + {:ended, winner_team} -> + winner_team_ids = Enum.map(winner_team, fn {id, _player} -> id end) + winner_team_number = Enum.random(winner_team) |> elem(1) |> get_in([:aditional_info, :team]) + state = put_in(state, [:game_state, :status], :ENDED) - |> update_in([:game_state], fn game_state -> put_player_position(game_state, winner.id) end) + |> update_in([:game_state], fn game_state -> put_player_position(game_state, winner_team_ids) 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) + broadcast_game_ended(winner_team, state.game_state) + GameTracker.finish_tracking(self(), winner_team_number) ## The idea of having this waiting period is in case websocket processes keep ## sending messages, this way we give some time before making them crash @@ -775,9 +790,9 @@ defmodule Arena.GameUpdater do PubSub.broadcast(Arena.PubSub, game_id, {:game_update, encoded_state}) end - defp broadcast_game_ended(winner, state) do + defp broadcast_game_ended(winners, state) do game_state = %GameFinished{ - winner: complete_entity(winner, :player), + winners: complete_entities(winners, :player), players: complete_entities(state.players, :player) } @@ -818,30 +833,38 @@ defmodule Arena.GameUpdater do ########################## # Create a new game - defp new_game(game_id, clients, config) do - now = System.monotonic_time(:millisecond) + defp new_game(game_id, players, config) do + initialize_game_params(game_id, config) + |> initialize_players(players, config) + |> initialize_obstacles(config.map.obstacles) + |> initialize_crates(config.map.crates) + |> initialize_bushes(config.map.bushes) + |> initialize_pools(config.map.pools) + end + + defp initialize_game_params(game_id, config) do initial_timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond) - new_game = - Map.new(game_id: game_id) - |> Map.put(:last_id, 0) - |> Map.put(:players, %{}) - |> Map.put(:power_ups, %{}) - |> Map.put(:projectiles, %{}) - |> Map.put(:items, %{}) - |> Map.put(:player_timestamps, %{}) - |> Map.put(:obstacles, %{}) - |> Map.put(:bushes, %{}) - |> Map.put(:server_timestamp, 0) - |> Map.put(:client_to_player_map, %{}) - |> Map.put(:pools, %{}) - |> Map.put(:killfeed, []) - |> Map.put(:damage_taken, %{}) - |> Map.put(:damage_done, %{}) - |> Map.put(:crates, %{}) - |> Map.put(:external_wall, Entities.new_external_wall(0, config.map.radius)) - |> Map.put(:square_wall, config.map.square_wall) - |> Map.put(:zone, %{ + %{ + game_id: game_id, + last_id: 0, + players: %{}, + power_ups: %{}, + projectiles: %{}, + items: %{}, + player_timestamps: %{}, + obstacles: %{}, + bushes: %{}, + server_timestamp: 0, + client_to_player_map: %{}, + pools: %{}, + killfeed: [], + damage_taken: %{}, + damage_done: %{}, + crates: %{}, + external_wall: Entities.new_external_wall(0, config.map.radius), + square_wall: config.map.square_wall, + zone: %{ radius: config.map.radius - 5000, should_start?: if(config.game.game_mode == :DEATHMATCH, do: false, else: config.game.zone_enabled), started: false, @@ -850,132 +873,108 @@ defmodule Arena.GameUpdater do next_zone_change_timestamp: initial_timestamp + config.game.zone_shrink_start_ms + config.game.start_game_time_ms + config.game.bounty_pick_time_ms - }) - |> Map.put(:status, :PREPARING) - |> Map.put( - :start_game_timestamp, - initial_timestamp + config.game.start_game_time_ms + config.game.bounty_pick_time_ms - ) - |> Map.put(:positions, %{}) - |> Map.put(:traps, %{}) - |> Map.put(:respawn_queue, %{}) - - {game, _} = - Enum.reduce(clients, {new_game, config.map.initial_positions}, fn {client_id, character_name, player_name, - _from_pid}, - {new_game, positions} -> - last_id = new_game.last_id + 1 + }, + status: :PREPARING, + start_game_timestamp: initial_timestamp + config.game.start_game_time_ms + config.game.bounty_pick_time_ms, + positions: %{}, + traps: %{}, + respawn_queue: %{} + } + end + + defp initialize_players(game_state, players, config) do + now = System.monotonic_time(:millisecond) + + {game_state, _} = + Enum.reduce(players, {game_state, config.map.initial_positions}, fn player, {game_acc, positions} -> + last_id = game_acc.last_id + 1 {pos, positions} = get_next_position(positions) direction = Physics.get_direction_from_positions(pos, %{x: 0.0, y: 0.0}) + new_player_params = %{ + id: last_id, + team: player.team, + player_name: player.name, + position: pos, + direction: direction, + character_name: player.character_name, + config: config, + now: now + } + players = - new_game.players - |> Map.put( - last_id, - Entities.new_player(last_id, character_name, player_name, pos, direction, config, now) - ) + game_acc.players + |> Map.put(last_id, Entities.new_player(new_player_params)) - new_game = - new_game + game_acc = + game_acc |> Map.put(:last_id, last_id) |> Map.put(:players, players) - |> put_in([:client_to_player_map, client_id], last_id) + |> put_in([:client_to_player_map, player.client_id], last_id) |> put_in([:player_timestamps, last_id], 0) - {new_game, positions} + {game_acc, positions} end) - {obstacles, last_id} = initialize_obstacles(config.map.obstacles, game.last_id) - {crates, last_id} = initialize_crates(config.map.crates, last_id) - {bushes, last_id} = initialize_bushes(config.map.bushes, last_id) - {pools, last_id} = initialize_pools(config.map.pools, last_id) - - game - |> Map.put(:last_id, last_id) - |> Map.put(:obstacles, obstacles) - |> Map.put(:bushes, bushes) - |> Map.put(:crates, crates) - |> Map.put(:pools, pools) + game_state end - # Initialize obstacles - defp initialize_obstacles(obstacles, last_id) do - Enum.reduce(obstacles, {Map.new(), last_id}, fn obstacle, {obstacles_acc, last_id} -> - last_id = last_id + 1 + defp initialize_obstacles(game_state, []), do: game_state - obstacle = - Entities.new_obstacle( - last_id, - obstacle - ) + defp initialize_obstacles(game_state, obstacles) do + Enum.reduce(obstacles, game_state, fn obstacle_config, game_state_acc -> + last_id = game_state_acc.last_id + 1 + game_state_acc = Map.put(game_state_acc, :last_id, last_id) obstacle = - if obstacle.aditional_info.type == "dynamic" do - Obstacle.handle_transition_init(obstacle) - else - obstacle - end - - obstacles_acc = - Map.put( - obstacles_acc, + Entities.new_obstacle( last_id, - obstacle + obstacle_config ) - {obstacles_acc, last_id} + put_in(game_state_acc, [:obstacles, last_id], obstacle) end) end - defp initialize_bushes(bushes, last_id) do - Enum.reduce(bushes, {Map.new(), last_id}, fn bush, {bush_acc, last_id} -> - last_id = last_id + 1 + defp initialize_bushes(game_state, []), do: game_state - bush_acc = - Map.put( - bush_acc, - last_id, - Entities.new_bush(last_id, bush.position, bush.radius, bush.shape, bush.vertices) - ) - - {bush_acc, last_id} + defp initialize_bushes(game_state, bushes) do + Enum.reduce(bushes, game_state, fn bush, game_state_acc -> + last_id = game_state_acc.last_id + 1 + game_state_acc = Map.put(game_state_acc, :last_id, last_id) + bush = Entities.new_bush(last_id, bush.position, bush.radius, bush.shape, bush.vertices) + put_in(game_state_acc, [:bushes, last_id], bush) end) end - # Initialize crates - defp initialize_crates(crates, last_id) do - Enum.reduce(crates, {Map.new(), last_id}, fn crate, {crates_acc, last_id} -> - last_id = last_id + 1 + defp initialize_crates(game_state, []), do: game_state - crates_acc = - Map.put( - crates_acc, - last_id, - Entities.new_crate( - last_id, - crate - ) - ) - - {crates_acc, last_id} + defp initialize_crates(game_state, crates) do + Enum.reduce(crates, game_state, fn crate, game_state_acc -> + last_id = game_state_acc.last_id + 1 + game_state_acc = Map.put(game_state_acc, :last_id, last_id) + crate = Entities.new_crate(last_id, crate) + put_in(game_state_acc, [:crates, last_id], crate) end) end - defp initialize_pools(pools, last_id) do - Enum.reduce(pools, {Map.new(), last_id}, fn pool, {pools_acc, last_id} -> - last_id = last_id + 1 + defp initialize_pools(game_state, []), do: game_state - pools_acc = - Map.put( - pools_acc, - last_id, - Entities.new_pool( - pool - |> Map.merge(%{id: last_id, owner_id: 9999, skill_key: "0", status: :READY}) - ) - ) + defp initialize_pools(game_state, pools) do + Enum.reduce(pools, game_state, fn pool, game_state_acc -> + last_id = game_state_acc.last_id + 1 + game_state_acc = Map.put(game_state_acc, :last_id, last_id) + + pool_params = + Map.merge(pool, %{ + id: last_id, + owner: %{id: 9999, aditional_info: %{team: :pool}}, + skill_key: "0", + status: :READY + }) - {pools_acc, last_id} + pool = Entities.new_pool(pool_params) + put_in(game_state_acc, [:pools, last_id], pool) end) end @@ -1426,21 +1425,30 @@ defmodule Arena.GameUpdater do Map.values(players) |> Enum.filter(&Player.alive?/1) - cond do - Enum.count(players_alive) == 1 && Enum.count(players) > 1 -> - {:ended, hd(players_alive)} + teams_alive = Enum.map(players_alive, fn player -> player.aditional_info.team end) - Enum.empty?(players_alive) -> + cond do + Enum.empty?(teams_alive) -> ## TODO: We probably should have a better tiebraker (e.g. most kills, less deaths, etc), ## but for now a random between the ones that were alive last is enough - player = Map.get(players, Enum.random(last_players_ids)) - {:ended, player} + random_team = Map.get(players, Enum.random(last_players_ids)) |> list_same_team_players(players) + {:ended, random_team} + + Enum.uniq(teams_alive) |> Enum.count() == 1 -> + random_alive_player = Enum.random(players_alive) + {:ended, list_same_team_players(random_alive_player, players)} true -> {:ongoing, Enum.map(players_alive, & &1.id)} end end + defp list_same_team_players(player, players) do + Enum.filter(players, fn {_id, current_player} -> + current_player.aditional_info.team == player.aditional_info.team + end) + end + defp maybe_receive_zone_damage(player, elapse_time, zone_damage_interval, zone_damage) when elapse_time > zone_damage_interval do Entities.take_damage(player, zone_damage, 9999) @@ -1560,7 +1568,9 @@ defmodule Arena.GameUpdater do defp decide_collided_entity(projectile, [entity_id | other_entities], external_wall_id, players, crates) do cond do Map.get(players, entity_id) -> - if Player.alive?(Map.get(players, entity_id)) do + target_player = Map.get(players, entity_id) + + if Entities.can_damage?(projectile, target_player) do entity_id else decide_collided_entity(projectile, other_entities, external_wall_id, players, crates) @@ -1650,9 +1660,11 @@ defmodule Arena.GameUpdater do end defp handle_pools(%{pools: pools, crates: crates, players: players} = game_state) do - entities = Map.merge(crates, players) + target_entities = Map.merge(crates, players) Enum.reduce(pools, game_state, fn {_pool_id, pool}, game_state -> + entities = Entities.filter_damageable(pool, target_entities) + Enum.reduce(entities, game_state, fn {entity_id, entity}, acc -> if entity_id in pool.collides_with and pool.aditional_info.status == :READY do add_pool_effects(acc, entity, pool) @@ -1734,7 +1746,7 @@ defmodule Arena.GameUpdater do visible_players = Map.delete(players, player_id) - |> Enum.reduce([], fn {candicandidate_player_id, candidate_player}, acc -> + |> Enum.reduce([], fn {candidate_player_id, candidate_player}, acc -> candidate_bush_collisions = Enum.filter(candidate_player.collides_with, fn collided_id -> Map.has_key?(bushes, collided_id) @@ -1756,9 +1768,12 @@ defmodule Arena.GameUpdater do player_is_executing_skill? = Player.player_executing_skill?(candidate_player) - if Enum.empty?(candidate_bush_collisions) or (players_in_same_bush? and players_close_enough?) or + # TODO: This needs a refactor. + # There are a lot of unnecessary calculations above when players are in the same team. + if Entities.same_team?(player, candidate_player) or Enum.empty?(candidate_bush_collisions) or + (players_in_same_bush? and players_close_enough?) or enough_time_since_last_skill? or player_has_item_effect? or player_is_executing_skill? do - [candicandidate_player_id | acc] + [candidate_player_id | acc] else acc end @@ -1836,6 +1851,12 @@ defmodule Arena.GameUpdater do defp get_entity_path(%{category: :trap}), do: :traps defp get_entity_path(%{category: :crate}), do: :crates + defp put_player_position(game_state, player_ids) when is_list(player_ids) do + Enum.reduce(player_ids, game_state, fn player_id, game_state_acc -> + put_player_position(game_state_acc, player_id) + end) + end + defp put_player_position(%{positions: positions} = game_state, player_id) do next_position = Application.get_env(:arena, :players_needed_in_match) - Enum.count(positions) diff --git a/apps/arena/lib/arena/matchmaking/deathmatch_mode.ex b/apps/arena/lib/arena/matchmaking/deathmatch_mode.ex index 91f7dd4ac..98104d52c 100644 --- a/apps/arena/lib/arena/matchmaking/deathmatch_mode.ex +++ b/apps/arena/lib/arena/matchmaking/deathmatch_mode.ex @@ -10,9 +10,6 @@ defmodule Arena.Matchmaking.DeathmatchMode do @match_duration 180_000 @respawn_time 5000 - # Time to wait to start game with any amount of clients - @start_timeout_ms 4_000 - # API def start_link(_) do GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -37,16 +34,24 @@ defmodule Arena.Matchmaking.DeathmatchMode do 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_id, character_name, player_name, from_pid}] + clients: clients ++ [client] }} end def handle_call({:leave, client_id}, _, state) do - clients = Enum.reject(state.clients, fn {id, _, _, _} -> id == client_id end) + clients = Enum.reject(state.clients, fn %{client_id: id} -> id == client_id end) {:reply, :ok, %{state | clients: clients}} end @@ -56,7 +61,7 @@ defmodule Arena.Matchmaking.DeathmatchMode do diff = System.monotonic_time(:millisecond) - state.batch_start_at if length(clients) >= Application.get_env(:arena, :players_needed_in_match) or - (diff >= @start_timeout_ms and length(clients) > 0) do + (diff >= Utils.start_timeout_ms() and length(clients) > 0) do send(self(), :start_game) end @@ -88,8 +93,8 @@ defmodule Arena.Matchmaking.DeathmatchMode do end defp spawn_bot_for_player(bot_clients, game_id) do - Enum.each(bot_clients, fn {bot_client, _, _, _} -> - send(self(), {:spawn_bot_for_player, bot_client, game_id}) + Enum.each(bot_clients, fn %{client_id: bot_client_id} -> + send(self(), {:spawn_bot_for_player, bot_client_id, game_id}) end) end @@ -99,16 +104,25 @@ defmodule Arena.Matchmaking.DeathmatchMode do |> 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, Enum.random(characters).name, Enum.at(Arena.Utils.bot_names(), i), nil} + %{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 @@ -116,22 +130,15 @@ defmodule Arena.Matchmaking.DeathmatchMode do _ -> [] end - {:ok, game_pid} = - GenServer.start(Arena.GameUpdater, %{ - clients: clients, - bot_clients: bot_clients, - game_params: - game_params - |> Map.put(:game_mode, :DEATHMATCH) - |> Map.put(:match_duration, @match_duration) - |> Map.put(:respawn_time, @respawn_time) - }) + 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 {_client_id, _character_name, _player_name, from_pid} -> + Enum.each(clients, fn %{from_pid: from_pid} -> Process.send(from_pid, {:join_game, game_id}, []) Process.send(from_pid, :leave_waiting_game, []) end) diff --git a/apps/arena/lib/arena/matchmaking/game_launcher.ex b/apps/arena/lib/arena/matchmaking/game_launcher.ex index 052b2c336..78f4f58b7 100644 --- a/apps/arena/lib/arena/matchmaking/game_launcher.ex +++ b/apps/arena/lib/arena/matchmaking/game_launcher.ex @@ -5,9 +5,6 @@ defmodule Arena.Matchmaking.GameLauncher do use GenServer - # Time to wait to start game with any amount of clients - @start_timeout_ms 4_000 - # API def start_link(_) do GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -32,16 +29,24 @@ defmodule Arena.Matchmaking.GameLauncher do 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_id, character_name, player_name, from_pid}] + clients: clients ++ [client] }} end def handle_call({:leave, client_id}, _, state) do - clients = Enum.reject(state.clients, fn {id, _, _, _} -> id == client_id end) + clients = Enum.reject(state.clients, fn %{client_id: id} -> id == client_id end) {:reply, :ok, %{state | clients: clients}} end @@ -51,7 +56,7 @@ defmodule Arena.Matchmaking.GameLauncher do diff = System.monotonic_time(:millisecond) - state.batch_start_at if length(clients) >= Application.get_env(:arena, :players_needed_in_match) or - (diff >= @start_timeout_ms and length(clients) > 0) do + (diff >= Utils.start_timeout_ms() and length(clients) > 0) do send(self(), :start_game) end @@ -83,8 +88,8 @@ defmodule Arena.Matchmaking.GameLauncher do end defp spawn_bot_for_player(bot_clients, game_id) do - Enum.each(bot_clients, fn {bot_client, _, _, _} -> - send(self(), {:spawn_bot_for_player, bot_client, game_id}) + Enum.each(bot_clients, fn %{client_id: bot_client_id} -> + send(self(), {:spawn_bot_for_player, bot_client_id, game_id}) end) end @@ -94,16 +99,20 @@ defmodule Arena.Matchmaking.GameLauncher do |> 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, Enum.random(characters).name, Enum.at(Arena.Utils.bot_names(), i), nil} + %{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 + 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 @@ -111,18 +120,15 @@ defmodule Arena.Matchmaking.GameLauncher do _ -> [] end - {:ok, game_pid} = - GenServer.start(Arena.GameUpdater, %{ - clients: clients, - bot_clients: bot_clients, - game_params: game_params |> Map.put(:game_mode, :BATTLE) - }) + 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 {_client_id, _character_name, _player_name, from_pid} -> + Enum.each(clients, fn %{from_pid: from_pid} -> Process.send(from_pid, {:join_game, game_id}, []) Process.send(from_pid, :leave_waiting_game, []) end) diff --git a/apps/arena/lib/arena/matchmaking/pair_mode.ex b/apps/arena/lib/arena/matchmaking/pair_mode.ex index 6d83da787..c4d096441 100644 --- a/apps/arena/lib/arena/matchmaking/pair_mode.ex +++ b/apps/arena/lib/arena/matchmaking/pair_mode.ex @@ -5,9 +5,6 @@ defmodule Arena.Matchmaking.PairMode do use GenServer - # Time to wait to start game with any amount of clients - @start_timeout_ms 10_000 - # API def start_link(_) do GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -32,16 +29,24 @@ defmodule Arena.Matchmaking.PairMode do 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_id, character_name, player_name, from_pid}] + clients: clients ++ [client] }} end def handle_call({:leave, client_id}, _, state) do - clients = Enum.reject(state.clients, fn {id, _, _, _} -> id == client_id end) + clients = Enum.reject(state.clients, fn %{client_id: id} -> id == client_id end) {:reply, :ok, %{state | clients: clients}} end @@ -51,7 +56,7 @@ defmodule Arena.Matchmaking.PairMode do diff = System.monotonic_time(:millisecond) - state.batch_start_at if length(clients) >= Application.get_env(:arena, :players_needed_in_match) or - (diff >= @start_timeout_ms and length(clients) > 0) do + (diff >= Utils.start_timeout_ms() and length(clients) > 0) do send(self(), :start_game) end @@ -88,22 +93,26 @@ defmodule Arena.Matchmaking.PairMode do |> 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, Enum.random(characters).name, Enum.at(Arena.Utils.bot_names(), i), nil} + %{client_id: client_id, character_name: Enum.random(characters).name, name: Enum.at(bot_names, i - 1), type: :bot} end) end defp spawn_bot_for_player(bot_clients, game_id) do - Enum.each(bot_clients, fn {bot_client, _, _, _} -> - send(self(), {:spawn_bot_for_player, bot_client, game_id}) + Enum.each(bot_clients, fn %{client_id: bot_client_id} -> + send(self(), {:spawn_bot_for_player, bot_client_id, game_id}) 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.put(game_params, :game_mode, :PAIR) + bot_clients = if Application.get_env(:arena, :spawn_bots) do get_bot_clients(Application.get_env(:arena, :players_needed_in_match) - Enum.count(clients)) @@ -111,18 +120,15 @@ defmodule Arena.Matchmaking.PairMode do [] end - {:ok, game_pid} = - GenServer.start(Arena.GameUpdater, %{ - clients: clients, - bot_clients: bot_clients, - game_params: game_params |> Map.put(:game_mode, :PAIR) - }) + players = Utils.assign_teams_to_players(clients ++ bot_clients, :pair) + + {: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 {_client_id, _character_name, _player_name, from_pid} -> + Enum.each(clients, fn %{from_pid: from_pid} -> Process.send(from_pid, {:join_game, game_id}, []) Process.send(from_pid, :leave_waiting_game, []) end) diff --git a/apps/arena/lib/arena/matchmaking/quick_game_mode.ex b/apps/arena/lib/arena/matchmaking/quick_game_mode.ex index 7ed4309a8..1954b143b 100644 --- a/apps/arena/lib/arena/matchmaking/quick_game_mode.ex +++ b/apps/arena/lib/arena/matchmaking/quick_game_mode.ex @@ -1,7 +1,7 @@ defmodule Arena.Matchmaking.QuickGameMode do @moduledoc false + alias Arena.Matchmaking.GameLauncher alias Arena.Utils - alias Ecto.UUID use GenServer @@ -26,17 +26,22 @@ defmodule Arena.Matchmaking.QuickGameMode do @impl true def handle_call({:join, client_id, character_name, player_name}, {from_pid, _}, state) do - create_game_for_clients([{client_id, character_name, player_name, from_pid}], %{ - bots_enabled: true, - zone_enabled: false - }) + client = %{ + client_id: client_id, + character_name: character_name, + name: player_name, + from_pid: from_pid, + type: :human + } + + GameLauncher.create_game_for_clients([client], %{bots_enabled: true, zone_enabled: false}) {:reply, :ok, 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) + GameLauncher.create_game_for_clients(game_clients) {:noreply, %{state | clients: remaining_clients}} end @@ -50,47 +55,4 @@ defmodule Arena.Matchmaking.QuickGameMode do {:noreply, state} end - - defp get_bot_clients(missing_clients) do - characters = - Arena.Configuration.get_game_config() - |> Map.get(:characters) - |> Enum.filter(fn character -> character.active end) - - Enum.map(1..missing_clients//1, fn i -> - client_id = UUID.generate() - - {client_id, Enum.random(characters).name, Enum.at(Arena.Utils.bot_names(), i), nil} - end) - end - - defp spawn_bot_for_player(bot_clients, game_id) do - Enum.each(bot_clients, fn {bot_client, _, _, _} -> - send(self(), {:spawn_bot_for_player, bot_client, game_id}) - 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 - # We will spawn bots in quick-game matches. - # Check https://github.com/lambdaclass/mirra_backend/pull/951 to know how to restore former behavior. - bot_clients = get_bot_clients(Application.get_env(:arena, :players_needed_in_match) - Enum.count(clients)) - - {:ok, game_pid} = - GenServer.start(Arena.GameUpdater, %{ - clients: clients, - bot_clients: bot_clients, - game_params: game_params |> Map.put(:game_mode, :QUICK_GAME) - }) - - game_id = game_pid |> :erlang.term_to_binary() |> Base58.encode() - - spawn_bot_for_player(bot_clients, game_id) - - Enum.each(clients, fn {_client_id, _character_name, _player_name, from_pid} -> - Process.send(from_pid, {:join_game, game_id}, []) - Process.send(from_pid, :leave_waiting_game, []) - end) - end end diff --git a/apps/arena/lib/arena/serialization/messages.pb.ex b/apps/arena/lib/arena/serialization/messages.pb.ex index ce9a5097f..bc82d9bb0 100644 --- a/apps/arena/lib/arena/serialization/messages.pb.ex +++ b/apps/arena/lib/arena/serialization/messages.pb.ex @@ -168,6 +168,15 @@ defmodule Arena.Serialization.BountySelected do field(:bounty, 1, type: Arena.Serialization.BountyInfo) end +defmodule Arena.Serialization.GameFinished.WinnersEntry do + @moduledoc false + + use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" + + field(:key, 1, type: :uint64) + field(:value, 2, type: Arena.Serialization.Entity) +end + defmodule Arena.Serialization.GameFinished.PlayersEntry do @moduledoc false @@ -182,7 +191,11 @@ defmodule Arena.Serialization.GameFinished do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field(:winner, 1, type: Arena.Serialization.Entity) + field(:winners, 1, + repeated: true, + type: Arena.Serialization.GameFinished.WinnersEntry, + map: true + ) field(:players, 2, repeated: true, @@ -551,6 +564,8 @@ defmodule Arena.Serialization.Player do type: :uint32, json_name: "currentBasicAnimation" ) + + field(:team, 19, proto3_optional: true, type: :uint32) end defmodule Arena.Serialization.Effect do diff --git a/apps/arena/lib/arena/utils.ex b/apps/arena/lib/arena/utils.ex index 5a5bae17f..564371213 100644 --- a/apps/arena/lib/arena/utils.ex +++ b/apps/arena/lib/arena/utils.ex @@ -4,24 +4,76 @@ defmodule Arena.Utils do It contains utility functions like math functions. """ - # The available names for bots to enter a match, we should change this in the future - @bot_names [ - "TheBlackSwordman", - "SlashJava", - "SteelBallRun", - "Jeff", - "Messi", - "Stone Ocean", - "Jeepers Creepers", - "Bob", - "El javo", - "Alberso", - "Thomas", - "Timmy", - "Pablito", - "Nicolino", - "Cangrejo", - "Mansito" + @bot_prefixes [ + "Astro", + "Blaze", + "Lunar", + "Nova", + "Pixel", + "Ember", + "Turbo", + "Echo", + "Frost", + "Zenith", + "Apex", + "Orbit", + "Cyber", + "Drift", + "Vivid", + "Solar", + "Nimbus", + "Quirk", + "Bolt", + "Hollow", + "AllRed", + "Rust", + "Metal", + "Golden", + "Reverse", + "Time", + "Chromian", + "Elegant", + "Jealous", + "Adorable", + "Dangerous", + "Charming", + "Royal" + ] + @bot_suffixes [ + "Hopper", + "Runner", + "Flyer", + "Rover", + "Spark", + "Skull", + "Whisper", + "Seeker", + "Rider", + "Chaser", + "Strider", + "Hunter", + "Shadow", + "Glimmer", + "Wave", + "Glow", + "Wing", + "Dash", + "Fang", + "Shade", + "Elixir", + "Cavalier", + "Lord", + "Socks", + "Creator", + "Suit", + "Greed", + "Gun", + "Balloon", + "Lawyer", + "Elevator", + "Spider", + "Dream", + "WashingMachine" ] def normalize(%{x: 0, y: 0}) do @@ -50,10 +102,38 @@ defmodule Arena.Utils do "#{protocol}#{bot_manager_host}/join/#{server_url}/#{game_id}/#{bot_client}" end - def bot_names() do - @bot_names - end - defp get_correct_protocol("localhost" <> _host), do: "http://" defp get_correct_protocol(_host), do: "https://" + + def assign_teams_to_players(players, :pair) do + Enum.chunk_every(players, 2) + |> Enum.with_index(fn player_pair, index -> + Enum.map(player_pair, fn player -> Map.put(player, :team, index) end) + end) + |> List.flatten() + end + + def assign_teams_to_players(players, :solo) do + Enum.with_index(players, fn player, index -> + Map.put(player, :team, index) + end) + end + + def assign_teams_to_players(players, _not_implemented), do: players + + def list_bot_names(amount) do + prefixes = Enum.take_random(@bot_prefixes, amount) + suffixes = Enum.take_random(@bot_suffixes, amount) + + generate_names(prefixes, suffixes) + end + + defp generate_names([], []), do: [] + + defp generate_names([prefix | prefixes], [suffix | suffixes]) do + [prefix <> suffix | generate_names(prefixes, suffixes)] + end + + # Time to wait to start game with any amount of clients + def start_timeout_ms(), do: 4_000 end diff --git a/apps/arena/mix.exs b/apps/arena/mix.exs index 427def626..93c5ddd82 100644 --- a/apps/arena/mix.exs +++ b/apps/arena/mix.exs @@ -4,7 +4,7 @@ defmodule Arena.MixProject do def project do [ app: :arena, - version: "0.11.1", + version: "0.12.1", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/arena_load_test/lib/arena_load_test/serialization/messages.pb.ex b/apps/arena_load_test/lib/arena_load_test/serialization/messages.pb.ex index b53fbca4c..244acfd6a 100644 --- a/apps/arena_load_test/lib/arena_load_test/serialization/messages.pb.ex +++ b/apps/arena_load_test/lib/arena_load_test/serialization/messages.pb.ex @@ -173,6 +173,15 @@ defmodule ArenaLoadTest.Serialization.BountySelected do field(:bounty, 1, type: ArenaLoadTest.Serialization.BountyInfo) end +defmodule ArenaLoadTest.Serialization.GameFinished.WinnersEntry do + @moduledoc false + + use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" + + field(:key, 1, type: :uint64) + field(:value, 2, type: ArenaLoadTest.Serialization.Entity) +end + defmodule ArenaLoadTest.Serialization.GameFinished.PlayersEntry do @moduledoc false @@ -187,7 +196,11 @@ defmodule ArenaLoadTest.Serialization.GameFinished do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field(:winner, 1, type: ArenaLoadTest.Serialization.Entity) + field(:winners, 1, + repeated: true, + type: ArenaLoadTest.Serialization.GameFinished.WinnersEntry, + map: true + ) field(:players, 2, repeated: true, @@ -604,6 +617,8 @@ defmodule ArenaLoadTest.Serialization.Player do type: :uint32, json_name: "currentBasicAnimation" ) + + field(:team, 19, proto3_optional: true, type: :uint32) end defmodule ArenaLoadTest.Serialization.Effect do diff --git a/apps/bot_manager/lib/protobuf/messages.pb.ex b/apps/bot_manager/lib/protobuf/messages.pb.ex index 9eddf0420..3e076f93c 100644 --- a/apps/bot_manager/lib/protobuf/messages.pb.ex +++ b/apps/bot_manager/lib/protobuf/messages.pb.ex @@ -168,6 +168,15 @@ defmodule BotManager.Protobuf.BountySelected do field(:bounty, 1, type: BotManager.Protobuf.BountyInfo) end +defmodule BotManager.Protobuf.GameFinished.WinnersEntry do + @moduledoc false + + use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" + + field(:key, 1, type: :uint64) + field(:value, 2, type: BotManager.Protobuf.Entity) +end + defmodule BotManager.Protobuf.GameFinished.PlayersEntry do @moduledoc false @@ -182,7 +191,11 @@ defmodule BotManager.Protobuf.GameFinished do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field(:winner, 1, type: BotManager.Protobuf.Entity) + field(:winners, 1, + repeated: true, + type: BotManager.Protobuf.GameFinished.WinnersEntry, + map: true + ) field(:players, 2, repeated: true, @@ -551,6 +564,8 @@ defmodule BotManager.Protobuf.Player do type: :uint32, json_name: "currentBasicAnimation" ) + + field(:team, 19, proto3_optional: true, type: :uint32) end defmodule BotManager.Protobuf.Effect do diff --git a/apps/game_client/assets/js/protobuf/messages_pb.js b/apps/game_client/assets/js/protobuf/messages_pb.js index c7d869b8e..7c732a5e5 100644 --- a/apps/game_client/assets/js/protobuf/messages_pb.js +++ b/apps/game_client/assets/js/protobuf/messages_pb.js @@ -2755,7 +2755,7 @@ proto.GameFinished.prototype.toObject = function(opt_includeInstance) { */ proto.GameFinished.toObject = function(includeInstance, msg) { var f, obj = { - winner: (f = msg.getWinner()) && proto.Entity.toObject(includeInstance, f), + winnersMap: (f = msg.getWinnersMap()) ? f.toObject(includeInstance, proto.Entity.toObject) : [], playersMap: (f = msg.getPlayersMap()) ? f.toObject(includeInstance, proto.Entity.toObject) : [] }; @@ -2794,9 +2794,10 @@ proto.GameFinished.deserializeBinaryFromReader = function(msg, reader) { var field = reader.getFieldNumber(); switch (field) { case 1: - var value = new proto.Entity; - reader.readMessage(value,proto.Entity.deserializeBinaryFromReader); - msg.setWinner(value); + var value = msg.getWinnersMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readUint64, jspb.BinaryReader.prototype.readMessage, proto.Entity.deserializeBinaryFromReader, 0, new proto.Entity()); + }); break; case 2: var value = msg.getPlayersMap(); @@ -2833,13 +2834,9 @@ proto.GameFinished.prototype.serializeBinary = function() { */ proto.GameFinished.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getWinner(); - if (f != null) { - writer.writeMessage( - 1, - f, - proto.Entity.serializeBinaryToWriter - ); + f = message.getWinnersMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(1, writer, jspb.BinaryWriter.prototype.writeUint64, jspb.BinaryWriter.prototype.writeMessage, proto.Entity.serializeBinaryToWriter); } f = message.getPlayersMap(true); if (f && f.getLength() > 0) { @@ -2849,39 +2846,25 @@ proto.GameFinished.serializeBinaryToWriter = function(message, writer) { /** - * optional Entity winner = 1; - * @return {?proto.Entity} + * map winners = 1; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} */ -proto.GameFinished.prototype.getWinner = function() { - return /** @type{?proto.Entity} */ ( - jspb.Message.getWrapperField(this, proto.Entity, 1)); -}; - - -/** - * @param {?proto.Entity|undefined} value - * @return {!proto.GameFinished} returns this -*/ -proto.GameFinished.prototype.setWinner = function(value) { - return jspb.Message.setWrapperField(this, 1, value); +proto.GameFinished.prototype.getWinnersMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 1, opt_noLazyCreate, + proto.Entity)); }; /** - * Clears the message field making it undefined. + * Clears values from the map. The map will be non-null. * @return {!proto.GameFinished} returns this */ -proto.GameFinished.prototype.clearWinner = function() { - return this.setWinner(undefined); -}; - - -/** - * Returns whether this field is set. - * @return {boolean} - */ -proto.GameFinished.prototype.hasWinner = function() { - return jspb.Message.getField(this, 1) != null; +proto.GameFinished.prototype.clearWinnersMap = function() { + this.getWinnersMap().clear(); + return this; }; @@ -7004,7 +6987,8 @@ proto.Player.toObject = function(includeInstance, msg) { forcedMovement: jspb.Message.getBooleanFieldWithDefault(msg, 15, false), bountyCompleted: jspb.Message.getBooleanFieldWithDefault(msg, 16, false), mana: jspb.Message.getFieldWithDefault(msg, 17, 0), - currentBasicAnimation: jspb.Message.getFieldWithDefault(msg, 18, 0) + currentBasicAnimation: jspb.Message.getFieldWithDefault(msg, 18, 0), + team: jspb.Message.getFieldWithDefault(msg, 19, 0) }; if (includeInstance) { @@ -7120,6 +7104,10 @@ proto.Player.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {number} */ (reader.readUint32()); msg.setCurrentBasicAnimation(value); break; + case 19: + var value = /** @type {number} */ (reader.readUint32()); + msg.setTeam(value); + break; default: reader.skipField(); break; @@ -7275,6 +7263,13 @@ proto.Player.serializeBinaryToWriter = function(message, writer) { f ); } + f = /** @type {number} */ (jspb.Message.getField(message, 19)); + if (f != null) { + writer.writeUint32( + 19, + f + ); + } }; @@ -7919,6 +7914,42 @@ proto.Player.prototype.hasCurrentBasicAnimation = function() { }; +/** + * optional uint32 team = 19; + * @return {number} + */ +proto.Player.prototype.getTeam = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 19, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.Player} returns this + */ +proto.Player.prototype.setTeam = function(value) { + return jspb.Message.setField(this, 19, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.Player} returns this + */ +proto.Player.prototype.clearTeam = function() { + return jspb.Message.setField(this, 19, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.Player.prototype.hasTeam = function() { + return jspb.Message.getField(this, 19) != null; +}; + + diff --git a/apps/game_client/lib/game_client/protobuf/messages.pb.ex b/apps/game_client/lib/game_client/protobuf/messages.pb.ex index e9b3ef553..8b543c336 100644 --- a/apps/game_client/lib/game_client/protobuf/messages.pb.ex +++ b/apps/game_client/lib/game_client/protobuf/messages.pb.ex @@ -168,6 +168,15 @@ defmodule GameClient.Protobuf.BountySelected do field(:bounty, 1, type: GameClient.Protobuf.BountyInfo) end +defmodule GameClient.Protobuf.GameFinished.WinnersEntry do + @moduledoc false + + use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" + + field(:key, 1, type: :uint64) + field(:value, 2, type: GameClient.Protobuf.Entity) +end + defmodule GameClient.Protobuf.GameFinished.PlayersEntry do @moduledoc false @@ -182,7 +191,11 @@ defmodule GameClient.Protobuf.GameFinished do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field(:winner, 1, type: GameClient.Protobuf.Entity) + field(:winners, 1, + repeated: true, + type: GameClient.Protobuf.GameFinished.WinnersEntry, + map: true + ) field(:players, 2, repeated: true, @@ -551,6 +564,8 @@ defmodule GameClient.Protobuf.Player do type: :uint32, json_name: "currentBasicAnimation" ) + + field(:team, 19, proto3_optional: true, type: :uint32) end defmodule GameClient.Protobuf.Effect do diff --git a/apps/serialization/messages.proto b/apps/serialization/messages.proto index 966fc70f3..ef282e217 100644 --- a/apps/serialization/messages.proto +++ b/apps/serialization/messages.proto @@ -52,7 +52,7 @@ message BountySelected { } message GameFinished { - Entity winner = 1; + map winners = 1; map players = 2; } @@ -216,6 +216,7 @@ message Player { optional bool bounty_completed = 16; optional uint64 mana = 17; optional uint32 current_basic_animation = 18; + optional uint32 team = 19; } message Effect {