diff --git a/.luacheckrc b/.luacheckrc index 73a56b4..8142729 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -3,6 +3,8 @@ globals = { "Focus", + "Day", + "Timer", "Util", "Decision", "Situation", diff --git a/impostor.inc b/impostor.inc index ab69e62..e8d222a 100644 --- a/impostor.inc +++ b/impostor.inc @@ -2,12 +2,14 @@ meta/meta.header.lua init/init.module.lua init/init.config.lua init/init.minigame.lua -init/init.meter.lua init/init.context.lua system/system.util.lua system/system.print.lua system/system.input.lua -system/system.focus.lua +logic/logic.meter.lua +logic/logic.focus.lua +logic/logic.day.lua +logic/logic.timer.lua system/system.ui.lua audio/audio.manager.lua audio/audio.songs.lua diff --git a/inc/audio/audio.manager.lua b/inc/audio/audio.manager.lua index 583ffd2..2e9f0eb 100644 --- a/inc/audio/audio.manager.lua +++ b/inc/audio/audio.manager.lua @@ -19,7 +19,6 @@ function Audio.music_play_room_street_1() end --- @within Audio function Audio.music_play_room_street_2() end --- Plays room music. --- TODO: function name is incomplete, determine the correct room identifier --- @within Audio function Audio.music_play_room_() end --- Plays room work music. diff --git a/inc/decision/decision.manager.lua b/inc/decision/decision.manager.lua index 396529c..1aeaf1c 100644 --- a/inc/decision/decision.manager.lua +++ b/inc/decision/decision.manager.lua @@ -98,3 +98,39 @@ function Decision.filter_available(decisions_list) end return available end + +--- Draws decision selector. +--- @within Decision +--- @param decisions table A table of decision items.
+--- @param selected_decision_index number The index of the selected decision.
+function Decision.draw(decisions, selected_decision_index) + local bar_height = 16 + local bar_y = Config.screen.height - bar_height + rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey) + if #decisions > 0 then + local selected_decision = decisions[selected_decision_index] + local decision_label = selected_decision.label + local text_width = #decision_label * 4 + local text_y = bar_y + 4 + local text_x = (Config.screen.width - text_width) / 2 + Print.text("<", 2, text_y, Config.colors.light_blue) + Print.text(decision_label, text_x, text_y, Config.colors.item) + Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue) + end +end + +--- Updates decision selector. +--- @within Decision +--- @param decisions table A table of decision items.
+--- @param selected_decision_index number The current index of the selected decision.
+--- @return number selected_decision_index The updated index of the selected decision. +function Decision.update(decisions, selected_decision_index) + if Input.left() then + Audio.sfx_beep() + selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1) + elseif Input.right() then + Audio.sfx_beep() + selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1) + end + return selected_decision_index +end diff --git a/inc/init/init.config.lua b/inc/init/init.config.lua index 336714c..ff2ef0d 100644 --- a/inc/init/init.config.lua +++ b/inc/init/init.config.lua @@ -9,15 +9,15 @@ function Config.initial_data() height = 136 }, colors = { - black = 2, - light_grey = 13, - dark_grey = 14, - red = 0, - light_blue = 7, - blue = 9, - white = 12, - item = 12, - meter_bg = 12 + black = 0, + light_grey = 2, + dark_grey = 1, + red = 13, + light_blue = 9, + blue = 3, + white = 4, + item = 7, + meter_bg = 1 }, timing = { splash_duration = 120 diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua index 62cf5bb..2710a95 100644 --- a/inc/init/init.context.lua +++ b/inc/init/init.context.lua @@ -34,10 +34,12 @@ function Context.initial_data() minigame_button_mash = Minigame.get_default_button_mash(), minigame_rhythm = Minigame.get_default_rhythm(), meters = Meter.get_initial(), + timer = Timer.get_initial(), game = { current_screen = "home", current_situation = nil, - } + }, + day_count = 1, } end diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua index 700caa5..d99472a 100644 --- a/inc/init/init.module.lua +++ b/inc/init/init.module.lua @@ -12,3 +12,5 @@ Input = {} Sprite = {} Audio = {} Focus = {} +Day = {} +Timer = {} \ No newline at end of file diff --git a/inc/logic/logic.day.lua b/inc/logic/logic.day.lua new file mode 100644 index 0000000..36bf617 --- /dev/null +++ b/inc/logic/logic.day.lua @@ -0,0 +1,18 @@ +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) \ No newline at end of file diff --git a/inc/system/system.focus.lua b/inc/logic/logic.focus.lua similarity index 100% rename from inc/system/system.focus.lua rename to inc/logic/logic.focus.lua diff --git a/inc/init/init.meter.lua b/inc/logic/logic.meter.lua similarity index 65% rename from inc/init/init.meter.lua rename to inc/logic/logic.meter.lua index 31d1ef1..0fb3c23 100644 --- a/inc/init/init.meter.lua +++ b/inc/logic/logic.meter.lua @@ -2,34 +2,17 @@ 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 --- 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. 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 ---- 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. --- @within Meter --- @return result table Initial meter values.
@@ -39,8 +22,7 @@ end --- * bm (number) Initial BM meter value.
--- * combo (number) Current combo count.
--- * combo_timer (number) Frames since last combo action.
---- * hidden (boolean) Whether meters are hidden.
---- * timer_progress (number) Clock timer revolution progress (0 to 1). +--- * hidden (boolean) Whether meters are hidden. function Meter.get_initial() return { ism = METER_DEFAULT, @@ -49,7 +31,6 @@ function Meter.get_initial() combo = 0, combo_timer = 0, hidden = false, - timer_progress = 0, } end @@ -72,6 +53,20 @@ 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. @@ -96,13 +91,6 @@ function Meter.update() m.combo_timer = 0 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 @@ -118,13 +106,6 @@ function Meter.add(key, amount) end end ---- Gets the timer decay as a percentage of the max meter value. ---- @within Meter ---- @return number The decay percentage per revolution (e.g. 2 means -2%). -function Meter.get_timer_decay_percentage() - return math.floor(meter_timer_decay_per_revolution / METER_MAX * 100) -end - --- Called on minigame completion. --- @within Meter function Meter.on_minigame_complete() @@ -136,3 +117,37 @@ function Meter.on_minigame_complete() m.combo = m.combo + 1 m.combo_timer = 0 end + +--- Draws meters. +--- @within Meter +function Meter.draw() + 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 max = Meter.get_max() + local bar_w = 44 + local bar_h = 2 + local bar_x = 182 + local label_x = 228 + local line_h = 5 + local start_y = 11 + local bar_offset = math.floor((line_h - bar_h) / 2) + + local meter_list = { + { key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 }, + { key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 }, + { key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 }, + } + + for _, meter in ipairs(meter_list) do + local label_y = start_y + meter.row * line_h + local bar_y = label_y + bar_offset + local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w)) + rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG) + if fill_w > 0 then + rect(bar_x, bar_y, fill_w, bar_h, meter.color) + end + print(meter.label, label_x, label_y, meter.color, false, 1, true) + end +end diff --git a/inc/logic/logic.timer.lua b/inc/logic/logic.timer.lua new file mode 100644 index 0000000..bf878e6 --- /dev/null +++ b/inc/logic/logic.timer.lua @@ -0,0 +1,88 @@ +--- @section Timer + +local timer_duration = 1800 + +--- Gets initial timer values. +--- @within Timer +--- @return result table Initial timer values.
+--- Fields:
+--- * 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 diff --git a/inc/screen/screen.toilet.lua b/inc/screen/screen.toilet.lua index dae74c3..f388400 100644 --- a/inc/screen/screen.toilet.lua +++ b/inc/screen/screen.toilet.lua @@ -30,8 +30,7 @@ Screen.register({ local bar_x = math.floor((sw - bar_w) / 2) local bar_h = 4 - -- TODO: Add day counter - Print.text_center("day 1", cx, 10, Config.colors.white) + Print.text_center("day " .. Context.day_count, cx, 10, Config.colors.white) local narrative = "reflecting on my past and present\n...\nboth eventually flushed." local wrapped = UI.word_wrap(narrative, 38) @@ -43,7 +42,7 @@ Screen.register({ local m = Context.meters local max_val = Meter.get_max() - local decay_pct = Meter.get_timer_decay_percentage() + local decay_pct = Meter.get_decay_percentage() local decay_text = string.format("-%d%%", decay_pct) local combo_mult = Meter.get_combo_multiplier() local combo_pct = math.floor((combo_mult - 1) * 100) diff --git a/inc/system/system.main.lua b/inc/system/system.main.lua index bed938a..1204d02 100644 --- a/inc/system/system.main.lua +++ b/inc/system/system.main.lua @@ -22,8 +22,9 @@ function TIC() handler() end Meter.update() + Timer.update() if Context.game_in_progress then - UI.draw_meters() - UI.draw_timer() + Meter.draw() + Timer.draw() end end diff --git a/inc/system/system.ui.lua b/inc/system/system.ui.lua index 31a76ed..d48fe3d 100644 --- a/inc/system/system.ui.lua +++ b/inc/system/system.ui.lua @@ -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.
@@ -85,176 +79,3 @@ function UI.word_wrap(text, max_chars_per_line) end return lines end - ---- Creates a numeric stepper. ---- @within UI ---- @param label string The label for the stepper.
---- @param value_getter function Function to get the current value.
---- @param value_setter function Function to set the current value.
---- @param min number The minimum value.
---- @param max number The maximum value.
---- @param step number The step increment.
---- @param[opt] format string The format string for displaying the value.
---- @return result table A numeric stepper control definition or nil.
---- Fields:
---- * label (string) The label for the stepper.
---- * get (function) Function to get the current value.
---- * set (function) Function to set the current value.
---- * min (number) The minimum value.
---- * max (number) The maximum value.
---- * step (number) The step increment.
---- * format (string) The format string for displaying the value.
---- * type (string) Control type identifier ("numeric_stepper").
-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.
---- @param action function The function to execute when the item is selected.
---- @return result table An action item control definition or nil.
---- Fields:
---- * label (string) The label for the action item.
---- * action (function) The function to execute when the item is selected.
---- * type (string) Control type identifier ("action_item").
-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.
---- @param selected_decision_index number The index of the selected decision.
-function UI.draw_decision_selector(decisions, selected_decision_index) - local bar_height = 16 - local bar_y = Config.screen.height - bar_height - rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey) - if #decisions > 0 then - local selected_decision = decisions[selected_decision_index] - local decision_label = selected_decision.label - local text_width = #decision_label * 4 - local text_y = bar_y + 4 - local text_x = (Config.screen.width - text_width) / 2 - Print.text("<", 2, text_y, Config.colors.light_blue) - Print.text(decision_label, text_x, text_y, Config.colors.item) - Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue) - 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%). ---- @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 and not Context.stat_screen_active 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.white) -end - ---- Draws meters. ---- @within UI -function UI.draw_meters() - 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 max = Meter.get_max() - local bar_w = 44 - local bar_h = 2 - local bar_x = 182 - local label_x = 228 - local line_h = 5 - local start_y = 11 - local bar_offset = math.floor((line_h - bar_h) / 2) - - local meter_list = { - { key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 }, - { key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 }, - { key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 }, - } - - for _, meter in ipairs(meter_list) do - local label_y = start_y + meter.row * line_h - local bar_y = label_y + bar_offset - local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w)) - rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG) - if fill_w > 0 then - rect(bar_x, bar_y, fill_w, bar_h, meter.color) - end - print(meter.label, label_x, label_y, meter.color, false, 1, true) - end -end - ---- Updates decision selector. ---- @within UI ---- @param decisions table A table of decision items.
---- @param selected_decision_index number The current index of the selected decision.
---- @return number selected_decision_index The updated index of the selected decision. -function UI.update_decision_selector(decisions, selected_decision_index) - if Input.left() then - Audio.sfx_beep() - selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1) - elseif Input.right() then - Audio.sfx_beep() - selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1) - end - return selected_decision_index -end \ No newline at end of file diff --git a/inc/window/window.configuration.lua b/inc/window/window.configuration.lua index 3a843d8..12392f4 100644 --- a/inc/window/window.configuration.lua +++ b/inc/window/window.configuration.lua @@ -8,14 +8,16 @@ ConfigurationWindow = { --- @within ConfigurationWindow function ConfigurationWindow.init() ConfigurationWindow.controls = { - UI.create_action_item( - "Save", - function() Config.save() end - ), - UI.create_action_item( - "Restore Defaults", - function() Config.reset() end - ), + { + label = "Save", + action = function() Config.save() end, + type = "action_item" + }, + { + label = "Restore Defaults", + action = function() Config.reset() end, + type = "action_item" + }, } end diff --git a/inc/window/window.game.lua b/inc/window/window.game.lua index f5286cb..d6241ed 100644 --- a/inc/window/window.game.lua +++ b/inc/window/window.game.lua @@ -13,7 +13,7 @@ function GameWindow.draw() end UI.draw_top_bar(screen.name) if not Context.stat_screen_active and #_available_decisions > 0 then - UI.draw_decision_selector(_available_decisions, _selected_decision_index) + Decision.draw(_available_decisions, _selected_decision_index) end Sprite.draw() Focus.draw() @@ -53,7 +53,7 @@ function GameWindow.update() _selected_decision_index = 1 end - local new_selected_decision_index = UI.update_decision_selector( + local new_selected_decision_index = Decision.update( _available_decisions, _selected_decision_index )