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