feature/imp-62-focus-handling #17
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ Print = {}
|
|||||||
Input = {}
|
Input = {}
|
||||||
Sprite = {}
|
Sprite = {}
|
||||||
Audio = {}
|
Audio = {}
|
||||||
|
Focus = {}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
166
inc/system/system.focus.lua
Normal 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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user