diff --git a/.gitignore b/.gitignore
index a1781fb..b381695 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.claude
.local
impostor.lua
impostor.original.lua
@@ -5,4 +6,4 @@ prompts
docs
minify.lua
*.tic
-*.zip
\ No newline at end of file
+*.zip
diff --git a/.luacheckrc b/.luacheckrc
index 0c64bc2..7162e25 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -65,6 +65,10 @@ globals = {
"map",
"time",
"RLE",
+ "mouse",
+ "Mouse",
+ "print",
+ "musicator_generate_pattern",
}
diff --git a/impostor.inc b/impostor.inc
index 9d71029..64da4c0 100644
--- a/impostor.inc
+++ b/impostor.inc
@@ -6,6 +6,7 @@ init/init.context.lua
system/system.util.lua
system/system.print.lua
system/system.input.lua
+system/system.mouse.lua
system/system.asciiart.lua
system/system.rle.lua
logic/logic.meter.lua
diff --git a/inc/decision/decision.manager.lua b/inc/decision/decision.manager.lua
index 8950ece..65412bc 100644
--- a/inc/decision/decision.manager.lua
+++ b/inc/decision/decision.manager.lua
@@ -134,6 +134,7 @@ end
--- @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.
+--- @return boolean mouse_confirmed True if the user clicked the center to confirm.
function Decision.update(decisions, selected_decision_index)
if Input.left() then
Audio.sfx_beep()
@@ -142,5 +143,27 @@ function Decision.update(decisions, selected_decision_index)
Audio.sfx_beep()
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
end
- return selected_decision_index
+
+ if Mouse.clicked() then
+ local mx = Mouse.x()
+ local my = Mouse.y()
+ local bar_height = 16
+ local bar_y = Config.screen.height - bar_height
+ if my >= bar_y then
+ if mx < 15 then
+ Audio.sfx_beep()
+ Mouse.consume()
+ selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1)
+ elseif mx > Config.screen.width - 15 then
+ Audio.sfx_beep()
+ Mouse.consume()
+ selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
+ else
+ Mouse.consume()
+ return selected_decision_index, true
+ end
+ end
+ end
+
+ return selected_decision_index, false
end
diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua
index ecdeb67..d15d850 100644
--- a/inc/init/init.module.lua
+++ b/inc/init/init.module.lua
@@ -8,6 +8,7 @@ Map = {}
UI = {}
Print = {}
Input = {}
+Mouse = {}
Sprite = {}
Audio = {}
Focus = {}
diff --git a/inc/screen/screen.mysterious_man.lua b/inc/screen/screen.mysterious_man.lua
index 5c23556..a0b6a05 100644
--- a/inc/screen/screen.mysterious_man.lua
+++ b/inc/screen/screen.mysterious_man.lua
@@ -240,9 +240,12 @@ Screen.register({
end
end
elseif state == STATE_CHOICE then
- selected_choice = UI.update_menu(MysteriousManScreen.choices, selected_choice)
+ local menu_x = (Config.screen.width - 60) / 2
+ local menu_y = (Config.screen.height - 20) / 2
+ local confirmed
+ selected_choice, confirmed = UI.update_menu(MysteriousManScreen.choices, selected_choice, menu_x, menu_y)
- if Input.select() then
+ if Input.select() or confirmed then
Audio.sfx_select()
if selected_choice == 1 then
MysteriousManScreen.wake_up()
diff --git a/inc/screen/screen.toilet.lua b/inc/screen/screen.toilet.lua
index 1d7be30..082267d 100644
--- a/inc/screen/screen.toilet.lua
+++ b/inc/screen/screen.toilet.lua
@@ -16,7 +16,7 @@ Screen.register({
end,
update = function()
if not Context.stat_screen_active then return end
- if Input.select() or Input.player_interact() then
+ if Input.select() or Input.select() then
Focus.stop()
Context.stat_screen_active = false
Meter.show()
diff --git a/inc/system/system.input.lua b/inc/system/system.input.lua
index aeb1900..3c015c2 100644
--- a/inc/system/system.input.lua
+++ b/inc/system/system.input.lua
@@ -3,12 +3,9 @@ local INPUT_KEY_UP = 0
local INPUT_KEY_DOWN = 1
local INPUT_KEY_LEFT = 2
local INPUT_KEY_RIGHT = 3
-local INPUT_KEY_A = 4
-local INPUT_KEY_B = 5
local INPUT_KEY_Y = 7
local INPUT_KEY_SPACE = 48
local INPUT_KEY_BACKSPACE = 51
-local INPUT_KEY_ENTER = 50
--- Checks if Up is pressed.
--- @within Input
@@ -22,22 +19,9 @@ function Input.left() return btnp(INPUT_KEY_LEFT) end
--- Checks if Right is pressed.
--- @within Input
function Input.right() return btnp(INPUT_KEY_RIGHT) end
---- Checks if Space is pressed.
---- @within Input
-function Input.space() return keyp(INPUT_KEY_SPACE) end
-
--- Checks if Select is pressed.
--- @within Input
-function Input.select() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_SPACE) end
---- Checks if Menu Confirm is pressed.
+function Input.select() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_SPACE) or Mouse.clicked() end
+--- Checks if Back is pressed.
--- @within Input
-function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end
---- Checks if Player Interact is pressed.
---- @within Input
-function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end
---- Checks if Menu Back is pressed.
---- @within Input
-function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end
---- Checks if Toggle Popup is pressed.
---- @within Input
-function Input.toggle_popup() return keyp(INPUT_KEY_ENTER) end
+function Input.back() return keyp(INPUT_KEY_BACKSPACE) end
diff --git a/inc/system/system.main.lua b/inc/system/system.main.lua
index 888ef84..69b4f86 100644
--- a/inc/system/system.main.lua
+++ b/inc/system/system.main.lua
@@ -17,6 +17,7 @@ end
--- @within Main
function TIC()
init_game()
+ Mouse.update()
local now = time()
if Context.last_frame_time == 0 then
diff --git a/inc/system/system.mouse.lua b/inc/system/system.mouse.lua
new file mode 100644
index 0000000..395147c
--- /dev/null
+++ b/inc/system/system.mouse.lua
@@ -0,0 +1,43 @@
+--- @section Mouse
+local _mx, _my = 0, 0
+local _mleft, _mleft_prev = false, false
+local _consumed = false
+
+--- Updates mouse state. Call once per frame.
+--- @within Mouse
+function Mouse.update()
+ _mleft_prev = _mleft
+ _consumed = false
+ local mt = {mouse()}
+ _mx, _my, _mleft = mt[1], mt[2], mt[3]
+end
+
+--- Returns current mouse X position.
+--- @within Mouse
+function Mouse.x() return _mx end
+
+--- Returns current mouse Y position.
+--- @within Mouse
+function Mouse.y() return _my end
+
+--- Returns true if the mouse button was just pressed this frame (and not yet consumed).
+--- @within Mouse
+function Mouse.clicked() return _mleft and not _mleft_prev and not _consumed end
+
+--- Returns true if the mouse button is held down.
+--- @within Mouse
+function Mouse.held() return _mleft end
+
+--- Marks the current click as consumed so Mouse.clicked() won't fire again this frame.
+--- @within Mouse
+function Mouse.consume() _consumed = true end
+
+--- Returns true if the mouse is within the given rectangle.
+--- @within Mouse
+--- @param x number Left edge.
+--- @param y number Top edge.
+--- @param w number Width.
+--- @param h number Height.
+function Mouse.in_rect(x, y, w, h)
+ return _mx >= x and _mx < x + w and _my >= y and _my < y + h
+end
diff --git a/inc/system/system.ui.lua b/inc/system/system.ui.lua
index e9f0cd4..f0dcb73 100644
--- a/inc/system/system.ui.lua
+++ b/inc/system/system.ui.lua
@@ -38,8 +38,12 @@ end
--- @within UI
--- @param items table A table of menu items.
--- @param selected_item number The current index of the selected item.
+--- @param[opt] x number Menu x position (required for mouse support).
+--- @param[opt] y number Menu y position (required for mouse support).
+--- @param[opt] centered boolean Whether the menu is centered horizontally.
--- @return number selected_item The updated index of the selected item.
-function UI.update_menu(items, selected_item)
+--- @return boolean mouse_confirmed True if the user clicked on a menu item.
+function UI.update_menu(items, selected_item, x, y, centered)
if Input.up() then
Audio.sfx_beep()
selected_item = selected_item - 1
@@ -53,7 +57,29 @@ function UI.update_menu(items, selected_item)
selected_item = 1
end
end
- return selected_item
+
+ if x ~= nil and y ~= nil and Mouse.clicked() then
+ local mx = Mouse.x()
+ local my = Mouse.y()
+ local menu_x = x
+ if centered then
+ local max_w = 0
+ for _, item in ipairs(items) do
+ local w = print(item.label, 0, -10, 0, false, 1, false)
+ if w > max_w then max_w = w end
+ end
+ menu_x = (Config.screen.width - max_w) / 2
+ end
+ for i, _ in ipairs(items) do
+ local item_y = y + (i - 1) * 10
+ if my >= item_y and my < item_y + 10 and mx >= menu_x - 8 then
+ Mouse.consume()
+ return i, true
+ end
+ end
+ end
+
+ return selected_item, false
end
--- Draws a bordered textbox with scrolling text.
diff --git a/inc/window/window.audiotest.lua b/inc/window/window.audiotest.lua
index 3c46419..36e5783 100644
--- a/inc/window/window.audiotest.lua
+++ b/inc/window/window.audiotest.lua
@@ -107,9 +107,9 @@ function AudioTestWindow.update()
AudioTestWindow.menuitems = AudioTestWindow.generate_menuitems(
AudioTestWindow.list_func, AudioTestWindow.index_func
)
- elseif Input.menu_confirm() then
+ elseif Input.select() then
AudioTestWindow.menuitems[AudioTestWindow.index_menu].decision()
- elseif Input.menu_back() then
+ elseif Input.back() then
AudioTestWindow.back()
end
end
diff --git a/inc/window/window.configuration.lua b/inc/window/window.configuration.lua
index 938da45..3823b18 100644
--- a/inc/window/window.configuration.lua
+++ b/inc/window/window.configuration.lua
@@ -65,7 +65,7 @@ end
--- Updates configuration window logic.
--- @within ConfigurationWindow
function ConfigurationWindow.update()
- if Input.menu_back() then
+ if Input.back() then
GameWindow.set_state("menu")
return
end
@@ -94,7 +94,7 @@ function ConfigurationWindow.update()
control.set(new_value)
end
elseif control.type == "action_item" then
- if Input.menu_confirm() then
+ if Input.select() then
control.action()
end
end
diff --git a/inc/window/window.continued.lua b/inc/window/window.continued.lua
index e5f0ed9..976e6ef 100644
--- a/inc/window/window.continued.lua
+++ b/inc/window/window.continued.lua
@@ -26,7 +26,7 @@ end
--- @within ContinuedWindow
function ContinuedWindow.update()
ContinuedWindow.timer = ContinuedWindow.timer - 1
- if ContinuedWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
+ if ContinuedWindow.timer <= 0 or Input.select() or Input.select() then
Window.set_current("menu")
MenuWindow.refresh_menu_items()
end
diff --git a/inc/window/window.end.lua b/inc/window/window.end.lua
index d3c08e1..291b054 100644
--- a/inc/window/window.end.lua
+++ b/inc/window/window.end.lua
@@ -52,7 +52,7 @@ function EndWindow.update()
end
end
- if Input.menu_confirm() then
+ if Input.select() then
Audio.sfx_select()
if Context._end.selection == 1 then
Context._end.state = "ending"
@@ -69,7 +69,7 @@ function EndWindow.update()
end
end
elseif Context._end.state == "ending" then
- if Input.menu_confirm() then
+ if Input.select() then
Window.set_current("menu")
MenuWindow.refresh_menu_items()
end
diff --git a/inc/window/window.game.lua b/inc/window/window.game.lua
index 3b3f06b..c143377 100644
--- a/inc/window/window.game.lua
+++ b/inc/window/window.game.lua
@@ -38,7 +38,7 @@ end
--- @within GameWindow
function GameWindow.update()
Focus.update()
- if Input.menu_back() then
+ if Input.back() then
Window.set_current("menu")
MenuWindow.refresh_menu_items()
return
@@ -60,7 +60,7 @@ function GameWindow.update()
_selected_decision_index = 1
end
- local new_selected_decision_index = Decision.update(
+ local new_selected_decision_index, mouse_confirmed = Decision.update(
_available_decisions,
_selected_decision_index
)
@@ -69,7 +69,7 @@ function GameWindow.update()
_selected_decision_index = new_selected_decision_index
end
- if Input.select() then
+ if Input.select() or mouse_confirmed then
local selected_decision = _available_decisions[_selected_decision_index]
if selected_decision and selected_decision.handle then
Audio.sfx_select()
diff --git a/inc/window/window.intro.brief.lua b/inc/window/window.intro.brief.lua
index 11a0f8e..1d75389 100644
--- a/inc/window/window.intro.brief.lua
+++ b/inc/window/window.intro.brief.lua
@@ -31,7 +31,7 @@ function BriefIntroWindow.update()
lines = lines + 1
end
- if BriefIntroWindow.y < -lines * 8 or Input.select() or Input.menu_confirm() then
+ if BriefIntroWindow.y < -lines * 8 or Input.select() or Input.select() then
Window.set_current("menu")
end
end
diff --git a/inc/window/window.intro.title.lua b/inc/window/window.intro.title.lua
index 825ace4..70f9105 100644
--- a/inc/window/window.intro.title.lua
+++ b/inc/window/window.intro.title.lua
@@ -30,7 +30,7 @@ end
--- @within TitleIntroWindow
function TitleIntroWindow.update()
TitleIntroWindow.timer = TitleIntroWindow.timer - 1
- if TitleIntroWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
+ if TitleIntroWindow.timer <= 0 or Input.select() or Input.select() then
Window.set_current("intro_ttg")
end
end
diff --git a/inc/window/window.intro.ttg.lua b/inc/window/window.intro.ttg.lua
index c84208c..ee75c00 100644
--- a/inc/window/window.intro.ttg.lua
+++ b/inc/window/window.intro.ttg.lua
@@ -28,12 +28,12 @@ function TTGIntroWindow.update()
end
-- Count menu_back presses during the intro
- if Input.menu_back() then
+ if Input.back() then
TTGIntroWindow.space_count = TTGIntroWindow.space_count + 1
end
TTGIntroWindow.timer = TTGIntroWindow.timer - 1
- if TTGIntroWindow.timer <= 0 or Input.menu_confirm() then
+ if TTGIntroWindow.timer <= 0 or Input.select() then
-- Evaluate exactly 3 presses at the end of the intro
if TTGIntroWindow.space_count == 3 then
Context.test_mode = true
diff --git a/inc/window/window.menu.lua b/inc/window/window.menu.lua
index bcd8a8b..16554a9 100644
--- a/inc/window/window.menu.lua
+++ b/inc/window/window.menu.lua
@@ -1,5 +1,6 @@
--- @section MenuWindow
local _menu_items = {}
+local _click_timer = 0
--- Draws the menu window.
--- @within MenuWindow
@@ -22,9 +23,28 @@ end
--- Updates the menu window logic.
--- @within MenuWindow
function MenuWindow.update()
- Context.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item)
+ local menu_h = #_menu_items * 10
+ local y = 10 + (Config.screen.height - 10 - 10 - menu_h) / 2
- if Input.menu_confirm() then
+ if _click_timer > 0 then
+ _click_timer = _click_timer - Context.delta_time
+ if _click_timer <= 0 then
+ _click_timer = 0
+ local selected_item = _menu_items[Context.current_menu_item]
+ if selected_item and selected_item.decision then
+ selected_item.decision()
+ end
+ end
+ return
+ end
+
+ local new_item, mouse_confirmed = UI.update_menu(_menu_items, Context.current_menu_item, 0, y, true)
+ Context.current_menu_item = new_item
+
+ if mouse_confirmed then
+ Audio.sfx_select()
+ _click_timer = 0.5
+ elseif Input.select() then
local selected_item = _menu_items[Context.current_menu_item]
if selected_item and selected_item.decision then
Audio.sfx_select()
@@ -115,4 +135,5 @@ function MenuWindow.refresh_menu_items()
table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit})
Context.current_menu_item = 1
+ _click_timer = 0
end
diff --git a/inc/window/window.minigame.ddr.lua b/inc/window/window.minigame.ddr.lua
index 8db1962..2cba4fd 100644
--- a/inc/window/window.minigame.ddr.lua
+++ b/inc/window/window.minigame.ddr.lua
@@ -355,6 +355,17 @@ function MinigameDDRWindow.update()
right = Input.right()
}
+ if Mouse.clicked() then
+ local mx = Mouse.x()
+ local my = Mouse.y()
+ for _, target in ipairs(mg.target_arrows) do
+ if mx >= target.x and mx < target.x + mg.arrow_size and
+ my >= mg.target_y and my < mg.target_y + mg.arrow_size then
+ input_map[target.dir] = true
+ end
+ end
+ end
+
for dir, pressed in pairs(input_map) do
if pressed and mg.input_cooldowns[dir] == 0 then
mg.input_cooldowns[dir] = mg.input_cooldown_duration
diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua
index c39bfd9..0d8af73 100644
--- a/inc/window/window.minigame.mash.lua
+++ b/inc/window/window.minigame.mash.lua
@@ -83,7 +83,14 @@ function MinigameButtonMashWindow.update()
return
end
- if Input.select() then
+ local mouse_on_button = false
+ if Mouse.clicked() then
+ local dx = Mouse.x() - mg.button_x
+ local dy = Mouse.y() - mg.button_y
+ mouse_on_button = (dx * dx + dy * dy) <= (mg.button_size * mg.button_size)
+ end
+
+ if Input.select() or mouse_on_button then
Audio.sfx_drum_high()
mg.bar_fill = mg.bar_fill + mg.fill_per_press
diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua
index 8e13276..3f8abff 100644
--- a/inc/window/window.minigame.rhythm.lua
+++ b/inc/window/window.minigame.rhythm.lua
@@ -95,7 +95,14 @@ function MinigameRhythmWindow.update()
if mg.press_cooldown > 0 then
mg.press_cooldown = mg.press_cooldown - 1
end
- if Input.select() and mg.press_cooldown == 0 then
+ local mouse_on_button = false
+ if Mouse.clicked() then
+ local dx = Mouse.x() - mg.button_x
+ local dy = Mouse.y() - mg.button_y
+ mouse_on_button = (dx * dx + dy * dy) <= (mg.button_size * mg.button_size)
+ end
+
+ if (Input.select() or mouse_on_button) and mg.press_cooldown == 0 then
mg.button_pressed_timer = mg.button_press_duration
mg.press_cooldown = mg.press_cooldown_duration
local target_left = mg.target_center - (mg.target_width / 2)
diff --git a/inc/window/window.popup.lua b/inc/window/window.popup.lua
index b7ec674..52bdfc5 100644
--- a/inc/window/window.popup.lua
+++ b/inc/window/window.popup.lua
@@ -28,7 +28,7 @@ end
--- @within PopupWindow
function PopupWindow.update()
if Context.popup.show then
- if Input.menu_confirm() or Input.menu_back() then
+ if Input.select() or Input.back() then
PopupWindow.hide()
end
end