239 lines
7.4 KiB
Elixir
239 lines
7.4 KiB
Elixir
defmodule Odinsea.Game.DamageCalc do
|
|
use Bitwise
|
|
@moduledoc """
|
|
Damage calculation and application module.
|
|
Ported from src/handling/channel/handler/DamageParse.java
|
|
"""
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.Game.{AttackInfo, Character, Map, Monster}
|
|
alias Odinsea.Net.Packet.Out
|
|
alias Odinsea.Net.Opcodes
|
|
alias Odinsea.Channel.Packets
|
|
|
|
@doc """
|
|
Apply attack to monsters on the map.
|
|
Ported from DamageParse.applyAttack()
|
|
|
|
Returns {:ok, total_damage} or {:error, reason}
|
|
"""
|
|
def apply_attack(attack_info, character_pid, map_pid, channel_id) do
|
|
with {:ok, character} <- Character.get_state(character_pid),
|
|
{:ok, map_data} <- Map.get_monsters(map_pid) do
|
|
# Check if character is alive
|
|
if not character.alive? do
|
|
Logger.warning("Character #{character.name} attacking while dead")
|
|
{:error, :attacking_while_dead}
|
|
else
|
|
# Calculate max damage per monster
|
|
max_damage_per_monster = calculate_max_damage(character, attack_info)
|
|
|
|
# Apply damage to each targeted monster
|
|
total_damage =
|
|
Enum.reduce(attack_info.all_damage, 0, fn damage_entry, acc_total ->
|
|
apply_damage_to_monster(
|
|
damage_entry,
|
|
attack_info,
|
|
character,
|
|
map_pid,
|
|
channel_id,
|
|
max_damage_per_monster
|
|
) + acc_total
|
|
end)
|
|
|
|
Logger.debug("Attack applied: #{total_damage} total damage to #{length(attack_info.all_damage)} monsters")
|
|
|
|
# Broadcast attack packet to other players
|
|
broadcast_attack(attack_info, character, map_pid, channel_id)
|
|
|
|
{:ok, total_damage}
|
|
end
|
|
else
|
|
error ->
|
|
Logger.error("Failed to apply attack: #{inspect(error)}")
|
|
{:error, :apply_failed}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Private Helper Functions
|
|
# ============================================================================
|
|
|
|
defp apply_damage_to_monster(damage_entry, attack_info, character, map_pid, channel_id, max_damage) do
|
|
# Calculate total damage to this monster
|
|
total_damage =
|
|
Enum.reduce(damage_entry.damages, 0, fn {damage, _crit}, acc ->
|
|
# Cap damage at max allowed
|
|
capped_damage = min(damage, trunc(max_damage))
|
|
acc + capped_damage
|
|
end)
|
|
|
|
if total_damage > 0 do
|
|
# Apply damage via Map module
|
|
case Map.damage_monster(map_pid, damage_entry.mob_oid, character.id, total_damage) do
|
|
{:ok, :killed} ->
|
|
Logger.debug("Monster #{damage_entry.mob_oid} killed by #{character.name}")
|
|
total_damage
|
|
|
|
{:ok, :damaged} ->
|
|
total_damage
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to damage monster #{damage_entry.mob_oid}: #{inspect(reason)}")
|
|
0
|
|
end
|
|
else
|
|
0
|
|
end
|
|
end
|
|
|
|
defp calculate_max_damage(character, attack_info) do
|
|
# Base damage calculation
|
|
# TODO: Implement full damage formula with stats, weapon attack, skill damage, etc.
|
|
# For now, use a simple formula based on level and stats
|
|
|
|
base_damage =
|
|
cond do
|
|
# Magic attack
|
|
attack_info.attack_type == :magic ->
|
|
character.stats.int * 5 + character.stats.luk + character.level * 10
|
|
|
|
# Ranged attack
|
|
attack_info.attack_type == :ranged or
|
|
attack_info.attack_type == :ranged_with_shadowpartner ->
|
|
character.stats.dex * 5 + character.stats.str + character.level * 10
|
|
|
|
# Melee attack
|
|
true ->
|
|
character.stats.str * 5 + character.stats.dex + character.level * 10
|
|
end
|
|
|
|
# Apply skill multiplier if skill is used
|
|
skill_multiplier =
|
|
if attack_info.skill > 0 do
|
|
# TODO: Get actual skill damage multiplier from SkillFactory
|
|
2.0
|
|
else
|
|
1.0
|
|
end
|
|
|
|
# Calculate max damage per hit
|
|
max_damage_per_hit = base_damage * skill_multiplier
|
|
|
|
# For now, return a reasonable cap
|
|
# TODO: Implement actual damage cap from config
|
|
min(max_damage_per_hit, 999_999)
|
|
end
|
|
|
|
defp broadcast_attack(attack_info, character, map_pid, channel_id) do
|
|
# Build attack packet based on attack type
|
|
attack_packet =
|
|
case attack_info.attack_type do
|
|
:melee ->
|
|
build_melee_attack_packet(attack_info, character)
|
|
|
|
:ranged ->
|
|
build_ranged_attack_packet(attack_info, character)
|
|
|
|
:magic ->
|
|
build_magic_attack_packet(attack_info, character)
|
|
|
|
_ ->
|
|
build_melee_attack_packet(attack_info, character)
|
|
end
|
|
|
|
# Broadcast to all players on map except attacker
|
|
Map.broadcast_except(
|
|
character.map_id,
|
|
channel_id,
|
|
character.id,
|
|
attack_packet
|
|
)
|
|
end
|
|
|
|
defp build_melee_attack_packet(attack_info, character) do
|
|
packet =
|
|
Out.new(Opcodes.lp_close_range_attack())
|
|
|> Out.encode_int(character.id)
|
|
|> Out.encode_byte(attack_info.tbyte)
|
|
|> Out.encode_byte(character.stats.skill_level)
|
|
|> Out.encode_int(attack_info.skill)
|
|
|> Out.encode_byte(attack_info.display &&& 0xFF)
|
|
|> Out.encode_byte((attack_info.display >>> 8) &&& 0xFF)
|
|
|> Out.encode_byte(attack_info.speed)
|
|
|> Out.encode_int(attack_info.last_attack_tick)
|
|
|
|
# Encode damage for each target
|
|
packet =
|
|
Enum.reduce(attack_info.all_damage, packet, fn damage_entry, acc ->
|
|
acc
|
|
|> Out.encode_int(damage_entry.mob_oid)
|
|
|> encode_damage_hits(damage_entry.damages)
|
|
end)
|
|
|
|
packet
|
|
|> Out.encode_short(attack_info.position.x)
|
|
|> Out.encode_short(attack_info.position.y)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
defp build_ranged_attack_packet(attack_info, character) do
|
|
packet =
|
|
Out.new(Opcodes.lp_ranged_attack())
|
|
|> Out.encode_int(character.id)
|
|
|> Out.encode_byte(attack_info.tbyte)
|
|
|> Out.encode_byte(character.stats.skill_level)
|
|
|> Out.encode_int(attack_info.skill)
|
|
|> Out.encode_byte(attack_info.display &&& 0xFF)
|
|
|> Out.encode_byte((attack_info.display >>> 8) &&& 0xFF)
|
|
|> Out.encode_byte(attack_info.speed)
|
|
|> Out.encode_int(attack_info.last_attack_tick)
|
|
|
|
# Encode damage for each target
|
|
packet =
|
|
Enum.reduce(attack_info.all_damage, packet, fn damage_entry, acc ->
|
|
acc
|
|
|> Out.encode_int(damage_entry.mob_oid)
|
|
|> encode_damage_hits(damage_entry.damages)
|
|
end)
|
|
|
|
packet
|
|
|> Out.encode_short(attack_info.position.x)
|
|
|> Out.encode_short(attack_info.position.y)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
defp build_magic_attack_packet(attack_info, character) do
|
|
packet =
|
|
Out.new(Opcodes.lp_magic_attack())
|
|
|> Out.encode_int(character.id)
|
|
|> Out.encode_byte(attack_info.tbyte)
|
|
|> Out.encode_byte(character.stats.skill_level)
|
|
|> Out.encode_int(attack_info.skill)
|
|
|> Out.encode_byte(attack_info.display &&& 0xFF)
|
|
|> Out.encode_byte((attack_info.display >>> 8) &&& 0xFF)
|
|
|> Out.encode_byte(attack_info.speed)
|
|
|> Out.encode_int(attack_info.last_attack_tick)
|
|
|
|
# Encode damage for each target
|
|
packet =
|
|
Enum.reduce(attack_info.all_damage, packet, fn damage_entry, acc ->
|
|
acc
|
|
|> Out.encode_int(damage_entry.mob_oid)
|
|
|> encode_damage_hits(damage_entry.damages)
|
|
end)
|
|
|
|
packet
|
|
|> Out.encode_short(attack_info.position.x)
|
|
|> Out.encode_short(attack_info.position.y)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
defp encode_damage_hits(packet, damages) do
|
|
Enum.reduce(damages, packet, fn {damage, _crit}, acc ->
|
|
acc |> Out.encode_int(damage)
|
|
end)
|
|
end
|
|
end
|