diff --git a/.luacheckrc b/.luacheckrc index 176e9f8..370bbd2 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -13,8 +13,9 @@ globals = { "Audio", "Config", "Context", - "Meters", - "Minigames", + "Meter", + "Minigame", + "Window", "SplashWindow", "IntroWindow", "MenuWindow", diff --git a/GEMINI.md b/GEMINI.md index 7687e0c..1b8decd 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -55,7 +55,7 @@ Based on the analysis of `impostor.lua`, the following regularities and conventi --- -# Impostor Minigames Documentation +# Impostor Minigame Documentation This document provides comprehensive documentation for all three minigames implemented in the Impostor game: Button Mash, Rhythm, and DDR (Dance Dance Revolution). @@ -74,7 +74,7 @@ This document provides comprehensive documentation for all three minigames imple The Impostor game includes three interactive minigames that can be triggered during gameplay. Each minigame presents a unique challenge that the player must complete to progress. All minigames feature: -- **Overlay rendering** - Minigames render over the current game window +- **Overlay rendering** - Minigame render over the current game window - **Progress tracking** - Visual indicators show completion status - **Return mechanism** - Automatic return to the calling window upon completion - **Visual feedback** - Button presses and hits are visually indicated diff --git a/impostor.inc b/impostor.inc index a254b6a..e865111 100644 --- a/impostor.inc +++ b/impostor.inc @@ -1,10 +1,16 @@ meta/meta.header.lua -init/init.modules.lua +init/init.module.lua init/init.config.lua -init/init.minigames.lua -init/init.meters.lua +init/init.minigame.lua +init/init.meter.lua +init/init.window.lua +init/init.context.lua system/system.util.lua -init/init.windows.lua +system/system.print.lua +system/system.input.lua +system/system.ui.lua +audio/audio.manager.lua +audio/audio.songs.lua sprite/sprite.manager.lua sprite/sprite.norman.lua situation/situation.manager.lua @@ -27,12 +33,7 @@ screen/screen.toilet.lua screen/screen.walking_to_office.lua screen/screen.office.lua screen/screen.walking_to_home.lua -init/init.context.lua -data/data.songs.lua -system/system.print.lua -system/system.input.lua -system/system.audio.lua -system/system.ui.lua +window/window.manager.lua window/window.splash.lua window/window.intro.lua window/window.menu.lua diff --git a/inc/system/system.audio.lua b/inc/audio/audio.manager.lua similarity index 100% rename from inc/system/system.audio.lua rename to inc/audio/audio.manager.lua diff --git a/inc/data/data.songs.lua b/inc/audio/audio.songs.lua similarity index 100% rename from inc/data/data.songs.lua rename to inc/audio/audio.songs.lua diff --git a/inc/decision/decision.have_a_coffee.lua b/inc/decision/decision.have_a_coffee.lua index 43d84f3..c9709f0 100644 --- a/inc/decision/decision.have_a_coffee.lua +++ b/inc/decision/decision.have_a_coffee.lua @@ -2,6 +2,7 @@ Decision.register({ id = "have_a_coffee", label = "Have a Coffee", handle = function() - Situation.apply("drink_coffee") + local new_situation_id = Situation.apply("drink_coffee", Context.game.current_screen) + Context.game.current_situation = new_situation_id end, }) diff --git a/inc/decision/decision.manager.lua b/inc/decision/decision.manager.lua index f14ff24..76f1b7b 100644 --- a/inc/decision/decision.manager.lua +++ b/inc/decision/decision.manager.lua @@ -33,6 +33,37 @@ end --- Gets all registered decisions. -- @return table A table of all registered decisions. -function Decision.get_all() +function Decision.get_all_registered() return _decisions end + +--- Gets decision objects based on a screen's data. +-- @param screen_data table The data for the screen, containing a 'decisions' field (list of decision IDs). +-- @return table A table containing decision objects relevant to the screen. +function Decision.get_for_screen(screen_data) + if not screen_data or not screen_data.decisions then + return {} + end + + local screen_decisions = {} + for _, decision_id in ipairs(screen_data.decisions) do + local decision = Decision.get(decision_id) + if decision then + table.insert(screen_decisions, decision) + end + end + return screen_decisions +end + +--- Filters a list of decision objects based on their condition function. +-- @param decisions_list table A table of decision objects. +-- @return table A new table containing only the decisions for which condition() is true. +function Decision.filter_available(decisions_list) + local available = {} + for _, decision in ipairs(decisions_list) do + if decision and decision.condition() then + table.insert(available, decision) + end + end + return available +end diff --git a/inc/decision/decision.play_button_mash.lua b/inc/decision/decision.play_button_mash.lua index b1f4820..462f926 100644 --- a/inc/decision/decision.play_button_mash.lua +++ b/inc/decision/decision.play_button_mash.lua @@ -1,5 +1,5 @@ Decision.register({ id = "play_button_mash", label = "Play Button Mash", - handle = function() Meters.hide() MinigameButtonMashWindow.start(WINDOW_GAME) end, + handle = function() Meter.hide() MinigameButtonMashWindow.start("game") end, }) diff --git a/inc/decision/decision.play_ddr.lua b/inc/decision/decision.play_ddr.lua index 57f8cf5..7b96a57 100644 --- a/inc/decision/decision.play_ddr.lua +++ b/inc/decision/decision.play_ddr.lua @@ -1,5 +1,5 @@ Decision.register({ id = "play_ddr", label = "Play DDR (Random)", - handle = function() Meters.hide() MinigameDDRWindow.start(WINDOW_GAME, nil) end, + handle = function() Meter.hide() MinigameDDRWindow.start("game", nil) end, }) diff --git a/inc/decision/decision.play_rhythm.lua b/inc/decision/decision.play_rhythm.lua index 3c8312e..25f047f 100644 --- a/inc/decision/decision.play_rhythm.lua +++ b/inc/decision/decision.play_rhythm.lua @@ -1,5 +1,5 @@ Decision.register({ id = "play_rhythm", label = "Play Rhythm Game", - handle = function() Meters.hide() MinigameRhythmWindow.start(WINDOW_GAME) end, + handle = function() Meter.hide() MinigameRhythmWindow.start("game") end, }) diff --git a/inc/init/init.config.lua b/inc/init/init.config.lua index af2d06f..2a4ca97 100644 --- a/inc/init/init.config.lua +++ b/inc/init/init.config.lua @@ -1,34 +1,35 @@ -local DEFAULT_CONFIG = { - screen = { - width = 240, - height = 136 - }, - colors = { - black = 0, - light_grey = 13, - dark_grey = 14, - red = 2, - green = 6, - blue = 9, - white = 12, - item = 12, - meter_bg = 12 - }, - player = { - sprite_id = 1 - }, - timing = { - splash_duration = 120 - } -} +Config = {} --- Game configuration settings. -Config = { - screen = DEFAULT_CONFIG.screen, - colors = DEFAULT_CONFIG.colors, - player = DEFAULT_CONFIG.player, - timing = DEFAULT_CONFIG.timing, -} +function Config.initial_data() + return { + screen = { + width = 240, + height = 136 + }, + colors = { + black = 0, + light_grey = 13, + dark_grey = 14, + red = 2, + green = 6, + blue = 9, + white = 12, + item = 12, + meter_bg = 12 + }, + timing = { + splash_duration = 120 + } + } +end + +--- Restores default configuration settings. +function Config.reset() + local initial = Config.initial_data() + Config.screen = initial.screen + Config.colors = initial.colors + Config.timing = initial.timing +end local CONFIG_SAVE_BANK = 7 local CONFIG_MAGIC_VALUE_ADDRESS = 2 @@ -38,6 +39,7 @@ local CONFIG_MAGIC_VALUE = 0xDE --- Saves the current configuration. function Config.save() mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) + mset(Config.timing.splash_duration, CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK) end --- Loads saved configuration. @@ -45,13 +47,8 @@ function Config.load() if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then Config.timing.splash_duration = mget(CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK) else - Config.restore_defaults() + Config.reset() end end ---- Restores default configuration settings. -function Config.restore_defaults() - Config.timing.splash_duration = DEFAULT_CONFIG.timing.splash_duration -end - Config.load() diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua index fde2d49..b6267ba 100644 --- a/inc/init/init.context.lua +++ b/inc/init/init.context.lua @@ -2,66 +2,34 @@ local SAVE_GAME_BANK = 6 local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0 local SAVE_GAME_MAGIC_VALUE = 0xCA -local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6 +--- Global game context. +Context = {} --- Gets initial data for Context. -- @return table Initial context data. -local function get_initial_data() +function Context.initial_data() return { - active_window = WINDOW_SPLASH, - intro = { - y = Config.screen.height, - speed = 0.5, - text = [[Norman Reds’ everyday life -seems ordinary: work, -meetings, coffee, and -endless notifications. -But beneath the surface -— within him, or around -him — something is -constantly building, and -it soon becomes clear -that there is more going -on than meets the eye.]] - }, - current_screen = 1, + current_menu_item = 1, splash_timer = Config.timing.splash_duration, popup = { show = false, content = {} }, - player = { - sprite_id = Config.player.sprite_id - }, - ground = { - x = 0, - y = Config.screen.height, - w = Config.screen.width, - h = 8 - }, - menu_items = {}, - selected_menu_item = 1, - selected_decision_index = 1, game_in_progress = false, - screens = {}, - minigame_ddr = Minigames.get_default_ddr(), - minigame_button_mash = Minigames.get_default_button_mash(), - minigame_rhythm = Minigames.get_default_rhythm(), - meters = Meters.get_initial(), - --- Active sprites. - sprites = {}, - --- Current situation ID. - current_situation = nil, + minigame_ddr = Minigame.get_default_ddr(), + minigame_button_mash = Minigame.get_default_button_mash(), + minigame_rhythm = Minigame.get_default_rhythm(), + meters = Meter.get_initial(), + game = { + current_screen = "home", + current_situation = nil, + } } end ---- Global game context. -Context = {} - --- Resets game context to initial state. -local function reset_context_to_initial_state() - local initial_data = get_initial_data() - +function Context.reset() + local initial_data = Context.initial_data() for k in pairs(Context) do if type(Context[k]) ~= "function" then Context[k] = nil end @@ -70,37 +38,20 @@ local function reset_context_to_initial_state() for k, v in pairs(initial_data) do Context[k] = v end - - Context.screens = {} - Context.screen_indices_by_id = {} - local screen_order = {"home", "toilet", "walking_to_office", "office", "walking_to_home"} - for i, screen_id in ipairs(screen_order) do - local screen_data = Screen.get_by_id(screen_id) - if screen_data then - table.insert(Context.screens, screen_data) - Context.screen_indices_by_id[screen_id] = i - else - PopupWindow.show({"Error: Screen '" .. screen_id .. "' not registered!"}) - end - end end -reset_context_to_initial_state() - --- Starts a new game. function Context.new_game() - reset_context_to_initial_state() + Context.reset() Context.game_in_progress = true MenuWindow.refresh_menu_items() - Context.screens[Context.current_screen].init() + Screen.get_by_id(Context.game.current_screen).init() end --- Saves the current game state. function Context.save_game() if not Context.game_in_progress then return end - mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) - mset(Context.current_screen, SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK) end --- Loads a saved game state. @@ -109,11 +60,8 @@ function Context.load_game() Context.new_game() return end - - reset_context_to_initial_state() - Context.current_screen = mget(SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK) - + Context.reset() Context.game_in_progress = true MenuWindow.refresh_menu_items() - Context.screens[Context.current_screen].init() + Screen.get_by_id(Context.game.current_screen).init() end diff --git a/inc/init/init.meters.lua b/inc/init/init.meter.lua similarity index 78% rename from inc/init/init.meters.lua rename to inc/init/init.meter.lua index d23243d..7520e85 100644 --- a/inc/init/init.meters.lua +++ b/inc/init/init.meter.lua @@ -7,14 +7,14 @@ local COMBO_MAX_BONUS = 0.16 local COMBO_TIMEOUT_FRAMES = 600 -- Internal meters for tracking game progress and player stats. -Meters.COLOR_ISM = Config.colors.red -Meters.COLOR_WPM = Config.colors.blue -Meters.COLOR_BM = Config.colors.black -Meters.COLOR_BG = Config.colors.meter_bg +Meter.COLOR_ISM = Config.colors.red +Meter.COLOR_WPM = Config.colors.blue +Meter.COLOR_BM = Config.colors.black +Meter.COLOR_BG = Config.colors.meter_bg --- Gets initial meter values. -- @return table A table of initial meter values. -function Meters.get_initial() +function Meter.get_initial() return { ism = METER_DEFAULT, wpm = METER_DEFAULT, @@ -26,24 +26,24 @@ function Meters.get_initial() end --- Hides meters. -function Meters.hide() +function Meter.hide() if Context and Context.meters then Context.meters.hidden = true end end --- Shows meters. -function Meters.show() +function Meter.show() if Context and Context.meters then Context.meters.hidden = false end end --- Gets max meter value. -- @return number The maximum meter value. -function Meters.get_max() +function Meter.get_max() return METER_MAX end --- Gets combo multiplier. -- @return number The current combo multiplier. -function Meters.get_combo_multiplier() +function Meter.get_combo_multiplier() if not Context or not Context.meters then return 1 end local combo = Context.meters.combo if combo == 0 then return 1 end @@ -51,7 +51,7 @@ function Meters.get_combo_multiplier() end --- Updates all meters. -function Meters.update() +function Meter.update() if not Context or not Context.game_in_progress or not Context.meters then return end local m = Context.meters m.ism = math.max(0, m.ism - METER_DECAY_PER_FRAME) @@ -69,7 +69,7 @@ end --- Adds amount to a meter. -- @param key string The meter key (e.g., "wpm", "ism", "bm"). -- @param amount number The amount to add. -function Meters.add(key, amount) +function Meter.add(key, amount) if not Context or not Context.meters then return end local m = Context.meters if m[key] ~= nil then @@ -78,12 +78,12 @@ function Meters.add(key, amount) end --- Called on minigame completion. -function Meters.on_minigame_complete() +function Meter.on_minigame_complete() local m = Context.meters - local gain = math.floor(METER_GAIN_PER_CHORE * Meters.get_combo_multiplier()) - Meters.add("wpm", gain) - Meters.add("ism", gain) - Meters.add("bm", gain) + local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier()) + Meter.add("wpm", gain) + Meter.add("ism", gain) + Meter.add("bm", gain) m.combo = m.combo + 1 m.combo_timer = 0 end diff --git a/inc/init/init.minigames.lua b/inc/init/init.minigame.lua similarity index 87% rename from inc/init/init.minigames.lua rename to inc/init/init.minigame.lua index 45fbdaa..9254b89 100644 --- a/inc/init/init.minigames.lua +++ b/inc/init/init.minigame.lua @@ -1,5 +1,5 @@ -- Manages minigame configurations and initial states. -Minigames = {} +Minigame = {} --- Applies parameters to defaults. -- @param defaults table The default configuration table. @@ -15,7 +15,7 @@ end --- Gets default DDR minigame configuration. -- @return table The default DDR minigame configuration. -function Minigames.get_default_ddr() +function Minigame.get_default_ddr() local arrow_size = 12 local arrow_spacing = 30 local total_width = (4 * arrow_size) + (3 * arrow_spacing) @@ -56,7 +56,7 @@ end --- Gets default button mash minigame configuration. -- @return table The default button mash minigame configuration. -function Minigames.get_default_button_mash() +function Minigame.get_default_button_mash() return { bar_fill = 0, max_fill = 100, @@ -78,7 +78,7 @@ end --- Gets default rhythm minigame configuration. -- @return table The default rhythm minigame configuration. -function Minigames.get_default_rhythm() +function Minigame.get_default_rhythm() return { line_position = 0, line_speed = 0.015, @@ -108,20 +108,20 @@ end --- Configures DDR minigame. -- @param params table Optional parameters to override defaults. -- @return table The configured DDR minigame state. -function Minigames.configure_ddr(params) - return apply_params(Minigames.get_default_ddr(), params) +function Minigame.configure_ddr(params) + return apply_params(Minigame.get_default_ddr(), params) end --- Configures button mash minigame. -- @param params table Optional parameters to override defaults. -- @return table The configured button mash minigame state. -function Minigames.configure_button_mash(params) - return apply_params(Minigames.get_default_button_mash(), params) +function Minigame.configure_button_mash(params) + return apply_params(Minigame.get_default_button_mash(), params) end --- Configures rhythm minigame. -- @param params table Optional parameters to override defaults. -- @return table The configured rhythm minigame state. -function Minigames.configure_rhythm(params) - return apply_params(Minigames.get_default_rhythm(), params) +function Minigame.configure_rhythm(params) + return apply_params(Minigame.get_default_rhythm(), params) end diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua new file mode 100644 index 0000000..d6f3382 --- /dev/null +++ b/inc/init/init.module.lua @@ -0,0 +1,13 @@ +Window = {} +Util = {} +Meter = {} +Minigame = {} +Decision = {} +Situation = {} +Screen = {} +Map = {} +UI = {} +Print = {} +Input = {} +Sprite = {} +Audio = {} diff --git a/inc/init/init.modules.lua b/inc/init/init.modules.lua deleted file mode 100644 index 47ad180..0000000 --- a/inc/init/init.modules.lua +++ /dev/null @@ -1,22 +0,0 @@ -SplashWindow = {} -IntroWindow = {} -MenuWindow = {} -GameWindow = {} -PopupWindow = {} -ConfigurationWindow = {} -AudioTestWindow = {} -MinigameButtonMashWindow = {} -MinigameRhythmWindow = {} -MinigameDDRWindow = {} -Util = {} -Meters = {} -Minigames = {} -Decision = {} -Situation = {} -Screen = {} -Map = {} -UI = {} -Print = {} -Input = {} -Sprite = {} -Audio = {} diff --git a/inc/init/init.window.lua b/inc/init/init.window.lua new file mode 100644 index 0000000..e69de29 diff --git a/inc/init/init.windows.lua b/inc/init/init.windows.lua deleted file mode 100644 index ed420a9..0000000 --- a/inc/init/init.windows.lua +++ /dev/null @@ -1,10 +0,0 @@ -local WINDOW_SPLASH = 0 -local WINDOW_INTRO = 1 -local WINDOW_MENU = 2 -local WINDOW_GAME = 3 -local WINDOW_POPUP = 4 -local WINDOW_CONFIGURATION = 7 -local WINDOW_MINIGAME_BUTTON_MASH = 8 -local WINDOW_MINIGAME_RHYTHM = 9 -local WINDOW_MINIGAME_DDR = 10 -local WINDOW_AUDIOTEST = 9001 diff --git a/inc/screen/screen.manager.lua b/inc/screen/screen.manager.lua index 4a75b72..becd690 100644 --- a/inc/screen/screen.manager.lua +++ b/inc/screen/screen.manager.lua @@ -24,3 +24,9 @@ end function Screen.get_by_id(screen_id) return _screens[screen_id] end + +--- Gets all registered screens. +-- @return table A table containing all registered screen data, indexed by their IDs. +function Screen.get_all() + return _screens +end diff --git a/inc/situation/situation.manager.lua b/inc/situation/situation.manager.lua index c16fa0e..18eb0c0 100644 --- a/inc/situation/situation.manager.lua +++ b/inc/situation/situation.manager.lua @@ -22,25 +22,44 @@ end --- Gets a situation by ID. -- @param id string The situation ID. -- @return table The situation table or nil. -function Situation.get(id) +function Situation.get_by_id(id) return _situations[id] end ---- Applies a situation. +--- Gets all registered situations, optionally filtered by screen ID. +-- @param screen_id string Optional. If provided, returns situations associated with this screen ID. +-- @return table A table containing all registered situation data, indexed by their IDs, or filtered by screen_id. +function Situation.get_all(screen_id) + if screen_id then + local filtered_situations = {} + for _, situation in pairs(_situations) do + if situation.screen_id == screen_id then + table.insert(filtered_situations, situation) + end + end + return filtered_situations + end + return _situations +end + +--- Applies a situation, checking screen compatibility and returning the new situation ID if successful. -- @param id string The situation ID to apply. -function Situation.apply(id) - local situation = Situation.get(id) +-- @param current_screen_id string The ID of the currently active screen. +-- @return string|nil The ID of the applied situation if successful, otherwise nil. +function Situation.apply(id, current_screen_id) + local situation = Situation.get_by_id(id) + local screen = Screen.get_by_id(current_screen_id) + if not situation then trace("Error: No situation found with id: " .. id) - return + return nil end - local current_screen_obj = Screen.get_by_id(Context.current_screen) - if current_screen_obj and not current_screen_obj.situations[id] then - trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. Context.current_screen .. ").") - return + if Util.contains(screen.situations, id) then + situation.handle() + return id + else + trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. current_screen_id .. ").") + return nil end - - Context.current_situation = id - situation.handle() end diff --git a/inc/sprite/sprite.manager.lua b/inc/sprite/sprite.manager.lua index 6ebcef6..021ce39 100644 --- a/inc/sprite/sprite.manager.lua +++ b/inc/sprite/sprite.manager.lua @@ -1,4 +1,5 @@ local _sprites = {} +local _active_sprites = {} --- Registers a sprite definition. -- @param sprite_data table A table containing the sprite definition. @@ -28,7 +29,7 @@ function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot) return end - Context.sprites[id] = { + _active_sprites[id] = { id = id, x = x, y = y, @@ -43,16 +44,16 @@ end --- Hides a displayed sprite. -- @param id string The unique identifier of the sprite. function Sprite.hide(id) - Context.sprites[id] = nil + _active_sprites[id] = nil end --- Draws all scheduled sprites. function Sprite.draw() - for id, params in pairs(Context.sprites) do + for id, params in pairs(_active_sprites) do local sprite_data = _sprites[id] if not sprite_data then - trace("Error: Sprite id " .. id .. " in Context.sprites is not registered.") - Context.sprites[id] = nil + trace("Error: Sprite id " .. id .. " in _active_sprites is not registered.") + _active_sprites[id] = nil end local colorkey = params.colorkey or sprite_data.colorkey or 0 diff --git a/inc/system/system.main.lua b/inc/system/system.main.lua index cd48c05..ce4438d 100644 --- a/inc/system/system.main.lua +++ b/inc/system/system.main.lua @@ -1,53 +1,10 @@ -local STATE_HANDLERS = { - [WINDOW_SPLASH] = function() - SplashWindow.update() - SplashWindow.draw() - end, - [WINDOW_INTRO] = function() - IntroWindow.update() - IntroWindow.draw() - end, - [WINDOW_MENU] = function() - MenuWindow.update() - MenuWindow.draw() - end, - [WINDOW_GAME] = function() - GameWindow.update() - GameWindow.draw() - end, - [WINDOW_POPUP] = function() - GameWindow.draw() - PopupWindow.update() - PopupWindow.draw() - end, - [WINDOW_CONFIGURATION] = function() - ConfigurationWindow.update() - ConfigurationWindow.draw() - end, - [WINDOW_AUDIOTEST] = function() - AudioTestWindow.update() - AudioTestWindow.draw() - end, - [WINDOW_MINIGAME_BUTTON_MASH] = function() - MinigameButtonMashWindow.update() - MinigameButtonMashWindow.draw() - end, - [WINDOW_MINIGAME_RHYTHM] = function() - MinigameRhythmWindow.update() - MinigameRhythmWindow.draw() - end, - [WINDOW_MINIGAME_DDR] = function() - MinigameDDRWindow.update() - MinigameDDRWindow.draw() - end, -} - local initialized_game = false --- Initializes game state. local function init_game() if initialized_game then return end - + Context.reset() + Window.set_current("splash") -- Set initial window using new manager MenuWindow.refresh_menu_items() initialized_game = true end @@ -56,11 +13,11 @@ end function TIC() init_game() cls(Config.colors.black) - local handler = STATE_HANDLERS[Context.active_window] + local handler = Window.get_current_handler() -- Get handler from Window manager if handler then handler() end - Meters.update() + Meter.update() if Context.game_in_progress then UI.draw_meters() end diff --git a/inc/system/system.ui.lua b/inc/system/system.ui.lua index ba70394..38efae9 100644 --- a/inc/system/system.ui.lua +++ b/inc/system/system.ui.lua @@ -135,7 +135,7 @@ function UI.draw_meters() if Context.meters.hidden then return end local m = Context.meters - local max = Meters.get_max() + local max = Meter.get_max() local bar_w = 44 local bar_h = 2 local bar_x = 182 @@ -145,16 +145,16 @@ function UI.draw_meters() local bar_offset = math.floor((line_h - bar_h) / 2) local meter_list = { - { key = "wpm", label = "WPM", color = Meters.COLOR_WPM, row = 0 }, - { key = "ism", label = "ISM", color = Meters.COLOR_ISM, row = 1 }, - { key = "bm", label = "BM", color = Meters.COLOR_BM, row = 2 }, + { key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 }, + { key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 }, + { key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 }, } for _, meter in ipairs(meter_list) do local label_y = start_y + meter.row * line_h local bar_y = label_y + bar_offset local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w)) - rect(bar_x, bar_y, bar_w, bar_h, Meters.COLOR_BG) + rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG) if fill_w > 0 then rect(bar_x, bar_y, fill_w, bar_h, meter.color) end diff --git a/inc/system/system.util.lua b/inc/system/system.util.lua index 8d1b4b9..6bc365b 100644 --- a/inc/system/system.util.lua +++ b/inc/system/system.util.lua @@ -12,10 +12,26 @@ end --- Navigates to a screen by its ID. -- @param screen_id string The ID of the screen to go to. function Util.go_to_screen_by_id(screen_id) - local screen_index = Context.screen_indices_by_id[screen_id] - if screen_index then - Context.current_screen = screen_index - Context.selected_decision_index = 1 else - PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found or not indexed!"}) + local screen = Screen.get_by_id(screen_id) + if screen then + Context.game.current_screen = screen_id + local all_decisions_for_screen = Decision.get_for_screen(screen) + Context.game.decisions = Decision.filter_available(all_decisions_for_screen) + Context.game.selected_decision_index = 1 + screen.init() -- Initialize the new screen + else + PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found!"}) end end + +-- Checks if a table contains a specific value. +-- @param t table The table to check. +-- @param value any The value to look for. +function Util.contains(t, value) + for i = 1, #t do + if t[i] == value then + return true + end + end + return false +end \ No newline at end of file diff --git a/inc/window/window.audiotest.lua b/inc/window/window.audiotest.lua index 4a0d107..a728f41 100644 --- a/inc/window/window.audiotest.lua +++ b/inc/window/window.audiotest.lua @@ -57,7 +57,7 @@ end --- Navigates back from audio test window. function AudioTestWindow.back() Audio.sfx_deselect() - GameWindow.set_state(WINDOW_MENU) + GameWindow.set_state("menu") end --- Initializes audio test window. diff --git a/inc/window/window.configuration.lua b/inc/window/window.configuration.lua index 7906000..543b0f4 100644 --- a/inc/window/window.configuration.lua +++ b/inc/window/window.configuration.lua @@ -60,7 +60,7 @@ end --- Updates configuration window logic. function ConfigurationWindow.update() if Input.menu_back() then - GameWindow.set_state(WINDOW_MENU) + GameWindow.set_state("menu") return end diff --git a/inc/window/window.game.lua b/inc/window/window.game.lua index 7be3992..081c042 100644 --- a/inc/window/window.game.lua +++ b/inc/window/window.game.lua @@ -1,82 +1,56 @@ +local _available_decisions = {} +local _selected_decision_index = 1 + --- Draws the game window. function GameWindow.draw() - local screen = Context.screens[Context.current_screen] + local screen = Screen.get_by_id(Context.game.current_screen) Map.draw(screen.background) UI.draw_top_bar(screen.name) - if screen and screen.decisions and #screen.decisions > 0 then - local available_decisions = {} - for _, decision_id in ipairs(screen.decisions) do - local decision = Decision.get(decision_id) - if decision and decision.condition() then - table.insert(available_decisions, decision) - end - end - if #available_decisions > 0 then - UI.draw_decision_selector(available_decisions, Context.selected_decision_index) - end + if #_available_decisions > 0 then + UI.draw_decision_selector(_available_decisions, _selected_decision_index) end Sprite.draw() end --- Updates the game window logic. function GameWindow.update() - local previous_screen_index = Context.current_screen - if Input.menu_back() then - Context.active_window = WINDOW_MENU + Context.current_window = "menu" MenuWindow.refresh_menu_items() return end - if Input.up() then - Context.current_screen = Context.current_screen - 1 - if Context.current_screen < 1 then - Context.current_screen = #Context.screens - end - Context.selected_decision_index = 1 elseif Input.down() then - Context.current_screen = Context.current_screen + 1 - if Context.current_screen > #Context.screens then - Context.current_screen = 1 - end - Context.selected_decision_index = 1 end - - local screen = Context.screens[Context.current_screen] + local screen = Screen.get_by_id(Context.game.current_screen) screen.update() - if previous_screen_index ~= Context.current_screen then - screen.init() - end - - if Context.current_situation then - local current_situation_obj = Situation.get(Context.current_situation) + -- Handle situations (Context.game.current_situation is still present) + if Context.game.current_situation then + local current_situation_obj = Situation.get_by_id(Context.game.current_situation) if current_situation_obj and current_situation_obj.update then current_situation_obj.update() end end - if screen and screen.decisions and #screen.decisions > 0 then - local available_decisions = {} - for _, decision_id in ipairs(screen.decisions) do - local decision = Decision.get(decision_id) - if decision and decision.condition() then table.insert(available_decisions, decision) - end - end + -- Fetch and filter decisions locally + local all_decisions_for_screen = Decision.get_for_screen(screen) + _available_decisions = Decision.filter_available(all_decisions_for_screen) - if #available_decisions == 0 then return end + if #_available_decisions == 0 then return end - local new_selected_decision_index = UI.update_decision_selector( - available_decisions, - Context.selected_decision_index - ) + local new_selected_decision_index = UI.update_decision_selector( + _available_decisions, + _selected_decision_index + ) - if new_selected_decision_index ~= Context.selected_decision_index then - Context.selected_decision_index = new_selected_decision_index - end + if new_selected_decision_index ~= _selected_decision_index then + _selected_decision_index = new_selected_decision_index + end - if Input.select() then - local selected_decision = available_decisions[Context.selected_decision_index] - if selected_decision and selected_decision.handle then Audio.sfx_select() selected_decision.handle() - end + if Input.select() then + local selected_decision = _available_decisions[_selected_decision_index] + if selected_decision and selected_decision.handle then + Audio.sfx_select() + selected_decision.handle() end end end @@ -84,5 +58,5 @@ end --- Sets the active window. -- @param new_state number The ID of the new active window. function GameWindow.set_state(new_state) - Context.active_window = new_state + Context.current_window = new_state end diff --git a/inc/window/window.intro.lua b/inc/window/window.intro.lua index 85d4547..b1908b4 100644 --- a/inc/window/window.intro.lua +++ b/inc/window/window.intro.lua @@ -1,23 +1,38 @@ +IntroWindow.y = Config.screen.height +IntroWindow.speed = 0.5 +IntroWindow.text = [[ +Norman Reds’ everyday life +seems ordinary: work, +meetings, coffee, and +endless notifications. +But beneath him, or around +him — something is +constantly building, and +it soon becomes clear +that there is more going +on than meets the eye. +]] + --- Draws the intro window. function IntroWindow.draw() local x = (Config.screen.width - 132) / 2 - Print.text(Context.intro.text, x, Context.intro.y, Config.colors.green) + Print.text(IntroWindow.text, x, IntroWindow.y, Config.colors.green) end --- Updates the intro window logic. function IntroWindow.update() - Context.intro.y = Context.intro.y - Context.intro.speed + IntroWindow.y = IntroWindow.y - IntroWindow.speed local lines = 1 - for _ in string.gmatch(Context.intro.text, "\n") do + for _ in string.gmatch(IntroWindow.text, "\n") do lines = lines + 1 end - if Context.intro.y < -lines * 8 then - GameWindow.set_state(WINDOW_MENU) + if IntroWindow.y < -lines * 8 then + GameWindow.set_state("menu") end if Input.menu_confirm() then - GameWindow.set_state(WINDOW_MENU) + GameWindow.set_state("menu") end end diff --git a/inc/window/window.manager.lua b/inc/window/window.manager.lua new file mode 100644 index 0000000..640ecc2 --- /dev/null +++ b/inc/window/window.manager.lua @@ -0,0 +1,64 @@ +local _windows = {} + +--- Registers a window table. +-- @param id string The ID of the window (e.g., "splash", "menu"). +-- @param window_table table The actual window module table (e.g., SplashWindow). +function Window.register(id, window_table) + _windows[id] = window_table +end + +--- Retrieves a registered window table by its ID. +-- @param id string The ID of the window. +-- @return table The window module table. +function Window.get(id) + return _windows[id] +end + +--- Sets the currently active window. +-- @param id string The ID of the window to activate. +function Window.set_current(id) + Context.current_window = id +end + +--- Gets the ID of the currently active window. +-- @return string The ID of the active window. +function Window.get_current_id() + return Context.current_window +end + +--- Gets the handler function for the currently active window. +-- This function is used by the main game loop to update and draw the active window. +-- @return function A function that updates and draws the current window. +function Window.get_current_handler() + local window_table = Window.get(Context.current_window) + if window_table and window_table.update and window_table.draw then + return function() + window_table.update() + window_table.draw() + end + else + -- Fallback handler for unregistered or incomplete windows + return function() trace("Error: No handler for window: " .. tostring(Context.current_window)) end + end +end +SplashWindow = {} +IntroWindow = {} +MenuWindow = {} +GameWindow = {} +PopupWindow = {} +ConfigurationWindow = {} +AudioTestWindow = {} +MinigameButtonMashWindow = {} +MinigameRhythmWindow = {} +MinigameDDRWindow = {} +-- Registration of all window modules +Window.register("splash", SplashWindow) +Window.register("intro", IntroWindow) +Window.register("menu", MenuWindow) +Window.register("game", GameWindow) +Window.register("popup", PopupWindow) +Window.register("configuration", ConfigurationWindow) +Window.register("audiotest", AudioTestWindow) +Window.register("minigame_button_mash", MinigameButtonMashWindow) +Window.register("minigame_rhythm", MinigameRhythmWindow) +Window.register("minigame_ddr", MinigameDDRWindow) diff --git a/inc/window/window.menu.lua b/inc/window/window.menu.lua index 4f8db26..0cfabf5 100644 --- a/inc/window/window.menu.lua +++ b/inc/window/window.menu.lua @@ -1,15 +1,17 @@ +local _menu_items = {} + --- Draws the menu window. function MenuWindow.draw() UI.draw_top_bar("Main Menu") - UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70) + UI.draw_menu(_menu_items, Context.current_menu_item, 108, 70) end --- Updates the menu window logic. function MenuWindow.update() - Context.selected_menu_item = UI.update_menu(Context.menu_items, Context.selected_menu_item) + Context.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item) if Input.menu_confirm() then - local selected_item = Context.menu_items[Context.selected_menu_item] + local selected_item = _menu_items[Context.current_menu_item] if selected_item and selected_item.decision then Audio.sfx_select() selected_item.decision() @@ -19,12 +21,12 @@ end --- Starts a new game from the menu. function MenuWindow.new_game() - Context.new_game() GameWindow.set_state(WINDOW_GAME) + Context.new_game() GameWindow.set_state("game") end --- Loads a game from the menu. function MenuWindow.load_game() - Context.load_game() GameWindow.set_state(WINDOW_GAME) + Context.load_game() GameWindow.set_state("game") end --- Saves the current game from the menu. @@ -33,7 +35,7 @@ function MenuWindow.save_game() --- Resumes the game from the menu. function MenuWindow.resume_game() - GameWindow.set_state(WINDOW_GAME) + GameWindow.set_state("game") end --- Exits the game. @@ -44,27 +46,28 @@ end --- Opens the configuration menu. function MenuWindow.configuration() ConfigurationWindow.init() - GameWindow.set_state(WINDOW_CONFIGURATION) + GameWindow.set_state("configuration") end --- Opens the audio test menu. function MenuWindow.audio_test() AudioTestWindow.init() - GameWindow.set_state(WINDOW_AUDIOTEST) + GameWindow.set_state("audiotest") end --- Refreshes menu items. function MenuWindow.refresh_menu_items() - Context.menu_items = {} + _menu_items = {} if Context.game_in_progress then - table.insert(Context.menu_items, {label = "Resume Game", decision = MenuWindow.resume_game}) - table.insert(Context.menu_items, {label = "Save Game", decision = MenuWindow.save_game}) + table.insert(_menu_items, {label = "Resume Game", decision = MenuWindow.resume_game}) + table.insert(_menu_items, {label = "Save Game", decision = MenuWindow.save_game}) end - table.insert(Context.menu_items, {label = "New Game", decision = MenuWindow.new_game}) - table.insert(Context.menu_items, {label = "Load Game", decision = MenuWindow.load_game}) - table.insert(Context.menu_items, {label = "Configuration", decision = MenuWindow.configuration}) - table.insert(Context.menu_items, {label = "Audio Test", decision = MenuWindow.audio_test}) - table.insert(Context.menu_items, {label = "Exit", decision = MenuWindow.exit}) + table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game}) + table.insert(_menu_items, {label = "Load Game", decision = MenuWindow.load_game}) + table.insert(_menu_items, {label = "Configuration", decision = MenuWindow.configuration}) + table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test}) + table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit}) - Context.selected_menu_item = 1 end + Context.current_menu_item = 1 +end diff --git a/inc/window/window.minigame.ddr.lua b/inc/window/window.minigame.ddr.lua index 715fb6f..7822be5 100644 --- a/inc/window/window.minigame.ddr.lua +++ b/inc/window/window.minigame.ddr.lua @@ -1,7 +1,7 @@ --- Initializes DDR minigame state. -- @param params table Optional parameters for configuration. function MinigameDDRWindow.init(params) - Context.minigame_ddr = Minigames.configure_ddr(params) + Context.minigame_ddr = Minigame.configure_ddr(params) end --- Starts the DDR minigame. @@ -10,7 +10,7 @@ end -- @param[opt] params table Optional parameters for minigame configuration. function MinigameDDRWindow.start(return_window, song_key, params) MinigameDDRWindow.init(params) - Context.minigame_ddr.return_window = return_window or WINDOW_GAME + Context.minigame_ddr.return_window = return_window or "game" Context.minigame_ddr.debug_song_key = song_key if song_key and Songs and Songs[song_key] then Context.minigame_ddr.current_song = Songs[song_key] @@ -25,7 +25,7 @@ function MinigameDDRWindow.start(return_window, song_key, params) Context.minigame_ddr.debug_status = "Random mode" end end - Context.active_window = WINDOW_MINIGAME_DDR + Context.current_window = "minigame_ddr" end --- Spawns a random arrow. @@ -99,17 +99,17 @@ end function MinigameDDRWindow.update() local mg = Context.minigame_ddr if mg.bar_fill >= mg.max_fill then - Meters.on_minigame_complete() - Meters.show() - Context.active_window = mg.return_window + Meter.on_minigame_complete() + Meter.show() + Context.current_window = mg.return_window return end mg.frame_counter = mg.frame_counter + 1 if mg.use_pattern and mg.current_song and mg.current_song.end_frame then if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then - Meters.on_minigame_complete() - Meters.show() - Context.active_window = mg.return_window + Meter.on_minigame_complete() + Meter.show() + Context.current_window = mg.return_window return end end @@ -196,11 +196,11 @@ function MinigameDDRWindow.draw() print("DDR ERROR: Context not initialized", 10, 10, 12) print("Press Z to return", 10, 20, 12) if Input.select() then - Context.active_window = WINDOW_GAME + Context.current_window = "game" end return end - if mg.return_window == WINDOW_GAME then + if mg.return_window == "game" then GameWindow.draw() end rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua index 2642ac8..5de5855 100644 --- a/inc/window/window.minigame.mash.lua +++ b/inc/window/window.minigame.mash.lua @@ -1,7 +1,7 @@ --- Initializes button mash minigame state. -- @param params table Optional parameters for configuration. function MinigameButtonMashWindow.init(params) - Context.minigame_button_mash = Minigames.configure_button_mash(params) + Context.minigame_button_mash = Minigame.configure_button_mash(params) end --- Starts the button mash minigame. @@ -9,8 +9,8 @@ end -- @param[opt] params table Optional parameters for minigame configuration. function MinigameButtonMashWindow.start(return_window, params) MinigameButtonMashWindow.init(params) - Context.minigame_button_mash.return_window = return_window or WINDOW_GAME - Context.active_window = WINDOW_MINIGAME_BUTTON_MASH + Context.minigame_button_mash.return_window = return_window or "game" + Context.current_window = "minigame_button_mash" end --- Updates button mash minigame logic. @@ -24,9 +24,9 @@ function MinigameButtonMashWindow.update() end end if mg.bar_fill >= mg.max_fill then - Meters.on_minigame_complete() - Meters.show() - Context.active_window = mg.return_window + Meter.on_minigame_complete() + Meter.show() + Context.current_window = mg.return_window return end local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier) @@ -42,7 +42,7 @@ end --- Draws button mash minigame. function MinigameButtonMashWindow.draw() local mg = Context.minigame_button_mash - if mg.return_window == WINDOW_GAME then + if mg.return_window == "game" then GameWindow.draw() end rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua index aa33bdc..c7ab203 100644 --- a/inc/window/window.minigame.rhythm.lua +++ b/inc/window/window.minigame.rhythm.lua @@ -1,7 +1,7 @@ --- Initializes rhythm minigame state. -- @param params table Optional parameters for configuration. function MinigameRhythmWindow.init(params) - Context.minigame_rhythm = Minigames.configure_rhythm(params) + Context.minigame_rhythm = Minigame.configure_rhythm(params) end --- Starts the rhythm minigame. @@ -9,8 +9,8 @@ end -- @param[opt] params table Optional parameters for minigame configuration. function MinigameRhythmWindow.start(return_window, params) MinigameRhythmWindow.init(params) - Context.minigame_rhythm.return_window = return_window or WINDOW_GAME - Context.active_window = WINDOW_MINIGAME_RHYTHM + Context.minigame_rhythm.return_window = return_window or "game" + Context.current_window = "minigame_rhythm" end --- Updates rhythm minigame logic. @@ -46,9 +46,9 @@ function MinigameRhythmWindow.update() end end if mg.score >= mg.max_score then - Meters.on_minigame_complete() - Meters.show() - Context.active_window = mg.return_window + Meter.on_minigame_complete() + Meter.show() + Context.current_window = mg.return_window return end if mg.button_pressed_timer > 0 then @@ -59,7 +59,7 @@ end --- Draws rhythm minigame. function MinigameRhythmWindow.draw() local mg = Context.minigame_rhythm - if mg.return_window == WINDOW_GAME then + if mg.return_window == "game" then GameWindow.draw() end rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) diff --git a/inc/window/window.popup.lua b/inc/window/window.popup.lua index e608451..07e32ff 100644 --- a/inc/window/window.popup.lua +++ b/inc/window/window.popup.lua @@ -9,12 +9,12 @@ local LINE_HEIGHT = 8 -- @param content_strings table A table of strings to display in the popup. function PopupWindow.show(content_strings) Context.popup.show = true - Context.popup.content = content_strings or {} GameWindow.set_state(WINDOW_POPUP) end + Context.popup.content = content_strings or {} GameWindow.set_state("popup") end --- Hides the popup window. function PopupWindow.hide() Context.popup.show = false - Context.popup.content = {} GameWindow.set_state(WINDOW_GAME) end + Context.popup.content = {} GameWindow.set_state("game") end --- Updates popup window logic. function PopupWindow.update() diff --git a/inc/window/window.splash.lua b/inc/window/window.splash.lua index 2ad4eff..b4bdd55 100644 --- a/inc/window/window.splash.lua +++ b/inc/window/window.splash.lua @@ -11,6 +11,6 @@ end function SplashWindow.update() Context.splash_timer = Context.splash_timer - 1 if Context.splash_timer <= 0 or Input.menu_confirm() then - GameWindow.set_state(WINDOW_INTRO) + GameWindow.set_state("intro") end end