input remapping + mouse control
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-04-02 18:19:25 +02:00
parent 6e1cf1db3e
commit 7697b35336
24 changed files with 178 additions and 45 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.claude
.local
impostor.lua
impostor.original.lua
@@ -5,4 +6,4 @@ prompts
docs
minify.lua
*.tic
*.zip
*.zip

View File

@@ -65,6 +65,10 @@ globals = {
"map",
"time",
"RLE",
"mouse",
"Mouse",
"print",
"musicator_generate_pattern",
}

View File

@@ -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

View File

@@ -134,6 +134,7 @@ end
--- @param decisions table A table of decision items.<br/>
--- @param selected_decision_index number The current index of the selected decision.<br/>
--- @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

View File

@@ -8,6 +8,7 @@ Map = {}
UI = {}
Print = {}
Input = {}
Mouse = {}
Sprite = {}
Audio = {}
Focus = {}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -17,6 +17,7 @@ end
--- @within Main
function TIC()
init_game()
Mouse.update()
local now = time()
if Context.last_frame_time == 0 then

View File

@@ -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

View File

@@ -38,8 +38,12 @@ end
--- @within UI
--- @param items table A table of menu items.<br/>
--- @param selected_item number The current index of the selected item.<br/>
--- @param[opt] x number Menu x position (required for mouse support).<br/>
--- @param[opt] y number Menu y position (required for mouse support).<br/>
--- @param[opt] centered boolean Whether the menu is centered horizontally.<br/>
--- @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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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