feature/imp-42-time-indicator #18

Merged
mr.two merged 1 commits from feature/imp-42-time-indicator into master 2026-02-26 14:49:01 +00:00
11 changed files with 114 additions and 26 deletions

View File

@@ -39,6 +39,8 @@ globals = {
"circb", "circb",
"cls", "cls",
"tri", "tri",
"pix",
"line",
"Songs", "Songs",
"frame_from_beat", "frame_from_beat",
"beats_to_pattern", "beats_to_pattern",

View File

@@ -13,7 +13,7 @@ function Config.initial_data()
light_grey = 13, light_grey = 13,
dark_grey = 14, dark_grey = 14,
red = 0, red = 0,
green = 7, light_blue = 7,
blue = 9, blue = 9,
white = 12, white = 12,
item = 12, item = 12,

View File

@@ -7,12 +7,30 @@ local COMBO_BASE_BONUS = 0.02
local COMBO_MAX_BONUS = 0.16 local COMBO_MAX_BONUS = 0.16
local COMBO_TIMEOUT_FRAMES = 600 local COMBO_TIMEOUT_FRAMES = 600
-- 1800 frames = 30 seconds (1800 ÷ 60 = 30)
local meter_timer_duration = 1800
local meter_timer_decay_per_revolution = 20
-- Internal meters for tracking game progress and player stats. -- Internal meters for tracking game progress and player stats.
Meter.COLOR_ISM = Config.colors.red Meter.COLOR_ISM = Config.colors.red
Meter.COLOR_WPM = Config.colors.blue Meter.COLOR_WPM = Config.colors.blue
Meter.COLOR_BM = Config.colors.black Meter.COLOR_BM = Config.colors.black
Meter.COLOR_BG = Config.colors.meter_bg Meter.COLOR_BG = Config.colors.meter_bg
--- Sets the number of frames for one full timer revolution.
--- @within Meter
--- @param frames number Frames per revolution (controls degradation speed).
function Meter.set_timer_duration(frames)
meter_timer_duration = frames
end
--- Sets the degradation amount applied to all meters per revolution.
--- @within Meter
--- @param amount number Amount to subtract from each meter per revolution.
function Meter.set_timer_decay(amount)
meter_timer_decay_per_revolution = amount
end
--- Gets initial meter values. --- Gets initial meter values.
--- @within Meter --- @within Meter
--- @return result table A table of initial meter values. --- @return result table A table of initial meter values.
@@ -22,6 +40,7 @@ Meter.COLOR_BG = Config.colors.meter_bg
--- @return result.combo number Current combo count. --- @return result.combo number Current combo count.
--- @return result.combo_timer number Frames since last combo action. --- @return result.combo_timer number Frames since last combo action.
--- @return result.hidden boolean Whether meters are hidden. --- @return result.hidden boolean Whether meters are hidden.
--- @return result.timer_progress number Clock timer revolution progress (0 to 1).
function Meter.get_initial() function Meter.get_initial()
return { return {
ism = METER_DEFAULT, ism = METER_DEFAULT,
@@ -30,6 +49,7 @@ function Meter.get_initial()
combo = 0, combo = 0,
combo_timer = 0, combo_timer = 0,
hidden = false, hidden = false,
timer_progress = 0,
} }
end end
@@ -67,6 +87,8 @@ end
function Meter.update() function Meter.update()
if not Context or not Context.game_in_progress or not Context.meters then return end if not Context or not Context.game_in_progress or not Context.meters then return end
local m = Context.meters local m = Context.meters
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
if not in_minigame then
m.ism = math.max(0, m.ism - METER_DECAY_PER_FRAME) m.ism = math.max(0, m.ism - METER_DECAY_PER_FRAME)
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_FRAME) m.wpm = math.max(0, m.wpm - METER_DECAY_PER_FRAME)
m.bm = math.max(0, m.bm - METER_DECAY_PER_FRAME) m.bm = math.max(0, m.bm - METER_DECAY_PER_FRAME)
@@ -77,6 +99,14 @@ function Meter.update()
m.combo_timer = 0 m.combo_timer = 0
end end
end end
m.timer_progress = m.timer_progress + (1 / meter_timer_duration)
if m.timer_progress >= 1 then
m.timer_progress = m.timer_progress - 1
m.ism = math.max(0, m.ism - meter_timer_decay_per_revolution)
m.wpm = math.max(0, m.wpm - meter_timer_decay_per_revolution)
m.bm = math.max(0, m.bm - meter_timer_decay_per_revolution)
end
end
end end
--- Adds amount to a meter. --- Adds amount to a meter.

View File

@@ -23,5 +23,6 @@ function TIC()
Meter.update() Meter.update()
if Context.game_in_progress then if Context.game_in_progress then
UI.draw_meters() UI.draw_meters()
UI.draw_timer()
end end
end end

View File

@@ -5,7 +5,7 @@
--- @param title string The title text to display. --- @param title string The title text to display.
function UI.draw_top_bar(title) function UI.draw_top_bar(title)
rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey) rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
Print.text(title, 3, 2, Config.colors.green) Print.text(title, 3, 2, Config.colors.light_blue)
end end
--- Draws dialog window. --- Draws dialog window.
@@ -24,9 +24,9 @@ function UI.draw_menu(items, selected_item, x, y)
for i, item in ipairs(items) do for i, item in ipairs(items) do
local current_y = y + (i-1)*10 local current_y = y + (i-1)*10
if i == selected_item then if i == selected_item then
Print.text(">", x - 8, current_y, Config.colors.green) Print.text(">", x - 8, current_y, Config.colors.light_blue)
end end
Print.text(item.label, x, current_y, Config.colors.green) Print.text(item.label, x, current_y, Config.colors.light_blue)
end end
end end
@@ -147,12 +147,67 @@ function UI.draw_decision_selector(decisions, selected_decision_index)
local text_width = #decision_label * 4 local text_width = #decision_label * 4
local text_y = bar_y + 4 local text_y = bar_y + 4
local text_x = (Config.screen.width - text_width) / 2 local text_x = (Config.screen.width - text_width) / 2
Print.text("<", 2, text_y, Config.colors.green) Print.text("<", 2, text_y, Config.colors.light_blue)
Print.text(decision_label, text_x, text_y, Config.colors.item) Print.text(decision_label, text_x, text_y, Config.colors.item)
Print.text(">", Config.screen.width - 6, text_y, Config.colors.green) Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
end end
end end
--- Draws the clock timer indicator as a circular progress bar in the top-left area.
--- Color transitions: white (0-50%), yellow (50-75%), red (75-100%).
--- Only visible when meters are visible.
--- @within UI
function UI.draw_timer()
if not Context or not Context.game_in_progress or not Context.meters then return end
if Context.meters.hidden then return end
local m = Context.meters
local cx = 10
local cy = 20
local r_outer = 5
local r_inner = 3
local progress = m.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.black)
end
--- Draws meters. --- Draws meters.
--- @within UI --- @within UI
function UI.draw_meters() function UI.draw_meters()

View File

@@ -30,7 +30,7 @@ function ConfigurationWindow.draw()
local char_width = 4 local char_width = 4
for i, control in ipairs(ConfigurationWindow.controls) do for i, control in ipairs(ConfigurationWindow.controls) do
local current_y = y_start + (i - 1) * 12 local current_y = y_start + (i - 1) * 12
local color = Config.colors.green local color = Config.colors.light_blue
if control.type == "numeric_stepper" then if control.type == "numeric_stepper" then
local value = control.get() local value = control.get()
local label_text = control.label local label_text = control.label

View File

@@ -18,7 +18,7 @@ on than meets the eye.
--- @within IntroWindow --- @within IntroWindow
function IntroWindow.draw() function IntroWindow.draw()
local x = (Config.screen.width - 132) / 2 local x = (Config.screen.width - 132) / 2
Print.text(IntroWindow.text, x, IntroWindow.y, Config.colors.green) Print.text(IntroWindow.text, x, IntroWindow.y, Config.colors.light_blue)
end end
--- Updates the intro window logic. --- Updates the intro window logic.

View File

@@ -219,7 +219,7 @@ function MinigameDDRWindow.draw()
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
if fill_width > 0 then if fill_width > 0 then
local bar_color = Config.colors.green local bar_color = Config.colors.light_blue
if mg.bar_fill > 66 then if mg.bar_fill > 66 then
bar_color = Config.colors.item bar_color = Config.colors.item
elseif mg.bar_fill > 33 then elseif mg.bar_fill > 33 then
@@ -232,7 +232,7 @@ function MinigameDDRWindow.draw()
if mg.target_arrows then if mg.target_arrows then
for _, target in ipairs(mg.target_arrows) do for _, target in ipairs(mg.target_arrows) do
local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0 local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0
local color = is_pressed and Config.colors.green or Config.colors.light_grey local color = is_pressed and Config.colors.light_blue or Config.colors.light_grey
draw_arrow(target.x, mg.target_y, target.dir, color) draw_arrow(target.x, mg.target_y, target.dir, color)
end end
end end
@@ -252,14 +252,14 @@ function MinigameDDRWindow.draw()
"PATTERN MODE - Frame:" .. mg.frame_counter, "PATTERN MODE - Frame:" .. mg.frame_counter,
Config.screen.width / 2, Config.screen.width / 2,
debug_y, debug_y,
Config.colors.green Config.colors.light_blue
) )
if mg.current_song and mg.current_song.pattern then if mg.current_song and mg.current_song.pattern then
Print.text_center( Print.text_center(
"Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index, "Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index,
Config.screen.width / 2, Config.screen.width / 2,
debug_y + 10, debug_y + 10,
Config.colors.green Config.colors.light_blue
) )
end end
else else

View File

@@ -68,7 +68,7 @@ function MinigameButtonMashWindow.draw()
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
if fill_width > 0 then if fill_width > 0 then
local bar_color = Config.colors.green local bar_color = Config.colors.light_blue
if mg.bar_fill > 66 then if mg.bar_fill > 66 then
bar_color = Config.colors.item bar_color = Config.colors.item
elseif mg.bar_fill > 33 then elseif mg.bar_fill > 33 then
@@ -78,7 +78,7 @@ function MinigameButtonMashWindow.draw()
end end
local button_color = Config.colors.light_grey local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
button_color = Config.colors.green button_color = Config.colors.light_blue
end end
circb(mg.button_x, mg.button_y, mg.button_size, button_color) circb(mg.button_x, mg.button_y, mg.button_size, button_color)
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then

View File

@@ -87,7 +87,7 @@ function MinigameRhythmWindow.draw()
local target_left = mg.target_center - (mg.target_width / 2) local target_left = mg.target_center - (mg.target_width / 2)
local target_x = mg.bar_x + (target_left * mg.bar_width) local target_x = mg.bar_x + (target_left * mg.bar_width)
local target_width_pixels = mg.target_width * mg.bar_width local target_width_pixels = mg.target_width * mg.bar_width
rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.green) rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.light_blue)
local line_x = mg.bar_x + (mg.line_position * mg.bar_width) local line_x = mg.bar_x + (mg.line_position * mg.bar_width)
rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.item) rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.item)
local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score
@@ -100,7 +100,7 @@ function MinigameRhythmWindow.draw()
) )
local button_color = Config.colors.light_grey local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
button_color = Config.colors.green button_color = Config.colors.light_blue
end end
circb(mg.button_x, mg.button_y, mg.button_size, button_color) circb(mg.button_x, mg.button_y, mg.button_size, button_color)
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then

View File

@@ -39,7 +39,7 @@ end
function PopupWindow.draw() function PopupWindow.draw()
if Context.popup.show then if Context.popup.show then
rect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.black) rect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.black)
rectb(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.green) rectb(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.light_blue)
local current_y = TEXT_MARGIN_Y local current_y = TEXT_MARGIN_Y
for _, line in ipairs(Context.popup.content) do for _, line in ipairs(Context.popup.content) do
@@ -47,6 +47,6 @@ function PopupWindow.draw()
current_y = current_y + LINE_HEIGHT current_y = current_y + LINE_HEIGHT
end end
Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.green) Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.light_blue)
end end
end end