diff --git a/apps/arena/lib/arena/configuration.ex b/apps/arena/lib/arena/configuration.ex index 42f5115a2..43f8b3cc4 100644 --- a/apps/arena/lib/arena/configuration.ex +++ b/apps/arena/lib/arena/configuration.ex @@ -13,12 +13,11 @@ defmodule Arena.Configuration do |> File.read() config = Jason.decode!(config_json, [{:keys, :atoms}]) - skills = parse_skills_config(config.skills) - characters = parse_characters_config(get_characters_config(), skills) + characters = parse_characters_config(get_characters_config()) client_config = get_client_config() game_config = get_game_configuration() - %{config | skills: skills} + config |> Map.put(:characters, characters) |> Map.put(:game, game_config) |> Map.put(:client_config, client_config) @@ -52,10 +51,16 @@ defmodule Arena.Configuration do Jason.decode!(payload.body, [{:keys, :atoms}]) end - defp parse_skills_config(skills_config) do - Enum.reduce(skills_config, [], fn skill_config, skills -> - skill = parse_skill_config(skill_config) - [skill | skills] + defp parse_characters_config(characters) do + Enum.map(characters, fn character -> + character_skills = %{ + "1" => parse_skill_config(character.basic_skill), + "2" => parse_skill_config(character.ultimate_skill), + "3" => parse_skill_config(character.dash_skill) + } + + Map.put(character, :skills, character_skills) + |> Map.drop([:basic_skill, :ultimate_skill, :dash_skill]) end) end @@ -89,53 +94,32 @@ defmodule Arena.Configuration do end) end - defp parse_mechanic_config(mechanic) when map_size(mechanic) > 1 do - raise "Config has mechanic with 2 values: #{inspect(mechanic)}" + defp parse_mechanic_config(nil) do + nil end defp parse_mechanic_config(mechanic) do - Map.to_list(mechanic) - |> Enum.map(&parse_mechanic_fields/1) - |> hd() - end - - defp parse_mechanic_fields({:leap, attrs}) do - {:leap, %{attrs | on_arrival_mechanic: parse_mechanic_config(attrs.on_arrival_mechanic)}} - end - - defp parse_mechanic_fields({name, %{on_explode_mechanics: on_explode_mechanics} = attrs}) do - {name, - %{ - attrs - | on_explode_mechanics: parse_mechanic_config(on_explode_mechanics) - }} - end - - defp parse_mechanic_fields(mechanic) do - mechanic - end - - defp parse_characters_config(characters, config_skills) do - Enum.map(characters, fn character -> - character_skills = - Enum.map(character.skills, fn {skill_key, skill_name} -> - skill = find_skill!(skill_name, config_skills) - {:erlang.atom_to_binary(skill_key), skill} - end) - |> Map.new() - - %{character | skills: character_skills} - end) - end - - defp find_skill!(skill_name, skills) do - skill = Enum.find(skills, fn skill -> skill.name == skill_name end) - - ## This is a sanity check when loading the config - if skill == nil do - raise "Skill #{inspect(skill_name)} does not exist in config" - else - skill - end + ## Why do we even need this? Well it happens that some of our fields are represented + ## as Decimal. To prevent precision loss this struct its converted to a string of the float + ## which should be read back and converted to Decimal + ## The not so small problem we have is that our code expects floats so we still need to parse + ## the strings, but end up with regular floats + %{ + mechanic + | angle_between: maybe_to_float(mechanic.angle_between), + move_by: maybe_to_float(mechanic.move_by), + radius: maybe_to_float(mechanic.radius), + range: maybe_to_float(mechanic.range), + speed: maybe_to_float(mechanic.speed), + on_arrival_mechanic: parse_mechanic_config(mechanic.on_arrival_mechanic), + on_explode_mechanics: parse_mechanics_config(mechanic.on_explode_mechanics) + } + end + + defp maybe_to_float(nil), do: nil + + defp maybe_to_float(float_string) do + {float, ""} = Float.parse(float_string) + float end end diff --git a/apps/arena/lib/arena/game/player.ex b/apps/arena/lib/arena/game/player.ex index 9ea779cce..a5b166e71 100644 --- a/apps/arena/lib/arena/game/player.ex +++ b/apps/arena/lib/arena/game/player.ex @@ -246,7 +246,7 @@ defmodule Arena.Game.Player do # This is a messy solution to get a mechanic result before actually running the mechanic since the client needed the # position in which the player will spawn when the skill start and not when we actually execute the teleport # this is also optimistic since we assume the destination will be always available - defp maybe_add_destination(action, game_state, player, skill_direction, %{mechanics: [{:teleport, teleport}]}) do + defp maybe_add_destination(action, game_state, player, skill_direction, %{mechanics: [%{type: "teleport"} = teleport]}) do target_position = %{ x: player.position.x + skill_direction.x * teleport.range, y: player.position.y + skill_direction.y * teleport.range @@ -454,7 +454,7 @@ defmodule Arena.Game.Player do ## so to simplify my life an executive decision was made to take thas as a fact ## When the time comes to have more than one mechanic per skill this function will need to be refactored, good thing ## is that it will crash here so not something we can ignore - defp calculate_duration(%{mechanics: [{:leap, leap}]}, position, direction, auto_aim?) do + defp calculate_duration(%{mechanics: [%{type: "leap"} = leap]}, position, direction, auto_aim?) do ## TODO: Cap target_position to leap.range direction = Skill.maybe_multiply_by_range(direction, auto_aim?, leap.range) diff --git a/apps/arena/lib/arena/game/skill.ex b/apps/arena/lib/arena/game/skill.ex index e83e14cb0..c033dbfd8 100644 --- a/apps/arena/lib/arena/game/skill.ex +++ b/apps/arena/lib/arena/game/skill.ex @@ -13,7 +13,12 @@ defmodule Arena.Game.Skill do end) end - def do_mechanic(game_state, entity, {:circle_hit, circle_hit}, %{skill_direction: skill_direction} = _skill_params) do + def do_mechanic( + game_state, + entity, + %{type: "circle_hit"} = circle_hit, + %{skill_direction: skill_direction} = _skill_params + ) do circle_center_position = get_position_with_offset(entity.position, skill_direction, circle_hit.offset) circular_damage_area = Entities.make_circular_area(entity.id, circle_center_position, circle_hit.range) @@ -63,7 +68,12 @@ defmodule Arena.Game.Skill do |> maybe_move_player(entity, circle_hit[:move_by]) end - def do_mechanic(game_state, entity, {:cone_hit, cone_hit}, %{skill_direction: skill_direction} = _skill_params) do + def do_mechanic( + game_state, + entity, + %{type: "cone_hit"} = cone_hit, + %{skill_direction: skill_direction} = _skill_params + ) do triangle_points = Physics.calculate_triangle_vertices( entity.position, @@ -117,9 +127,9 @@ defmodule Arena.Game.Skill do |> maybe_move_player(entity, cone_hit[:move_by]) end - def do_mechanic(game_state, entity, {:multi_cone_hit, multi_cone_hit}, skill_params) do + def do_mechanic(game_state, entity, %{type: "multi_cone_hit"} = multi_cone_hit, skill_params) do Enum.each(1..(multi_cone_hit.amount - 1), fn i -> - mechanic = {:cone_hit, multi_cone_hit} + mechanic = %{multi_cone_hit | type: "cone_hit"} Process.send_after( self(), @@ -128,12 +138,12 @@ defmodule Arena.Game.Skill do ) end) - do_mechanic(game_state, entity, {:cone_hit, multi_cone_hit}, skill_params) + do_mechanic(game_state, entity, %{multi_cone_hit | type: "cone_hit"}, skill_params) end - def do_mechanic(game_state, entity, {:multi_circle_hit, multi_circle_hit}, skill_params) do + def do_mechanic(game_state, entity, %{type: "multi_circle_hit"} = multi_circle_hit, skill_params) do Enum.each(1..(multi_circle_hit.amount - 1), fn i -> - mechanic = {:circle_hit, multi_circle_hit} + mechanic = %{multi_circle_hit | type: "circle_hit"} Process.send_after( self(), @@ -142,16 +152,16 @@ defmodule Arena.Game.Skill do ) end) - do_mechanic(game_state, entity, {:circle_hit, multi_circle_hit}, skill_params) + do_mechanic(game_state, entity, %{multi_circle_hit | type: "circle_hit"}, skill_params) end def do_mechanic( game_state, entity, - {:dash, %{speed: speed, duration: duration}}, + %{type: "dash", speed: speed, duration_ms: duration_ms}, %{skill_direction: skill_direction} = _skill_params ) do - Process.send_after(self(), {:stop_dash, entity.id, entity.aditional_info.base_speed}, duration) + Process.send_after(self(), {:stop_dash, entity.id, entity.aditional_info.base_speed}, duration_ms) ## Modifying base_speed rather than speed because effects will reset the speed on game tick ## by modifying base_speed we ensure that the dash speed is kept as expected @@ -167,7 +177,7 @@ defmodule Arena.Game.Skill do %{game_state | players: players} end - def do_mechanic(game_state, entity, {:repeated_shot, repeated_shot}, skill_params) do + def do_mechanic(game_state, entity, %{type: "repeated_shot"} = repeated_shot, skill_params) do remaining_amount = repeated_shot.amount - 1 if remaining_amount > 0 do @@ -175,7 +185,7 @@ defmodule Arena.Game.Skill do Process.send_after( self(), - {:trigger_mechanic, entity.id, {:repeated_shot, repeated_shot}, skill_params}, + {:trigger_mechanic, entity.id, repeated_shot, skill_params}, repeated_shot.interval_ms ) end @@ -205,7 +215,12 @@ defmodule Arena.Game.Skill do |> put_in([:projectiles, projectile.id], projectile) end - def do_mechanic(game_state, entity, {:multi_shoot, multishot}, %{skill_direction: skill_direction} = skill_params) do + def do_mechanic( + game_state, + entity, + %{type: "multi_shoot"} = multishot, + %{skill_direction: skill_direction} = skill_params + ) do entity_player_owner = get_entity_player_owner(game_state, entity) calculate_angle_directions(multishot.amount, multishot.angle_between, skill_direction) @@ -234,7 +249,12 @@ defmodule Arena.Game.Skill do end) end - def do_mechanic(game_state, entity, {:simple_shoot, simple_shoot}, %{skill_direction: skill_direction} = skill_params) do + def do_mechanic( + game_state, + entity, + %{type: "simple_shoot"} = simple_shoot, + %{skill_direction: skill_direction} = skill_params + ) do last_id = game_state.last_id + 1 entity_player_owner = get_entity_player_owner(game_state, entity) @@ -259,7 +279,7 @@ defmodule Arena.Game.Skill do |> put_in([:projectiles, projectile.id], projectile) end - def do_mechanic(game_state, entity, {:leap, leap}, %{execution_duration: execution_duration}) do + def do_mechanic(game_state, entity, %{type: "leap"} = leap, %{execution_duration: execution_duration}) do Process.send_after( self(), {:stop_leap, entity.id, entity.aditional_info.base_speed, leap.on_arrival_mechanic}, @@ -278,7 +298,7 @@ defmodule Arena.Game.Skill do put_in(game_state, [:players, player.id], player) end - def do_mechanic(game_state, entity, {:teleport, _teleport}, %{skill_destination: skill_destination}) do + def do_mechanic(game_state, entity, %{type: "teleport"}, %{skill_destination: skill_destination}) do entity = entity |> Map.put(:aditional_info, entity.aditional_info) @@ -287,15 +307,10 @@ defmodule Arena.Game.Skill do put_in(game_state, [:players, entity.id], entity) end - def do_mechanic(game_state, player, {:spawn_pool, pool_params}, skill_params) do - %{ - skill_direction: skill_direction, - auto_aim?: auto_aim? - } = skill_params - + def do_mechanic(game_state, player, %{type: "spawn_pool"} = pool_params, skill_params) do last_id = game_state.last_id + 1 - skill_direction = maybe_multiply_by_range(skill_direction, auto_aim?, pool_params.range) + skill_direction = maybe_multiply_by_range(skill_params.skill_direction, skill_params.auto_aim?, pool_params.range) target_position = %{ x: player.position.x + skill_direction.x, diff --git a/apps/arena/lib/arena/game_socket_handler.ex b/apps/arena/lib/arena/game_socket_handler.ex index 78aba7665..f5e689b29 100644 --- a/apps/arena/lib/arena/game_socket_handler.ex +++ b/apps/arena/lib/arena/game_socket_handler.ex @@ -188,15 +188,14 @@ defmodule Arena.GameSocketHandler do defp to_broadcast_skill({key, skill}) do ## TODO: This will break once a skill has more than 1 mechanic, until then ## we can use this "shortcut" and deal with it when the time comes - [{_mechanic, params}] = skill.mechanics - - extra_params = - %{ - targetting_radius: params[:radius], - targetting_angle: params[:angle], - targetting_range: params[:range], - targetting_offset: params[:offset] || params[:projectile_offset] - } + [mechanic] = skill.mechanics + + extra_params = %{ + targetting_radius: mechanic[:radius], + targetting_angle: mechanic[:angle], + targetting_range: mechanic[:range], + targetting_offset: mechanic[:offset] || mechanic[:projectile_offset] + } {key, Map.merge(skill, extra_params)} end diff --git a/apps/configurator/lib/configurator_web/controllers/character_controller.ex b/apps/configurator/lib/configurator_web/controllers/character_controller.ex index 1cecacf48..b547ba5b4 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_controller.ex +++ b/apps/configurator/lib/configurator_web/controllers/character_controller.ex @@ -11,15 +11,13 @@ defmodule ConfiguratorWeb.CharacterController do def new(conn, _params) do changeset = Ecto.Changeset.change(%Character{}) - render(conn, :new, changeset: changeset) + skills = get_curse_skills_by_type() + render(conn, :new, changeset: changeset, skills: skills) end def create(conn, %{"character" => character_params}) do - # TODO This should be removed once we have the skills relationship, issue: https://github.com/lambdaclass/mirra_backend/issues/717 - skills = Jason.decode!(character_params["skills"]) - character_params = - Map.put(character_params, "skills", skills) + character_params |> Map.put("game_id", GameBackend.Utils.get_game_id(:curse_of_mirra)) |> Map.put("faction", "curse") @@ -30,7 +28,8 @@ defmodule ConfiguratorWeb.CharacterController do |> redirect(to: ~p"/characters/#{character}") {:error, %Ecto.Changeset{} = changeset} -> - render(conn, :new, changeset: changeset) + skills = get_curse_skills_by_type() + render(conn, :new, changeset: changeset, skills: skills) end end @@ -42,13 +41,11 @@ defmodule ConfiguratorWeb.CharacterController do def edit(conn, %{"id" => id}) do character = Characters.get_character(id) changeset = Ecto.Changeset.change(character) - render(conn, :edit, character: character, changeset: changeset) + skills = get_curse_skills_by_type() + render(conn, :edit, character: character, changeset: changeset, skills: skills) end def update(conn, %{"id" => id, "character" => character_params}) do - # TODO This should be removed once we have the skills relationship, issue: https://github.com/lambdaclass/mirra_backend/issues/717 - skills = Jason.decode!(character_params["skills"]) - character_params = Map.put(character_params, "skills", skills) character = Characters.get_character(id) case Characters.update_character(character, character_params) do @@ -58,7 +55,8 @@ defmodule ConfiguratorWeb.CharacterController do |> redirect(to: ~p"/characters/#{character}") {:error, %Ecto.Changeset{} = changeset} -> - render(conn, :edit, character: character, changeset: changeset) + skills = get_curse_skills_by_type() + render(conn, :edit, character: character, changeset: changeset, skills: skills) end end @@ -70,4 +68,9 @@ defmodule ConfiguratorWeb.CharacterController do |> put_flash(:success, "Character deleted successfully.") |> redirect(to: ~p"/characters") end + + defp get_curse_skills_by_type() do + GameBackend.Units.Skills.list_curse_skills() + |> Enum.group_by(& &1.type) + end end diff --git a/apps/configurator/lib/configurator_web/controllers/character_html.ex b/apps/configurator/lib/configurator_web/controllers/character_html.ex index 24dcbd08f..541722299 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_html.ex +++ b/apps/configurator/lib/configurator_web/controllers/character_html.ex @@ -8,6 +8,23 @@ defmodule ConfiguratorWeb.CharacterHTML do """ attr :changeset, Ecto.Changeset, required: true attr :action, :string, required: true + attr :skills, :list, required: true def character_form(assigns) + + attr :field, Phoenix.HTML.FormField, required: true + attr :label, :string, required: true + attr :skills, :list, required: true + + def skill_select(assigns) do + ~H""" + <.input + field={@field} + type="select" + label={@label} + prompt="Select a skill" + options={Enum.map(@skills, &{&1.name, &1.id})} + /> + """ + end end diff --git a/apps/configurator/lib/configurator_web/controllers/character_html/character_form.html.heex b/apps/configurator/lib/configurator_web/controllers/character_html/character_form.html.heex index 73aa2a08a..216cadf7f 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_html/character_form.html.heex +++ b/apps/configurator/lib/configurator_web/controllers/character_html/character_form.html.heex @@ -5,7 +5,7 @@ <.input field={f[:name]} type="text" label="Name" /> <.input field={f[:active]} type="checkbox" label="Active" /> <.input field={f[:base_speed]} type="number" label="Base speed" step="any" /> - <.input field={f[:base_size]} type="number" label="Base size" step="any" /> + <.input field={f[:base_size]} type="number" label="Base size" step="any" lang="en" /> <.input field={f[:base_health]} type="number" label="Base health" /> <.input field={f[:base_stamina]} type="number" label="Base stamina" /> <.input field={f[:stamina_interval]} type="number" label="Stamina Interval" /> @@ -14,7 +14,9 @@ <.input field={f[:natural_healing_interval]} type="number" label="Natural healing interval" /> <.input field={f[:natural_healing_damage_interval]} type="number" label="Natural healing damage interval" /> - <.input field={f[:skills]} type="text" label="Skills" value={Jason.encode!(f.data.skills)} /> + <.skill_select field={f[:basic_skill_id]} label="Basic skill" skills={@skills[:basic]} /> + <.skill_select field={f[:dash_skill_id]} label="Dash skill" skills={@skills[:dash]} /> + <.skill_select field={f[:ultimate_skill_id]} label="Ultimate skill" skills={@skills[:ultimate]} /> <:actions> <.button>Save Character diff --git a/apps/configurator/lib/configurator_web/controllers/character_html/edit.html.heex b/apps/configurator/lib/configurator_web/controllers/character_html/edit.html.heex index fb271da45..e6c65b2fb 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_html/edit.html.heex +++ b/apps/configurator/lib/configurator_web/controllers/character_html/edit.html.heex @@ -3,6 +3,6 @@ <:subtitle>Use this form to manage character records in your database. -<.character_form changeset={@changeset} action={~p"/characters/#{@character}"} /> +<.character_form changeset={@changeset} action={~p"/characters/#{@character}"} skills={@skills} /> <.back navigate={~p"/characters"}>Back to characters diff --git a/apps/configurator/lib/configurator_web/controllers/character_html/index.html.heex b/apps/configurator/lib/configurator_web/controllers/character_html/index.html.heex index 1673b1b74..a4a7857d1 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_html/index.html.heex +++ b/apps/configurator/lib/configurator_web/controllers/character_html/index.html.heex @@ -14,7 +14,11 @@ <:col :let={character} label="Base size"><%= character.base_size %> <:col :let={character} label="Base health"><%= character.base_health %> <:col :let={character} label="Base stamina"><%= character.base_stamina %> - <:col :let={character} label="Stamina Interval"><%= character.stamina_interval %> + <:col :let={character} label="Basic skill"><%= if character.basic_skill, do: character.basic_skill.name %> + <:col :let={character} label="Dash skill"><%= if character.dash_skill, do: character.dash_skill.name %> + <:col :let={character} label="Ultimate skill"> + <%= if character.ultimate_skill, do: character.ultimate_skill.name %> + <:action :let={character}>
diff --git a/apps/configurator/lib/configurator_web/controllers/character_html/new.html.heex b/apps/configurator/lib/configurator_web/controllers/character_html/new.html.heex index 5bbd0af66..ff3d56330 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_html/new.html.heex +++ b/apps/configurator/lib/configurator_web/controllers/character_html/new.html.heex @@ -3,6 +3,6 @@ <:subtitle>Use this form to manage character records in your database. -<.character_form changeset={@changeset} action={~p"/characters"} /> +<.character_form changeset={@changeset} action={~p"/characters"} skills={@skills} /> <.back navigate={~p"/characters"}>Back to characters diff --git a/apps/configurator/lib/configurator_web/controllers/character_html/show.html.heex b/apps/configurator/lib/configurator_web/controllers/character_html/show.html.heex index f9e1f01e5..e86add641 100644 --- a/apps/configurator/lib/configurator_web/controllers/character_html/show.html.heex +++ b/apps/configurator/lib/configurator_web/controllers/character_html/show.html.heex @@ -1,5 +1,5 @@ <.header> - Character <%= @character.id %> + Character <%= @character.name %> <:subtitle>This is a character record from your database. <:actions> <.link href={~p"/characters/#{@character}/edit"}> @@ -19,7 +19,9 @@ <:item title="Max inventory size"><%= @character.max_inventory_size %> <:item title="Natural healing interval"><%= @character.natural_healing_interval %> <:item title="Natural healing damage interval"><%= @character.natural_healing_damage_interval %> - <:item title="Skills"><%= Jason.encode!(@character.skills) %> + <:item title="Basic skill"><%= if @character.basic_skill, do: @character.basic_skill.name %> + <:item title="Dash skill"><%= if @character.dash_skill, do: @character.dash_skill.name %> + <:item title="Ultimate skill"><%= if @character.ultimate_skill, do: @character.ultimate_skill.name %> <.back navigate={~p"/characters"}>Back to characters diff --git a/apps/configurator/lib/configurator_web/controllers/home_html/home.html.heex b/apps/configurator/lib/configurator_web/controllers/home_html/home.html.heex index 02fa8d62d..673bb2d38 100644 --- a/apps/configurator/lib/configurator_web/controllers/home_html/home.html.heex +++ b/apps/configurator/lib/configurator_web/controllers/home_html/home.html.heex @@ -5,4 +5,5 @@ <.list> <:item title="Character settings"><.link href={~p"/characters"}>Link <:item title="Game settings"><.link href={~p"/game_configurations"}>Link + <:item title="Skill settings"><.link href={~p"/skills"}>Link diff --git a/apps/configurator/lib/configurator_web/controllers/skill_controller.ex b/apps/configurator/lib/configurator_web/controllers/skill_controller.ex new file mode 100644 index 000000000..b3af2327d --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_controller.ex @@ -0,0 +1,78 @@ +defmodule ConfiguratorWeb.SkillController do + use ConfiguratorWeb, :controller + + alias GameBackend.Units.Skills + alias GameBackend.Units.Skills.Mechanic + alias GameBackend.Units.Skills.Skill + alias GameBackend.Utils + + def index(conn, _params) do + skills = Skills.list_curse_skills() + render(conn, :index, skills: skills) + end + + def new(conn, _params) do + changeset = Skills.change_skill(%Skill{mechanics: [%Mechanic{}]}) + render(conn, :new, changeset: changeset) + end + + def create(conn, %{"skill" => skill_params}) do + skill_params = Map.put(skill_params, "game_id", Utils.get_game_id(:curse_of_mirra)) + + case Skills.insert_skill(skill_params) do + {:ok, skill} -> + conn + |> put_flash(:info, "Skill created successfully.") + |> redirect(to: ~p"/skills/#{skill}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + skill = Skills.get_skill!(id) + render(conn, :show, skill: skill) + end + + def edit(conn, %{"id" => id}) do + skill = Skills.get_skill!(id) + changeset = Skills.change_skill(skill) + render(conn, :edit, skill: skill, changeset: changeset) + end + + def update(conn, %{"id" => id, "skill" => skill_params}) do + skill = Skills.get_skill!(id) + + case Skills.update_skill(skill, skill_params) do + {:ok, skill} -> + conn + |> put_flash(:info, "Skill updated successfully.") + |> redirect(to: ~p"/skills/#{skill}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :edit, skill: skill, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + skill = Skills.get_skill!(id) + + case Skills.delete_skill(skill) do + {:error, %{errors: [characters: {_, [constraint: :foreign, constraint_name: "characters_basic_skill_id_fkey"]}]}} -> + conn + |> put_flash(:error, "Skill being used by a Character.") + |> redirect(to: ~p"/skills/#{skill}") + + {:error, _changeset} -> + conn + |> put_flash(:info, "Something went wrong.") + |> redirect(to: ~p"/skills/#{skill}") + + {:ok, _skill} -> + conn + |> put_flash(:success, "Skill deleted successfully.") + |> redirect(to: ~p"/skills") + end + end +end diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html.ex b/apps/configurator/lib/configurator_web/controllers/skill_html.ex new file mode 100644 index 000000000..b998b3603 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html.ex @@ -0,0 +1,36 @@ +defmodule ConfiguratorWeb.SkillHTML do + use ConfiguratorWeb, :html + alias GameBackend.Units.Skills.Mechanic + + embed_templates "skill_html/*" + + @doc """ + Renders a skill form. + """ + attr :changeset, Ecto.Changeset, required: true + attr :action, :string, required: true + + def skill_form(assigns) + + @doc """ + Renders the inputs for a mechanic inside a skill form. + """ + attr :skill_form, Phoenix.HTML.FormField, required: true + + def skill_mechanic_inputs(assigns) + + @doc """ + Renders the inputs for a nested mechanic inside skill_mechanic_inputs/1. + """ + attr :parent_form, Phoenix.HTML.FormField, required: true + attr :parent_field, :atom, required: true + + def nested_mechanic_inputs(assigns) + + @doc """ + Renders to show a mechanic. + """ + attr :mechanic, Mechanic, required: true + + def mechanic_show(assigns) +end diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/edit.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/edit.html.heex new file mode 100644 index 000000000..dce7511bf --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/edit.html.heex @@ -0,0 +1,8 @@ +<.header> + Edit Skill <%= @skill.name %> + <:subtitle>Use this form to manage skill records in your database. + + +<.skill_form changeset={@changeset} action={~p"/skills/#{@skill}"} /> + +<.back navigate={~p"/skills"}>Back to skills diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/index.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/index.html.heex new file mode 100644 index 000000000..e7461f077 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/index.html.heex @@ -0,0 +1,65 @@ +<.header> + Listing Config skills + <:actions> + <.link href={~p"/skills/new"}> + <.button>New Skill + + + + +<.table id="skills" rows={@skills} row_click={&JS.navigate(~p"/skills/#{&1}")}> + <:col :let={skill} label="Name"><%= skill.name %> + <:col :let={skill} label="Activation delay (ms)"><%= skill.activation_delay_ms %> + <:col :let={skill} label="Autoaim"><%= skill.autoaim %> + <:col :let={skill} label="Block movement"><%= skill.block_movement %> + <:col :let={skill} label="Can pick destination"><%= skill.can_pick_destination %> + <:col :let={skill} label="Cooldown mechanism"><%= skill.cooldown_mechanism %> + <:col :let={skill} label="Cooldown (ms)"><%= skill.cooldown_ms %> + <:col :let={skill} label="Execution duration (ms)"><%= skill.execution_duration_ms %> + <:col :let={skill} label="Inmune while executing"><%= skill.inmune_while_executing %> + <:col :let={skill} label="Is passive"><%= skill.is_passive %> + <:col :let={skill} label="Max autoaim range"><%= skill.max_autoaim_range %> + <:col :let={skill} label="Stamina cost"><%= skill.stamina_cost %> + <:action :let={skill}> + <.button type="button" phx-click={show_modal("skill-mechanics-#{skill.id}")}>Mechanics + <.modal id={"skill-mechanics-#{skill.id}"}> + <.header> + Mechanics for the skill + + <%= for mechanic <- skill.mechanics do %> + <.mechanic_show mechanic={mechanic} /> + + <%= if not is_nil(mechanic.on_arrival_mechanic) do %> + <.button type="button" phx-click={show_modal("on-arrival-mechanic-modal")}>Show on arrival mechanic + <.modal id="on-arrival-mechanic-modal"> + <.header> + On arrival mechanic + + <.mechanic_show mechanic={mechanic.on_arrival_mechanic} /> + + <% end %> + + <.button type="button" phx-click={show_modal("on-explode-mechanics-modal")}>Show on explode mechanics + <.modal id="on-explode-mechanics-modal"> + <.header> + On explode mechanics + + <%= for mechanic <- mechanic.on_explode_mechanics do %> + <.mechanic_show mechanic={mechanic} /> + <% end %> + + <% end %> + + + <:action :let={skill}> +
+ <.link navigate={~p"/skills/#{skill}"}>Show +
+ <.link navigate={~p"/skills/#{skill}/edit"}>Edit + + <:action :let={skill}> + <.link href={~p"/skills/#{skill}"} method="delete" data-confirm="Are you sure?"> + Delete + + + diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/mechanic_show.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/mechanic_show.html.heex new file mode 100644 index 000000000..514fd4ee9 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/mechanic_show.html.heex @@ -0,0 +1,16 @@ +<.list> + <:item title="Type"><%= @mechanic.type %> + <:item title="Name"><%= @mechanic.name %> + <:item title="Angle between"><%= @mechanic.angle_between %> + <:item title="Damage"><%= @mechanic.damage %> + <:item title="Duration (ms)"><%= @mechanic.duration_ms %> + <:item title="Interval (ms)"><%= @mechanic.interval_ms %> + <:item title="Move by"><%= @mechanic.move_by %> + <:item title="Offset"><%= @mechanic.offset %> + <:item title="Projectile offset"><%= @mechanic.projectile_offset %> + <:item title="Radius"><%= @mechanic.radius %> + <:item title="Range"><%= @mechanic.range %> + <:item title="Remove on collision"><%= @mechanic.remove_on_collision %> + <:item title="Speed"><%= @mechanic.speed %> + <:item title="Effects to apply"><%= @mechanic.effects_to_apply %> + diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/nested_mechanic_inputs.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/nested_mechanic_inputs.html.heex new file mode 100644 index 000000000..bd6f1a431 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/nested_mechanic_inputs.html.heex @@ -0,0 +1,29 @@ +<.inputs_for :let={fp} field={@parent_form[@parent_field]}> + <.input + field={fp[:type]} + type="select" + label="Type" + prompt="Choose a value" + options={Ecto.Enum.values(GameBackend.Units.Skills.Mechanic, :type)} + /> + <.input field={fp[:name]} type="text" label="Name" /> + <.input field={fp[:amount]} type="number" label="Amount" /> + <.input field={fp[:angle_between]} type="number" label="Angle between" /> + <.input field={fp[:damage]} type="number" label="Damage" /> + <.input field={fp[:duration_ms]} type="number" label="Duration (ms)" /> + <.input field={fp[:interval_ms]} type="number" label="Interval (ms)" /> + <.input field={fp[:move_by]} type="number" label="Move by" /> + <.input field={fp[:offset]} type="number" label="Offset" /> + <.input field={fp[:projectile_offset]} type="number" label="Projectile offset" /> + <.input field={fp[:radius]} type="number" label="Radius" /> + <.input field={fp[:range]} type="number" label="Range" /> + <.input field={fp[:remove_on_collision]} type="checkbox" label="Remove on collision" /> + <.input field={fp[:speed]} type="number" label="speed" /> + <.input + field={fp[:effects_to_apply]} + type="select" + label="Effects to apply" + multiple + options={["singularity", "denial_of_service", "invisible"]} + /> + diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/new.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/new.html.heex new file mode 100644 index 000000000..5dd46d2f8 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/new.html.heex @@ -0,0 +1,8 @@ +<.header> + New Skill + <:subtitle>Use this form to manage skill records in your database. + + +<.skill_form changeset={@changeset} action={~p"/skills"} /> + +<.back navigate={~p"/skills"}>Back to skills diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/show.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/show.html.heex new file mode 100644 index 000000000..6f329a416 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/show.html.heex @@ -0,0 +1,53 @@ +<.header> + Skill: <%= @skill.name %> + <:subtitle>This is a skill record from your database. + <:actions> + <.link href={~p"/skills/#{@skill}/edit"}> + <.button>Edit skill + + + + +<.list> + <:item title="Name"><%= @skill.name %> + <:item title="Activation delay (ms)"><%= @skill.activation_delay_ms %> + <:item title="Autoaim"><%= @skill.autoaim %> + <:item title="Block movement"><%= @skill.block_movement %> + <:item title="Can pick destination"><%= @skill.can_pick_destination %> + <:item title="Cooldown mechanism"><%= @skill.cooldown_mechanism %> + <:item title="Cooldown (ms)"><%= @skill.cooldown_ms %> + <:item title="Execution duration (ms)"><%= @skill.execution_duration_ms %> + <:item title="Inmune while executing"><%= @skill.inmune_while_executing %> + <:item title="Is passive"><%= @skill.is_passive %> + <:item title="Max autoaim range"><%= @skill.max_autoaim_range %> + <:item title="Stamina cost"><%= @skill.stamina_cost %> + + +<.header> + Mechanics + <:subtitle>This are the mechanics for the skill + + +<%= for mechanic <- @skill.mechanics do %> + <.mechanic_show mechanic={mechanic} /> + + <.button type="button" phx-click={show_modal("on-arrival-mechanic-modal")}>Show on arrival mechanic + <.modal id="on-arrival-mechanic-modal"> + <.header> + On arrival mechanic + + <.mechanic_show :if={not is_nil(mechanic.on_arrival_mechanic)} mechanic={mechanic.on_arrival_mechanic} /> + + + <.button type="button" phx-click={show_modal("on-explode-mechanics-modal")}>Show on explode mechanics + <.modal id="on-explode-mechanics-modal"> + <.header> + On explode mechanics + + <%= for mechanic <- mechanic.on_explode_mechanics do %> + <.mechanic_show mechanic={mechanic} /> + <% end %> + +<% end %> + +<.back navigate={~p"/skills"}>Back to skills diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/skill_form.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/skill_form.html.heex new file mode 100644 index 000000000..b91b0f991 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/skill_form.html.heex @@ -0,0 +1,36 @@ +<.simple_form :let={f} for={@changeset} action={@action}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + + <.input field={f[:name]} type="text" label="Name" required /> + <.input + field={f[:type]} + type="select" + label="Type" + prompt="Choose a value" + options={Ecto.Enum.values(GameBackend.Units.Skills.Skill, :type)} + /> + <.input field={f[:activation_delay_ms]} type="number" label="Activation delay (ms)" /> + <.input field={f[:autoaim]} type="checkbox" label="Autoaim" /> + <.input field={f[:block_movement]} type="checkbox" label="Block movement" /> + <.input field={f[:can_pick_destination]} type="checkbox" label="Can pick destination" /> + <.input + field={f[:cooldown_mechanism]} + type="select" + label="Cooldown mechanism" + prompt="Choose a value" + options={Ecto.Enum.values(GameBackend.Units.Skills.Skill, :cooldown_mechanism)} + /> + <.input field={f[:cooldown_ms]} type="number" label="Cooldown (ms)" /> + <.input field={f[:execution_duration_ms]} type="number" label="Execution duration (ms)" /> + <.input field={f[:inmune_while_executing]} type="checkbox" label="Inmune while executing" /> + <.input field={f[:is_passive]} type="checkbox" label="Is passive" /> + <.input field={f[:max_autoaim_range]} type="number" label="Max autoaim range" /> + <.input field={f[:stamina_cost]} type="number" label="Stamina cost" /> + + <.skill_mechanic_inputs skill_form={f} /> + + <:actions> + <.button>Save Skill + + diff --git a/apps/configurator/lib/configurator_web/controllers/skill_html/skill_mechanic_inputs.html.heex b/apps/configurator/lib/configurator_web/controllers/skill_html/skill_mechanic_inputs.html.heex new file mode 100644 index 000000000..deda469a6 --- /dev/null +++ b/apps/configurator/lib/configurator_web/controllers/skill_html/skill_mechanic_inputs.html.heex @@ -0,0 +1,53 @@ +<.header> + Skill mechanic + <:subtitle>Manage the mechanic for the skill + + +<.inputs_for :let={fp} field={@skill_form[:mechanics]}> + <.input + field={fp[:type]} + type="select" + label="Type" + prompt="Choose a value" + required + options={Ecto.Enum.values(GameBackend.Units.Skills.Mechanic, :type)} + /> + <.input field={fp[:name]} type="text" label="Name" /> + <.input field={fp[:amount]} type="number" label="Amount" /> + <.input field={fp[:angle_between]} type="number" label="Angle between" /> + <.input field={fp[:damage]} type="number" label="Damage" /> + <.input field={fp[:duration_ms]} type="number" label="Duration (ms)" /> + <.input field={fp[:interval_ms]} type="number" label="Interval (ms)" /> + <.input field={fp[:move_by]} type="number" label="Move by" /> + <.input field={fp[:offset]} type="number" label="Offset" /> + <.input field={fp[:projectile_offset]} type="number" label="Projectile offset" /> + <.input field={fp[:radius]} type="number" label="Radius" /> + <.input field={fp[:range]} type="number" label="Range" /> + <.input field={fp[:remove_on_collision]} type="checkbox" label="Remove on collision" /> + <.input field={fp[:speed]} type="number" label="speed" /> + <.input + field={fp[:effects_to_apply]} + type="select" + label="Effects to apply" + multiple + options={["singularity", "denial_of_service", "invisible"]} + /> + + <.button type="button" phx-click={show_modal("on-arrival-mechanic-modal")}>Edit on arrival mechanic + <.modal id="on-arrival-mechanic-modal"> + <.header> + On arrival mechanic + <:subtitle>Details to use on mechanic when arriving + + <.nested_mechanic_inputs parent_form={fp} parent_field={:on_arrival_mechanic} /> + + + <.button type="button" phx-click={show_modal("on-explode-mechanics-modal")}>Edit on explode mechanics + <.modal id="on-explode-mechanics-modal"> + <.header> + On explode mechanics + <:subtitle>Details to use on mechanic when exploding + + <.nested_mechanic_inputs parent_form={fp} parent_field={:on_explode_mechanics} /> + + diff --git a/apps/configurator/lib/configurator_web/router.ex b/apps/configurator/lib/configurator_web/router.ex index a4eaf46e5..03ae4e6bd 100644 --- a/apps/configurator/lib/configurator_web/router.ex +++ b/apps/configurator/lib/configurator_web/router.ex @@ -17,6 +17,11 @@ defmodule ConfiguratorWeb.Router do plug :accepts, ["json"] end + # Other scopes may use custom stacks. + # scope "/api", ConfiguratorWeb do + # pipe_through :api + # end + # Enable LiveDashboard in development if Application.compile_env(:configurator, :dev_routes) do # If you want to use the LiveDashboard in production, you should put @@ -51,6 +56,7 @@ defmodule ConfiguratorWeb.Router do get "/", HomeController, :home resources "/characters", CharacterController + resources "/skills", SkillController resources "/game_configurations", GameConfigurationController end diff --git a/apps/configurator/mix.exs b/apps/configurator/mix.exs index 10317b64e..975f559e8 100644 --- a/apps/configurator/mix.exs +++ b/apps/configurator/mix.exs @@ -55,7 +55,8 @@ defmodule Configurator.MixProject do {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, {:jason, "~> 1.2"}, - {:bandit, "~> 1.2"} + {:bandit, "~> 1.2"}, + {:game_backend, in_umbrella: true} ] end diff --git a/apps/configurator/test/configurator_web/controllers/character_controller_test.exs b/apps/configurator/test/configurator_web/controllers/character_controller_test.exs index 1cce30f3f..43a82c930 100644 --- a/apps/configurator/test/configurator_web/controllers/character_controller_test.exs +++ b/apps/configurator/test/configurator_web/controllers/character_controller_test.exs @@ -66,7 +66,7 @@ defmodule ConfiguratorWeb.CharacterControllerTest do assert redirected_to(conn) == ~p"/characters/#{id}" conn = get(conn, ~p"/characters/#{id}") - assert html_response(conn, 200) =~ "Character #{id}" + assert html_response(conn, 200) =~ "Character #{@create_attrs[:name]}" end test "renders errors when data is invalid", %{conn: conn} do diff --git a/apps/game_backend/lib/game_backend/units/characters.ex b/apps/game_backend/lib/game_backend/units/characters.ex index 6f43ac616..36172756e 100644 --- a/apps/game_backend/lib/game_backend/units/characters.ex +++ b/apps/game_backend/lib/game_backend/units/characters.ex @@ -88,7 +88,7 @@ defmodule GameBackend.Units.Characters do iex> get_character(wrong_id) {:error, :not_found} """ - def get_character(id), do: Repo.get(Character, id) |> Repo.preload([:basic_skill, :ultimate_skill]) + def get_character(id), do: Repo.get(Character, id) |> Repo.preload([:basic_skill, :ultimate_skill, :dash_skill]) @doc """ Get all Characters. @@ -173,6 +173,17 @@ defmodule GameBackend.Units.Characters do """ def get_curse_characters() do curse_id = GameBackend.Utils.get_game_id(:curse_of_mirra) - Repo.all(from(c in Character, where: ^curse_id == c.game_id)) + + q = + from(c in Character, + where: ^curse_id == c.game_id, + preload: [ + basic_skill: [mechanics: [:on_arrival_mechanic, :on_explode_mechanics, :parent_mechanic]], + ultimate_skill: [mechanics: [:on_arrival_mechanic, :on_explode_mechanics, :parent_mechanic]], + dash_skill: [mechanics: [:on_arrival_mechanic, :on_explode_mechanics, :parent_mechanic]] + ] + ) + + Repo.all(q) end end diff --git a/apps/game_backend/lib/game_backend/units/characters/character.ex b/apps/game_backend/lib/game_backend/units/characters/character.ex index 0d06ec816..3cd8dd342 100644 --- a/apps/game_backend/lib/game_backend/units/characters/character.ex +++ b/apps/game_backend/lib/game_backend/units/characters/character.ex @@ -8,22 +8,6 @@ defmodule GameBackend.Units.Characters.Character do alias GameBackend.Units.Skills.Skill - @derive {Jason.Encoder, - only: [ - :active, - :name, - :base_attack, - :base_health, - :base_defense, - :base_stamina, - :stamina_interval, - :max_inventory_size, - :natural_healing_interval, - :natural_healing_damage_interval, - :base_speed, - :base_size, - :skills - ]} schema "characters" do field(:game_id, :integer) field(:active, :boolean, default: true) @@ -46,11 +30,9 @@ defmodule GameBackend.Units.Characters.Character do field(:base_speed, :float) field(:base_size, :float) - # TODO This should be removed once we have the skills relationship, issue: https://github.com/lambdaclass/mirra_backend/issues/717 - field(:skills, {:map, :string}) - belongs_to(:basic_skill, Skill, on_replace: :update) belongs_to(:ultimate_skill, Skill, on_replace: :update) + belongs_to(:dash_skill, Skill, on_replace: :update) timestamps() end @@ -79,8 +61,10 @@ defmodule GameBackend.Units.Characters.Character do :max_inventory_size, :natural_healing_interval, :natural_healing_damage_interval, - :skills, - :base_defense + :base_defense, + :basic_skill_id, + :dash_skill_id, + :ultimate_skill_id ]) |> cast_assoc(:basic_skill) |> cast_assoc(:ultimate_skill) diff --git a/apps/game_backend/lib/game_backend/units/skills.ex b/apps/game_backend/lib/game_backend/units/skills.ex index 17b7b6544..405454c91 100644 --- a/apps/game_backend/lib/game_backend/units/skills.ex +++ b/apps/game_backend/lib/game_backend/units/skills.ex @@ -53,4 +53,65 @@ defmodule GameBackend.Units.Skills do detail_type in Mechanic.mechanic_types() and mechanic[detail_type] != nil end) end + + def list_curse_skills() do + curse_id = GameBackend.Utils.get_game_id(:curse_of_mirra) + + q = + from(s in Skill, + where: ^curse_id == s.game_id, + preload: [mechanics: [:on_arrival_mechanic, :on_explode_mechanics]] + ) + + Repo.all(q) + end + + @doc """ + Gets a single skill. + + Raises `Ecto.NoResultsError` if the Skill does not exist. + + ## Examples + + iex> get_skill!(123) + %Skill{} + + iex> get_skill!(456) + ** (Ecto.NoResultsError) + + """ + def get_skill!(id) do + Repo.get!(Skill, id) + |> Repo.preload(mechanics: [:on_arrival_mechanic, :on_explode_mechanics]) + end + + @doc """ + Deletes a skill. + + ## Examples + + iex> delete_skill(skill) + {:ok, %Skill{}} + + iex> delete_skill(skill) + {:error, %Ecto.Changeset{}} + + """ + def delete_skill(%Skill{} = skill) do + Skill.changeset(skill) + |> Repo.delete() + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking skill changes. + + ## Examples + + iex> change_skill(skill) + %Ecto.Changeset{data: %Skill{}} + + """ + def change_skill(%Skill{} = skill, attrs \\ %{}) do + Skill.changeset(skill, attrs) + end end diff --git a/apps/game_backend/lib/game_backend/units/skills/mechanic.ex b/apps/game_backend/lib/game_backend/units/skills/mechanic.ex index 86b643e21..65f813127 100644 --- a/apps/game_backend/lib/game_backend/units/skills/mechanic.ex +++ b/apps/game_backend/lib/game_backend/units/skills/mechanic.ex @@ -5,37 +5,80 @@ defmodule GameBackend.Units.Skills.Mechanic do import Ecto.Changeset alias GameBackend.Units.Skills.Skill - alias GameBackend.Units.Skills.Mechanics.{ApplyEffectsTo, PassiveEffect} + alias GameBackend.Units.Skills.Mechanics.{ApplyEffectsTo, PassiveEffect, OnCollideEffects} schema "mechanics" do + field(:amount, :integer) + field(:angle_between, :decimal) + field(:damage, :integer) + field(:duration_ms, :integer) + field(:effects_to_apply, {:array, :string}) + field(:interval_ms, :integer) + field(:move_by, :decimal) + field(:name, :string) + field(:offset, :integer) + field(:projectile_offset, :integer) + field(:radius, :decimal) + field(:range, :decimal) + field(:remove_on_collision, :boolean, default: false) + field(:speed, :decimal) + field(:activation_delay, :integer) field(:trigger_delay, :integer) - belongs_to(:skill, Skill) + field(:type, Ecto.Enum, + values: [:circle_hit, :spawn_pool, :leap, :multi_shoot, :dash, :multi_circle_hit, :teleport, :simple_shoot] + ) + + belongs_to(:skill, Skill) belongs_to(:apply_effects_to, ApplyEffectsTo) - # Not yet implemented, added to define how different Mechanic types will be handled + has_many(:on_explode_mechanics, __MODULE__, foreign_key: :parent_mechanic_id) belongs_to(:passive_effects, PassiveEffect) + belongs_to(:on_arrival_mechanic, __MODULE__) + belongs_to(:parent_mechanic, __MODULE__, foreign_key: :parent_mechanic_id) + embeds_one(:on_collide_effects, OnCollideEffects) end + def mechanic_types(), do: [:apply_effects_to, :passive_effects] + @doc false def changeset(mechanic, attrs \\ %{}) do mechanic - |> cast(attrs, [:trigger_delay, :skill_id]) + |> cast(attrs, [ + :trigger_delay, + :on_arrival_mechanic_id, + :parent_mechanic_id, + :skill_id, + :type, + :amount, + :angle_between, + :damage, + :duration_ms, + :effects_to_apply, + :interval_ms, + :move_by, + :name, + :offset, + :projectile_offset, + :radius, + :range, + :remove_on_collision, + :activation_delay, + :speed + ]) |> cast_assoc(:apply_effects_to) |> cast_assoc(:passive_effects) - |> validate_only_one_type() - |> validate_required([:trigger_delay]) + |> cast_assoc(:parent_mechanic, with: &assoc_changeset/2) + |> cast_assoc(:on_arrival_mechanic, with: &assoc_changeset/2) + |> cast_assoc(:on_explode_mechanics, with: &assoc_changeset/2) + |> cast_embed(:on_collide_effects) end - defp validate_only_one_type(changeset) do - if Enum.count(mechanic_types(), fn type -> Map.has_key?(changeset.changes, type) end) == 1, - do: changeset, - else: - add_error( - changeset, - hd(mechanic_types()), - "Exactly 1 of these fields must be present: #{inspect(mechanic_types())}" - ) - end + defp assoc_changeset(struct, params) do + changeset = changeset(struct, params) - def mechanic_types(), do: [:apply_effects_to, :passive_effects] + case get_field(changeset, :type) do + nil -> %{changeset | action: :ignore} + _ -> changeset + end + end end diff --git a/apps/game_backend/lib/game_backend/units/skills/mechanics/on_collide_effects.ex b/apps/game_backend/lib/game_backend/units/skills/mechanics/on_collide_effects.ex new file mode 100644 index 000000000..7e7979cc6 --- /dev/null +++ b/apps/game_backend/lib/game_backend/units/skills/mechanics/on_collide_effects.ex @@ -0,0 +1,21 @@ +defmodule GameBackend.Units.Skills.Mechanics.OnCollideEffects do + @moduledoc """ + A schema that defines the strategy for how a mechanic will choose its targets. + """ + + use GameBackend.Schema + import Ecto.Changeset + + @derive Jason.Encoder + @primary_key false + embedded_schema do + field(:apply_effect_to_entity_type, {:array, :string}) + field(:effects, {:array, :string}) + end + + @doc false + def changeset(targeting_strategy, attrs \\ %{}) do + targeting_strategy + |> cast(attrs, [:apply_effect_to_entity_type, :effects]) + end +end diff --git a/apps/game_backend/lib/game_backend/units/skills/skill.ex b/apps/game_backend/lib/game_backend/units/skills/skill.ex index cb6aea8e9..377e801c1 100644 --- a/apps/game_backend/lib/game_backend/units/skills/skill.ex +++ b/apps/game_backend/lib/game_backend/units/skills/skill.ex @@ -9,12 +9,26 @@ defmodule GameBackend.Units.Skills.Skill do schema "skills" do field(:name, :string) - has_many(:mechanics, Mechanic, on_replace: :delete) + field(:game_id, :integer) field(:cooldown, :integer) field(:energy_regen, :integer) field(:animation_duration, :integer) + field(:activation_delay_ms, :integer) + field(:autoaim, :boolean, default: false) + field(:block_movement, :boolean, default: false) + field(:can_pick_destination, :boolean, default: false) + field(:cooldown_mechanism, Ecto.Enum, values: [:stamina, :time]) + field(:cooldown_ms, :integer) + field(:execution_duration_ms, :integer) + field(:inmune_while_executing, :boolean, default: false) + field(:is_passive, :boolean, default: false) + field(:max_autoaim_range, :integer) + field(:stamina_cost, :integer) + field(:effects_to_apply, {:array, :string}) + field(:type, Ecto.Enum, values: [:basic, :dash, :ultimate]) belongs_to(:buff, Buff) + has_many(:mechanics, Mechanic, on_replace: :delete) timestamps() end @@ -22,7 +36,29 @@ defmodule GameBackend.Units.Skills.Skill do @doc false def changeset(skill, attrs \\ %{}) do skill - |> cast(attrs, [:name, :cooldown, :energy_regen, :animation_duration, :buff_id]) + |> cast(attrs, [ + :name, + :game_id, + :cooldown, + :energy_regen, + :animation_duration, + :buff_id, + :activation_delay_ms, + :autoaim, + :block_movement, + :can_pick_destination, + :cooldown_mechanism, + :cooldown_ms, + :execution_duration_ms, + :inmune_while_executing, + :is_passive, + :max_autoaim_range, + :stamina_cost, + :effects_to_apply, + :type + ]) |> cast_assoc(:mechanics) + |> unique_constraint([:game_id, :name]) + |> foreign_key_constraint(:characters, name: "characters_basic_skill_id_fkey") end end diff --git a/apps/game_backend/priv/repo/migrations/20240626175150_add_config_skills_and_mechanics.exs b/apps/game_backend/priv/repo/migrations/20240626175150_add_config_skills_and_mechanics.exs new file mode 100644 index 000000000..b1dc83314 --- /dev/null +++ b/apps/game_backend/priv/repo/migrations/20240626175150_add_config_skills_and_mechanics.exs @@ -0,0 +1,55 @@ +defmodule Configurator.Repo.Migrations.AddConfigSkillsAndMechanics do + use Ecto.Migration + + def change do + alter table(:skills) do + add :game_id, :integer + add :activation_delay_ms, :integer + add :autoaim, :boolean, default: false, null: false + add :block_movement, :boolean, default: false, null: false + add :can_pick_destination, :boolean, default: false, null: false + add :cooldown_mechanism, :string + add :cooldown_ms, :integer + add :execution_duration_ms, :integer + add :inmune_while_executing, :boolean, default: false, null: false + add :is_passive, :boolean, default: false, null: false + add :max_autoaim_range, :integer + add :stamina_cost, :integer + add :type, :string + add :effects_to_apply, {:array, :string} + end + + create unique_index(:skills, [:game_id, :name]) + + alter table(:mechanics) do + add :type, :string + add :amount, :integer + add :angle_between, :decimal + add :damage, :integer + add :duration_ms, :integer + add :effects_to_apply, {:array, :string} + add :interval_ms, :integer + add :move_by, :decimal + add :name, :string + add :offset, :integer + add :projectile_offset, :integer + add :radius, :decimal + add :range, :decimal + add :remove_on_collision, :boolean, default: false, null: false + add :speed, :decimal + add :activation_delay, :integer + add :on_arrival_mechanic_id, references(:mechanics, on_delete: :nothing) + add :parent_mechanic_id, references(:mechanics, on_delete: :nothing) + add :on_collide_effects, :map + modify :skill_id, references(:skills, on_delete: :nilify_all), from: references(:skills) + end + + create index(:mechanics, [:on_arrival_mechanic_id]) + create index(:mechanics, [:parent_mechanic_id]) + + alter table :characters do + remove :skills + add :dash_skill_id, references(:skills, on_delete: :delete_all) + end + end +end diff --git a/apps/gateway/lib/gateway/controllers/curse_of_mirra/configuration_controller.ex b/apps/gateway/lib/gateway/controllers/curse_of_mirra/configuration_controller.ex index 55c5da43a..afba0f1a7 100644 --- a/apps/gateway/lib/gateway/controllers/curse_of_mirra/configuration_controller.ex +++ b/apps/gateway/lib/gateway/controllers/curse_of_mirra/configuration_controller.ex @@ -4,11 +4,12 @@ defmodule Gateway.Controllers.CurseOfMirra.ConfigurationController do """ use Gateway, :controller alias GameBackend.Configuration + alias GameBackend.Units.Characters action_fallback Gateway.Controllers.FallbackController def get_characters_configuration(conn, _params) do - case GameBackend.Units.Characters.get_curse_characters() do + case Characters.get_curse_characters() do [] -> {:error, :not_found} @@ -16,7 +17,7 @@ defmodule Gateway.Controllers.CurseOfMirra.ConfigurationController do send_resp( conn, 200, - Jason.encode!(characters) + Jason.encode!(encode_characters(characters)) ) end end @@ -25,4 +26,69 @@ defmodule Gateway.Controllers.CurseOfMirra.ConfigurationController do game_configuration = Configuration.get_latest_game_configuration() send_resp(conn, 200, Jason.encode!(game_configuration)) end + + defp encode_characters(characters) when is_list(characters) do + Enum.map(characters, fn character -> + character + |> Map.put(:basic_skill, encode_skill(character.basic_skill)) + |> Map.put(:ultimate_skill, encode_skill(character.ultimate_skill)) + |> Map.put(:dash_skill, encode_skill(character.dash_skill)) + |> ecto_struct_to_map() + end) + end + + defp encode_skill(skill) do + skill + |> Map.put(:mechanics, encode_mechanics(skill.mechanics)) + |> ecto_struct_to_map() + |> Map.drop([:buff]) + end + + defp encode_mechanics(mechanics) when is_list(mechanics) do + Enum.map(mechanics, &encode_mechanic/1) + end + + defp encode_mechanic(nil) do + nil + end + + defp encode_mechanic(mechanic) do + # This is done to avoid the infinite nested mechanics loop + # Once we enter on a mechanic, we don't want to go deeper (yet). + on_explode_mechanics = + Enum.map(mechanic.on_explode_mechanics, fn explode_mechanic -> + explode_mechanic + |> Map.put(:on_arrival_mechanic, nil) + |> Map.put(:on_explode_mechanics, []) + |> Map.put(:parent_mechanic, nil) + end) + + on_arrival_mechanic = + if mechanic.on_arrival_mechanic do + mechanic.on_arrival_mechanic + |> Map.put(:on_arrival_mechanic, nil) + |> Map.put(:on_explode_mechanics, []) + |> Map.put(:parent_mechanic, nil) + else + nil + end + + mechanic + |> Map.put(:on_arrival_mechanic, mechanic.on_arrival_mechanic_id && encode_mechanic(on_arrival_mechanic)) + |> Map.put(:on_explode_mechanics, encode_mechanics(on_explode_mechanics)) + |> ecto_struct_to_map() + |> Map.drop([:skill, :apply_effects_to, :passive_effects]) + end + + defp ecto_struct_to_map(ecto_struct) when is_struct(ecto_struct) do + ecto_struct + |> Map.from_struct() + |> Map.drop([ + :__meta__, + :__struct__, + :inserted_at, + :updated_at, + :id + ]) + end end diff --git a/config/test.exs b/config/test.exs index 6b15c48bb..c1c297883 100644 --- a/config/test.exs +++ b/config/test.exs @@ -16,6 +16,20 @@ config :logger, level: :warning # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime +config :joken, + default_signer: [ + signer_alg: "Ed25519", + key_openssh: """ + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACDVgskcQdNGPgcP9UJIwA6AB1FUnvCyO19dChVY3EFuZQAAAKDVn3NU1Z9z + VAAAAAtzc2gtZWQyNTUxOQAAACDVgskcQdNGPgcP9UJIwA6AB1FUnvCyO19dChVY3EFuZQ + AAAECOw1cqNcGfb/U3HgERb+cujt5dvVM+QzIWMMEWeaua5NWCyRxB00Y+Bw/1QkjADoAH + UVSe8LI7X10KFVjcQW5lAAAAF2FyZW5hQGdhdGV3YXkubWlycmEuZGV2AQIDBAUG + -----END OPENSSH PRIVATE KEY----- + """ + ] + ############################ # App configuration: arena # ############################ diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 6a17edb86..2d19c9ade 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -1,3 +1,4 @@ +alias GameBackend.Units.Skills alias GameBackend.{Gacha, Repo, Users, Utils} alias GameBackend.Campaigns.Rewards.AfkRewardRate alias GameBackend.Users.{KalineTreeLevel, Upgrade} @@ -229,6 +230,323 @@ Champions.Config.import_dungeon_levels_config() %{email: "admin@configurator.com", password: "letmepass1234"} |> Configurator.Accounts.register_user() +## Skills +skills = [ + %{ + "name" => "muflus_crush", + "type" => "basic", + "cooldown_mechanism" => "stamina", + "execution_duration_ms" => 450, + "activation_delay_ms" => 150, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 700, + "stamina_cost" => 1, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "circle_hit", + "damage" => 64, + "range" => 350.0, + "offset" => 400 + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "muflus_leap", + "type" => "ultimate", + "cooldown_mechanism" => "time", + "cooldown_ms" => 8000, + "execution_duration_ms" => 800, + "activation_delay_ms" => 200, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 1300, + "can_pick_destination" => true, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "leap", + "range" => 1300.0, + "speed" => 1.7, + "radius" => 600, + "on_arrival_mechanic" => %{ + "type" => "circle_hit", + "damage" => 92, + "range" => 600.0, + "offset" => 0 + } + } + ] + }, + %{ + "name" => "muflus_dash", + "type" => "dash", + "cooldown_mechanism" => "time", + "cooldown_ms" => 4500, + "execution_duration_ms" => 330, + "activation_delay_ms" => 0, + "is_passive" => false, + "autoaim" => false, + "max_autoaim_range" => 0, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "dash", + "speed" => 3.3, + "duration_ms" => 330 + } + ] + }, + %{ + "name" => "h4ck_slingshot", + "type" => "basic", + "cooldown_mechanism" => "stamina", + "execution_duration_ms" => 250, + "activation_delay_ms" => 0, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 1300, + "stamina_cost" => 1, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "multi_shoot", + "angle_between" => 22.0, + "amount" => 3, + "speed" => 1.1, + "duration_ms" => 1000, + "remove_on_collision" => true, + "projectile_offset" => 100, + "damage" => 44, + "radius" => 40.0 + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "h4ck_dash", + "type" => "dash", + "cooldown_mechanism" => "time", + "cooldown_ms" => 5500, + "execution_duration_ms" => 250, + "activation_delay_ms" => 0, + "is_passive" => false, + "autoaim" => false, + "max_autoaim_range" => 0, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "dash", + "speed" => 4.0, + "duration_ms" => 250 + } + ] + }, + %{ + "name" => "h4ck_denial_of_service", + "type" => "ultimate", + "cooldown_mechanism" => "time", + "cooldown_ms" => 9000, + "execution_duration_ms" => 200, + "activation_delay_ms" => 300, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 1200, + "can_pick_destination" => true, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "spawn_pool", + "name" => "denial_of_service", + "activation_delay" => 250, + "duration_ms" => 2500, + "radius" => 500.0, + "range" => 1200.0, + "effects_to_apply" => [ + "denial_of_service" + ] + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "uma_avenge", + "type" => "basic", + "cooldown_mechanism" => "stamina", + "execution_duration_ms" => 500, + "activation_delay_ms" => 0, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 650, + "stamina_cost" => 1, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "multi_circle_hit", + "damage" => 22, + "range" => 280.0, + "interval_ms" => 200, + "amount" => 3, + "offset" => 200 + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "uma_veil_radiance", + "type" => "ultimate", + "cooldown_mechanism" => "time", + "cooldown_ms" => 9000, + "execution_duration_ms" => 300, + "activation_delay_ms" => 150, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 0, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "circle_hit", + "damage" => 80, + "range" => 800.0, + "offset" => 0 + } + ], + "effects_to_apply" => [ + "invisible" + ] + }, + %{ + "name" => "uma_sneak", + "type" => "dash", + "cooldown_mechanism" => "time", + "cooldown_ms" => 5000, + "execution_duration_ms" => 250, + "activation_delay_ms" => 0, + "is_passive" => false, + "autoaim" => false, + "max_autoaim_range" => 0, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "dash", + "speed" => 4.0, + "duration_ms" => 250 + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "valt_singularity", + "type" => "ultimate", + "cooldown_mechanism" => "time", + "cooldown_ms" => 9000, + "execution_duration_ms" => 500, + "activation_delay_ms" => 300, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 1200, + "can_pick_destination" => true, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "spawn_pool", + "name" => "singularity", + "activation_delay" => 400, + "duration_ms" => 5000, + "radius" => 450.0, + "range" => 1200.0, + "effects_to_apply" => [ + "singularity" + ] + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "valt_warp", + "type" => "dash", + "cooldown_mechanism" => "time", + "cooldown_ms" => 6000, + "execution_duration_ms" => 450, + "inmune_while_executing" => true, + "activation_delay_ms" => 300, + "is_passive" => false, + "autoaim" => false, + "max_autoaim_range" => 0, + "can_pick_destination" => true, + "block_movement" => true, + "stamina_cost" => 1, + "mechanics" => [ + %{ + "type" => "teleport", + "range" => 1100, + "duration_ms" => 150 + } + ], + "effects_to_apply" => [] + }, + %{ + "name" => "valt_antimatter", + "type" => "basic", + "cooldown_mechanism" => "stamina", + "execution_duration_ms" => 450, + "activation_delay_ms" => 150, + "is_passive" => false, + "autoaim" => true, + "max_autoaim_range" => 1300, + "stamina_cost" => 1, + "can_pick_destination" => false, + "block_movement" => true, + "mechanics" => [ + %{ + "type" => "simple_shoot", + "speed" => 1.8, + "duration_ms" => 1100, + "remove_on_collision" => true, + "projectile_offset" => 100, + "radius" => 100.0, + "damage" => 0, + "on_explode_mechanics" => [ + %{ + "type" => "circle_hit", + "damage" => 58, + "range" => 250.0, + "offset" => 0 + } + ], + "on_collide_effects" => %{ + "apply_effect_to_entity_type" => [ + "pool" + ], + "effects" => [ + "buff_singularity" + ] + } + } + ], + "effects_to_apply" => [] + } +] + +skills = + Enum.map(skills, fn skill_params -> + {:ok, skill} = + Map.put(skill_params, "game_id", curse_of_mirra_id) + |> Skills.insert_skill() + + {skill.name, skill.id} + end) + |> Map.new() + # Characters params muflus_params = %{ name: "muflus", @@ -241,11 +559,9 @@ muflus_params = %{ max_inventory_size: 1, natural_healing_interval: 1000, natural_healing_damage_interval: 3500, - skills: %{ - "1": "muflus_crush", - "2": "muflus_leap", - "3": "muflus_dash" - } + basic_skill_id: skills["muflus_crush"], + ultimate_skill_id: skills["muflus_leap"], + dash_skill_id: skills["muflus_dash"] } h4ck_params = %{ @@ -259,11 +575,9 @@ h4ck_params = %{ max_inventory_size: 1, natural_healing_interval: 1000, natural_healing_damage_interval: 3500, - skills: %{ - "1": "h4ck_slingshot", - "2": "h4ck_denial_of_service", - "3": "h4ck_dash" - } + basic_skill_id: skills["h4ck_slingshot"], + ultimate_skill_id: skills["h4ck_denial_of_service"], + dash_skill_id: skills["h4ck_dash"] } uma_params = %{ @@ -277,11 +591,9 @@ uma_params = %{ max_inventory_size: 1, natural_healing_interval: 1000, natural_healing_damage_interval: 3500, - skills: %{ - "1": "uma_avenge", - "2": "uma_veil_radiance", - "3": "uma_sneak" - } + basic_skill_id: skills["uma_avenge"], + ultimate_skill_id: skills["uma_veil_radiance"], + dash_skill_id: skills["uma_sneak"] } valtimer_params = %{ @@ -295,11 +607,9 @@ valtimer_params = %{ max_inventory_size: 1, natural_healing_interval: 1000, natural_healing_damage_interval: 3500, - skills: %{ - "1": "valt_antimatter", - "2": "valt_singularity", - "3": "valt_warp" - } + basic_skill_id: skills["valt_antimatter"], + ultimate_skill_id: skills["valt_singularity"], + dash_skill_id: skills["valt_warp"] } # Insert characters