This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
local _day_increase_handlers = {}
|
||||
|
||||
function Day.increase()
|
||||
Context.day_count = Context.day_count + 1
|
||||
for _, handler in ipairs(_day_increase_handlers) do
|
||||
handler()
|
||||
end
|
||||
end
|
||||
function Day.register_handler(handler)
|
||||
table.insert(_day_increase_handlers, handler)
|
||||
end
|
||||
|
||||
Day.register_handler(function()
|
||||
local m = Context.meters
|
||||
m.ism = math.max(0, m.ism - METER_DECAY_PER_DAY)
|
||||
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_DAY)
|
||||
m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY)
|
||||
end)
|
||||
@@ -1,166 +0,0 @@
|
||||
--- @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 cx number The x-coordinate of the circle center.
|
||||
--- @param cy 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 cx number The x-coordinate of the circle center.
|
||||
--- @param cy 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 cx number The x-coordinate of the circle center.
|
||||
--- @param cy 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
|
||||
@@ -1,119 +0,0 @@
|
||||
--- @section Meter
|
||||
local METER_MAX = 1000
|
||||
local METER_DEFAULT = 500
|
||||
local METER_GAIN_PER_CHORE = 100
|
||||
local METER_DECAY_PER_DAY = 20
|
||||
local COMBO_BASE_BONUS = 0.02
|
||||
local COMBO_MAX_BONUS = 0.16
|
||||
local COMBO_TIMEOUT_FRAMES = 600
|
||||
|
||||
-- Internal meters for tracking game progress and player stats.
|
||||
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.
|
||||
--- @within Meter
|
||||
--- @return result table Initial meter values. </br>
|
||||
--- Fields: </br>
|
||||
--- * ism (number) Initial ISM meter value.<br/>
|
||||
--- * wpm (number) Initial WPM meter value.<br/>
|
||||
--- * bm (number) Initial BM meter value.<br/>
|
||||
--- * combo (number) Current combo count.<br/>
|
||||
--- * combo_timer (number) Frames since last combo action.<br/>
|
||||
--- * hidden (boolean) Whether meters are hidden.
|
||||
function Meter.get_initial()
|
||||
return {
|
||||
ism = METER_DEFAULT,
|
||||
wpm = METER_DEFAULT,
|
||||
bm = METER_DEFAULT,
|
||||
combo = 0,
|
||||
combo_timer = 0,
|
||||
hidden = false,
|
||||
}
|
||||
end
|
||||
|
||||
--- Hides meters.
|
||||
--- @within Meter
|
||||
function Meter.hide()
|
||||
if Context and Context.meters then Context.meters.hidden = true end
|
||||
end
|
||||
|
||||
--- Shows meters.
|
||||
--- @within Meter
|
||||
function Meter.show()
|
||||
if Context and Context.meters then Context.meters.hidden = false end
|
||||
end
|
||||
|
||||
--- Gets max meter value.
|
||||
--- @within Meter
|
||||
--- @return number The maximum meter value.
|
||||
function Meter.get_max()
|
||||
return METER_MAX
|
||||
end
|
||||
|
||||
--- Sets the decay amount applied to all meters per day.
|
||||
--- @within Meter
|
||||
--- @param amount number Amount to subtract from each meter.
|
||||
function Meter.set_decay(amount)
|
||||
METER_DECAY_PER_DAY = amount
|
||||
end
|
||||
|
||||
--- Gets the meter decay as a percentage of the max meter value.
|
||||
--- @within Meter
|
||||
--- @return number The decay percentage per day.
|
||||
function Meter.get_decay_percentage()
|
||||
return math.floor(METER_DECAY_PER_DAY / METER_MAX * 100)
|
||||
end
|
||||
|
||||
--- Gets combo multiplier.
|
||||
--- @within Meter
|
||||
--- @return number The current 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
|
||||
return 1 + math.min(COMBO_MAX_BONUS, COMBO_BASE_BONUS * (2 ^ (combo - 1)))
|
||||
end
|
||||
|
||||
--- Updates all meters.
|
||||
--- @within Meter
|
||||
function Meter.update()
|
||||
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||
local m = Context.meters
|
||||
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
|
||||
if not in_minigame then
|
||||
if m.combo > 0 then
|
||||
m.combo_timer = m.combo_timer + 1
|
||||
if m.combo_timer >= COMBO_TIMEOUT_FRAMES then
|
||||
m.combo = 0
|
||||
m.combo_timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Adds amount to a meter.
|
||||
--- @within Meter
|
||||
--- @param key string The meter key (e.g., "wpm", "ism", "bm").
|
||||
--- @param amount number The amount to add.
|
||||
function Meter.add(key, amount)
|
||||
if not Context or not Context.meters then return end
|
||||
local m = Context.meters
|
||||
if m[key] ~= nil then
|
||||
m[key] = math.min(METER_MAX, m[key] + amount)
|
||||
end
|
||||
end
|
||||
|
||||
--- Called on minigame completion.
|
||||
--- @within Meter
|
||||
function Meter.on_minigame_complete()
|
||||
local m = Context.meters
|
||||
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
|
||||
@@ -1,88 +0,0 @@
|
||||
--- @section Timer
|
||||
|
||||
local timer_duration = 1800
|
||||
|
||||
--- Gets initial timer values.
|
||||
--- @within Timer
|
||||
--- @return result table Initial timer values. </br>
|
||||
--- Fields: </br>
|
||||
--- * progress (number) Clock timer revolution progress (0 to 1).
|
||||
function Timer.get_initial()
|
||||
return {
|
||||
progress = 0,
|
||||
}
|
||||
end
|
||||
|
||||
--- Sets the number of frames for one full timer revolution.
|
||||
--- @within Timer
|
||||
--- @param frames number Frames per revolution.
|
||||
function Timer.set_duration(frames)
|
||||
timer_duration = frames
|
||||
end
|
||||
|
||||
--- Updates the timer and handles revolution events.
|
||||
--- @within Timer
|
||||
function Timer.update()
|
||||
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
|
||||
local t = Context.timer
|
||||
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
|
||||
|
||||
if not in_minigame then
|
||||
t.progress = t.progress + (1 / timer_duration)
|
||||
if t.progress >= 1 then
|
||||
Day.increase()
|
||||
t.progress = t.progress - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Draws the clock timer indicator as a circular progress bar.
|
||||
--- @within Timer
|
||||
function Timer.draw()
|
||||
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
|
||||
if Context.meters.hidden and not Context.stat_screen_active then return end
|
||||
|
||||
local cx = 10
|
||||
local cy = 20
|
||||
local r_outer = 5
|
||||
local r_inner = 3
|
||||
local progress = Context.timer.progress
|
||||
|
||||
local fg_color
|
||||
if progress <= 0.25 then
|
||||
fg_color = Config.colors.white
|
||||
elseif progress <= 0.5 then
|
||||
fg_color = Config.colors.light_blue
|
||||
elseif progress <= 0.75 then
|
||||
fg_color = Config.colors.blue
|
||||
elseif progress <= 1 then
|
||||
fg_color = Config.colors.red
|
||||
end
|
||||
|
||||
local bg_color = Config.colors.dark_grey
|
||||
local start_angle = -math.pi * 0.5
|
||||
local progress_angle = progress * 2 * math.pi
|
||||
local r_outer_sq = r_outer * r_outer
|
||||
local r_inner_sq = r_inner * r_inner
|
||||
|
||||
for dy = -r_outer, r_outer do
|
||||
for dx = -r_outer, r_outer do
|
||||
local dist_sq = dx * dx + dy * dy
|
||||
if dist_sq <= r_outer_sq and dist_sq > r_inner_sq then
|
||||
local angle = math.atan(dy, dx)
|
||||
local relative = angle - start_angle
|
||||
if relative < 0 then relative = relative + 2 * math.pi end
|
||||
if relative <= progress_angle then
|
||||
pix(cx + dx, cy + dy, fg_color)
|
||||
else
|
||||
pix(cx + dx, cy + dy, bg_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hand_angle = start_angle + progress_angle
|
||||
local hand_x = math.floor(cx + math.cos(hand_angle) * (r_inner - 1) + 0.5)
|
||||
local hand_y = math.floor(cy + math.sin(hand_angle) * (r_inner - 1) + 0.5)
|
||||
line(cx, cy, hand_x, hand_y, Config.colors.white)
|
||||
end
|
||||
@@ -8,12 +8,6 @@ function UI.draw_top_bar(title)
|
||||
Print.text(title, 3, 2, Config.colors.light_blue)
|
||||
end
|
||||
|
||||
--- Draws dialog window.
|
||||
--- @within UI
|
||||
function UI.draw_dialog()
|
||||
PopupWindow.draw()
|
||||
end
|
||||
|
||||
--- Draws a menu.
|
||||
--- @within UI
|
||||
--- @param items table A table of menu items.<br/>
|
||||
@@ -86,55 +80,6 @@ function UI.word_wrap(text, max_chars_per_line)
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Creates a numeric stepper.
|
||||
--- @within UI
|
||||
--- @param label string The label for the stepper.<br/>
|
||||
--- @param value_getter function Function to get the current value.<br/>
|
||||
--- @param value_setter function Function to set the current value.<br/>
|
||||
--- @param min number The minimum value.<br/>
|
||||
--- @param max number The maximum value.<br/>
|
||||
--- @param step number The step increment.<br/>
|
||||
--- @param[opt] format string The format string for displaying the value.<br/>
|
||||
--- @return result table A numeric stepper control definition or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * label (string) The label for the stepper.<br/>
|
||||
--- * get (function) Function to get the current value.<br/>
|
||||
--- * set (function) Function to set the current value.<br/>
|
||||
--- * min (number) The minimum value.<br/>
|
||||
--- * max (number) The maximum value.<br/>
|
||||
--- * step (number) The step increment.<br/>
|
||||
--- * format (string) The format string for displaying the value.<br/>
|
||||
--- * type (string) Control type identifier ("numeric_stepper").<br/>
|
||||
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
|
||||
return {
|
||||
label = label,
|
||||
get = value_getter,
|
||||
set = value_setter,
|
||||
min = min,
|
||||
max = max,
|
||||
step = step,
|
||||
format = format or "%.1f",
|
||||
type = "numeric_stepper"
|
||||
}
|
||||
end
|
||||
|
||||
--- Creates an action item.
|
||||
--- @within UI
|
||||
--- @param label string The label for the action item.<br/>
|
||||
--- @param action function The function to execute when the item is selected.<br/>
|
||||
--- @return result table An action item control definition or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * label (string) The label for the action item.<br/>
|
||||
--- * action (function) The function to execute when the item is selected.<br/>
|
||||
--- * type (string) Control type identifier ("action_item").<br/>
|
||||
function UI.create_action_item(label, action)
|
||||
return {
|
||||
label = label,
|
||||
action = action,
|
||||
type = "action_item"
|
||||
}
|
||||
end
|
||||
|
||||
--- Draws decision selector.
|
||||
--- @within UI
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
|
||||
Reference in New Issue
Block a user