Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-399] Deal damage over time #662

Merged
merged 34 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8c20a5f
Add executions over time. Adapt skills.json
ncontinanza May 28, 2024
e7dc796
Add empty executions over time in skills.json and tests
ncontinanza May 28, 2024
7b7e93e
Add execution_over_time to model. Write a failing test that uses a DoT
ncontinanza May 28, 2024
100a739
WIP: process executions over time
ncontinanza May 29, 2024
8b86536
WIP: process executions over time
ncontinanza May 29, 2024
2c83739
Merge branch 'main' of github.com:lambdaclass/game_backend into gh-39…
ncontinanza May 29, 2024
3c66ab9
First working version of the DoT
ncontinanza May 30, 2024
2ed4298
Add period delay to DoT execution
ncontinanza May 30, 2024
f3e8cc6
Refactor duplicate code
ncontinanza May 30, 2024
0659b5a
Remove commented code
ncontinanza May 30, 2024
915b70c
Simplify the use of the process_execution_over_time function
ncontinanza May 30, 2024
80927e2
Simplify the use of the apply_execution_over_time function
ncontinanza May 30, 2024
e21dce7
Change period for interval. Remove period from executions over time
ncontinanza May 31, 2024
930cd8d
Format
ncontinanza May 31, 2024
8142ce9
Process executions over time after checking the ability to cast a skill
ncontinanza Jun 5, 2024
69ba52f
Use unit initial state to process executions over time
ncontinanza Jun 5, 2024
24862e8
Merge branch 'main' of github.com:lambdaclass/game_backend into gh-39…
ncontinanza Jun 5, 2024
ef92bb8
Calculate interval steps from interval when mapping the effect
ncontinanza Jun 5, 2024
38d7a97
replace function pattern matching for a case statement in process_exe…
ncontinanza Jun 5, 2024
3d12b61
Improve function naming and make it private
ncontinanza Jun 5, 2024
2a34bc7
Add explanatory comment to calculate_damage/3
ncontinanza Jun 5, 2024
8e5d064
Improve test naming
ncontinanza Jun 5, 2024
8718b8e
Improve test documentation
ncontinanza Jun 5, 2024
1def3b3
Fix required steps calculation and add assert to verify that one less…
ncontinanza Jun 5, 2024
4c02dc0
Refactor apply_deal_damage_over_time function to solve credo warning
ncontinanza Jun 5, 2024
dce506b
Merge branch 'main' of github.com:lambdaclass/game_backend into gh-39…
ncontinanza Jun 10, 2024
82416c4
Simplify new_state update
ncontinanza Jun 10, 2024
d755b7c
Replace case for if-else statement
ncontinanza Jun 10, 2024
11c3ad3
Improve test explanatory comment
ncontinanza Jun 10, 2024
a5a24ea
Use unit initial state to calculate damage
ncontinanza Jun 10, 2024
fdafe26
Use the whole execution to compare, instead of the skill_id, to preve…
ncontinanza Jun 10, 2024
4313ec6
Add function's explanatory comment above the function definition
ncontinanza Jun 10, 2024
1b4e2c3
Handle nil apply_tags case
ncontinanza Jun 10, 2024
beab235
Compare executions instead of skill ids
ncontinanza Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 160 additions & 13 deletions apps/champions/lib/champions/battle/simulator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
agustinesco marked this conversation as resolved.
Show resolved Hide resolved
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) ->
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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}."
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a really, really edge-y edge case here where this fails still. Do you think we should add an issue to revisit this later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, the case in which two units of the same character apply the same EoT to a target would be handled by this comparison because the execution over time would have different values in the caster field.

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)))
Expand Down Expand Up @@ -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
lotuuu marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand All @@ -1173,6 +1314,7 @@ defmodule Champions.Battle.Simulator do
multiplicatives: [],
overrides: []
},
executions_over_time: [],
tags: []
}}

Expand Down Expand Up @@ -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)}
Expand All @@ -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(%{
Expand Down
Loading
Loading