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