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
)