From 954a39aef18dbee1d39f51e55d590f944798de5b Mon Sep 17 00:00:00 2001 From: Zoltan Timar Date: Thu, 26 Feb 2026 14:53:22 +0100 Subject: [PATCH] feat: added new functionality with focus, added base background to screens, created Focus.close(), Focus.start(), Focus.driven() methods for different use-cases, added focus to screens --- impostor.inc | 1 + inc/decision/decision.play_button_mash.lua | 9 +- inc/decision/decision.play_rhythm.lua | 9 +- inc/init/init.minigame.lua | 10 +- inc/init/init.module.lua | 1 + inc/screen/screen.office.lua | 1 + inc/screen/screen.walking_to_home.lua | 1 + inc/screen/screen.walking_to_office.lua | 1 + inc/system/system.focus.lua | 166 +++++++++++++++++++++ inc/window/window.game.lua | 10 +- inc/window/window.minigame.mash.lua | 16 +- inc/window/window.minigame.rhythm.lua | 16 +- 12 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 inc/system/system.focus.lua diff --git a/impostor.inc b/impostor.inc index 1ba7fee..bf7eea1 100644 --- a/impostor.inc +++ b/impostor.inc @@ -7,6 +7,7 @@ init/init.context.lua system/system.util.lua system/system.print.lua system/system.input.lua +system/system.focus.lua system/system.ui.lua audio/audio.manager.lua audio/audio.songs.lua diff --git a/inc/decision/decision.play_button_mash.lua b/inc/decision/decision.play_button_mash.lua index 462f926..b111cdf 100644 --- a/inc/decision/decision.play_button_mash.lua +++ b/inc/decision/decision.play_button_mash.lua @@ -1,5 +1,12 @@ Decision.register({ id = "play_button_mash", label = "Play Button Mash", - handle = function() Meter.hide() MinigameButtonMashWindow.start("game") end, + handle = function() + Meter.hide() + MinigameButtonMashWindow.start("game", { + focus_center_x = Config.screen.width / 2, + focus_center_y = Config.screen.height / 2, + focus_initial_radius = 0, + }) + end, }) diff --git a/inc/decision/decision.play_rhythm.lua b/inc/decision/decision.play_rhythm.lua index 25f047f..e8cff8e 100644 --- a/inc/decision/decision.play_rhythm.lua +++ b/inc/decision/decision.play_rhythm.lua @@ -1,5 +1,12 @@ Decision.register({ id = "play_rhythm", label = "Play Rhythm Game", - handle = function() Meter.hide() MinigameRhythmWindow.start("game") end, + handle = function() + Meter.hide() + MinigameRhythmWindow.start("game", { + focus_center_x = Config.screen.width / 2, + focus_center_y = Config.screen.height / 2, + focus_initial_radius = 0, + }) + end, }) diff --git a/inc/init/init.minigame.lua b/inc/init/init.minigame.lua index 2de6d63..c985707 100644 --- a/inc/init/init.minigame.lua +++ b/inc/init/init.minigame.lua @@ -115,7 +115,10 @@ function Minigame.get_default_button_mash() bar_height = 12, button_x = 20, button_y = 110, - button_size = 12 + button_size = 12, + focus_center_x = nil, + focus_center_y = nil, + focus_initial_radius = 0 } end @@ -167,7 +170,10 @@ function Minigame.get_default_rhythm() button_y = 110, button_size = 10, press_cooldown = 0, - press_cooldown_duration = 15 + press_cooldown_duration = 15, + focus_center_x = nil, + focus_center_y = nil, + focus_initial_radius = 0 } end diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua index d6f3382..700caa5 100644 --- a/inc/init/init.module.lua +++ b/inc/init/init.module.lua @@ -11,3 +11,4 @@ Print = {} Input = {} Sprite = {} Audio = {} +Focus = {} diff --git a/inc/screen/screen.office.lua b/inc/screen/screen.office.lua index b3bb3c7..3d5320c 100644 --- a/inc/screen/screen.office.lua +++ b/inc/screen/screen.office.lua @@ -1,6 +1,7 @@ Screen.register({ id = "office", name = "Office", + background_color = Config.colors.dark_grey, decisions = { "play_button_mash", "play_rhythm", diff --git a/inc/screen/screen.walking_to_home.lua b/inc/screen/screen.walking_to_home.lua index 675ed35..b6d598a 100644 --- a/inc/screen/screen.walking_to_home.lua +++ b/inc/screen/screen.walking_to_home.lua @@ -1,6 +1,7 @@ Screen.register({ id = "walking_to_home", name = "Walking to home", + background_color = Config.colors.dark_grey, decisions = { "go_to_home", "go_to_office", diff --git a/inc/screen/screen.walking_to_office.lua b/inc/screen/screen.walking_to_office.lua index a940084..1b7f201 100644 --- a/inc/screen/screen.walking_to_office.lua +++ b/inc/screen/screen.walking_to_office.lua @@ -1,6 +1,7 @@ Screen.register({ id = "walking_to_office", name = "Walking to office", + background_color = Config.colors.dark_grey, decisions = { "go_to_home", "go_to_office", diff --git a/inc/system/system.focus.lua b/inc/system/system.focus.lua new file mode 100644 index 0000000..308b836 --- /dev/null +++ b/inc/system/system.focus.lua @@ -0,0 +1,166 @@ +--- @section Focus + +local FOCUS_DEFAULT_SPEED = 5 + +local active = false +local closing = false +local driven = false +local center_x = 0 +local center_y = 0 +local radius = 0 +local speed = FOCUS_DEFAULT_SPEED +local on_complete = nil +local driven_initial_r = 0 +local driven_max_r = 0 + +local function max_radius(cx, cy) + local dx = math.max(cx, Config.screen.width - cx) + local dy = math.max(cy, Config.screen.height - cy) + return math.sqrt(dx * dx + dy * dy) +end + +--- Starts a focus overlay that reveals content through an expanding circle. +--- @within Focus +--- @param center_x number The x-coordinate of the circle center. +--- @param center_y number The y-coordinate of the circle center. +--- @param[opt] params table Optional parameters: `speed` (number) expansion rate in pixels/frame, `initial_radius` (number) starting radius in pixels (default 0), `on_complete` (function) callback when overlay disperses. +function Focus.start(cx, cy, params) + params = params or {} + active = true + closing = false + driven = false + center_x = cx + center_y = cy + radius = params.initial_radius or 0 + speed = params.speed or FOCUS_DEFAULT_SPEED + on_complete = params.on_complete +end + +--- Starts a closing focus overlay that hides content by shrinking the visible circle. +--- @within Focus +--- @param center_x number The x-coordinate of the circle center. +--- @param center_y number The y-coordinate of the circle center. +--- @param[opt] params table Optional parameters: `speed` (number) shrink rate in pixels/frame, `on_complete` (function) callback when screen is fully covered. +function Focus.close(cx, cy, params) + params = params or {} + active = true + closing = true + driven = false + center_x = cx + center_y = cy + radius = max_radius(cx, cy) + speed = params.speed or FOCUS_DEFAULT_SPEED + on_complete = params.on_complete +end + +--- Starts a driven focus overlay whose radius is controlled externally via Focus.set_percentage(). +--- The radius maps linearly from initial_radius (at 0%) to the screen corner distance (at 100%). +--- @within Focus +--- @param center_x number The x-coordinate of the circle center. +--- @param center_y number The y-coordinate of the circle center. +--- @param[opt] params table Optional parameters: `initial_radius` (number) radius at 0% (default 0). +function Focus.start_driven(cx, cy, params) + params = params or {} + active = true + closing = false + driven = true + center_x = cx + center_y = cy + driven_initial_r = params.initial_radius or 0 + driven_max_r = max_radius(cx, cy) + radius = driven_initial_r + on_complete = nil +end + +--- Sets the visible radius as a percentage of the full screen extent. +--- Only has effect when the overlay is in driven mode (started via Focus.start_driven). +--- @within Focus +--- @param pct number A value from 0 to 1 (0 = initial_radius, 1 = full screen). +function Focus.set_percentage(pct) + if not driven then return end + radius = driven_initial_r + pct * (driven_max_r - driven_initial_r) +end + +--- Checks whether the focus overlay is currently active. +--- @within Focus +--- @return boolean Whether the focus overlay is active. +function Focus.is_active() + return active +end + +--- Stops the focus overlay immediately. +--- @within Focus +function Focus.stop() + active = false + closing = false + driven = false + radius = 0 + on_complete = nil +end + +--- Updates the focus overlay animation. No-op in driven mode. +--- @within Focus +function Focus.update() + if not active then return end + if driven then return end + + if closing then + radius = radius - speed + if radius <= 0 then + local cb = on_complete + Focus.stop() + if cb then cb() end + end + else + radius = radius + speed + if radius >= max_radius(center_x, center_y) then + local cb = on_complete + Focus.stop() + if cb then cb() end + end + end +end + +--- Draws the focus overlay (black screen with circular cutout). +--- Must be called after all other drawing to appear on top of every visual layer. +--- @within Focus +function Focus.draw() + if not active then return end + + local cx = center_x + local cy = center_y + local r = radius + local w = Config.screen.width + local h = Config.screen.height + local color = Config.colors.black + + if closing and r <= 0 then + rect(0, 0, w, h, color) + return + end + + local top = math.max(0, math.floor(cy - r)) + local bottom = math.min(h - 1, math.ceil(cy + r)) + + if top > 0 then + rect(0, 0, w, top, color) + end + + if bottom < h - 1 then + rect(0, bottom + 1, w, h - bottom - 1, color) + end + + for y = top, bottom do + local dy = y - cy + local half_w = math.sqrt(math.max(0, r * r - dy * dy)) + local left = math.floor(cx - half_w) + local right = math.ceil(cx + half_w) + + if left > 0 then + rect(0, y, left, 1, color) + end + if right < w then + rect(right, y, w - right, 1, color) + end + end +end diff --git a/inc/window/window.game.lua b/inc/window/window.game.lua index 1d02205..ec18904 100644 --- a/inc/window/window.game.lua +++ b/inc/window/window.game.lua @@ -6,17 +6,23 @@ local _selected_decision_index = 1 --- @within GameWindow function GameWindow.draw() local screen = Screen.get_by_id(Context.game.current_screen) - if screen.background then Map.draw(screen.background) end + if screen.background then + Map.draw(screen.background) + elseif screen.background_color then + rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color) + end UI.draw_top_bar(screen.name) if #_available_decisions > 0 then UI.draw_decision_selector(_available_decisions, _selected_decision_index) end Sprite.draw() + Focus.draw() end --- Updates the game window logic. --- @within GameWindow function GameWindow.update() + Focus.update() if Input.menu_back() then Window.set_current("menu") MenuWindow.refresh_menu_items() @@ -60,6 +66,8 @@ function GameWindow.update() selected_decision.handle() end end + + end --- Sets the active window. diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua index 66cef1d..d2c02b4 100644 --- a/inc/window/window.minigame.mash.lua +++ b/inc/window/window.minigame.mash.lua @@ -13,7 +13,13 @@ 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 "game" + local mg = Context.minigame_button_mash + mg.return_window = return_window or "game" + if mg.focus_center_x then + Focus.start_driven(mg.focus_center_x, mg.focus_center_y, { + initial_radius = mg.focus_initial_radius + }) + end Window.set_current("minigame_button_mash") end @@ -31,6 +37,7 @@ function MinigameButtonMashWindow.update() if mg.bar_fill >= mg.max_fill then Meter.on_minigame_complete() Meter.show() + if mg.focus_center_x then Focus.stop() end Window.set_current(mg.return_window) return end @@ -42,6 +49,9 @@ function MinigameButtonMashWindow.update() if mg.button_pressed_timer > 0 then mg.button_pressed_timer = mg.button_pressed_timer - 1 end + if mg.focus_center_x then + Focus.set_percentage(mg.bar_fill / mg.max_fill) + end end --- Draws button mash minigame. @@ -51,7 +61,9 @@ function MinigameButtonMashWindow.draw() if mg.return_window == "game" then GameWindow.draw() end - rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + if not mg.focus_center_x then + rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + end rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua index 6cc0cd3..039e313 100644 --- a/inc/window/window.minigame.rhythm.lua +++ b/inc/window/window.minigame.rhythm.lua @@ -13,7 +13,13 @@ 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 "game" + local mg = Context.minigame_rhythm + mg.return_window = return_window or "game" + if mg.focus_center_x then + Focus.start_driven(mg.focus_center_x, mg.focus_center_y, { + initial_radius = mg.focus_initial_radius + }) + end Window.set_current("minigame_rhythm") end @@ -53,12 +59,16 @@ function MinigameRhythmWindow.update() if mg.score >= mg.max_score then Meter.on_minigame_complete() Meter.show() + if mg.focus_center_x then Focus.stop() end Window.set_current(mg.return_window) return end if mg.button_pressed_timer > 0 then mg.button_pressed_timer = mg.button_pressed_timer - 1 end + if mg.focus_center_x then + Focus.set_percentage(1 - mg.score / mg.max_score) + end end --- Draws rhythm minigame. @@ -68,7 +78,9 @@ function MinigameRhythmWindow.draw() if mg.return_window == "game" then GameWindow.draw() end - rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + if not mg.focus_center_x then + rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + end rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey) -- 2.49.1