diff --git a/.github/workflows/arena-brazil-testing-deploy.yml b/.github/workflows/arena-brazil-testing-deploy.yml index 1e31e905b..09cb7ce3d 100644 --- a/.github/workflows/arena-brazil-testing-deploy.yml +++ b/.github/workflows/arena-brazil-testing-deploy.yml @@ -58,8 +58,11 @@ jobs: PHX_SERVER: ${{ vars.PHX_SERVER }} PHX_HOST: ${{ vars.HOST }} PORT: ${{ vars.ARENA_PORT }} + BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }} + BOT_MANAGER_HOST: ${{ vars.LOADTEST_CLIENT_HOST }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} - NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME_BRAZIL }} + NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME }} NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }} run: | @@ -70,7 +73,10 @@ jobs: RELEASE=${RELEASE} \ PHX_SERVER=${PHX_SERVER} \ PHX_HOST=${PHX_HOST} \ + BOT_MANAGER_HOST=${BOT_MANAGER_HOST} \ PORT=${PORT} \ + BOT_MANAGER_PORT=${BOT_MANAGER_PORT} \ + DATABASE_URL=${DATABASE_URL} \ SECRET_KEY_BASE=${SECRET_KEY_BASE} \ NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \ NEWRELIC_KEY=${NEWRELIC_KEY} \ diff --git a/.github/workflows/arena-europe-testing-deploy.yml b/.github/workflows/arena-europe-testing-deploy.yml index cbcb3d419..59b99d413 100644 --- a/.github/workflows/arena-europe-testing-deploy.yml +++ b/.github/workflows/arena-europe-testing-deploy.yml @@ -33,7 +33,7 @@ jobs: - name: Create ssh private key file from env var env: SSH_KEY: ${{ secrets.SSH_KEY }} - TS_HOST: ${{ vars.TS_EUROPE_HOST }} + TS_HOST: ${{ vars.TS_HOST }} run: | set -ex mkdir -p ~/.ssh/ @@ -44,22 +44,25 @@ jobs: - name: Copy deploy script env: SSH_USERNAME: ${{ vars.SSH_USERNAME }} - SSH_HOST: ${{ vars.TS_EUROPE_HOST }} + SSH_HOST: ${{ vars.TS_HOST }} run: | set -ex rsync -avz --mkpath devops/deploy.sh ${SSH_USERNAME}@${SSH_HOST}:/home/${SSH_USERNAME}/deploy-script/ - name: Execute deploy script env: - SSH_HOST: ${{ vars.TS_EUROPE_HOST }} + SSH_HOST: ${{ vars.TS_HOST }} SSH_USERNAME: ${{ vars.SSH_USERNAME }} MIX_ENV: ${{ vars.MIX_ENV }} RELEASE: ${{ inputs.release }} PHX_SERVER: ${{ vars.PHX_SERVER }} - PHX_HOST: ${{ vars.EUROPE_HOST }} + PHX_HOST: ${{ vars.HOST }} PORT: ${{ vars.ARENA_PORT }} + BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }} + BOT_MANAGER_HOST: ${{ vars.LOADTEST_CLIENT_HOST }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} - NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME_EUROPE }} + NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME }} NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }} BRANCH_NAME: ${{ github.head_ref || github.ref_name }} run: | @@ -71,6 +74,9 @@ jobs: PHX_SERVER=${PHX_SERVER} \ PHX_HOST=${PHX_HOST} \ PORT=${PORT} \ + BOT_MANAGER_PORT=${BOT_MANAGER_PORT} \ + BOT_MANAGER_HOST=${BOT_MANAGER_HOST} \ + DATABASE_URL=${DATABASE_URL} \ SECRET_KEY_BASE=${SECRET_KEY_BASE} \ NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \ NEWRELIC_KEY=${NEWRELIC_KEY} \ diff --git a/.github/workflows/loadtest-brazil-client-deploy.yml b/.github/workflows/loadtest-brazil-client-deploy.yml index d4d25ad4b..542d14e93 100644 --- a/.github/workflows/loadtest-brazil-client-deploy.yml +++ b/.github/workflows/loadtest-brazil-client-deploy.yml @@ -10,6 +10,7 @@ on: - arena - arena_load_test - game_client + - bot_manager required: true target_server: type: choice @@ -62,11 +63,11 @@ jobs: MIX_ENV: ${{ vars.MIX_ENV }} RELEASE: ${{ inputs.release }} TARGET_SERVER: ${{ inputs.target_server }} - BRAZIL_HOST: ${{ vars.brazil_host }} - EUROPE_HOST: ${{ vars.europe_host }} PHX_SERVER: ${{ vars.PHX_SERVER }} PHX_HOST: ${{ vars.LOADTEST_CLIENT_HOST }} PORT: ${{ vars.ARENA_PORT }} + BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME_LOADTEST }} NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }} @@ -78,11 +79,11 @@ jobs: MIX_ENV=${MIX_ENV} \ RELEASE=${RELEASE} \ TARGET_SERVER=${TARGET_SERVER} \ - BRAZIL_HOST=${BRAZIL_HOST} \ - EUROPE_HOST=${EUROPE_HOST} \ PHX_SERVER=${PHX_SERVER} \ PHX_HOST=${PHX_HOST} \ PORT=${PORT} \ + BOT_MANAGER_PORT=${BOT_MANAGER_PORT} \ + DATABASE_URL=${DATABASE_URL} \ SECRET_KEY_BASE=${SECRET_KEY_BASE} \ NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \ NEWRELIC_KEY=${NEWRELIC_KEY} \ diff --git a/.github/workflows/loadtest-brazil-server-deploy.yml b/.github/workflows/loadtest-brazil-server-deploy.yml index b0e1a5121..1ddc68e92 100644 --- a/.github/workflows/loadtest-brazil-server-deploy.yml +++ b/.github/workflows/loadtest-brazil-server-deploy.yml @@ -55,11 +55,11 @@ jobs: SSH_USERNAME: ${{ vars.SSH_USERNAME }} MIX_ENV: ${{ vars.MIX_ENV }} RELEASE: ${{ inputs.release }} - BRAZIL_HOST: ${{ vars.brazil_host }} - EUROPE_HOST: ${{ vars.europe_host }} PHX_SERVER: ${{ vars.PHX_SERVER }} PHX_HOST: ${{ vars.LOADTEST_SERVER_HOST }} PORT: ${{ vars.ARENA_PORT }} + BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME_LOADTEST }} NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }} @@ -70,11 +70,11 @@ jobs: BRANCH_NAME=${BRANCH_NAME} \ MIX_ENV=${MIX_ENV} \ RELEASE=${RELEASE} \ - BRAZIL_HOST=${BRAZIL_HOST} \ - EUROPE_HOST=${EUROPE_HOST} \ PHX_SERVER=${PHX_SERVER} \ PHX_HOST=${PHX_HOST} \ PORT=${PORT} \ + BOT_MANAGER_PORT=${BOT_MANAGER_PORT} \ + DATABASE_URL=${DATABASE_URL} \ SECRET_KEY_BASE=${SECRET_KEY_BASE} \ NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \ NEWRELIC_KEY=${NEWRELIC_KEY} \ diff --git a/.github/workflows/loadtest-europe-client-deploy.yml b/.github/workflows/loadtest-europe-client-deploy.yml index a3f620e73..85ae93de1 100644 --- a/.github/workflows/loadtest-europe-client-deploy.yml +++ b/.github/workflows/loadtest-europe-client-deploy.yml @@ -10,6 +10,7 @@ on: - arena - arena_load_test - game_client + - bot_manager required: true target_server: type: choice @@ -62,11 +63,11 @@ jobs: MIX_ENV: ${{ vars.MIX_ENV }} RELEASE: ${{ inputs.release }} TARGET_SERVER: ${{ inputs.target_server }} - BRAZIL_HOST: ${{ vars.brazil_host }} - EUROPE_HOST: ${{ vars.europe_host }} PHX_SERVER: ${{ vars.PHX_SERVER }} PHX_HOST: ${{ vars.LOADTEST_CLIENT_HOST }} PORT: ${{ vars.ARENA_PORT }} + BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME_LOADTEST }} NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }} @@ -78,11 +79,11 @@ jobs: MIX_ENV=${MIX_ENV} \ RELEASE=${RELEASE} \ TARGET_SERVER=${TARGET_SERVER} \ - BRAZIL_HOST=${BRAZIL_HOST} \ - EUROPE_HOST=${EUROPE_HOST} \ PHX_SERVER=${PHX_SERVER} \ PHX_HOST=${PHX_HOST} \ PORT=${PORT} \ + BOT_MANAGER_PORT=${BOT_MANAGER_PORT} \ + DATABASE_URL=${DATABASE_URL} \ SECRET_KEY_BASE=${SECRET_KEY_BASE} \ NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \ NEWRELIC_KEY=${NEWRELIC_KEY} \ diff --git a/.github/workflows/loadtest-europe-server-deploy.yml b/.github/workflows/loadtest-europe-server-deploy.yml index 89e6d00c4..645a3ba94 100644 --- a/.github/workflows/loadtest-europe-server-deploy.yml +++ b/.github/workflows/loadtest-europe-server-deploy.yml @@ -55,11 +55,11 @@ jobs: SSH_USERNAME: ${{ vars.SSH_USERNAME }} MIX_ENV: ${{ vars.MIX_ENV }} RELEASE: ${{ inputs.release }} - BRAZIL_HOST: ${{ vars.brazil_host }} - EUROPE_HOST: ${{ vars.europe_host }} PHX_SERVER: ${{ vars.PHX_SERVER }} PHX_HOST: ${{ vars.LOADTEST_SERVER_HOST }} PORT: ${{ vars.ARENA_PORT }} + BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME_LOADTEST }} NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }} @@ -70,11 +70,11 @@ jobs: BRANCH_NAME=${BRANCH_NAME} \ MIX_ENV=${MIX_ENV} \ RELEASE=${RELEASE} \ - BRAZIL_HOST=${BRAZIL_HOST} \ - EUROPE_HOST=${EUROPE_HOST} \ PHX_SERVER=${PHX_SERVER} \ PHX_HOST=${PHX_HOST} \ PORT=${PORT} \ + BOT_MANAGER_PORT=${BOT_MANAGER_PORT} \ + DATABASE_URL=${DATABASE_URL} \ SECRET_KEY_BASE=${SECRET_KEY_BASE} \ NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \ NEWRELIC_KEY=${NEWRELIC_KEY} \ diff --git a/apps/arena/docs/collision-detection.md b/apps/arena/docs/collision-detection.md index d11f9481a..93779d623 100644 --- a/apps/arena/docs/collision-detection.md +++ b/apps/arena/docs/collision-detection.md @@ -1,5 +1,6 @@ # Collision detection +## Jeffrey Thompson's implementation A collision detection library implementation based on [https://www.jeffreythompson.org/collision-detection/](https://www.jeffreythompson.org/collision-detection/). We are working with entities. @@ -54,3 +55,35 @@ If you only need to detect when a circle collides with the "walls" of the polygo It is necessary to clarify that there are other types of collisions that we do not need to implement at the moment, for example Polygon/Polygon. +## SAT (Simple Axis Theorem) implementation +A collision detection library implementation based on the [SAT theorem](https://dyn4j.org/2010/01/sat/). + +We'll keep working with entities with the same shapes as the ones used by Jeffrey Thompson. + +To detect a collision between two entities, we'll iterate over an axis for each pair of vertices from both entities that collided. +For circular shapes, we'll check the axis that's formed between the circle center and the closest polygon vertex, and then cast the +maximum and minimum vertex to the normal of each pair of vertices. As soon as we find an axis where the cast of the shapes are not overlapping, we can safely say that the entities are not colliding. + +#### Triangle example for axis where the cast in the axis are overlapping +![Polygons axis overlapping](./images/sat-overlapping.jpg "Polygons axis overlapping") +#### Triangle example for axis where the cast in the axis are not overlapping +![Polygons axis not overlapping](./images/sat-no-overlapping.jpg "Polygons axis not overlapping") + +The collisions we currently support are: + +1. Circle with polygon: +![Circle/Polygon not colliding](./images/circle-polygon-not-colliding.jpg "Circle/Polygon not colliding") + +![Circle/Polygon colliding](./images/circle-polygon-colliding.jpg "Circle/Polygon colliding") + + + +## Collision resolution + +The Jeffrey Thompson's implementation for collision detection is ideal since it's the cheapest way to +detect that two entities collided, but it's incomplete since we can only detect that a collision occurred +and nothing else, so we decided to take the following approach: + +1. We use the Jeffrey Thompson's implementation to detect that a collision actually happened. +2. Using SAT we can get the normal that will resolve that collision and the amount of movement we should do +so the entities are no longer colliding, so when a collision occurs we will use SAT to push the entities in the direction that, with the minimum movement, will resolve that collision. diff --git a/apps/arena/docs/images/sat-no-overlapping.jpg b/apps/arena/docs/images/sat-no-overlapping.jpg new file mode 100644 index 000000000..b49d4546a Binary files /dev/null and b/apps/arena/docs/images/sat-no-overlapping.jpg differ diff --git a/apps/arena/docs/images/sat-overlapping.jpg b/apps/arena/docs/images/sat-overlapping.jpg new file mode 100644 index 000000000..beaa12681 Binary files /dev/null and b/apps/arena/docs/images/sat-overlapping.jpg differ diff --git a/apps/arena/lib/arena/entities.ex b/apps/arena/lib/arena/entities.ex index eb650f289..0fa43220e 100644 --- a/apps/arena/lib/arena/entities.ex +++ b/apps/arena/lib/arena/entities.ex @@ -157,15 +157,15 @@ defmodule Arena.Entities do } end - def new_circular_obstacle(id, position, radius) do + def new_obstacle(id, %{position: position, radius: radius, shape: shape, vertices: vertices}) do %{ id: id, category: :obstacle, - shape: :circle, + shape: get_shape(shape), name: "Obstacle" <> Integer.to_string(id), position: position, radius: radius, - vertices: [], + vertices: vertices, speed: 0.0, direction: %{ x: 0.0, @@ -244,6 +244,13 @@ defmodule Arena.Entities do }} end + def maybe_add_custom_info(entity) when entity.category == :obstacle do + {:obstacle, + %Arena.Serialization.Obstacle{ + color: "red" + }} + end + def maybe_add_custom_info(entity) when entity.category == :pool do {:pool, %Arena.Serialization.Pool{ @@ -261,4 +268,10 @@ defmodule Arena.Entities do def maybe_add_custom_info(_entity) do nil end + + defp get_shape("polygon"), do: :polygon + defp get_shape("circle"), do: :circle + defp get_shape("line"), do: :line + defp get_shape("point"), do: :point + defp get_shape(_), do: nil end diff --git a/apps/arena/lib/arena/game/player.ex b/apps/arena/lib/arena/game/player.ex index 5e55a70ab..fa4576f45 100644 --- a/apps/arena/lib/arena/game/player.ex +++ b/apps/arena/lib/arena/game/player.ex @@ -6,11 +6,11 @@ defmodule Arena.Game.Player do alias Arena.Utils alias Arena.Game.Skill - def add_action(player, action_name, duration_ms) do - Process.send_after(self(), {:remove_skill_action, player.id, action_name}, duration_ms) + def add_action(player, action) do + Process.send_after(self(), {:remove_skill_action, player.id, action.action}, action.duration) update_in(player, [:aditional_info, :current_actions], fn current_actions -> - current_actions ++ [%{action: action_name, duration: duration_ms}] + current_actions ++ [action] end) end @@ -189,19 +189,39 @@ defmodule Arena.Game.Player do skill.activation_delay_ms ) - action_name = skill_key_execution_action(skill_key) + action = + %{ + action: skill_key_execution_action(skill_key), + duration: skill.execution_duration_ms + } + |> maybe_add_destination(player, skill_direction, skill) player = - add_action(player, action_name, skill.execution_duration_ms) + add_action(player, action) |> apply_skill_cooldown(skill_key, skill) |> put_in([:direction], skill_direction |> Utils.normalize()) |> put_in([:is_moving], false) |> put_in([:aditional_info, :last_skill_triggered], System.monotonic_time(:millisecond)) + |> maybe_make_invincible(skill) put_in(game_state, [:players, player.id], player) end end + # This is a messy solution to get a mechanic result before actually running the mechanic since the client needed the + # position in wich the player will spawn when the skill start and not when we actually execute the teleport + # this is also optimistic since we asume the destination will be always available + defp maybe_add_destination(action, player, skill_direction, %{mechanics: [{:teleport, teleport}]}) do + target_position = %{ + x: player.position.x + skill_direction.x * teleport.range, + y: player.position.y + skill_direction.y * teleport.range + } + + Map.put(action, :destination, target_position) + end + + defp maybe_add_destination(action, _, _, _), do: action + @doc """ Receives a player that owns the damage and the damage number @@ -362,4 +382,16 @@ defmodule Arena.Game.Player do player end end + + defp maybe_make_invincible( + player, + %{inmune_while_executing: true, execution_duration_ms: execution_duration_ms} = _skill + ) do + Process.send_after(self(), {:remove_damage_immunity, player.id}, execution_duration_ms) + put_in(player, [:aditional_info, :damage_immunity], true) + end + + defp maybe_make_invincible(player, _) do + player + end end diff --git a/apps/arena/lib/arena/game/skill.ex b/apps/arena/lib/arena/game/skill.ex index bf64de394..05655fd6e 100644 --- a/apps/arena/lib/arena/game/skill.ex +++ b/apps/arena/lib/arena/game/skill.ex @@ -115,13 +115,13 @@ defmodule Arena.Game.Skill do ) do Process.send_after(self(), {:stop_dash, entity.id, entity.speed}, duration) - player = + entity = entity |> Map.put(:is_moving, true) |> Map.put(:speed, speed) |> put_in([:aditional_info, :forced_movement], true) - players = Map.put(game_state.players, entity.id, player) + players = Map.put(game_state.players, entity.id, entity) %{game_state | players: players} end @@ -202,7 +202,7 @@ defmodule Arena.Game.Skill do last_id, get_position_with_offset( entity_player_owner.position, - entity_player_owner.directio, + entity_player_owner.direction, simple_shoot.projectile_offset ), entity.direction, @@ -240,7 +240,21 @@ defmodule Arena.Game.Skill do |> Map.put(:speed, speed) |> put_in([:aditional_info, :forced_movement], true) - put_in(game_state, [:players, entity.id], player) + put_in(game_state, [:players, player.id], player) + end + + def do_mechanic(game_state, entity, {:teleport, teleport}, %{skill_direction: skill_target}) do + target_position = %{ + x: entity.position.x + skill_target.x * teleport.range, + y: entity.position.y + skill_target.y * teleport.range + } + + entity = + entity + |> Physics.move_entity_to_position(target_position, game_state.external_wall) + |> Map.put(:aditional_info, entity.aditional_info) + + put_in(game_state, [:players, entity.id], entity) end def do_mechanic(game_state, player, {:spawn_pool, pool_params}, %{ @@ -347,11 +361,15 @@ defmodule Arena.Game.Skill do player pool -> - direction = Physics.get_direction_from_positions(player.position, pool.position) - - Physics.move_entity_to_direction(player, direction, pull_params.force) - |> Map.put(:aditional_info, player.aditional_info) - |> Map.put(:collides_with, player.collides_with) + if player.aditional_info.damage_immunity do + player + else + direction = Physics.get_direction_from_positions(player.position, pool.position) + + Physics.move_entity_to_direction(player, direction, pull_params.force) + |> Map.put(:aditional_info, player.aditional_info) + |> Map.put(:collides_with, player.collides_with) + end end end diff --git a/apps/arena/lib/arena/game_updater.ex b/apps/arena/lib/arena/game_updater.ex index 40797719c..8feeda421 100644 --- a/apps/arena/lib/arena/game_updater.ex +++ b/apps/arena/lib/arena/game_updater.ex @@ -497,7 +497,8 @@ defmodule Arena.GameUpdater do damage_taken: state.damage_taken, damage_done: state.damage_done, status: state.status, - start_game_timestamp: state.start_game_timestamp + start_game_timestamp: state.start_game_timestamp, + obstacles: complete_entities(state.obstacles) }} }) @@ -526,7 +527,6 @@ defmodule Arena.GameUpdater do defp complete_entity(entity) do Map.put(entity, :category, to_string(entity.category)) |> Map.put(:shape, to_string(entity.shape)) - |> Map.put(:name, entity.name) |> Map.put(:aditional_info, entity |> Entities.maybe_add_custom_info()) end @@ -551,6 +551,7 @@ defmodule Arena.GameUpdater do |> Map.put(:projectiles, %{}) |> Map.put(:items, %{}) |> Map.put(:player_timestamps, %{}) + |> Map.put(:obstacles, %{}) |> Map.put(:server_timestamp, 0) |> Map.put(:client_to_player_map, %{}) |> Map.put(:pools, %{}) @@ -609,7 +610,10 @@ defmodule Arena.GameUpdater do Map.put( obstacles_acc, last_id, - Entities.new_circular_obstacle(last_id, obstacle.position, obstacle.radius) + Entities.new_obstacle( + last_id, + obstacle + ) ) {obstacles_acc, last_id} diff --git a/apps/arena/lib/arena/serialization/messages.pb.ex b/apps/arena/lib/arena/serialization/messages.pb.ex index 17928ba85..0e91a0fcb 100644 --- a/apps/arena/lib/arena/serialization/messages.pb.ex +++ b/apps/arena/lib/arena/serialization/messages.pb.ex @@ -241,6 +241,15 @@ defmodule Arena.Serialization.GameState.ItemsEntry do field(:value, 2, type: Arena.Serialization.Entity) end +defmodule Arena.Serialization.GameState.ObstaclesEntry do + @moduledoc false + + use Protobuf, map: true, protoc_gen_elixir_version: "0.12.0", syntax: :proto3 + + field(:key, 1, type: :uint64) + field(:value, 2, type: Arena.Serialization.Entity) +end + defmodule Arena.Serialization.GameState.PoolsEntry do @moduledoc false @@ -299,7 +308,14 @@ defmodule Arena.Serialization.GameState do field(:status, 11, type: Arena.Serialization.GameStatus, enum: true) field(:start_game_timestamp, 12, type: :int64, json_name: "startGameTimestamp") field(:items, 13, repeated: true, type: Arena.Serialization.GameState.ItemsEntry, map: true) - field(:pools, 14, repeated: true, type: Arena.Serialization.GameState.PoolsEntry, map: true) + + field(:obstacles, 14, + repeated: true, + type: Arena.Serialization.GameState.ObstaclesEntry, + map: true + ) + + field(:pools, 15, repeated: true, type: Arena.Serialization.GameState.PoolsEntry, map: true) end defmodule Arena.Serialization.Entity do @@ -431,6 +447,7 @@ defmodule Arena.Serialization.PlayerAction do field(:action, 1, type: Arena.Serialization.PlayerActionType, enum: true) field(:duration, 2, type: :uint64) + field(:destination, 3, type: Arena.Serialization.Position) end defmodule Arena.Serialization.Move do diff --git a/apps/arena/lib/physics.ex b/apps/arena/lib/physics.ex index 599fd2260..e458f559c 100644 --- a/apps/arena/lib/physics.ex +++ b/apps/arena/lib/physics.ex @@ -17,6 +17,10 @@ defmodule Physics do def move_entity(_entity, _ticks_to_move, _external_wall, _obstacles), do: :erlang.nif_error(:nif_not_loaded) + def move_entity(_entity, _ticks_to_move, _external_wall), do: :erlang.nif_error(:nif_not_loaded) + + def move_entity_to_position(_entity, _new_position, _external_wall), do: :erlang.nif_error(:nif_not_loaded) + def move_entity_to_direction(_entity, _direction, _amount), do: :erlang.nif_error(:nif_not_loaded) diff --git a/apps/arena/native/physics/src/collision_detection.rs b/apps/arena/native/physics/src/collision_detection.rs index 23a9a89ac..1711ca999 100644 --- a/apps/arena/native/physics/src/collision_detection.rs +++ b/apps/arena/native/physics/src/collision_detection.rs @@ -1,5 +1,5 @@ use crate::map::{Entity, Position}; - +pub mod sat; /* * Determines if a collision has occured between a point and a circle * If the distance between the point and the center of the circle is less diff --git a/apps/arena/native/physics/src/collision_detection/sat.rs b/apps/arena/native/physics/src/collision_detection/sat.rs new file mode 100644 index 000000000..6615a4d21 --- /dev/null +++ b/apps/arena/native/physics/src/collision_detection/sat.rs @@ -0,0 +1,279 @@ +use std::mem::swap; + +use crate::map::{Entity, Position}; +/* + Collision detection using the [SAT theorem](https://dyn4j.org/2010/01/sat/) + To determine if a pair of shapes are colliding we'll try to draw a line from an axis where the entities + are not overlaped, if we found at least one axis that meet this we can ensure that the entities are not + overlaping + + DISCLAIMER: this algorithm works for collisions with polygons that are CONVEX ONLY which means + that ALL of his internal angles has less than 180° degrees, if we would have a CONCAVE polygon this + should be built differently. The usage of various convex polygons can be a solution. + +*/ + +// Handle the intesection between a circle and a polygon, the return value is a tuple of 3 elements: +// 1: bool = true if the entities are colliding +// 2: Position = nomalized line of collision +// 3: f32 = the minimum amount of overlap between the shapes that would solve the collision +pub(crate) fn intersect_circle_polygon( + circle: &mut Entity, + polygon: &Entity, +) -> (bool, Position, f32) { + // The normal will be the vector in which the polygons should move to stop colliding + let mut normal = Position { x: 0.0, y: 0.0 }; + // The depth is the amount of overlapping between both entities + let mut result_depth: f32 = f32::MAX; + + let mut axis: Position; + + // Check normal and depth for polygon + for current_vertex_index in 0..polygon.vertices.len() { + let mut next_vertex_index = current_vertex_index + 1; + if next_vertex_index == polygon.vertices.len() { + next_vertex_index = 0 + }; + let current_vertex = polygon.vertices[current_vertex_index]; + let next_vertex = polygon.vertices[next_vertex_index]; + + let current_line = Position::sub(current_vertex, next_vertex); + // the axis will be the perpendicular line drawn from the current line of the polygon + axis = Position { + x: -current_line.y, + y: current_line.x, + }; + + // FIXME normalizing on this loop may be bad + axis.normalize(); + let (min_polygon_cast_point, max_polygon_cast_point) = + project_vertices(&polygon.vertices, axis); + let (min_circle_cast_point, max_circle_cast_point) = project_circle(circle, axis); + + // If there's a gap between the polygon it means they do not collide and we can safely return false + if min_polygon_cast_point >= max_circle_cast_point + || min_circle_cast_point >= max_polygon_cast_point + { + return (false, normal, result_depth); + } + + let circle_overlap_depth = max_circle_cast_point - min_polygon_cast_point; + + let polygon_overlap_depth = max_polygon_cast_point - min_circle_cast_point; + + let min_depth = f32::min(circle_overlap_depth, polygon_overlap_depth); + + if min_depth < result_depth { + // If we hit the polygon from the right or top we need to turn around the direction + if polygon_overlap_depth > circle_overlap_depth { + normal = Position { + x: -axis.x, + y: -axis.y, + }; + } else { + normal = axis; + } + result_depth = min_depth; + } + } + + // Check normal and depth for center + let closest_vertex = find_closest_vertex(&circle.position, &polygon.vertices); + axis = Position::sub(closest_vertex, circle.position); + axis.normalize(); + + let (min_polygon_cast_point, max_polygon_cast_point) = + project_vertices(&polygon.vertices, axis); + let (min_circle_cast_point, max_circle_cast_point) = project_circle(circle, axis); + + // If there's a gap between the polygon it means they do not collide and we can safely return false + if min_polygon_cast_point >= max_circle_cast_point + || min_circle_cast_point >= max_polygon_cast_point + { + return (false, normal, result_depth); + } + + let circle_overlap_depth = max_circle_cast_point - min_polygon_cast_point; + + let polygon_overlap_depth = max_polygon_cast_point - min_circle_cast_point; + + let axis_depth = f32::min(circle_overlap_depth, polygon_overlap_depth); + + if axis_depth < result_depth { + // If we hit the polygon from the right or top we need to turn around the direction + if polygon_overlap_depth > circle_overlap_depth { + normal = Position { + x: -axis.x, + y: -axis.y, + }; + } else { + normal = axis; + } + result_depth = axis_depth; + } + + (true, normal, result_depth) +} + +// Uncomment this if we need a polygon-polygon collision detection + +// Handle the intesection between two polygons, the return value is a tuple of 3 elements +// a: bool = true if the entities are colliding +// b: Position = nomalized line of collision +// c: f32 = the amount of overlap between the shapes +// pub(crate) fn intersect_polygon_polygon( +// polygonA: &Entity, +// polygonB: &Entity, +// ) -> (bool, Position, f32) { +// // The normal will be the vector in wich the polygons should move to stop colliding +// let mut normal = Position { x: 0.0, y: 0.0 }; +// // The depth is the amount of overlapping between both entities +// let mut depth: f32 = f32::MAX; + +// let mut axis: Position; + +// // Check normal and depth for polygonA +// for current in 0..polygonA.vertices.len() { +// let mut next = current + 1; +// if next == polygonA.vertices.len() { +// next = 0 +// }; +// let va = polygonA.vertices[current]; +// let vb = polygonA.vertices[next]; + +// let edge = Position::sub(va, vb); +// axis = Position { +// x: -edge.y, +// y: edge.x, +// }; +// // FIXME normalizing on this loop may be bad +// axis.normalize(); +// let (min_a, max_a) = project_vertices(&polygonA.vertices, axis); +// let (min_b, max_b) = project_vertices(&polygonB.vertices, axis); + +// // If there's a gap between the polygon it means they do not collide and we can safely return false +// if min_a >= max_b || min_b >= max_a { +// return (false, normal, depth); +// } + +// let depth_a = max_b - min_a; +// let depth_b = max_a - min_b; +// let axis_depth = f32::min(depth_a, depth_b); + +// if axis_depth < depth { +// depth = axis_depth; +// if depth_b > depth_a { +// normal = Position { +// x: -axis.x, +// y: -axis.y, +// }; +// } else { +// normal = axis; +// } +// } +// } +// // Check normal and depth for polygonB +// for current in 0..polygonB.vertices.len() { +// let mut next = current + 1; +// if next == polygonB.vertices.len() { +// next = 0 +// }; +// let va = polygonB.vertices[current]; +// let vb = polygonB.vertices[next]; + +// let edge = Position::sub(va, vb); +// axis = Position { +// x: -edge.y, +// y: edge.x, +// }; +// // FIXME normalizing on this loop may be bad +// axis.normalize(); +// let (min_a, max_a) = project_vertices(&polygonB.vertices, axis); +// let (min_b, max_b) = project_vertices(&polygonA.vertices, axis); + +// // If there's a gap between the polygon it means they do not collide and we can safely return false +// if min_a >= max_b || min_b >= max_a { +// return (false, normal, depth); +// } + +// let depth_a = max_b - min_a; +// let depth_b = max_a - min_b; +// let axis_depth = f32::min(depth_a, depth_b); + +// if axis_depth < depth { +// depth = axis_depth; +// if depth_b > depth_a { +// normal = Position { +// x: -axis.x, +// y: -axis.y, +// }; +// } else { +// normal = axis; +// } +// } +// } + +// (true, normal, depth) +// } + +// Get the min and max values from a polygon projected on a specific axis +fn project_vertices(vertices: &Vec, axis: Position) -> (f32, f32) { + let mut min = f32::MAX; + let mut max = f32::MIN; + + for current in vertices { + let projection = dot(current, axis); + + if projection < min { + min = projection + }; + if projection > max { + max = projection + }; + } + + (min, max) +} + +// Get the min and max values from a circle projected on a specific axis +fn project_circle(circle: &Entity, axis: Position) -> (f32, f32) { + let mut min; + let mut max; + + let direction_radius = Position { + x: axis.x * circle.radius, + y: axis.y * circle.radius, + }; + + let position_plus_radius = Position::add(circle.position, direction_radius); + let position_sub_radius = Position::sub(circle.position, direction_radius); + + min = dot(&position_plus_radius, axis); + max = dot(&position_sub_radius, axis); + + if min > max { + swap(&mut max, &mut min); + } + + (min, max) +} + +// Receives a position x and a vector of positions and returns the closest vector to the +// x position +fn find_closest_vertex(center: &Position, vertices: &Vec) -> Position { + let mut result = Position { x: 0.0, y: 0.0 }; + let mut min_distance = f32::MAX; + for current in vertices { + let distance = center.distance_to_position(current); + if distance < min_distance { + min_distance = distance; + result = *current; + } + } + + result +} + +fn dot(a: &Position, b: Position) -> f32 { + a.x * b.x + a.y * b.y +} diff --git a/apps/arena/native/physics/src/lib.rs b/apps/arena/native/physics/src/lib.rs index c08301f9a..979111d78 100644 --- a/apps/arena/native/physics/src/lib.rs +++ b/apps/arena/native/physics/src/lib.rs @@ -31,8 +31,11 @@ fn move_entities( let collides_with = entity.collides_with(obstacles.clone().into_values().collect()); if entity.category == Category::Player && !collides_with.is_empty() { - entity - .move_to_next_valid_position_outside(obstacles.get(&collides_with[0]).unwrap()); + let collided_with: Vec<&Entity> = collides_with + .iter() + .map(|id| obstacles.get(id).unwrap()) + .collect(); + entity.move_to_next_valid_position_outside(collided_with); } } } @@ -58,13 +61,32 @@ fn move_entity( let collides_with = entity.collides_with(obstacles.clone().into_values().collect()); if entity.category == Category::Player && !collides_with.is_empty() { - entity.move_to_next_valid_position_outside(obstacles.get(&collides_with[0]).unwrap()); + let collided_with: Vec<&Entity> = collides_with + .iter() + .map(|id| obstacles.get(id).unwrap()) + .collect(); + entity.move_to_next_valid_position_outside(collided_with); } } entity } +#[rustler::nif()] +fn move_entity_to_position( + entity: Entity, + new_position: Position, + external_wall: Entity, +) -> Entity { + let mut entity: Entity = entity; + entity.position = new_position; + + if entity.category == Category::Player && !entity.is_inside_map(&external_wall) { + entity.move_to_next_valid_position_inside(&external_wall); + } + entity +} + #[rustler::nif()] fn move_entity_to_direction(entity: Entity, direction: Position, amount: f32) -> Entity { let mut entity: Entity = entity; @@ -186,6 +208,7 @@ rustler::init!( calculate_triangle_vertices, get_direction_from_positions, calculate_speed, - nearest_entity_direction + nearest_entity_direction, + move_entity_to_position ] ); diff --git a/apps/arena/native/physics/src/map.rs b/apps/arena/native/physics/src/map.rs index eecc5fda6..f1b24ea94 100644 --- a/apps/arena/native/physics/src/map.rs +++ b/apps/arena/native/physics/src/map.rs @@ -1,11 +1,11 @@ use rustler::{NifMap, NifTaggedEnum}; use serde::Deserialize; +use crate::collision_detection::sat::intersect_circle_polygon; use crate::collision_detection::{ circle_circle_collision, circle_polygon_collision, line_circle_collision, point_circle_collision, }; - #[derive(NifMap, Clone)] pub struct Polygon { pub id: u64, @@ -55,6 +55,33 @@ pub enum Category { Item, } +impl Position { + pub fn normalize(&mut self) { + let length = (self.x.powi(2) + self.y.powi(2)).sqrt(); + self.x /= length; + self.y /= length; + } + + pub fn add(a: Position, b: Position) -> Position { + Position { + x: a.x + b.x, + y: a.y + b.y, + } + } + pub fn sub(a: Position, b: Position) -> Position { + Position { + x: a.x - b.x, + y: a.y - b.y, + } + } + + pub fn distance_to_position(&self, other_position: &Position) -> f32 { + let x = self.x - other_position.x; + let y = self.y - other_position.y; + (x.powi(2) + y.powi(2)).sqrt() + } +} + impl Entity { pub fn new_point(id: u64, position: Position) -> Entity { Entity { @@ -152,8 +179,8 @@ impl Entity { self.position = self.find_edge_position_inside(external_wall); } - pub fn move_to_next_valid_position_outside(&mut self, external_wall: &Entity) { - self.position = self.find_edge_position_outside(external_wall); + pub fn move_to_next_valid_position_outside(&mut self, collided_with: Vec<&Entity>) { + self.move_to_edge_position_outside(collided_with); } pub fn find_edge_position_inside(&mut self, external_wall: &Entity) -> Position { @@ -172,21 +199,37 @@ impl Entity { } } - pub fn find_edge_position_outside(&mut self, entity: &Entity) -> Position { - let x = self.position.x - entity.position.x; - let y = self.position.y - entity.position.y; - let length = (x.powf(2.) + y.powf(2.)).sqrt(); - let normalized_direction = Position { - x: x / length, - y: y / length, - }; - Position { - x: entity.position.x - + normalized_direction.x * entity.radius - + normalized_direction.x * self.radius, - y: entity.position.y - + normalized_direction.y * entity.radius - + normalized_direction.y * self.radius, + pub fn move_to_edge_position_outside(&mut self, collisions: Vec<&Entity>) { + for entity in collisions { + match entity.shape { + Shape::Circle => { + let mut normalized_direction = Position::sub(self.position, entity.position); + normalized_direction.normalize(); + + let new_pos = Position { + x: entity.position.x + + normalized_direction.x * entity.radius + + normalized_direction.x * self.radius, + y: entity.position.y + + normalized_direction.y * entity.radius + + normalized_direction.y * self.radius, + }; + + self.position = new_pos; + } + Shape::Polygon => { + let (collided, direction, depth) = intersect_circle_polygon(self, entity); + + if collided { + let new_pos = Position { + x: self.position.x + direction.x * depth, + y: self.position.y + direction.y * depth, + }; + self.position = new_pos; + } + } + _ => continue, + } } } diff --git a/apps/arena/priv/config.json b/apps/arena/priv/config.json index a4841aaac..be102e91f 100644 --- a/apps/arena/priv/config.json +++ b/apps/arena/priv/config.json @@ -49,21 +49,27 @@ "x": -6044.0, "y": -1439.0 }, - "radius": 1567.0 + "radius": 1567.0, + "shape": "circle", + "vertices": [] }, { "position": { "x": -860.0, "y": 5338.0 }, - "radius": 1090.0 + "radius": 1090.0, + "shape": "circle", + "vertices": [] }, { "position": { "x": -1630.0, "y": 4588.0 }, - "radius": 317.0 + "radius": 317.0, + "shape": "circle", + "vertices": [] } ] }, @@ -304,6 +310,27 @@ ], "effects_to_apply": [] }, + { + "name": "valt_warp", + "cooldown_mechanism": "time", + "cooldown_ms": 2000, + "execution_duration_ms": 800, + "inmune_while_executing": true, + "activation_delay_ms": 600, + "is_passive": false, + "autoaim": false, + "can_pick_destination": true, + "stamina_cost": 1, + "mechanics": [ + { + "teleport": { + "range": 1000, + "duration_ms": 300 + } + } + ], + "effects_to_apply": [] + }, { "name": "uma_sneak", "cooldown_mechanism": "time", @@ -454,7 +481,7 @@ "skills": { "1": "valt_antimatter", "2": "valt_singularity", - "3": "valt_sneak" + "3": "valt_warp" } } ], diff --git a/apps/arena_load_test/lib/arena_load_test/game_socket_handler.ex b/apps/arena_load_test/lib/arena_load_test/game_socket_handler.ex index 4206d5163..00faebd5c 100644 --- a/apps/arena_load_test/lib/arena_load_test/game_socket_handler.ex +++ b/apps/arena_load_test/lib/arena_load_test/game_socket_handler.ex @@ -23,13 +23,17 @@ defmodule ArenaLoadTest.GameSocketHandler do end # Callbacks - def handle_frame({_type, _msg} = _frame, state) do - {:ok, state} + def handle_frame({:binary, msg} = _frame, state) do + {event_type, _state} = Serialization.GameEvent.decode(msg) |> Map.get(:event) + + if event_type == :finished do + {:stop, state} + else + {:ok, state} + end end def handle_info(:move, state) do - Logger.info("Sending GameAction frame with MOVE payload") - {x, y} = create_random_movement() timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond) @@ -53,7 +57,6 @@ defmodule ArenaLoadTest.GameSocketHandler do end def handle_info(:attack, state) do - Logger.info("Sending GameAction frame with ATTACK payload") timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond) {x, y} = create_random_movement() @@ -83,6 +86,19 @@ defmodule ArenaLoadTest.GameSocketHandler do {:reply, frame, state} end + def terminate(_, state) do + case :ets.lookup(:clients, state.client_id) do + [{client_id, _}] -> + :ets.delete(:clients, client_id) + + [] -> + raise KeyError, message: "Client with ID #{state.client_id} doesn't exist." + end + + Logger.info("Player websocket terminated. Game Ended.") + exit(:normal) + end + # Private defp create_random_movement() do Enum.random([ 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 85c4fc69b..eb3d7d0b7 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 @@ -457,6 +457,7 @@ defmodule ArenaLoadTest.Serialization.PlayerAction do field(:action, 1, type: ArenaLoadTest.Serialization.PlayerActionType, enum: true) field(:duration, 2, type: :uint64) + field(:destination, 3, type: ArenaLoadTest.Serialization.Position) end defmodule ArenaLoadTest.Serialization.Move do diff --git a/apps/arena_load_test/lib/arena_load_test/socket_supervisor.ex b/apps/arena_load_test/lib/arena_load_test/socket_supervisor.ex index 3d88eb034..d8bdf2d9e 100644 --- a/apps/arena_load_test/lib/arena_load_test/socket_supervisor.ex +++ b/apps/arena_load_test/lib/arena_load_test/socket_supervisor.ex @@ -5,6 +5,7 @@ defmodule ArenaLoadTest.SocketSupervisor do use DynamicSupervisor alias ArenaLoadTest.SocketHandler alias ArenaLoadTest.GameSocketHandler + require Logger def start_link(args) do DynamicSupervisor.start_link(__MODULE__, args, name: __MODULE__, max_restarts: 1) @@ -31,9 +32,17 @@ defmodule ArenaLoadTest.SocketSupervisor do # Creates `num_clients` clients to join a game def spawn_players(num_clients) do - for i <- 1..num_clients do - {:ok, _pid} = add_new_client(i) + case :ets.whereis(:clients) do + :undefined -> :ets.new(:clients, [:set, :named_table, :public]) + _table_exists_already -> nil end + + Enum.each(1..num_clients, fn client_number -> + Logger.info("Iteration: #{client_number}") + {:ok, _pid} = add_new_client(client_number) + true = :ets.insert(:clients, {client_number, "1"}) + Logger.info("Clients alive: #{:ets.info(:clients, :size)}") + end) end def get_server_url("Brazil"), do: System.get_env("BRAZIL_HOST") diff --git a/apps/bot_manager/lib/bot_supervisor.ex b/apps/bot_manager/lib/bot_supervisor.ex index c0579506a..56ecd26d0 100644 --- a/apps/bot_manager/lib/bot_supervisor.ex +++ b/apps/bot_manager/lib/bot_supervisor.ex @@ -13,6 +13,8 @@ defmodule BotManager.BotSupervisor do end def add_bot_to_game(bot_config) do - DynamicSupervisor.start_child(__MODULE__, {BotManager.GameSocketHandler, bot_config}) + if System.get_env("BOTS_ACTIVE") == "true" do + DynamicSupervisor.start_child(__MODULE__, {BotManager.GameSocketHandler, bot_config}) + end end end diff --git a/apps/bot_manager/lib/endpoint.ex b/apps/bot_manager/lib/endpoint.ex index 9be3a627c..519c72d94 100644 --- a/apps/bot_manager/lib/endpoint.ex +++ b/apps/bot_manager/lib/endpoint.ex @@ -26,7 +26,7 @@ defmodule BotManager.Endpoint do plug(:dispatch) get "/join/:game_id/:bot_client" do - bot_pid = BotManager.BotSupervisor.add_bot_to_game(conn.params) + bot_pid = BotManager.BotSupervisor.add_bot_to_game(conn.params) || "" conn |> put_resp_content_type("application/json") diff --git a/apps/game_client/assets/js/protobuf/messages_pb.js b/apps/game_client/assets/js/protobuf/messages_pb.js index 285d6000e..7f3fcff49 100644 --- a/apps/game_client/assets/js/protobuf/messages_pb.js +++ b/apps/game_client/assets/js/protobuf/messages_pb.js @@ -2991,6 +2991,7 @@ proto.GameState.toObject = function(includeInstance, msg) { status: jspb.Message.getFieldWithDefault(msg, 11, 0), startGameTimestamp: jspb.Message.getFieldWithDefault(msg, 12, 0), itemsMap: (f = msg.getItemsMap()) ? f.toObject(includeInstance, proto.Entity.toObject) : [], + obstaclesMap: (f = msg.getObstaclesMap()) ? f.toObject(includeInstance, proto.Entity.toObject) : [], poolsMap: (f = msg.getPoolsMap()) ? f.toObject(includeInstance, proto.Entity.toObject) : [] }; @@ -3097,6 +3098,12 @@ proto.GameState.deserializeBinaryFromReader = function(msg, reader) { }); break; case 14: + var value = msg.getObstaclesMap(); + 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 15: var value = msg.getPoolsMap(); 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()); @@ -3203,10 +3210,14 @@ proto.GameState.serializeBinaryToWriter = function(message, writer) { if (f && f.getLength() > 0) { f.serializeBinary(13, writer, jspb.BinaryWriter.prototype.writeUint64, jspb.BinaryWriter.prototype.writeMessage, proto.Entity.serializeBinaryToWriter); } - f = message.getPoolsMap(true); + f = message.getObstaclesMap(true); if (f && f.getLength() > 0) { f.serializeBinary(14, writer, jspb.BinaryWriter.prototype.writeUint64, jspb.BinaryWriter.prototype.writeMessage, proto.Entity.serializeBinaryToWriter); } + f = message.getPoolsMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(15, writer, jspb.BinaryWriter.prototype.writeUint64, jspb.BinaryWriter.prototype.writeMessage, proto.Entity.serializeBinaryToWriter); + } }; @@ -3519,18 +3530,41 @@ proto.GameState.prototype.clearItemsMap = function() { /** - * map pools = 14; + * map obstacles = 14; * @param {boolean=} opt_noLazyCreate Do not create the map if * empty, instead returning `undefined` * @return {!jspb.Map} */ -proto.GameState.prototype.getPoolsMap = function(opt_noLazyCreate) { +proto.GameState.prototype.getObstaclesMap = function(opt_noLazyCreate) { return /** @type {!jspb.Map} */ ( jspb.Message.getMapField(this, 14, opt_noLazyCreate, proto.Entity)); }; +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.GameState} returns this + */ +proto.GameState.prototype.clearObstaclesMap = function() { + this.getObstaclesMap().clear(); + return this; +}; + + +/** + * map pools = 15; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.GameState.prototype.getPoolsMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 15, opt_noLazyCreate, + proto.Entity)); +}; + + /** * Clears values from the map. The map will be non-null. * @return {!proto.GameState} returns this @@ -5882,7 +5916,8 @@ proto.PlayerAction.prototype.toObject = function(opt_includeInstance) { proto.PlayerAction.toObject = function(includeInstance, msg) { var f, obj = { action: jspb.Message.getFieldWithDefault(msg, 1, 0), - duration: jspb.Message.getFieldWithDefault(msg, 2, 0) + duration: jspb.Message.getFieldWithDefault(msg, 2, 0), + destination: (f = msg.getDestination()) && proto.Position.toObject(includeInstance, f) }; if (includeInstance) { @@ -5927,6 +5962,11 @@ proto.PlayerAction.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {number} */ (reader.readUint64()); msg.setDuration(value); break; + case 3: + var value = new proto.Position; + reader.readMessage(value,proto.Position.deserializeBinaryFromReader); + msg.setDestination(value); + break; default: reader.skipField(); break; @@ -5970,6 +6010,14 @@ proto.PlayerAction.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getDestination(); + if (f != null) { + writer.writeMessage( + 3, + f, + proto.Position.serializeBinaryToWriter + ); + } }; @@ -6009,6 +6057,43 @@ proto.PlayerAction.prototype.setDuration = function(value) { }; +/** + * optional Position destination = 3; + * @return {?proto.Position} + */ +proto.PlayerAction.prototype.getDestination = function() { + return /** @type{?proto.Position} */ ( + jspb.Message.getWrapperField(this, proto.Position, 3)); +}; + + +/** + * @param {?proto.Position|undefined} value + * @return {!proto.PlayerAction} returns this +*/ +proto.PlayerAction.prototype.setDestination = function(value) { + return jspb.Message.setWrapperField(this, 3, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.PlayerAction} returns this + */ +proto.PlayerAction.prototype.clearDestination = function() { + return this.setDestination(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.PlayerAction.prototype.hasDestination = function() { + return jspb.Message.getField(this, 3) != 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 c9205ad2a..615b08665 100644 --- a/apps/game_client/lib/game_client/protobuf/messages.pb.ex +++ b/apps/game_client/lib/game_client/protobuf/messages.pb.ex @@ -241,6 +241,15 @@ defmodule GameClient.Protobuf.GameState.ItemsEntry do field(:value, 2, type: GameClient.Protobuf.Entity) end +defmodule GameClient.Protobuf.GameState.ObstaclesEntry do + @moduledoc false + + use Protobuf, map: true, protoc_gen_elixir_version: "0.12.0", syntax: :proto3 + + field(:key, 1, type: :uint64) + field(:value, 2, type: GameClient.Protobuf.Entity) +end + defmodule GameClient.Protobuf.GameState.PoolsEntry do @moduledoc false @@ -299,7 +308,14 @@ defmodule GameClient.Protobuf.GameState do field(:status, 11, type: GameClient.Protobuf.GameStatus, enum: true) field(:start_game_timestamp, 12, type: :int64, json_name: "startGameTimestamp") field(:items, 13, repeated: true, type: GameClient.Protobuf.GameState.ItemsEntry, map: true) - field(:pools, 14, repeated: true, type: GameClient.Protobuf.GameState.PoolsEntry, map: true) + + field(:obstacles, 14, + repeated: true, + type: GameClient.Protobuf.GameState.ObstaclesEntry, + map: true + ) + + field(:pools, 15, repeated: true, type: GameClient.Protobuf.GameState.PoolsEntry, map: true) end defmodule GameClient.Protobuf.Entity do @@ -431,6 +447,7 @@ defmodule GameClient.Protobuf.PlayerAction do field(:action, 1, type: GameClient.Protobuf.PlayerActionType, enum: true) field(:duration, 2, type: :uint64) + field(:destination, 3, type: GameClient.Protobuf.Position) end defmodule GameClient.Protobuf.Move do diff --git a/apps/game_client/lib/game_client_web/live/pages/board/show.ex b/apps/game_client/lib/game_client_web/live/pages/board/show.ex index ad2fd5ab7..e66dca598 100644 --- a/apps/game_client/lib/game_client_web/live/pages/board/show.ex +++ b/apps/game_client/lib/game_client_web/live/pages/board/show.ex @@ -16,6 +16,8 @@ defmodule GameClientWeb.BoardLive.Show do mocked_board_width = 2000 mocked_board_height = 2000 + backend_board_size = 10_000 + back_size_to_front_ratio = backend_board_size / mocked_board_width game_data = %{0 => %{0 => player_name(player_id)}} @@ -28,7 +30,9 @@ defmodule GameClientWeb.BoardLive.Show do board_width: mocked_board_width, board_height: mocked_board_height, game_data: game_data, - game_socket_handler_pid: game_socket_handler_pid + game_socket_handler_pid: game_socket_handler_pid, + backend_board_size: backend_board_size, + back_size_to_front_ratio: back_size_to_front_ratio )} end @@ -67,7 +71,7 @@ defmodule GameClientWeb.BoardLive.Show do defp handle_game_event({:update, game_state}, socket) do entities = - Enum.concat([game_state.players, game_state.projectiles, game_state.items, game_state.pools]) + Enum.concat([game_state.players, game_state.projectiles, game_state.items, game_state.obstacles, game_state.pools]) |> Enum.map(&transform_entity_entry/1) {:noreply, push_event(socket, "updateEntities", %{entities: entities})} @@ -91,7 +95,7 @@ defmodule GameClientWeb.BoardLive.Show do x: entity.position.x / 5 + 1000, y: entity.position.y / 5 + 1000, radius: entity.radius / 5, - coords: entity.vertices |> Enum.map(fn vertex -> [vertex.x / 5 + 1000, vertex.y / 5 + 1000] end), + coords: entity.vertices |> Enum.map(fn vertex -> [vertex.x / 5, vertex.y / 5] end), is_colliding: entity.collides_with |> Enum.any?() } end diff --git a/apps/serialization/messages.proto b/apps/serialization/messages.proto index 4ed352042..712b14a2b 100644 --- a/apps/serialization/messages.proto +++ b/apps/serialization/messages.proto @@ -95,7 +95,8 @@ message GameState { GameStatus status = 11; int64 start_game_timestamp = 12; map items = 13; - map pools = 14; + map obstacles = 14; + map pools = 15; } enum GameStatus { @@ -197,6 +198,7 @@ message Pool { message PlayerAction { PlayerActionType action = 1; uint64 duration = 2; + Position destination = 3; } enum PlayerActionType { diff --git a/config/prod.exs b/config/prod.exs index 6c11e64dd..62b5a144d 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -66,3 +66,5 @@ config :game_client, GameClientWeb.Endpoint, # before starting your production server. config :configurator, ConfiguratorWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +config :bot_manager, :end_point, port: [port: System.get_env("BOT_MANAGER_PORT") || 4003] diff --git a/devops/deploy.sh b/devops/deploy.sh index 24ef86040..9ac48b860 100755 --- a/devops/deploy.sh +++ b/devops/deploy.sh @@ -58,7 +58,12 @@ DATABASE_URL=${DATABASE_URL} PHX_SERVER=${PHX_SERVER} SECRET_KEY_BASE=${SECRET_KEY_BASE} PORT=${PORT} +BOT_MANAGER_PORT=${BOT_MANAGER_PORT} +BOT_MANAGER_HOST=${BOT_MANAGER_HOST} RELEASE=${RELEASE} +TARGET_SERVER=${TARGET_SERVER} +EUROPE_HOST=${EUROPE_HOST} +BRAZIL_HOST=${BRAZIL_HOST} EOF systemctl --user stop $RELEASE diff --git a/mix.exs b/mix.exs index bf35c79b4..22884f035 100644 --- a/mix.exs +++ b/mix.exs @@ -45,6 +45,7 @@ defmodule MirraBackend.MixProject do [ arena: [applications: [arena: :permanent]], arena_load_test: [applications: [arena_load_test: :permanent]], + bot_manager: [applications: [bot_manager: :permanent]], game_client: [applications: [game_client: :permanent]], main_server: [ applications: [