kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View File

@@ -0,0 +1,478 @@
defmodule Odinsea.Game.QuestRequirement do
@moduledoc """
Quest Requirement module - defines conditions for quest start/completion.
Requirements are checked when:
- Starting a quest (start_requirements)
- Completing a quest (complete_requirements)
## Requirement Types
- `:job` - Required job class
- `:item` - Required items in inventory
- `:quest` - Required quest completion status
- `:lvmin` - Minimum level
- `:lvmax` - Maximum level
- `:mob` - Required mob kills
- `:npc` - NPC to talk to
- `:fieldEnter` - Enter specific map(s)
- `:pop` - Minimum fame
- `:interval` - Time interval for repeatable quests
- `:skill` - Required skill level
- `:pet` - Required pet
- `:mbmin` - Monster book minimum cards
- `:mbcard` - Specific monster book card level
- `:questComplete` - Minimum number of completed quests
- `:subJobFlags` - Sub-job flags (e.g., Dual Blade)
- `:pettamenessmin` - Minimum pet closeness
- `:partyQuest_S` - S-rank party quest completions
- `:charmMin`, `:senseMin`, `:craftMin`, `:willMin`, `:charismaMin`, `:insightMin` - Trait minimums
- `:dayByDay` - Daily quest
- `:normalAutoStart` - Auto-start quest
- `:startscript`, `:endscript` - Custom scripts
"""
@type t :: %__MODULE__{
type: atom(),
data: any()
}
defstruct [:type, :data]
## Public API
@doc "Creates a new quest requirement"
@spec new(atom(), any()) :: t()
def new(type, data) do
%__MODULE__{
type: type,
data: data
}
end
@doc "Builds a requirement from a map (JSON deserialization)"
@spec from_map(map()) :: t()
def from_map(map) do
type = parse_type(Map.get(map, :type, Map.get(map, "type", "undefined")))
data = parse_data(type, Map.get(map, :data, Map.get(map, "data", nil)))
%__MODULE__{
type: type,
data: data
}
end
@doc "Checks if a character meets this requirement"
@spec check(t(), Odinsea.Game.Character.t()) :: boolean()
def check(%__MODULE__{} = req, character) do
do_check(req.type, req.data, character)
end
@doc "Parses a WZ requirement name into an atom"
@spec parse_type(String.t() | atom()) :: atom()
def parse_type(type) when is_atom(type), do: type
def parse_type(type_str) when is_binary(type_str) do
case String.downcase(type_str) do
"job" -> :job
"item" -> :item
"quest" -> :quest
"lvmin" -> :lvmin
"lvmax" -> :lvmax
"end" -> :end
"mob" -> :mob
"npc" -> :npc
"fieldenter" -> :fieldEnter
"interval" -> :interval
"startscript" -> :startscript
"endscript" -> :endscript
"pet" -> :pet
"pettamenessmin" -> :pettamenessmin
"mbmin" -> :mbmin
"questcomplete" -> :questComplete
"pop" -> :pop
"skill" -> :skill
"mbcard" -> :mbcard
"subjobflags" -> :subJobFlags
"daybyday" -> :dayByDay
"normalautostart" -> :normalAutoStart
"partyquest_s" -> :partyQuest_S
"charmmin" -> :charmMin
"sensemin" -> :senseMin
"craftmin" -> :craftMin
"willmin" -> :willMin
"charismamin" -> :charismaMin
"insightmin" -> :insightMin
_ -> :undefined
end
end
## Private Functions
defp parse_data(:job, data) when is_list(data), do: data
defp parse_data(:job, data) when is_integer(data), do: [data]
defp parse_data(:job, data) when is_binary(data), do: [String.to_integer(data)]
defp parse_data(:item, data) when is_map(data), do: data
defp parse_data(:item, data) when is_list(data) do
Enum.reduce(data, %{}, fn item, acc ->
item_id = Map.get(item, :id, Map.get(item, "id", 0))
count = Map.get(item, :count, Map.get(item, "count", 1))
Map.put(acc, item_id, count)
end)
end
defp parse_data(:quest, data) when is_map(data), do: data
defp parse_data(:quest, data) when is_list(data) do
Enum.reduce(data, %{}, fn quest, acc ->
quest_id = Map.get(quest, :id, Map.get(quest, "id", 0))
state = Map.get(quest, :state, Map.get(quest, "state", 0))
Map.put(acc, quest_id, state)
end)
end
defp parse_data(:mob, data) when is_map(data), do: data
defp parse_data(:mob, data) when is_list(data) do
Enum.reduce(data, %{}, fn mob, acc ->
mob_id = Map.get(mob, :id, Map.get(mob, "id", 0))
count = Map.get(mob, :count, Map.get(mob, "count", 1))
Map.put(acc, mob_id, count)
end)
end
defp parse_data(:lvmin, data) when is_integer(data), do: data
defp parse_data(:lvmin, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:lvmax, data) when is_integer(data), do: data
defp parse_data(:lvmax, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:npc, data) when is_integer(data), do: data
defp parse_data(:npc, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:pop, data) when is_integer(data), do: data
defp parse_data(:pop, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:interval, data) when is_integer(data), do: data
defp parse_data(:interval, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:fieldEnter, data) when is_list(data), do: data
defp parse_data(:fieldEnter, data) when is_integer(data), do: [data]
defp parse_data(:fieldEnter, data) when is_binary(data) do
case Integer.parse(data) do
{int, _} -> [int]
:error -> []
end
end
defp parse_data(:questComplete, data) when is_integer(data), do: data
defp parse_data(:questComplete, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:mbmin, data) when is_integer(data), do: data
defp parse_data(:mbmin, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:pettamenessmin, data) when is_integer(data), do: data
defp parse_data(:pettamenessmin, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:subJobFlags, data) when is_integer(data), do: data
defp parse_data(:subJobFlags, data) when is_binary(data), do: String.to_integer(data)
defp parse_data(:skill, data) when is_list(data) do
Enum.map(data, fn skill ->
id = Map.get(skill, :id, Map.get(skill, "id", 0))
acquire = Map.get(skill, :acquire, Map.get(skill, "acquire", 0))
{id, acquire > 0}
end)
end
defp parse_data(:mbcard, data) when is_list(data) do
Enum.map(data, fn card ->
id = Map.get(card, :id, Map.get(card, "id", 0))
min = Map.get(card, :min, Map.get(card, "min", 0))
{id, min}
end)
end
defp parse_data(:pet, data) when is_list(data), do: data
defp parse_data(:pet, data) when is_integer(data), do: [data]
defp parse_data(:startscript, data), do: to_string(data)
defp parse_data(:endscript, data), do: to_string(data)
defp parse_data(:end, data), do: to_string(data)
# Trait minimums
defp parse_data(type, data) when type in [:charmMin, :senseMin, :craftMin, :willMin, :charismaMin, :insightMin] do
if is_binary(data), do: String.to_integer(data), else: data
end
defp parse_data(_type, data), do: data
# Requirement checking implementations
defp do_check(:job, required_jobs, character) do
# Check if character's job is in the list of acceptable jobs
character_job = Map.get(character, :job, 0)
character_job in required_jobs || Map.get(character, :gm, false)
end
defp do_check(:item, required_items, character) do
# Check if character has required items
# This is a simplified check - full implementation needs inventory lookup
inventory = Map.get(character, :inventory, %{})
Enum.all?(required_items, fn {item_id, count} ->
has_item_count(inventory, item_id, count)
end)
end
defp do_check(:quest, required_quests, character) do
# Check quest completion status
quest_progress = Map.get(character, :quest_progress, %{})
Enum.all?(required_quests, fn {quest_id, required_state} ->
actual_state = Map.get(quest_progress, quest_id, 0)
# State: 0 = not started, 1 = in progress, 2 = completed
actual_state == required_state
end)
end
defp do_check(:lvmin, min_level, character) do
Map.get(character, :level, 1) >= min_level
end
defp do_check(:lvmax, max_level, character) do
Map.get(character, :level, 1) <= max_level
end
defp do_check(:mob, required_mobs, character) do
# Check mob kill counts from quest progress
mob_kills = Map.get(character, :quest_mob_kills, %{})
Enum.all?(required_mobs, fn {mob_id, count} ->
Map.get(mob_kills, mob_id, 0) >= count
end)
end
defp do_check(:npc, npc_id, character) do
# NPC check is usually done at runtime with the actual NPC ID
# This is a placeholder that returns true
true
end
defp do_check(:npc, npc_id, character, talking_npc_id) do
npc_id == talking_npc_id
end
defp do_check(:fieldEnter, maps, character) do
current_map = Map.get(character, :map_id, 0)
current_map in maps
end
defp do_check(:pop, min_fame, character) do
Map.get(character, :fame, 0) >= min_fame
end
defp do_check(:interval, interval_minutes, character) do
# Check if enough time has passed for repeatable quest
last_completion = Map.get(character, :last_quest_completion, %{})
quest_id = Map.get(character, :checking_quest_id, 0)
last_time = Map.get(last_completion, quest_id, 0)
if last_time == 0 do
true
else
current_time = System.system_time(:second)
(current_time - last_time) >= interval_minutes * 60
end
end
defp do_check(:questComplete, min_completed, character) do
completed_count =
character
|> Map.get(:quest_progress, %{})
|> Enum.count(fn {_id, state} -> state == 2 end)
completed_count >= min_completed
end
defp do_check(:mbmin, min_cards, character) do
# Monster book card count check
monster_book = Map.get(character, :monster_book, %{})
card_count = map_size(monster_book)
card_count >= min_cards
end
defp do_check(:skill, required_skills, character) do
skills = Map.get(character, :skills, %{})
Enum.all?(required_skills, fn {skill_id, should_have} ->
skill_level = Map.get(skills, skill_id, 0)
master_level = Map.get(character, :skill_master_levels, %{}) |> Map.get(skill_id, 0)
has_skill = skill_level > 0 || master_level > 0
if should_have do
has_skill
else
not has_skill
end
end)
end
defp do_check(:pet, pet_ids, character) do
pets = Map.get(character, :pets, [])
Enum.any?(pet_ids, fn pet_id ->
Enum.any?(pets, fn pet ->
Map.get(pet, :item_id) == pet_id && Map.get(pet, :summoned, false)
end)
end)
end
defp do_check(:pettamenessmin, min_closeness, character) do
pets = Map.get(character, :pets, [])
Enum.any?(pets, fn pet ->
Map.get(pet, :summoned, false) && Map.get(pet, :closeness, 0) >= min_closeness
end)
end
defp do_check(:subJobFlags, flags, character) do
subcategory = Map.get(character, :subcategory, 0)
# Sub-job flags check (used for Dual Blade, etc.)
subcategory == div(flags, 2)
end
defp do_check(:mbcard, required_cards, character) do
monster_book = Map.get(character, :monster_book, %{})
Enum.all?(required_cards, fn {card_id, min_level} ->
Map.get(monster_book, card_id, 0) >= min_level
end)
end
defp do_check(:dayByDay, _data, _character) do
# Daily quest - handled separately
true
end
defp do_check(:normalAutoStart, _data, _character) do
# Auto-start flag
true
end
defp do_check(:partyQuest_S, _data, character) do
# S-rank party quest check - simplified
# Real implementation would check character's PQ history
true
end
# Trait minimum checks
defp do_check(trait_type, min_level, character) when trait_type in [:charmMin, :senseMin, :craftMin, :willMin, :charismaMin, :insightMin] do
trait_name =
case trait_type do
:charmMin -> :charm
:senseMin -> :sense
:craftMin -> :craft
:willMin -> :will
:charismaMin -> :charisma
:insightMin -> :insight
end
traits = Map.get(character, :traits, %{})
trait_level = Map.get(traits, trait_name, 0)
trait_level >= min_level
end
defp do_check(:end, time_str, _character) do
# Event end time check
if time_str == nil || time_str == "" do
true
else
# Parse YYYYMMDDHH format
case String.length(time_str) do
10 ->
year = String.slice(time_str, 0, 4) |> String.to_integer()
month = String.slice(time_str, 4, 2) |> String.to_integer()
day = String.slice(time_str, 6, 2) |> String.to_integer()
hour = String.slice(time_str, 8, 2) |> String.to_integer()
end_time = NaiveDateTime.new!(year, month, day, hour, 0, 0)
now = NaiveDateTime.utc_now()
NaiveDateTime.compare(now, end_time) == :lt
_ ->
true
end
end
end
defp do_check(:startscript, _script, _character), do: true
defp do_check(:endscript, _script, _character), do: true
defp do_check(:undefined, _data, _character), do: true
defp do_check(_type, _data, _character), do: true
# Helper functions
defp has_item_count(inventory, item_id, required_count) when required_count > 0 do
# Count items across all inventory types
total =
inventory
|> Map.values()
|> Enum.flat_map(& &1)
|> Enum.filter(fn item -> Map.get(item, :item_id) == item_id end)
|> Enum.map(fn item -> Map.get(item, :quantity, 1) end)
|> Enum.sum()
total >= required_count
end
defp has_item_count(inventory, item_id, required_count) when required_count <= 0 do
# For negative counts (checking we DON'T have too many)
total =
inventory
|> Map.values()
|> Enum.flat_map(& &1)
|> Enum.filter(fn item -> Map.get(item, :item_id) == item_id end)
|> Enum.map(fn item -> Map.get(item, :quantity, 1) end)
|> Enum.sum()
# If required_count is 0 or negative, we should have 0 of the item
# or specifically, not more than the absolute value
total <= abs(required_count)
end
@doc "Checks if an item should show in drop for this quest"
@spec shows_drop?(t(), integer(), Odinsea.Game.Character.t()) :: boolean()
def shows_drop?(%__MODULE__{type: :item} = req, item_id, character) do
# Check if this item is needed for the quest and should be shown in drops
required_items = req.data
case Map.get(required_items, item_id) do
nil ->
false
required_count ->
# Check if player still needs more of this item
inventory = Map.get(character, :inventory, %{})
current_count = count_items(inventory, item_id)
# Show drop if player needs more (required > current)
# or if required_count is 0/negative (special case)
current_count < required_count || required_count <= 0
end
end
def shows_drop?(_req, _item_id, _character), do: false
defp count_items(inventory, item_id) do
inventory
|> Map.values()
|> Enum.flat_map(& &1)
|> Enum.filter(fn item -> Map.get(item, :item_id) == item_id end)
|> Enum.map(fn item -> Map.get(item, :quantity, 1) end)
|> Enum.sum()
end
end