diff --git a/apps/champions/lib/champions/battle/simulator.ex b/apps/champions/lib/champions/battle/simulator.ex index 8678755f2..b95216fe7 100644 --- a/apps/champions/lib/champions/battle/simulator.ex +++ b/apps/champions/lib/champions/battle/simulator.ex @@ -7,7 +7,7 @@ defmodule Champions.Battle.Simulator do The primary skill has a cooldown and it's cast when it's available if the ultimate is not. Skills possess many mechanics. The only implemented mechanic right now is `ApplyEffectsTo`, which is composed of many effects - and a targeting strategy. Effects are composed of `Components`, `Modifiers` and `Executions` (check module docs for more info on each). + and a targeting strategy. Effects are composed of `Components`, `Modifiers`, `Executions` and `ExecutionsOverTime` (check docs for more info on each). ### ApplyEffectsTo mechanics @@ -268,6 +268,8 @@ defmodule Champions.Battle.Simulator do # Calculate the new state of the battle after a step passes for a unit. # Updates cooldowns, casts skills and reduces self-affecting modifier durations. defp process_step_for_unit(unit, current_state, initial_step_state, history) do + current_state = Map.put(current_state, :units, Map.put(current_state.units, unit.id, unit)) + {new_state, new_history} = cond do not can_attack(unit, initial_step_state) -> @@ -371,6 +373,10 @@ defmodule Champions.Battle.Simulator do |> put_in([:units, unit.id, :modifiers], new_modifiers) |> put_in([:units, unit.id, :tags], new_tags) + {new_unit, new_history} = process_executions_over_time(unit, new_state.units[unit.id], new_history) + + new_state = put_in(new_state, [:units, unit.id], new_unit) + {new_state, new_history} end @@ -858,8 +864,26 @@ defmodule Champions.Battle.Simulator do else: {target, history} end) - Enum.reduce(effect.executions, {target_after_tags, new_history}, fn execution, {target_acc, history_acc} -> - process_execution(execution, target_acc, caster, history_acc, effect.skill_id) + {target_after_executions, new_history} = + Enum.reduce(effect.executions, {target_after_tags, new_history}, fn execution, {target_acc, history_acc} -> + process_execution(execution, target_acc, caster, history_acc, effect.skill_id) + end) + + Enum.reduce(effect.executions_over_time, {target_after_executions, new_history}, fn execution_over_time, + {target_acc, _history_acc} -> + update_in(target_acc, [:executions_over_time], fn current_executions -> + [ + %{ + execution: execution_over_time, + caster: caster, + skill_id: effect.skill_id, + remaining_duration: get_duration(effect.type), + remaining_interval_steps: get_interval_steps(execution_over_time) + } + | current_executions + ] + end) + |> apply_tags(execution_over_time["apply_tags"], effect, new_history) end) end @@ -892,6 +916,11 @@ defmodule Champions.Battle.Simulator do # If the effect type doesn't have a duration, then we assume it is permanent. defp get_duration(_type), do: -1 + defp get_interval_steps(execution_over_time) do + # Decrement in 1 because we're already processing the execution in the next step + execution_over_time["interval"] - 1 + end + # Return whether an effect hits. defp effect_hits?(effect, target_id) when is_binary(target_id), do: !chance_to_apply_hits?(effect) @@ -934,7 +963,7 @@ defmodule Champions.Battle.Simulator do steps = get_duration(effect.type) {new_tags, new_history} = - Enum.reduce(tags_to_apply, {[], history}, fn tag, {acc, history} -> + Enum.reduce(tags_to_apply || [], {[], history}, fn tag, {acc, history} -> Logger.info(~c"Applying tag \"#{tag}\" to unit #{format_unit_name(target)} for #{steps} steps.") new_history = @@ -971,13 +1000,7 @@ defmodule Champions.Battle.Simulator do history, skill_id ) do - damage_before_defense = max(floor(attack_ratio * calculate_unit_stat(caster, :attack)), 0) - - # FINAL_DMG = DMG * (100 / (100 + DEFENSE)) - damage_after_defense = - Decimal.mult(damage_before_defense, Decimal.div(100, 100 + target.defense)) - |> Decimal.round() - |> Decimal.to_integer() + damage_after_defense = calculate_damage(caster, target, attack_ratio) Logger.info( "#{format_unit_name(caster)} dealing #{damage_after_defense} damage to #{format_unit_name(target)} (#{target.health} -> #{target.health - damage_after_defense}). Target energy recharge: #{energy_recharge}." @@ -1088,6 +1111,110 @@ defmodule Champions.Battle.Simulator do {target, history} end + defp process_executions_over_time(unit_initial_state, current_unit, history) do + Enum.reduce(unit_initial_state.executions_over_time, {current_unit, history}, fn execution_over_time, + {unit_acc, history_acc} -> + process_execution_over_time(execution_over_time, unit_acc, history_acc, unit_initial_state) + end) + end + + defp process_execution_over_time( + %{remaining_duration: -1} = execution_over_time, + target, + history, + _target_initial_state + ) do + # If the execution is over, we remove it from the target + new_target = + update_in(target, [:executions_over_time], fn current_executions -> + Enum.filter(current_executions, fn exec -> exec != execution_over_time end) + end) + + {new_target, history} + end + + defp process_execution_over_time( + %{execution: %{"type" => "DealDamageOverTime"}, remaining_interval_steps: remaining_interval_steps} = + execution_over_time, + target, + history, + target_initial_state + ) do + if remaining_interval_steps == 0 do + apply_deal_damage_over_time( + execution_over_time, + target, + history, + target_initial_state + ) + else + execution = target.executions_over_time |> Enum.find(fn exec -> exec == execution_over_time end) + + new_execution_over_time = + Map.put(execution_over_time, :remaining_interval_steps, execution.remaining_interval_steps - 1) + + new_target = + update_in(target, [:executions_over_time], fn current_executions -> + Enum.filter(current_executions, fn exec -> exec != execution end) ++ [new_execution_over_time] + end) + + Logger.info("Remaining period: #{new_execution_over_time.remaining_interval_steps}") + + {new_target, history} + end + end + + defp process_execution_over_time( + execution_over_time, + target, + history, + _target_initial_state + ) do + Logger.warning( + "#{format_unit_name(execution_over_time.caster)} tried to apply an unknown execution over time to #{format_unit_name(target)}" + ) + + {target, history} + end + + defp apply_deal_damage_over_time(execution_over_time, target, history, target_initial_state) do + damage_after_defense = + calculate_damage(execution_over_time.caster, target_initial_state, execution_over_time.execution["attack_ratio"]) + + Logger.info( + "#{format_unit_name(execution_over_time.caster)} dealing #{damage_after_defense} damage to #{format_unit_name(target)} (#{target.health} -> #{target.health - damage_after_defense}). Steps remaining: #{execution_over_time.remaining_duration}." + ) + + new_history = + add_to_history( + history, + %{ + target_id: target.id, + skill_id: execution_over_time.skill_id, + stat_affected: %{stat: :HEALTH, amount: -damage_after_defense} + }, + :execution_received + ) + + initial_interval_steps = get_interval_steps(execution_over_time.execution) + + new_target = + target + |> Map.put(:health, target.health - damage_after_defense) + |> update_in([:executions_over_time], fn current_executions -> + Enum.map(current_executions, fn exec -> + if exec == execution_over_time do + Map.put(exec, :remaining_interval_steps, initial_interval_steps) + |> Map.put(:remaining_duration, exec.remaining_duration - 1) + else + exec + end + end) + end) + + {new_target, new_history} + end + # Calculate the current amount of the given attribute that the unit has, based on its modifiers. defp calculate_unit_stat(unit, attribute) do overrides = Enum.filter(unit.modifiers.overrides, &(&1.attribute == Atom.to_string(attribute))) @@ -1148,6 +1275,20 @@ defmodule Champions.Battle.Simulator do ), history} end + # Calculates the damage dealt by an attacker to its target, considering the target's defense. + # We used this function to determine the damage to be dealt by an execution over time, as well as by a DealDamage execution. + defp calculate_damage(unit, target, attack_ratio) do + damage_before_defense = max(floor(attack_ratio * calculate_unit_stat(unit, :attack)), 0) + + # FINAL_DMG = DMG * (100 / (100 + DEFENSE)) + damage_after_defense = + Decimal.mult(damage_before_defense, Decimal.div(100, 100 + target.defense)) + |> Decimal.round() + |> Decimal.to_integer() + + damage_after_defense + end + # Used to create the initial unit maps to be used during simulation. defp create_unit_map(%Unit{character: character} = unit, team), do: @@ -1173,6 +1314,7 @@ defmodule Champions.Battle.Simulator do multiplicatives: [], overrides: [] }, + executions_over_time: [], tags: [] }} @@ -1242,8 +1384,8 @@ defmodule Champions.Battle.Simulator do end # Used to create the initial effect maps to be used during simulation. - defp create_effect_map(%Effect{} = effect, skill_id), - do: %{ + defp create_effect_map(%Effect{} = effect, skill_id) do + %{ type: Enum.into(effect.type, %{}, fn {"type", type} -> {:type, string_to_atom(type)} @@ -1254,8 +1396,13 @@ defmodule Champions.Battle.Simulator do components: effect.components, modifiers: Enum.map(effect.modifiers, &Map.put(&1, :skill_id, skill_id)), executions: effect.executions, + executions_over_time: + Enum.map(effect.executions_over_time, fn eot -> + Map.put(eot, "interval", div(eot["interval"], @miliseconds_per_step)) + end), skill_id: skill_id } + end # Format step state for logs. defp format_step_state(%{ diff --git a/apps/champions/priv/skills.json b/apps/champions/priv/skills.json index 9ae9549a2..79d3a1561 100644 --- a/apps/champions/priv/skills.json +++ b/apps/champions/priv/skills.json @@ -27,7 +27,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -62,7 +63,8 @@ "attack_ratio": 1.16, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -97,7 +99,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -131,7 +134,8 @@ "attack_ratio": 1.64, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -166,7 +170,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -200,7 +205,8 @@ "attack_ratio": 0.79, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -235,7 +241,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -269,7 +276,8 @@ "attack_ratio": 2.51, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -304,7 +312,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -338,7 +347,8 @@ "attack_ratio": 1.8, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -373,7 +383,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -408,7 +419,8 @@ "type": "Heal", "attack_ratio": 1.8 } - ] + ], + "executions_over_time": [] } ] } @@ -443,7 +455,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -477,7 +490,8 @@ "attack_ratio": 1.13, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -512,7 +526,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -548,7 +563,8 @@ "attack_ratio": 3.1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -585,7 +601,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -621,7 +638,8 @@ "attack_ratio": 3.2, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -656,7 +674,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -690,7 +709,8 @@ "attack_ratio": 2.36, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -725,7 +745,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -758,7 +779,8 @@ "type": "Heal", "attack_ratio": 0.72 } - ] + ], + "executions_over_time": [] } ] } @@ -793,7 +815,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -827,7 +850,8 @@ "attack_ratio": 1.45, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -862,7 +886,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -891,7 +916,8 @@ "attack_ratio": 1.79, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -918,7 +944,8 @@ "type": "Heal", "attack_ratio": 1.4 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -960,7 +987,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -993,12 +1021,14 @@ "type": "DealDamage", "attack_ratio": 1.16, "energy_recharge": 50 - }, + } + ], + "executions_over_time": [ { "type": "DealDamageOverTime", "attack_ratio": 0.24, - "energy_recharge": 50, - "dot_type": "Burn" + "apply_tags": ["Burn"], + "interval": 2000 } ] } @@ -1037,7 +1067,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1079,7 +1110,8 @@ "attack_ratio": 2.48, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1109,7 +1141,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1150,7 +1183,8 @@ "attack_ratio": 2.51, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1186,7 +1220,8 @@ "energy_recharge": 50, "delay": 0 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1226,7 +1261,8 @@ "attack_ratio": 1.42, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1266,7 +1302,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1300,7 +1337,8 @@ "attack_ratio": 0.96, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1335,7 +1373,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1371,7 +1410,8 @@ "attack_ratio": 1.44, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1411,7 +1451,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1451,7 +1492,8 @@ "attack_ratio": 2.79, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1486,7 +1528,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1518,7 +1561,8 @@ "magnitude": 0.2 } ], - "executions": [] + "executions": [], + "executions_over_time": [] }, { "type": { @@ -1533,7 +1577,8 @@ "type": "Heal", "attack_ratio": 0.38 } - ] + ], + "executions_over_time": [] } ] } @@ -1568,7 +1613,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1597,7 +1643,8 @@ "type": "Heal", "attack_ratio": 0.48 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1639,7 +1686,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1681,7 +1729,8 @@ "attack_ratio": 2.57, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1716,7 +1765,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1756,7 +1806,8 @@ "attack_ratio": 1.98, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -1786,7 +1837,8 @@ "attack_ratio": 0.8, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1827,7 +1879,8 @@ "attack_ratio": 2.05, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1863,7 +1916,8 @@ "energy_recharge": 50, "delay": 0 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1886,8 +1940,7 @@ { "type": { "type": "duration", - "duration": 10000, - "period": 10 + "duration": 10000 }, "initial_delay": 0, "components": [], @@ -1898,12 +1951,14 @@ "attack_ratio": 0.73, "energy_recharge": 50, "delay": 0 - }, + } + ], + "executions_over_time": [ { "type": "DealDamageOverTime", "attack_ratio": 0.32, - "energy_recharge": 50, - "dot_type": "Burn" + "apply_tags": ["Burn"], + "interval": 10000 } ] } @@ -1941,7 +1996,8 @@ "energy_recharge": 50, "delay": 0 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -1979,7 +2035,8 @@ "energy_recharge": 50, "delay": 0 } - ] + ], + "executions_over_time": [] } ], "targeting_strategy": { @@ -2008,7 +2065,8 @@ "magnitude": 1.08 } ], - "executions": [] + "executions": [], + "executions_over_time": [] } ], "targeting_strategy": { @@ -2048,7 +2106,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2081,7 +2140,8 @@ "attack_ratio": 0.63, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2116,7 +2176,8 @@ "attack_ratio": 0.78, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2151,7 +2212,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2185,10 +2247,12 @@ "magnitude": 1.07 } ], - "executions": [ + "executions": [], + "executions_over_time": [ { "type": "HealOverTime", - "attack_ratio": 0.17 + "attack_ratio": 0.17, + "interval": 5000 } ] } @@ -2225,7 +2289,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2266,7 +2331,8 @@ "type": "AddEnergy", "amount": -200 } - ] + ], + "executions_over_time": [] } ] } @@ -2301,7 +2367,8 @@ "attack_ratio": 1, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2342,7 +2409,8 @@ "attack_ratio": 0.49, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2377,7 +2445,8 @@ "attack_ratio": 0.8, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] }, { "type": { @@ -2392,7 +2461,8 @@ "attack_ratio": 0.8, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } @@ -2426,7 +2496,8 @@ "attack_ratio": 0.38, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] }, { "type": { @@ -2441,7 +2512,8 @@ "attack_ratio": 0.59, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] }, { "type": { @@ -2456,7 +2528,8 @@ "attack_ratio": 0.83, "energy_recharge": 50 } - ] + ], + "executions_over_time": [] } ] } diff --git a/apps/champions/test/battle_test.exs b/apps/champions/test/battle_test.exs index a4a671470..774cbb505 100644 --- a/apps/champions/test/battle_test.exs +++ b/apps/champions/test/battle_test.exs @@ -2053,4 +2053,72 @@ defmodule Champions.Test.BattleTest do assert unit_initial_state_with_modifiers.health == base_health * health_multiplier end end + + describe "Executions over time" do + test "DealDamageOverTime", %{target_dummy: target_dummy} do + # We will create a battle between a damaging unit and a target dummy. + # The unit's basic skill will deal 4 points of damage to the target dummy over 6 steps, killing it in the sixth one. The cooldown is such that the skill can be used only once. + # The battle should finish with a victory for team_1 after the last step, or in timeout if the steps are less than the required ones to kill the target dummy. + attacker_cooldown = 9 + dot_duration = 3 + dot_interval = 2 + + # We add 1 to consider the step in which the attack is performed + required_steps = attacker_cooldown + dot_duration * dot_interval + 1 + + deal_damage_over_time_params = + TestUtils.build_skill(%{ + name: "DealDamageOverTime", + mechanics: [ + %{ + trigger_delay: 0, + apply_effects_to: + TestUtils.build_apply_effects_to_mechanic(%{ + effects: [ + TestUtils.build_effect(%{ + type: %{ + "type" => "duration", + "duration" => dot_duration * @miliseconds_per_step + }, + executions_over_time: [ + %{ + type: "DealDamageOverTime", + attack_ratio: 1, + apply_tags: ["Burn"], + interval: dot_interval * @miliseconds_per_step + } + ] + }) + ], + targeting_strategy: %{ + count: 1, + type: "nearest", + target_allies: false + } + }) + } + ], + cooldown: attacker_cooldown * @miliseconds_per_step + }) + + {:ok, character} = + TestUtils.build_character(%{ + name: "DealDamageOverTime Character", + basic_skill: deal_damage_over_time_params, + ultimate_skill: TestUtils.build_skill(%{name: "DealDamageOverTime Empty Skill"}), + base_attack: 4 + }) + |> Characters.insert_character() + + {:ok, unit} = TestUtils.build_unit(%{character_id: character.id}) |> Units.insert_unit() + + {:ok, unit} = Units.get_unit(unit.id) + + assert "team_1" == + Champions.Battle.Simulator.run_battle([unit], [target_dummy], maximum_steps: required_steps).result + + assert "timeout" == + Champions.Battle.Simulator.run_battle([unit], [target_dummy], maximum_steps: required_steps - 1).result + end + end end diff --git a/apps/champions/test/support/test_utils.ex b/apps/champions/test/support/test_utils.ex index 4d4ff434b..71f829ec5 100644 --- a/apps/champions/test/support/test_utils.ex +++ b/apps/champions/test/support/test_utils.ex @@ -69,7 +69,8 @@ defmodule Champions.TestUtils do initial_delay: 0, components: [], modifiers: [], - executions: [] + executions: [], + executions_over_time: [] }, params ) diff --git a/apps/game_backend/lib/game_backend/units/skills/mechanics/effect.ex b/apps/game_backend/lib/game_backend/units/skills/mechanics/effect.ex index 18d6c80d7..3fa5de4d5 100644 --- a/apps/game_backend/lib/game_backend/units/skills/mechanics/effect.ex +++ b/apps/game_backend/lib/game_backend/units/skills/mechanics/effect.ex @@ -14,6 +14,7 @@ defmodule GameBackend.Units.Skills.Mechanics.Effect do field(:components, {:array, :map}) embeds_many(:modifiers, Modifier) field(:executions, {:array, :map}) + field(:executions_over_time, {:array, :map}) end @doc false @@ -23,13 +24,15 @@ defmodule GameBackend.Units.Skills.Mechanics.Effect do :type, :initial_delay, :components, - :executions + :executions, + :executions_over_time ]) |> validate_required([ :type, :initial_delay, :components, - :executions + :executions, + :executions_over_time ]) |> validate_change(:executions, fn :executions, executions -> valid? = @@ -43,6 +46,12 @@ defmodule GameBackend.Units.Skills.Mechanics.Effect do if valid?, do: [], else: [components: "A component is invalid"] end) + |> validate_change(:executions_over_time, fn :executions_over_time, executions_over_time -> + valid? = + Enum.all?(executions_over_time, fn eot -> valid_execution_over_time?(eot) end) + + if valid?, do: [], else: [executions_over_time: "An execution over time is invalid"] + end) |> cast_embed(:modifiers) end @@ -56,28 +65,36 @@ defmodule GameBackend.Units.Skills.Mechanics.Effect do true %{ - type: "DealDamageOverTime", - attack_ratio: _attack_ratio, - energy_recharge: _energy_recharge, - dot_type: _dot_type + type: "Heal", + attack_ratio: _attack_ratio } -> true %{ - type: "Heal", - attack_ratio: _attack_ratio + type: "AddEnergy", + amount: _amount } -> true + _ -> + false + end + end + + defp valid_execution_over_time?(execution_over_time) do + case execution_over_time do %{ - type: "HealOverTime", - attack_ratio: _attack_ratio + type: "DealDamageOverTime", + attack_ratio: _attack_ratio, + apply_tags: _apply_tags, + interval: _interval } -> true %{ - type: "AddEnergy", - amount: _amount + type: "HealOverTime", + attack_ratio: _attack_ratio, + interval: _interval } -> true