Files
odinsea-elixir/lib/odinsea/game/events/ox_quiz.ex
2026-02-14 23:12:33 -07:00

350 lines
8.9 KiB
Elixir

defmodule Odinsea.Game.Events.OxQuiz do
@moduledoc """
OX Quiz Event - True/False quiz game with position-based answers.
Ported from Java `server.events.MapleOxQuiz`.
## Gameplay
- 10 questions are asked
- Players stand on O (true/right side) or X (false/left side) side
- Wrong answer = eliminated (HP set to 0)
- Correct answer = gain EXP
- Last players standing win
## Maps
- Single map: 109020001
- X side: x < -234, y > -26
- O side: x > -234, y > -26
## Win Condition
- Answer correctly to survive all 10 questions
- Remaining players at end get prize
"""
alias Odinsea.Game.Event
alias Odinsea.Game.Timer.EventTimer
alias Odinsea.Game.Events.OxQuizQuestions
require Logger
# ============================================================================
# Types
# ============================================================================
@typedoc "OX Quiz event state"
@type t :: %__MODULE__{
base: Event.t(),
times_asked: non_neg_integer(),
current_question: OxQuizQuestions.question() | nil,
finished: boolean(),
question_delay: non_neg_integer(), # ms before showing question
answer_delay: non_neg_integer(), # ms before revealing answer
schedules: [reference()]
}
defstruct [
:base,
times_asked: 0,
current_question: nil,
finished: false,
question_delay: 10_000,
answer_delay: 10_000,
schedules: []
]
# ============================================================================
# Constants
# ============================================================================
@map_ids [109020001]
@max_questions 10
# Position boundaries for O (true) vs X (false)
@o_side_bounds %{x_min: -234, x_max: 9999, y_min: -26, y_max: 9999}
@x_side_bounds %{x_min: -9999, x_max: -234, y_min: -26, y_max: 9999}
# ============================================================================
# Event Implementation
# ============================================================================
@doc """
Creates a new OX Quiz event for the given channel.
"""
def new(channel_id) do
base = Event.new(:ox_quiz, channel_id, @map_ids)
%__MODULE__{
base: base,
times_asked: 0,
current_question: nil,
finished: false,
question_delay: 10_000,
answer_delay: 10_000,
schedules: []
}
end
@doc """
Returns the map IDs for this event type.
"""
def map_ids, do: @map_ids
@doc """
Resets the event state for a new game.
"""
def reset(%__MODULE__{} = event) do
# Cancel existing schedules
cancel_schedules(event)
# Close entry portal
set_portal_state(event, "join00", false)
base = %{event.base | is_running: true, player_count: 0}
%__MODULE__{
event |
base: base,
times_asked: 0,
current_question: nil,
finished: false,
schedules: []
}
end
@doc """
Cleans up the event after it ends.
"""
def unreset(%__MODULE__{} = event) do
cancel_schedules(event)
# Open entry portal
set_portal_state(event, "join00", true)
base = %{event.base | is_running: false, player_count: 0}
%__MODULE__{
event |
base: base,
times_asked: 0,
current_question: nil,
finished: false,
schedules: []
}
end
@doc """
Called when a player finishes.
OX Quiz doesn't use this - winners determined by survival.
"""
def finished(_event, _character) do
:ok
end
@doc """
Called when a player loads into the event map.
Unmutes player (allows chat during quiz).
"""
def on_map_load(%__MODULE__{} = _event, character) do
# Unmute player (allow chat)
# In real implementation: Character.set_temp_mute(character, false)
Logger.debug("Player #{character.name} loaded OX Quiz map, unmuting")
:ok
end
@doc """
Starts the OX Quiz event gameplay.
Begins asking questions.
"""
def start_event(%__MODULE__{} = event) do
Logger.info("Starting OX Quiz event on channel #{event.base.channel_id}")
# Close entry portal
set_portal_state(event, "join00", false)
# Start asking questions
send_question(%{event | finished: false})
end
@doc """
Sends the next question to all players.
"""
def send_question(%__MODULE__{finished: true} = event), do: event
def send_question(%__MODULE__{} = event) do
# Grab random question
question = OxQuizQuestions.get_random_question()
# Schedule question display
question_ref = EventTimer.schedule(
fn -> display_question(event, question) end,
event.question_delay
)
# Schedule answer reveal
answer_ref = EventTimer.schedule(
fn -> reveal_answer(event, question) end,
event.question_delay + event.answer_delay
)
%__MODULE__{
event |
current_question: question,
schedules: [question_ref, answer_ref | event.schedules]
}
end
@doc """
Displays the question to all players.
"""
def display_question(%__MODULE__{finished: true}, _question), do: :ok
def display_question(%__MODULE__{} = event, question) do
# Check end conditions
if should_end_event?(event) do
end_event(event)
else
# Broadcast question
{question_set, question_id} = question.ids
Event.broadcast_to_event(event.base, {:ox_quiz_show, question_set, question_id, true})
Event.broadcast_to_event(event.base, {:clock, 10}) # 10 seconds to answer
Logger.debug("OX Quiz: Displaying question #{question_id} from set #{question_set}")
end
:ok
end
@doc """
Reveals the answer and processes results.
"""
def reveal_answer(%__MODULE__{finished: true}, _question), do: :ok
def reveal_answer(%__MODULE__{} = event, question) do
if event.finished do
:ok
else
# Broadcast answer reveal
{question_set, question_id} = question.ids
Event.broadcast_to_event(event.base, {:ox_quiz_hide, question_set, question_id})
# Process each player
# In real implementation:
# - Get all players on map
# - Check their position vs answer
# - Wrong position: set HP to 0
# - Correct position: give EXP
Logger.debug("OX Quiz: Revealing answer for question #{question_id}: #{question.answer}")
# Increment question count
event = %{event | times_asked: event.times_asked + 1}
# Continue to next question
send_question(event)
end
end
@doc """
Checks if a player's position corresponds to the correct answer.
## Parameters
- answer: :o (true) or :x (false)
- x: Player X position
- y: Player Y position
## Returns
- true if position matches answer
- false if wrong position
"""
def correct_position?(:o, x, y) do
x > -234 and y > -26
end
def correct_position?(:x, x, y) do
x < -234 and y > -26
end
@doc """
Processes a player's answer based on their position.
Returns {:correct, exp} or {:wrong, 0}
"""
def check_player_answer(question_answer, player_x, player_y) do
player_answer = if player_x > -234, do: :o, else: :x
if player_answer == question_answer do
{:correct, 3000} # 3000 EXP for correct answer
else
{:wrong, 0}
end
end
@doc """
Gets the current question number.
"""
def current_question_number(%__MODULE__{times_asked: asked}), do: asked + 1
@doc """
Gets the maximum number of questions.
"""
def max_questions, do: @max_questions
@doc """
Mutes a player (after event ends).
"""
def mute_player(character) do
# In real implementation: Character.set_temp_mute(character, true)
Logger.debug("Muting player #{character.name}")
:ok
end
# ============================================================================
# Private Functions
# ============================================================================
defp should_end_event?(%__MODULE__{} = event) do
# End if 10 questions asked or only 1 player left
event.times_asked >= @max_questions or count_alive_players(event) <= 1
end
defp count_alive_players(%__MODULE__{} = _event) do
# In real implementation:
# - Get all players on map
# - Count non-GM, alive players
# For now, return placeholder
10
end
defp cancel_schedules(%__MODULE__{schedules: schedules} = event) do
Enum.each(schedules, fn ref ->
EventTimer.cancel(ref)
end)
%{event | schedules: []}
end
defp set_portal_state(%__MODULE__{}, _portal_name, _state) do
# In real implementation, update map portal state
:ok
end
defp end_event(%__MODULE__{} = event) do
Logger.info("OX Quiz event ended on channel #{event.base.channel_id}")
# Mark as finished
event = %{event | finished: true}
# Broadcast end
Event.broadcast_to_event(event.base, {:server_notice, "The event has ended"})
# Process winners
# In real implementation:
# - Get all alive, non-GM players
# - Give prize to each
# - Give achievement (ID 19)
# - Mute players
# - Warp back
# Unreset event
unreset(event)
end
end