kimi gone wild
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,917 @@
|
||||
defmodule Odinsea.World.Guild do
|
||||
@moduledoc """
|
||||
Guild management service.
|
||||
Ported from src/handling/world/guild/MapleGuild.java
|
||||
|
||||
Manages guild state including members, ranks, skills, and alliance.
|
||||
Supports guild creation, joining, leaving, and rank management.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Database.Repo
|
||||
import Ecto.Query
|
||||
|
||||
@default_capacity 10
|
||||
@max_capacity 200
|
||||
@rank_titles ["Master", "Jr. Master", "Member", "Member", "Member"]
|
||||
@create_cost 500_000
|
||||
|
||||
# ============================================================================
|
||||
# Data Structures
|
||||
# ============================================================================
|
||||
|
||||
defmodule GuildCharacter do
|
||||
@moduledoc "Guild member representation"
|
||||
defstruct [
|
||||
:id, :name, :level, :job, :channel,
|
||||
:guild_rank, :alliance_rank, :guild_contribution,
|
||||
:online
|
||||
]
|
||||
end
|
||||
|
||||
defmodule GuildSkill do
|
||||
@moduledoc "Guild skill representation"
|
||||
defstruct [
|
||||
:skill_id, :level, :timestamp, :purchaser, :activators
|
||||
]
|
||||
end
|
||||
|
||||
defmodule BBSThread do
|
||||
@moduledoc "Guild BBS thread"
|
||||
defstruct [
|
||||
:thread_id, :local_id, :name, :content,
|
||||
:poster_id, :timestamp, :icon, :replies
|
||||
]
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Client API
|
||||
# ============================================================================
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new guild.
|
||||
Returns {:ok, guild_id} on success, {:error, reason} on failure.
|
||||
"""
|
||||
def create_guild(leader_id, name) do
|
||||
GenServer.call(__MODULE__, {:create_guild, leader_id, name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a guild by ID.
|
||||
Returns the guild struct or nil if not found.
|
||||
"""
|
||||
def get_guild(guild_id) do
|
||||
GenServer.call(__MODULE__, {:get_guild, guild_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets guild by member character ID.
|
||||
"""
|
||||
def get_guild_by_character(character_id) do
|
||||
GenServer.call(__MODULE__, {:get_guild_by_character, character_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a member to a guild.
|
||||
"""
|
||||
def add_member(guild_id, character) do
|
||||
GenServer.call(__MODULE__, {:add_member, guild_id, character})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes a member from a guild (leave).
|
||||
"""
|
||||
def leave_guild(guild_id, character_id) do
|
||||
GenServer.call(__MODULE__, {:leave_guild, guild_id, character_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expels a member from a guild.
|
||||
"""
|
||||
def expel_member(guild_id, expeller_id, target_id, target_name) do
|
||||
GenServer.call(__MODULE__, {:expel_member, guild_id, expeller_id, target_id, target_name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes a member's guild rank.
|
||||
"""
|
||||
def change_rank(guild_id, character_id, new_rank, changer_id) do
|
||||
GenServer.call(__MODULE__, {:change_rank, guild_id, character_id, new_rank, changer_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes guild rank titles.
|
||||
"""
|
||||
def change_rank_titles(guild_id, titles, changer_id) do
|
||||
GenServer.call(__MODULE__, {:change_rank_titles, guild_id, titles, changer_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes the guild leader.
|
||||
"""
|
||||
def change_leader(guild_id, new_leader_id, current_leader_id) do
|
||||
GenServer.call(__MODULE__, {:change_leader, guild_id, new_leader_id, current_leader_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets guild emblem.
|
||||
"""
|
||||
def set_emblem(guild_id, bg, bg_color, logo, logo_color, changer_id) do
|
||||
GenServer.call(__MODULE__, {:set_emblem, guild_id, bg, bg_color, logo, logo_color, changer_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets guild notice.
|
||||
"""
|
||||
def set_notice(guild_id, notice, changer_id) do
|
||||
GenServer.call(__MODULE__, {:set_notice, guild_id, notice, changer_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Increases guild capacity.
|
||||
"""
|
||||
def increase_capacity(guild_id, leader_id, true_max \\ false) do
|
||||
GenServer.call(__MODULE__, {:increase_capacity, guild_id, leader_id, true_max})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gains guild points (GP).
|
||||
"""
|
||||
def gain_gp(guild_id, amount, character_id \\ nil, broadcast \\ true) do
|
||||
GenServer.call(__MODULE__, {:gain_gp, guild_id, amount, character_id, broadcast})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets member online status.
|
||||
"""
|
||||
def set_online(guild_id, character_id, online, channel) do
|
||||
GenServer.call(__MODULE__, {:set_online, guild_id, character_id, online, channel})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates member info (level/job change).
|
||||
"""
|
||||
def update_member(guild_id, character) do
|
||||
GenServer.call(__MODULE__, {:update_member, guild_id, character})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disbands a guild.
|
||||
"""
|
||||
def disband_guild(guild_id, leader_id) do
|
||||
GenServer.call(__MODULE__, {:disband_guild, guild_id, leader_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets guild skills.
|
||||
"""
|
||||
def get_skills(guild_id) do
|
||||
GenServer.call(__MODULE__, {:get_skills, guild_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Purchases a guild skill.
|
||||
"""
|
||||
def purchase_skill(guild_id, skill_id, purchaser_name, purchaser_id) do
|
||||
GenServer.call(__MODULE__, {:purchase_skill, guild_id, skill_id, purchaser_name, purchaser_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Activates a guild skill.
|
||||
"""
|
||||
def activate_skill(guild_id, skill_id, activator_name) do
|
||||
GenServer.call(__MODULE__, {:activate_skill, guild_id, skill_id, activator_name})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts a packet to all online guild members.
|
||||
"""
|
||||
def broadcast(guild_id, packet, except_character_id \\ nil) do
|
||||
GenServer.cast(__MODULE__, {:broadcast, guild_id, packet, except_character_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Guild chat - sends message to all online guild members.
|
||||
"""
|
||||
def guild_chat(guild_id, sender_name, sender_id, message) do
|
||||
GenServer.cast(__MODULE__, {:guild_chat, guild_id, sender_name, sender_id, message})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets alliance ID for a guild.
|
||||
"""
|
||||
def set_alliance(guild_id, alliance_id, alliance_rank) do
|
||||
GenServer.call(__MODULE__, {:set_alliance, guild_id, alliance_id, alliance_rank})
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Server Callbacks
|
||||
# ============================================================================
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{guilds: %{}}}
|
||||
# Load guilds from database on startup
|
||||
guilds = load_guilds_from_db()
|
||||
|
||||
Logger.info("Guild service initialized with #{map_size(guilds)} guilds")
|
||||
{:ok, %{guilds: guilds}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_guild, leader_id, name}, _from, state) do
|
||||
# Validate name
|
||||
cond do
|
||||
String.length(name) > 12 ->
|
||||
{:reply, {:error, :name_too_long}, state}
|
||||
|
||||
String.length(name) < 3 ->
|
||||
{:reply, {:error, :name_too_short}, state}
|
||||
|
||||
not valid_guild_name?(name) ->
|
||||
{:reply, {:error, :invalid_name}, state}
|
||||
|
||||
true ->
|
||||
case create_guild_in_db(leader_id, name) do
|
||||
{:ok, guild_id} ->
|
||||
guild = create_new_guild_struct(guild_id, leader_id, name)
|
||||
new_state = %{state | guilds: Map.put(state.guilds, guild_id, guild)}
|
||||
|
||||
Logger.info("Guild '#{name}' (ID: #{guild_id}) created by leader #{leader_id}")
|
||||
{:reply, {:ok, guild_id}, new_state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_guild, guild_id}, _from, state) do
|
||||
{:reply, Map.get(state.guilds, guild_id), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_guild_by_character, character_id}, _from, state) do
|
||||
guild = state.guilds
|
||||
|> Map.values()
|
||||
|> Enum.find(fn g ->
|
||||
Enum.any?(g.members, fn m -> m.id == character_id end)
|
||||
end)
|
||||
|
||||
{:reply, guild, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:add_member, guild_id, character}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
if length(guild.members) >= guild.capacity do
|
||||
{:reply, {:error, :guild_full}, state}
|
||||
else
|
||||
# Create new member with rank 5 (lowest)
|
||||
member = %GuildCharacter{
|
||||
id: character.id,
|
||||
name: character.name,
|
||||
level: character.level,
|
||||
job: character.job,
|
||||
channel: character.channel_id || 1,
|
||||
guild_rank: 5,
|
||||
alliance_rank: if(guild.alliance_id > 0, do: 3, else: 0),
|
||||
guild_contribution: 0,
|
||||
online: true
|
||||
}
|
||||
|
||||
updated_guild = %{guild | members: guild.members ++ [member]}
|
||||
|
||||
# Save to database
|
||||
save_member_to_db(guild_id, member)
|
||||
|
||||
# Broadcast new member to guild
|
||||
broadcast_new_member(updated_guild, member)
|
||||
|
||||
# Gain GP for new member
|
||||
updated_guild = %{updated_guild | gp: guild.gp + 500}
|
||||
|
||||
{:reply, {:ok, member}, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:leave_guild, guild_id, character_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
member = Enum.find(guild.members, fn m -> m.id == character_id end)
|
||||
|
||||
if member do
|
||||
# Remove member
|
||||
members = Enum.reject(guild.members, fn m -> m.id == character_id end)
|
||||
|
||||
# If leader leaves and there are members, promote next highest rank
|
||||
updated_guild = if guild.leader_id == character_id && length(members) > 0 do
|
||||
# Find highest ranked member (lowest rank number)
|
||||
new_leader = Enum.min_by(members, fn m -> m.guild_rank end)
|
||||
%{guild | members: members, leader_id: new_leader.id}
|
||||
else
|
||||
%{guild | members: members}
|
||||
end
|
||||
|
||||
# Remove from database
|
||||
remove_member_from_db(guild_id, character_id)
|
||||
|
||||
# Broadcast member left
|
||||
broadcast_member_left(updated_guild, member)
|
||||
|
||||
# Deduct GP
|
||||
gp_loss = if member.guild_contribution > 0, do: -member.guild_contribution, else: -50
|
||||
updated_guild = %{updated_guild | gp: max(0, guild.gp + gp_loss)}
|
||||
|
||||
# If no members left, mark for disband
|
||||
final_state = if length(members) == 0 do
|
||||
disband_guild_in_db(guild_id)
|
||||
%{state | guilds: Map.delete(state.guilds, guild_id)}
|
||||
else
|
||||
%{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}
|
||||
end
|
||||
|
||||
{:reply, :ok, final_state}
|
||||
else
|
||||
{:reply, {:error, :not_in_guild}, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:expel_member, guild_id, expeller_id, target_id, target_name}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
expeller = Enum.find(guild.members, fn m -> m.id == expeller_id end)
|
||||
target = Enum.find(guild.members, fn m -> m.id == target_id end)
|
||||
|
||||
cond do
|
||||
not expeller || expeller.guild_rank > 2 ->
|
||||
{:reply, {:error, :no_permission}, state}
|
||||
|
||||
not target || target.guild_rank <= expeller.guild_rank ->
|
||||
{:reply, {:error, :cannot_expel}, state}
|
||||
|
||||
true ->
|
||||
# Remove member
|
||||
members = Enum.reject(guild.members, fn m -> m.id == target_id end)
|
||||
updated_guild = %{guild | members: members}
|
||||
|
||||
# Remove from database
|
||||
remove_member_from_db(guild_id, target_id)
|
||||
|
||||
# Send note if offline
|
||||
unless target.online do
|
||||
send_note(target_name, expeller.name, "You have been expelled from the guild.")
|
||||
end
|
||||
|
||||
# Broadcast
|
||||
broadcast_member_expelled(updated_guild, target)
|
||||
|
||||
# Deduct GP
|
||||
gp_loss = if target.guild_contribution > 0, do: -target.guild_contribution, else: -50
|
||||
updated_guild = %{updated_guild | gp: max(0, guild.gp + gp_loss)}
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:change_rank, guild_id, character_id, new_rank, changer_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
changer = Enum.find(guild.members, fn m -> m.id == changer_id end)
|
||||
target = Enum.find(guild.members, fn m -> m.id == character_id end)
|
||||
|
||||
cond do
|
||||
not changer || changer.guild_rank > 2 ->
|
||||
{:reply, {:error, :no_permission}, state}
|
||||
|
||||
new_rank <= 1 || new_rank > 5 ->
|
||||
{:reply, {:error, :invalid_rank}, state}
|
||||
|
||||
new_rank <= 2 && changer.guild_rank != 1 ->
|
||||
{:reply, {:error, :no_permission}, state}
|
||||
|
||||
not target ->
|
||||
{:reply, {:error, :member_not_found}, state}
|
||||
|
||||
true ->
|
||||
# Update rank
|
||||
members = Enum.map(guild.members, fn m ->
|
||||
if m.id == character_id do
|
||||
%{m | guild_rank: new_rank}
|
||||
else
|
||||
m
|
||||
end
|
||||
end)
|
||||
|
||||
updated_guild = %{guild | members: members}
|
||||
|
||||
# Save to database
|
||||
update_rank_in_db(character_id, new_rank)
|
||||
|
||||
# Broadcast
|
||||
broadcast_rank_changed(updated_guild, target, new_rank)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:change_rank_titles, guild_id, titles, changer_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
changer = Enum.find(guild.members, fn m -> m.id == changer_id end)
|
||||
|
||||
if not changer || changer.guild_rank != 1 do
|
||||
{:reply, {:error, :no_permission}, state}
|
||||
else
|
||||
updated_guild = %{guild | rank_titles: titles}
|
||||
|
||||
# Save to database
|
||||
update_rank_titles_in_db(guild_id, titles)
|
||||
|
||||
# Broadcast
|
||||
broadcast_rank_titles_changed(updated_guild, titles)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:change_leader, guild_id, new_leader_id, current_leader_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
%{leader_id: actual_leader} when actual_leader != current_leader_id ->
|
||||
{:reply, {:error, :not_leader}, state}
|
||||
|
||||
guild ->
|
||||
unless Enum.any?(guild.members, fn m -> m.id == new_leader_id end) do
|
||||
{:reply, {:error, :not_in_guild}, state}
|
||||
else
|
||||
# Update ranks: new leader -> 1, old leader -> 2
|
||||
members = Enum.map(guild.members, fn m ->
|
||||
cond do
|
||||
m.id == new_leader_id -> %{m | guild_rank: 1}
|
||||
m.id == current_leader_id -> %{m | guild_rank: 2}
|
||||
true -> m
|
||||
end
|
||||
end)
|
||||
|
||||
updated_guild = %{guild | members: members, leader_id: new_leader_id}
|
||||
|
||||
# Save to database
|
||||
update_leader_in_db(guild_id, new_leader_id)
|
||||
update_rank_in_db(new_leader_id, 1)
|
||||
update_rank_in_db(current_leader_id, 2)
|
||||
|
||||
# Broadcast
|
||||
broadcast_leader_changed(updated_guild, new_leader_id)
|
||||
|
||||
Logger.info("Guild #{guild_id} leader changed from #{current_leader_id} to #{new_leader_id}")
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:set_emblem, guild_id, bg, bg_color, logo, logo_color, changer_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
changer = Enum.find(guild.members, fn m -> m.id == changer_id end)
|
||||
|
||||
if not changer || changer.guild_rank != 1 do
|
||||
{:reply, {:error, :no_permission}, state}
|
||||
else
|
||||
updated_guild = %{guild |
|
||||
logo_bg: bg,
|
||||
logo_bg_color: bg_color,
|
||||
logo: logo,
|
||||
logo_color: logo_color
|
||||
}
|
||||
|
||||
# Save to database
|
||||
update_emblem_in_db(guild_id, bg, bg_color, logo, logo_color)
|
||||
|
||||
# Broadcast
|
||||
broadcast_emblem_changed(updated_guild)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:set_notice, guild_id, notice, changer_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
changer = Enum.find(guild.members, fn m -> m.id == changer_id end)
|
||||
|
||||
if not changer || changer.guild_rank > 2 do
|
||||
{:reply, {:error, :no_permission}, state}
|
||||
else
|
||||
updated_guild = %{guild | notice: String.slice(notice, 0, 100)}
|
||||
|
||||
# Save to database
|
||||
update_notice_in_db(guild_id, updated_guild.notice)
|
||||
|
||||
# Broadcast
|
||||
broadcast_notice_changed(updated_guild)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:increase_capacity, guild_id, leader_id, true_max}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
max_cap = if true_max, do: @max_capacity, else: div(@max_capacity, 2)
|
||||
|
||||
cond do
|
||||
guild.leader_id != leader_id ->
|
||||
{:reply, {:error, :not_leader}, state}
|
||||
|
||||
guild.capacity >= max_cap ->
|
||||
{:reply, {:error, :max_capacity}, state}
|
||||
|
||||
true ->
|
||||
new_capacity = min(guild.capacity + 5, max_cap)
|
||||
updated_guild = %{guild | capacity: new_capacity}
|
||||
|
||||
# Save to database
|
||||
update_capacity_in_db(guild_id, new_capacity)
|
||||
|
||||
# Broadcast
|
||||
broadcast_capacity_changed(updated_guild)
|
||||
|
||||
{:reply, {:ok, new_capacity}, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:gain_gp, guild_id, amount, _character_id, broadcast}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
new_gp = max(0, guild.gp + amount)
|
||||
updated_guild = %{guild | gp: new_gp}
|
||||
|
||||
# Save to database
|
||||
update_gp_in_db(guild_id, new_gp)
|
||||
|
||||
# Optionally broadcast
|
||||
if broadcast do
|
||||
broadcast_gp_changed(updated_guild, amount)
|
||||
end
|
||||
|
||||
{:reply, {:ok, new_gp}, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:set_online, guild_id, character_id, online, channel}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
members = Enum.map(guild.members, fn m ->
|
||||
if m.id == character_id do
|
||||
%{m | online: online, channel: if(online, do: channel, else: -1)}
|
||||
else
|
||||
m
|
||||
end
|
||||
end)
|
||||
|
||||
updated_guild = %{guild | members: members}
|
||||
|
||||
# Broadcast online status to other members
|
||||
broadcast_member_online(updated_guild, character_id, online)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:update_member, guild_id, character}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
members = Enum.map(guild.members, fn m ->
|
||||
if m.id == character.id do
|
||||
%{m |
|
||||
level: character.level || m.level,
|
||||
job: character.job || m.job,
|
||||
channel: character.channel_id || m.channel
|
||||
}
|
||||
else
|
||||
m
|
||||
end
|
||||
end)
|
||||
|
||||
updated_guild = %{guild | members: members}
|
||||
|
||||
# Broadcast level/job change if applicable
|
||||
broadcast_member_info_updated(updated_guild, character)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:disband_guild, guild_id, leader_id}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
%{leader_id: actual_leader} when actual_leader != leader_id ->
|
||||
{:reply, {:error, :not_leader}, state}
|
||||
|
||||
guild ->
|
||||
# Broadcast disband
|
||||
broadcast_guild_disband(guild)
|
||||
|
||||
# Remove from database
|
||||
disband_guild_in_db(guild_id)
|
||||
|
||||
Logger.info("Guild #{guild_id} (#{guild.name}) disbanded")
|
||||
{:reply, :ok, %{state | guilds: Map.delete(state.guilds, guild_id)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:set_alliance, guild_id, alliance_id, alliance_rank}, _from, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :guild_not_found}, state}
|
||||
|
||||
guild ->
|
||||
members = Enum.map(guild.members, fn m ->
|
||||
%{m | alliance_rank: alliance_rank}
|
||||
end)
|
||||
|
||||
updated_guild = %{guild | alliance_id: alliance_id, members: members}
|
||||
|
||||
# Save to database
|
||||
update_alliance_in_db(guild_id, alliance_id, alliance_rank)
|
||||
|
||||
{:reply, :ok, %{state | guilds: Map.put(state.guilds, guild_id, updated_guild)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:broadcast, guild_id, packet, except_id}, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil -> :ok
|
||||
guild ->
|
||||
Enum.each(guild.members, fn member ->
|
||||
if member.online && member.id != except_id do
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] -> send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:guild_chat, guild_id, sender_name, sender_id, message}, state) do
|
||||
case Map.get(state.guilds, guild_id) do
|
||||
nil -> :ok
|
||||
guild ->
|
||||
Enum.each(guild.members, fn member ->
|
||||
if member.online && member.id != sender_id do
|
||||
# Check blacklist
|
||||
# TODO: Implement blacklist check
|
||||
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] ->
|
||||
packet = build_guild_chat_packet(sender_name, message)
|
||||
send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Database Functions (Stub implementations - would use Ecto)
|
||||
# ============================================================================
|
||||
|
||||
defp load_guilds_from_db do
|
||||
# TODO: Implement actual database loading
|
||||
# For now, return empty map
|
||||
%{}
|
||||
end
|
||||
|
||||
defp create_guild_in_db(leader_id, name) do
|
||||
# TODO: Implement database insert
|
||||
# Return a new guild ID
|
||||
{:ok, System.unique_integer([:positive])}
|
||||
end
|
||||
|
||||
defp save_member_to_db(_guild_id, _member) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp remove_member_from_db(_guild_id, _character_id) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_rank_in_db(_character_id, _rank) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_leader_in_db(_guild_id, _leader_id) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_rank_titles_in_db(_guild_id, _titles) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_emblem_in_db(_guild_id, _bg, _bg_color, _logo, _logo_color) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_notice_in_db(_guild_id, _notice) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_capacity_in_db(_guild_id, _capacity) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_gp_in_db(_guild_id, _gp) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp update_alliance_in_db(_guild_id, _alliance_id, _alliance_rank) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp disband_guild_in_db(_guild_id) do
|
||||
# TODO: Implement
|
||||
:ok
|
||||
end
|
||||
|
||||
defp send_note(_to_name, _from_name, _message) do
|
||||
# TODO: Implement note sending
|
||||
:ok
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
defp valid_guild_name?(name) do
|
||||
# Only allow letters
|
||||
Regex.match?(~r/^[a-zA-Z]+$/, name)
|
||||
end
|
||||
|
||||
defp create_new_guild_struct(guild_id, leader_id, name) do
|
||||
%{
|
||||
id: guild_id,
|
||||
name: name,
|
||||
leader_id: leader_id,
|
||||
gp: 0,
|
||||
logo: 0,
|
||||
logo_color: 0,
|
||||
logo_bg: 0,
|
||||
logo_bg_color: 0,
|
||||
capacity: @default_capacity,
|
||||
rank_titles: @rank_titles,
|
||||
notice: "",
|
||||
signature: System.system_time(:second),
|
||||
alliance_id: 0,
|
||||
members: [],
|
||||
skills: %{},
|
||||
bbs: %{}
|
||||
}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Broadcast Functions
|
||||
# ============================================================================
|
||||
|
||||
defp broadcast_new_member(guild, member) do
|
||||
# TODO: Implement packet
|
||||
Logger.debug("Broadcast new member #{member.name} to guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_member_left(guild, member) do
|
||||
Logger.debug("Broadcast member left #{member.name} to guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_member_expelled(guild, member) do
|
||||
Logger.debug("Broadcast member expelled #{member.name} to guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_rank_changed(guild, member, new_rank) do
|
||||
Logger.debug("Broadcast rank change for #{member.name} to #{new_rank} in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_rank_titles_changed(guild, titles) do
|
||||
Logger.debug("Broadcast rank titles changed in guild #{guild.id}: #{inspect(titles)}")
|
||||
end
|
||||
|
||||
defp broadcast_leader_changed(guild, new_leader_id) do
|
||||
Logger.debug("Broadcast leader changed to #{new_leader_id} in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_emblem_changed(guild) do
|
||||
Logger.debug("Broadcast emblem changed in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_notice_changed(guild) do
|
||||
Logger.debug("Broadcast notice changed in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_capacity_changed(guild) do
|
||||
Logger.debug("Broadcast capacity changed to #{guild.capacity} in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_gp_changed(guild, amount) do
|
||||
Logger.debug("Broadcast GP change #{amount} in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_member_online(guild, character_id, online) do
|
||||
Logger.debug("Broadcast member #{character_id} online=#{online} in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_member_info_updated(guild, character) do
|
||||
Logger.debug("Broadcast member info update for #{character.id} in guild #{guild.id}")
|
||||
end
|
||||
|
||||
defp broadcast_guild_disband(guild) do
|
||||
Logger.debug("Broadcast guild disband for #{guild.id}")
|
||||
end
|
||||
|
||||
defp build_guild_chat_packet(_sender_name, _message) do
|
||||
# TODO: Implement proper packet
|
||||
<<>>
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,543 @@
|
||||
defmodule Odinsea.World.Party do
|
||||
@moduledoc """
|
||||
Party management service.
|
||||
Ported from src/handling/world/MapleParty.java
|
||||
|
||||
Manages party state including members, leader, and operations.
|
||||
Supports cross-channel party functionality.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Channel.Packets
|
||||
|
||||
@max_party_size 6
|
||||
@loot_rules [:free_for_all, :round_robin, :master, :master_looter]
|
||||
|
||||
# ============================================================================
|
||||
# Client API
|
||||
# ============================================================================
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new party with the given leader character.
|
||||
Returns {:ok, party_id} on success, {:error, reason} on failure.
|
||||
"""
|
||||
def create_party(leader_character) do
|
||||
GenServer.call(__MODULE__, {:create_party, leader_character})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a party linked to an expedition.
|
||||
"""
|
||||
def create_expedition_party(leader_character, expedition_id) do
|
||||
GenServer.call(__MODULE__, {:create_expedition_party, leader_character, expedition_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a party by ID.
|
||||
Returns the party struct or nil if not found.
|
||||
"""
|
||||
def get_party(party_id) do
|
||||
GenServer.call(__MODULE__, {:get_party, party_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates party with a member operation (join, leave, expel, etc.).
|
||||
"""
|
||||
def update_party(party_id, operation, character) do
|
||||
GenServer.call(__MODULE__, {:update_party, party_id, operation, character})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disbands a party.
|
||||
"""
|
||||
def disband_party(party_id, leader_id) do
|
||||
GenServer.call(__MODULE__, {:disband_party, party_id, leader_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changes the party leader.
|
||||
"""
|
||||
def change_leader(party_id, new_leader_id, current_leader_id) do
|
||||
GenServer.call(__MODULE__, {:change_leader, party_id, new_leader_id, current_leader_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets a character's online status in the party.
|
||||
"""
|
||||
def set_online(party_id, character_id, online, channel) do
|
||||
GenServer.call(__MODULE__, {:set_online, party_id, character_id, online, channel})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates character info (level, job, map) for a party member.
|
||||
"""
|
||||
def update_member(party_id, character) do
|
||||
GenServer.call(__MODULE__, {:update_member, party_id, character})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets all parties (for admin/debug purposes).
|
||||
"""
|
||||
def get_all_parties do
|
||||
GenServer.call(__MODULE__, :get_all_parties)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets party members for broadcasting.
|
||||
Returns list of {character_id, channel} tuples.
|
||||
"""
|
||||
def get_member_channels(party_id) do
|
||||
GenServer.call(__MODULE__, {:get_member_channels, party_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts a packet to all party members except the sender.
|
||||
"""
|
||||
def broadcast_to_party(party_id, packet, except_character_id \\ nil) do
|
||||
GenServer.cast(__MODULE__, {:broadcast_to_party, party_id, packet, except_character_id})
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Server Callbacks
|
||||
# ============================================================================
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{parties: %{}, next_id: 1}}
|
||||
state = %{
|
||||
parties: %{},
|
||||
next_id: 1
|
||||
}
|
||||
|
||||
Logger.info("Party service initialized")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_party, leader}, _from, state) do
|
||||
party_id = state.next_id
|
||||
|
||||
party = %{
|
||||
id: party_id,
|
||||
leader_id: leader.id,
|
||||
members: [create_party_character(leader)],
|
||||
expedition_id: -1,
|
||||
disbanded: false,
|
||||
loot_rule: :free_for_all,
|
||||
created_at: System.system_time(:second)
|
||||
}
|
||||
|
||||
new_state = %{
|
||||
state
|
||||
| parties: Map.put(state.parties, party_id, party),
|
||||
next_id: party_id + 1
|
||||
}
|
||||
|
||||
Logger.info("Party #{party_id} created by #{leader.name}")
|
||||
{:reply, {:ok, party}, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_expedition_party, leader, expedition_id}, _from, state) do
|
||||
party_id = state.next_id
|
||||
|
||||
party = %{
|
||||
id: party_id,
|
||||
leader_id: leader.id,
|
||||
members: [create_party_character(leader)],
|
||||
expedition_id: expedition_id,
|
||||
disbanded: false,
|
||||
loot_rule: :free_for_all,
|
||||
created_at: System.system_time(:second)
|
||||
}
|
||||
|
||||
new_state = %{
|
||||
state
|
||||
| parties: Map.put(state.parties, party_id, party),
|
||||
next_id: party_id + 1
|
||||
}
|
||||
|
||||
Logger.info("Expedition party #{party_id} created for expedition #{expedition_id}")
|
||||
{:reply, {:ok, party}, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_party, party_id}, _from, state) do
|
||||
party = Map.get(state.parties, party_id)
|
||||
|
||||
# Don't return disbanded parties
|
||||
if party && party.disbanded do
|
||||
{:reply, nil, state}
|
||||
else
|
||||
{:reply, party, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:update_party, party_id, operation, character}, _from, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :party_not_found}, state}
|
||||
|
||||
party when party.disbanded ->
|
||||
{:reply, {:error, :party_disbanded}, state}
|
||||
|
||||
party ->
|
||||
case apply_operation(party, operation, character) do
|
||||
{:ok, updated_party, result} ->
|
||||
new_state = %{state | parties: Map.put(state.parties, party_id, updated_party)}
|
||||
|
||||
# Broadcast update to party members
|
||||
broadcast_party_update(updated_party, operation, character)
|
||||
|
||||
{:reply, {:ok, result}, new_state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:disband_party, party_id, leader_id}, _from, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :party_not_found}, state}
|
||||
|
||||
%{leader_id: actual_leader} when actual_leader != leader_id ->
|
||||
{:reply, {:error, :not_leader}, state}
|
||||
|
||||
party ->
|
||||
updated_party = %{party | disbanded: true, members: []}
|
||||
|
||||
# Notify all members
|
||||
broadcast_party_disband(party)
|
||||
|
||||
# Remove from state after a delay (for cleanup)
|
||||
:timer.apply_after(60_000, __MODULE__, :cleanup_party, [party_id])
|
||||
|
||||
Logger.info("Party #{party_id} disbanded by leader #{leader_id}")
|
||||
{:reply, :ok, %{state | parties: Map.put(state.parties, party_id, updated_party)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:change_leader, party_id, new_leader_id, current_leader_id}, _from, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :party_not_found}, state}
|
||||
|
||||
%{leader_id: actual_leader} when actual_leader != current_leader_id ->
|
||||
{:reply, {:error, :not_leader}, state}
|
||||
|
||||
party ->
|
||||
# Check if new leader is in party
|
||||
unless Enum.any?(party.members, fn m -> m.id == new_leader_id end) do
|
||||
{:reply, {:error, :not_in_party}, state}
|
||||
else
|
||||
updated_party = %{party | leader_id: new_leader_id}
|
||||
|
||||
broadcast_leader_changed(updated_party, new_leader_id)
|
||||
|
||||
Logger.info("Party #{party_id} leader changed to #{new_leader_id}")
|
||||
{:reply, :ok, %{state | parties: Map.put(state.parties, party_id, updated_party)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:set_online, party_id, character_id, online, channel}, _from, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :party_not_found}, state}
|
||||
|
||||
party when party.disbanded ->
|
||||
{:reply, {:error, :party_disbanded}, state}
|
||||
|
||||
party ->
|
||||
members = Enum.map(party.members, fn member ->
|
||||
if member.id == character_id do
|
||||
%{member | online: online, channel: channel}
|
||||
else
|
||||
member
|
||||
end
|
||||
end)
|
||||
|
||||
updated_party = %{party | members: members}
|
||||
|
||||
# Broadcast online status change
|
||||
broadcast_member_online(updated_party, character_id, online)
|
||||
|
||||
{:reply, :ok, %{state | parties: Map.put(state.parties, party_id, updated_party)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:update_member, party_id, character}, _from, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil ->
|
||||
{:reply, {:error, :party_not_found}, state}
|
||||
|
||||
party when party.disbanded ->
|
||||
{:reply, {:error, :party_disbanded}, state}
|
||||
|
||||
party ->
|
||||
members = Enum.map(party.members, fn member ->
|
||||
if member.id == character.id do
|
||||
update_party_character(member, character)
|
||||
else
|
||||
member
|
||||
end
|
||||
end)
|
||||
|
||||
updated_party = %{party | members: members}
|
||||
{:reply, :ok, %{state | parties: Map.put(state.parties, party_id, updated_party)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_all_parties, _from, state) do
|
||||
active_parties = state.parties
|
||||
|> Map.values()
|
||||
|> Enum.reject(fn p -> p.disbanded end)
|
||||
|
||||
{:reply, active_parties, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_member_channels, party_id}, _from, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil -> {:reply, [], state}
|
||||
%{disbanded: true} -> {:reply, [], state}
|
||||
party ->
|
||||
channels = party.members
|
||||
|> Enum.filter(fn m -> m.online end)
|
||||
|> Enum.map(fn m -> {m.id, m.channel} end)
|
||||
{:reply, channels, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:broadcast_to_party, party_id, packet, except_id}, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
nil -> :ok
|
||||
%{disbanded: true} -> :ok
|
||||
party ->
|
||||
# Broadcast to all online members except sender
|
||||
Enum.each(party.members, fn member ->
|
||||
if member.online && member.id != except_id do
|
||||
# Get character PID and send packet
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] -> send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Party Operations
|
||||
# ============================================================================
|
||||
|
||||
defp apply_operation(party, :join, character) do
|
||||
if length(party.members) >= @max_party_size do
|
||||
{:error, :party_full}
|
||||
else
|
||||
# Check if already in party
|
||||
if Enum.any?(party.members, fn m -> m.id == character.id end) do
|
||||
{:error, :already_in_party}
|
||||
else
|
||||
party_char = create_party_character(character)
|
||||
updated_party = %{party | members: party.members ++ [party_char]}
|
||||
{:ok, updated_party, party_char}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_operation(party, :leave, character) do
|
||||
members = Enum.reject(party.members, fn m -> m.id == character.id end)
|
||||
|
||||
# If leader leaves and there are other members, promote next member
|
||||
updated_party = if party.leader_id == character.id && length(members) > 0 do
|
||||
[new_leader | _] = members
|
||||
%{party | members: members, leader_id: new_leader.id}
|
||||
else
|
||||
%{party | members: members}
|
||||
end
|
||||
|
||||
{:ok, updated_party, :ok}
|
||||
end
|
||||
|
||||
defp apply_operation(party, :expel, character) do
|
||||
# Only leader can expel
|
||||
members = Enum.reject(party.members, fn m -> m.id == character.id end)
|
||||
updated_party = %{party | members: members}
|
||||
{:ok, updated_party, :ok}
|
||||
end
|
||||
|
||||
defp apply_operation(party, :silent_update, character) do
|
||||
# Update member info without broadcasting
|
||||
members = Enum.map(party.members, fn member ->
|
||||
if member.id == character.id do
|
||||
update_party_character(member, character)
|
||||
else
|
||||
member
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, %{party | members: members}, :ok}
|
||||
end
|
||||
|
||||
defp apply_operation(party, :log_onoff, character) do
|
||||
members = Enum.map(party.members, fn member ->
|
||||
if member.id == character.id do
|
||||
%{member | online: character.online, channel: character.channel}
|
||||
else
|
||||
member
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, %{party | members: members}, :ok}
|
||||
end
|
||||
|
||||
defp apply_operation(_party, operation, _character) do
|
||||
{:error, {:unknown_operation, operation}}
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
defp create_party_character(character) do
|
||||
%{
|
||||
id: character.id,
|
||||
name: character.name,
|
||||
level: character.level,
|
||||
job: character.job,
|
||||
channel: character.channel_id || 1,
|
||||
map_id: character.map_id || 100000000,
|
||||
online: true,
|
||||
# Door info for mystic door skill
|
||||
door_town: 999999999,
|
||||
door_target: 999999999,
|
||||
door_skill: 0,
|
||||
door_x: 0,
|
||||
door_y: 0
|
||||
}
|
||||
end
|
||||
|
||||
defp update_party_character(existing, character) do
|
||||
%{
|
||||
existing
|
||||
| level: character.level || existing.level,
|
||||
job: character.job || existing.job,
|
||||
channel: character.channel_id || existing.channel,
|
||||
map_id: character.map_id || existing.map_id
|
||||
}
|
||||
end
|
||||
|
||||
defp broadcast_party_update(party, operation, character) do
|
||||
# Build party update packet and broadcast to members
|
||||
Enum.each(party.members, fn member ->
|
||||
if member.online && member.id != character.id do
|
||||
# Send party update packet
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] ->
|
||||
packet = build_party_update_packet(party, operation, character)
|
||||
send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp broadcast_party_disband(party) do
|
||||
Enum.each(party.members, fn member ->
|
||||
if member.online do
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] ->
|
||||
packet = build_party_disband_packet(party.id)
|
||||
send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp broadcast_leader_changed(party, new_leader_id) do
|
||||
Enum.each(party.members, fn member ->
|
||||
if member.online do
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] ->
|
||||
packet = build_leader_change_packet(party.id, new_leader_id)
|
||||
send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp broadcast_member_online(party, character_id, online) do
|
||||
Enum.each(party.members, fn member ->
|
||||
if member.online && member.id != character_id do
|
||||
case Registry.lookup(Odinsea.CharacterRegistry, member.id) do
|
||||
[{pid, _}] ->
|
||||
packet = build_member_online_packet(party.id, character_id, online)
|
||||
send(pid, {:send_packet, packet})
|
||||
[] -> :ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Packet Builders (to be implemented in Channel.Packets)
|
||||
# ============================================================================
|
||||
|
||||
defp build_party_update_packet(_party, _operation, _character) do
|
||||
# TODO: Implement party update packet
|
||||
# For now, return empty (needs proper packet structure)
|
||||
<<>>
|
||||
end
|
||||
|
||||
defp build_party_disband_packet(_party_id) do
|
||||
# TODO: Implement party disband packet
|
||||
<<>>
|
||||
end
|
||||
|
||||
defp build_leader_change_packet(_party_id, _new_leader_id) do
|
||||
# TODO: Implement leader change packet
|
||||
<<>>
|
||||
end
|
||||
|
||||
defp build_member_online_packet(_party_id, _character_id, _online) do
|
||||
# TODO: Implement member online packet
|
||||
<<>>
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cleanup a disbanded party (called after delay).
|
||||
"""
|
||||
def cleanup_party(party_id) do
|
||||
GenServer.cast(__MODULE__, {:cleanup_party, party_id})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:cleanup_party, party_id}, state) do
|
||||
case Map.get(state.parties, party_id) do
|
||||
%{disbanded: true} ->
|
||||
{:noreply, %{state | parties: Map.delete(state.parties, party_id)}}
|
||||
_ ->
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user