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
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful

This commit is contained in:
Zoltan Timar
2026-02-26 14:53:22 +01:00
parent 226d75d905
commit 954a39aef1
12 changed files with 232 additions and 9 deletions

View File

@@ -7,6 +7,7 @@ init/init.context.lua
system/system.util.lua system/system.util.lua
system/system.print.lua system/system.print.lua
system/system.input.lua system/system.input.lua
system/system.focus.lua
system/system.ui.lua system/system.ui.lua
audio/audio.manager.lua audio/audio.manager.lua
audio/audio.songs.lua audio/audio.songs.lua

View File

@@ -1,5 +1,12 @@
Decision.register({ Decision.register({
id = "play_button_mash", id = "play_button_mash",
label = "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,
}) })

View File

@@ -1,5 +1,12 @@
Decision.register({ Decision.register({
id = "play_rhythm", id = "play_rhythm",
label = "Play Rhythm Game", 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,
}) })

View File

@@ -115,7 +115,10 @@ function Minigame.get_default_button_mash()
bar_height = 12, bar_height = 12,
button_x = 20, button_x = 20,
button_y = 110, button_y = 110,
button_size = 12 button_size = 12,
focus_center_x = nil,
focus_center_y = nil,
focus_initial_radius = 0
} }
end end
@@ -167,7 +170,10 @@ function Minigame.get_default_rhythm()
button_y = 110, button_y = 110,
button_size = 10, button_size = 10,
press_cooldown = 0, press_cooldown = 0,
press_cooldown_duration = 15 press_cooldown_duration = 15,
focus_center_x = nil,
focus_center_y = nil,
focus_initial_radius = 0
} }
end end

View File

@@ -11,3 +11,4 @@ Print = {}
Input = {} Input = {}
Sprite = {} Sprite = {}
Audio = {} Audio = {}
Focus = {}

View File

@@ -1,6 +1,7 @@
Screen.register({ Screen.register({
id = "office", id = "office",
name = "Office", name = "Office",
background_color = Config.colors.dark_grey,
decisions = { decisions = {
"play_button_mash", "play_button_mash",
"play_rhythm", "play_rhythm",

View File

@@ -1,6 +1,7 @@
Screen.register({ Screen.register({
id = "walking_to_home", id = "walking_to_home",
name = "Walking to home", name = "Walking to home",
background_color = Config.colors.dark_grey,
decisions = { decisions = {
"go_to_home", "go_to_home",
"go_to_office", "go_to_office",

View File

@@ -1,6 +1,7 @@
Screen.register({ Screen.register({
id = "walking_to_office", id = "walking_to_office",
name = "Walking to office", name = "Walking to office",
background_color = Config.colors.dark_grey,
decisions = { decisions = {
"go_to_home", "go_to_home",
"go_to_office", "go_to_office",

166
inc/system/system.focus.lua Normal file
View File

@@ -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

View File

@@ -6,17 +6,23 @@ local _selected_decision_index = 1
--- @within GameWindow --- @within GameWindow
function GameWindow.draw() function GameWindow.draw()
local screen = Screen.get_by_id(Context.game.current_screen) 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) UI.draw_top_bar(screen.name)
if #_available_decisions > 0 then if #_available_decisions > 0 then
UI.draw_decision_selector(_available_decisions, _selected_decision_index) UI.draw_decision_selector(_available_decisions, _selected_decision_index)
end end
Sprite.draw() Sprite.draw()
Focus.draw()
end end
--- Updates the game window logic. --- Updates the game window logic.
--- @within GameWindow --- @within GameWindow
function GameWindow.update() function GameWindow.update()
Focus.update()
if Input.menu_back() then if Input.menu_back() then
Window.set_current("menu") Window.set_current("menu")
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
@@ -60,6 +66,8 @@ function GameWindow.update()
selected_decision.handle() selected_decision.handle()
end end
end end
end end
--- Sets the active window. --- Sets the active window.

View File

@@ -13,7 +13,13 @@ end
--- @param[opt] params table Optional parameters for minigame configuration. --- @param[opt] params table Optional parameters for minigame configuration.
function MinigameButtonMashWindow.start(return_window, params) function MinigameButtonMashWindow.start(return_window, params)
MinigameButtonMashWindow.init(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") Window.set_current("minigame_button_mash")
end end
@@ -31,6 +37,7 @@ function MinigameButtonMashWindow.update()
if mg.bar_fill >= mg.max_fill then if mg.bar_fill >= mg.max_fill then
Meter.on_minigame_complete() Meter.on_minigame_complete()
Meter.show() Meter.show()
if mg.focus_center_x then Focus.stop() end
Window.set_current(mg.return_window) Window.set_current(mg.return_window)
return return
end end
@@ -42,6 +49,9 @@ function MinigameButtonMashWindow.update()
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
mg.button_pressed_timer = mg.button_pressed_timer - 1 mg.button_pressed_timer = mg.button_pressed_timer - 1
end end
if mg.focus_center_x then
Focus.set_percentage(mg.bar_fill / mg.max_fill)
end
end end
--- Draws button mash minigame. --- Draws button mash minigame.
@@ -51,7 +61,9 @@ function MinigameButtonMashWindow.draw()
if mg.return_window == "game" then if mg.return_window == "game" then
GameWindow.draw() GameWindow.draw()
end 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) 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) 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 local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width

View File

@@ -13,7 +13,13 @@ end
--- @param[opt] params table Optional parameters for minigame configuration. --- @param[opt] params table Optional parameters for minigame configuration.
function MinigameRhythmWindow.start(return_window, params) function MinigameRhythmWindow.start(return_window, params)
MinigameRhythmWindow.init(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") Window.set_current("minigame_rhythm")
end end
@@ -53,12 +59,16 @@ function MinigameRhythmWindow.update()
if mg.score >= mg.max_score then if mg.score >= mg.max_score then
Meter.on_minigame_complete() Meter.on_minigame_complete()
Meter.show() Meter.show()
if mg.focus_center_x then Focus.stop() end
Window.set_current(mg.return_window) Window.set_current(mg.return_window)
return return
end end
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
mg.button_pressed_timer = mg.button_pressed_timer - 1 mg.button_pressed_timer = mg.button_pressed_timer - 1
end end
if mg.focus_center_x then
Focus.set_percentage(1 - mg.score / mg.max_score)
end
end end
--- Draws rhythm minigame. --- Draws rhythm minigame.
@@ -68,7 +78,9 @@ function MinigameRhythmWindow.draw()
if mg.return_window == "game" then if mg.return_window == "game" then
GameWindow.draw() GameWindow.draw()
end 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) 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) 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) rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey)