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