diff --git a/.gitignore b/.gitignore index a1781fb..36a1642 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.claude .local impostor.lua impostor.original.lua @@ -5,4 +6,5 @@ prompts docs minify.lua *.tic -*.zip \ No newline at end of file +*.zip +NOTES_* diff --git a/.luacheckrc b/.luacheckrc index 8f58d89..c8dfabd 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -10,7 +10,6 @@ globals = { "Discussion", "Util", "Decision", - "Situation", "Screen", "Sprite", "UI", @@ -31,7 +30,7 @@ globals = { "MenuWindow", "GameWindow", "PopupWindow", - "ConfigurationWindow", + "ControlsWindow", "AudioTestWindow", "MinigameButtonMashWindow", "MinigameRhythmWindow", @@ -66,6 +65,10 @@ globals = { "map", "time", "RLE", + "mouse", + "Mouse", + "print", + "musicator_generate_pattern", } diff --git a/impostor.inc b/impostor.inc index 7f39293..8b09814 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 @@ -38,10 +39,7 @@ sprite/sprite.matrix_architect.lua sprite/sprite.matrix_neo.lua sprite/sprite.matrix_oraculum.lua sprite/sprite.matrix_trinity.lua -situation/situation.manager.lua -situation/situation.drink_coffee.lua decision/decision.manager.lua -decision/decision.have_a_coffee.lua decision/decision.go_to_home.lua decision/decision.go_to_toilet.lua decision/decision.go_to_walking_to_office.lua @@ -72,7 +70,7 @@ window/window.intro.title.lua window/window.intro.ttg.lua window/window.intro.brief.lua window/window.menu.lua -window/window.configuration.lua +window/window.controls.lua window/window.audiotest.lua window/window.popup.lua window/window.minigame.mash.lua diff --git a/inc/decision/decision.have_a_coffee.lua b/inc/decision/decision.have_a_coffee.lua deleted file mode 100644 index a7c5212..0000000 --- a/inc/decision/decision.have_a_coffee.lua +++ /dev/null @@ -1,16 +0,0 @@ -Decision.register({ - id = "have_a_coffee", - label = "Have a Coffee", - handle = function() - local new_situation_id = Situation.apply("drink_coffee", Context.game.current_screen) - local level = Ascension.get_level() - local disc_id = "coworker_disc_0" - -- TODO: Add more discussions for levels above 3 - if level >= 1 and level <= 3 then - local suffix = Context.have_done_work_today and ("_asc_" .. level) or ("_" .. level) - disc_id = "coworker_disc" .. suffix - end - Discussion.start(disc_id, "game") - Context.game.current_situation = new_situation_id - end, -}) \ No newline at end of file diff --git a/inc/decision/decision.manager.lua b/inc/decision/decision.manager.lua index 8950ece..b0ce2c1 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,22 @@ 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 + + local bar_h = 16 + local bar_y = Config.screen.height - bar_h + local prev_zone = { x = 0, y = bar_y, w = 15, h = bar_h } + local next_zone = { x = Config.screen.width-15, y = bar_y, w = 15, h = bar_h } + local confirm_zone = { x = 15, y = bar_y, w = Config.screen.width-30, h = bar_h } + + if Mouse.zone(prev_zone) then + Audio.sfx_beep() + selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1) + elseif Mouse.zone(next_zone) then + Audio.sfx_beep() + selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1) + elseif Mouse.zone(confirm_zone) then + return selected_decision_index, true + end + + return selected_decision_index, false end diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua index a3d9100..728ab68 100644 --- a/inc/init/init.context.lua +++ b/inc/init/init.context.lua @@ -23,7 +23,7 @@ Context = {} --- * have_met_sumphore (boolean) Whether the player has talked to the homeless guy.
--- * have_been_to_office (boolean) Whether the player has been to the office.
--- * have_done_work_today (boolean) Whether the player has done work today.
---- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.
+--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID
function Context.initial_data() return { current_menu_item = 1, @@ -48,7 +48,6 @@ function Context.initial_data() have_met_sumphore = false, game = { current_screen = "home", - current_situation = nil, }, day_count = 1, delta_time = 0, diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua index b5c6d56..d15d850 100644 --- a/inc/init/init.module.lua +++ b/inc/init/init.module.lua @@ -3,12 +3,12 @@ Util = {} Meter = {} Minigame = {} Decision = {} -Situation = {} Screen = {} Map = {} UI = {} Print = {} Input = {} +Mouse = {} Sprite = {} Audio = {} Focus = {} diff --git a/inc/meta/meta.header.lua b/inc/meta/meta.header.lua index 0e64ae7..c001a35 100644 --- a/inc/meta/meta.header.lua +++ b/inc/meta/meta.header.lua @@ -4,5 +4,5 @@ -- desc: Life of a programmer -- site: https://git.teletype.hu/games/impostor -- license: MIT License --- version: 1.0-beta1 +-- version: 1.0-beta2 -- script: lua diff --git a/inc/screen/screen.manager.lua b/inc/screen/screen.manager.lua index c56419a..9d3eb0a 100644 --- a/inc/screen/screen.manager.lua +++ b/inc/screen/screen.manager.lua @@ -8,7 +8,6 @@ local _screens = {} --- @param screen_data.name string Display name of the screen. --- @param screen_data.decisions table Array of decision ID strings available on this screen. --- @param screen_data.background string Map ID used as background. ---- @param[opt] screen_data.situations table Array of situation ID strings. Defaults to {}. --- @param[opt] screen_data.init function Called when the screen is entered. Defaults to noop. --- @param[opt] screen_data.update function Called each frame while screen is active. Defaults to noop. --- @param[opt] screen_data.draw function Called after the focus overlay to draw screen-specific overlays. Defaults to noop. @@ -16,9 +15,6 @@ function Screen.register(screen_data) if _screens[screen_data.id] then trace("Warning: Overwriting screen with id: " .. screen_data.id) end - if not screen_data.situations then - screen_data.situations = {} - end if not screen_data.init then screen_data.init = function() end end @@ -43,7 +39,6 @@ end --- * name (string) Display name.
--- * decisions (table) Array of decision ID strings.
--- * background (string) Map ID used as background.
---- * situations (table) Array of situation ID strings.
--- * init (function) Called when the screen is entered.
--- * update (function) Called each frame while screen is active. function Screen.get_by_id(screen_id) @@ -58,7 +53,6 @@ end --- * name (string) Display name of the screen.
--- * decisions (table) Array of decision ID strings available on this screen.
--- * background (string) Map ID used as background.
---- * situations (table) Array of situation ID strings.
--- * init (function) Called when the screen is entered.
--- * update (function) Called each frame while screen is active.
function Screen.get_all() 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.office.lua b/inc/screen/screen.office.lua index 917f953..8d8aa89 100644 --- a/inc/screen/screen.office.lua +++ b/inc/screen/screen.office.lua @@ -6,9 +6,6 @@ Screen.register({ "go_to_walking_to_home", "have_a_coffee", }, - situations = { - "drink_coffee", - }, init = function() Audio.music_play_room_work() end, 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/situation/situation.drink_coffee.lua b/inc/situation/situation.drink_coffee.lua deleted file mode 100644 index d791d0e..0000000 --- a/inc/situation/situation.drink_coffee.lua +++ /dev/null @@ -1,6 +0,0 @@ -Situation.register({ - id = "drink_coffee", - handle = function() - Audio.sfx_select() - end, -}) diff --git a/inc/situation/situation.manager.lua b/inc/situation/situation.manager.lua deleted file mode 100644 index 17d2e4e..0000000 --- a/inc/situation/situation.manager.lua +++ /dev/null @@ -1,84 +0,0 @@ ---- @section Situation -local _situations = {} - ---- Registers a situation definition. ---- @within Situation ---- @param situation table The situation data table. ---- @param situation.id string Unique situation identifier.
---- @param[opt] situation.screen_id string ID of the screen this situation belongs to.
---- @param[opt] situation.handle function Called when the situation is applied. Defaults to noop.
---- @param[opt] situation.update function Called each frame while situation is active. Defaults to noop.
-function Situation.register(situation) - if not situation or not situation.id then - PopupWindow.show({"Error: Invalid situation object registered (missing id)!"}) - return - end - if not situation.handle then - situation.handle = function() end - end - if not situation.update then - situation.update = function() end - end - if _situations[situation.id] then - trace("Warning: Overwriting situation with id: " .. situation.id) - end - _situations[situation.id] = situation -end - ---- Gets a situation by ID. ---- @within Situation ---- @param id string The situation ID. ---- @return result table The situation table or nil.
---- Fields:
---- * id (string) Unique situation identifier.
---- * screen_id (string) ID of the screen this situation belongs to.
---- * handle (function) Called when the situation is applied.
---- * update (function) Called each frame while situation is active.
-function Situation.get_by_id(id) - return _situations[id] -end - ---- Gets all registered situations, optionally filtered by screen ID. ---- @within Situation ---- @param screen_id string Optional. If provided, returns situations associated with this screen ID. ---- @return result table A table containing all registered situation data, indexed by their IDs, or an array filtered by screen_id.
---- Fields:
---- * id (string) Unique situation identifier.
---- * screen_id (string) ID of the screen this situation belongs to.
---- * handle (function) Called when the situation is applied.
---- * update (function) Called each frame while situation is active.
-function Situation.get_all(screen_id) - if screen_id then - local filtered_situations = {} - for _, situation in pairs(_situations) do - if situation.screen_id == screen_id then - table.insert(filtered_situations, situation) - end - end - return filtered_situations - end - return _situations -end - ---- Applies a situation, checking screen compatibility and returning the new situation ID if successful. ---- @within Situation ---- @param id string The situation ID to apply. ---- @param current_screen_id string The ID of the currently active screen. ---- @return string|nil The ID of the applied situation if successful, otherwise nil. -function Situation.apply(id, current_screen_id) - local situation = Situation.get_by_id(id) - local screen = Screen.get_by_id(current_screen_id) - - if not situation then - trace("Error: No situation found with id: " .. id) - return nil - end - - if Util.contains(screen.situations, id) then - situation.handle() - return id - else - trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. current_screen_id .. ").") - return nil - end -end diff --git a/inc/system/system.input.lua b/inc/system/system.input.lua index aeb1900..5f0cc15 100644 --- a/inc/system/system.input.lua +++ b/inc/system/system.input.lua @@ -5,10 +5,9 @@ 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 +local INPUT_KEY_BACKSPACE = 51 --- Checks if Up is pressed. --- @within Input @@ -22,22 +21,12 @@ 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_A) 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. +function Input.back() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_BACKSPACE) end +--- Checks if Enter 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.enter() return keyp(INPUT_KEY_ENTER) 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..3140ffa --- /dev/null +++ b/inc/system/system.mouse.lua @@ -0,0 +1,76 @@ +--- @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 + +--- Returns true if the mouse is within the given circle. +--- @within Mouse +--- @param cx number Center x. +--- @param cy number Center y. +--- @param r number Radius. +function Mouse.in_circle(cx, cy, r) + local dx = _mx - cx + local dy = _my - cy + return (dx * dx + dy * dy) <= (r * r) +end + +--- Returns true if the mouse was clicked inside the given rectangle, and consumes the click. +--- @within Mouse +--- @param rect table A table with fields: x, y, w, h. +function Mouse.zone(rect) + if Mouse.clicked() and Mouse.in_rect(rect.x, rect.y, rect.w, rect.h) then + Mouse.consume() + return true + end + return false +end + +--- Returns true if the mouse was clicked inside the given circle, and consumes the click. +--- @within Mouse +--- @param circle table A table with fields: x, y, r. +function Mouse.zone_circle(circle) + if Mouse.clicked() and Mouse.in_circle(circle.x, circle.y, circle.r) then + Mouse.consume() + return true + end + return false +end diff --git a/inc/system/system.print.lua b/inc/system/system.print.lua index b38b67d..ffbeba1 100644 --- a/inc/system/system.print.lua +++ b/inc/system/system.print.lua @@ -10,7 +10,7 @@ function Print.text(text, x, y, color, fixed, scale) local shadow_color = Config.colors.black if color == shadow_color then shadow_color = Config.colors.light_grey end scale = scale or 1 - print(text, x + 1, y + 1, shadow_color, fixed, scale) + print(text, x + scale, y + scale, shadow_color, fixed, scale) print(text, x, y, color, fixed, scale) end @@ -24,7 +24,7 @@ end --- @param[opt] scale number The scaling factor.
function Print.text_center(text, x, y, color, fixed, scale) scale = scale or 1 - local text_width = print(text, 0, -6, 0, fixed, scale) + local text_width = print(text, 0, -6 * scale, 0, fixed, scale) local centered_x = x - (text_width / 2) Print.text(text, centered_x, y, color, fixed, scale) end diff --git a/inc/system/system.ui.lua b/inc/system/system.ui.lua index e9f0cd4..4fb219b 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,25 @@ function UI.update_menu(items, selected_item) selected_item = 1 end end - return selected_item + + if x ~= nil and y ~= nil then + 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 + if Mouse.zone({ x = menu_x - 8, y = y + (i-1) * 10, w = Config.screen.width, h = 10 }) then + 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 deleted file mode 100644 index 938da45..0000000 --- a/inc/window/window.configuration.lua +++ /dev/null @@ -1,102 +0,0 @@ ---- @section ConfigurationWindow -ConfigurationWindow.controls = {} -ConfigurationWindow.selected_control = 1 - ---- Initializes configuration window. ---- @within ConfigurationWindow -function ConfigurationWindow.init() - ConfigurationWindow.controls = { - { - label = "Save", - action = function() Config.save() end, - type = "action_item" - }, - { - label = "Restore Defaults", - action = function() Config.reset() end, - type = "action_item" - }, - } -end - ---- Draws configuration window. ---- @within ConfigurationWindow -function ConfigurationWindow.draw() - UI.draw_top_bar("Configuration") - - local x_start = 10 - local y_start = 40 - local x_value_right_align = Config.screen.width - 10 - local char_width = 4 - for i, control in ipairs(ConfigurationWindow.controls) do - local current_y = y_start + (i - 1) * 12 - local color = Config.colors.light_blue - if control.type == "numeric_stepper" then - local value = control.get() - local label_text = control.label - local value_text = string.format(control.format, value) - local value_x = x_value_right_align - (#value_text * char_width) - - if i == ConfigurationWindow.selected_control then - color = Config.colors.item - Print.text("<", x_start - 8, current_y, color) - Print.text(label_text, x_start, current_y, color) - Print.text(value_text, value_x, current_y, color) - Print.text(">", x_value_right_align + 4, current_y, color) - else - Print.text(label_text, x_start, current_y, color) - Print.text(value_text, value_x, current_y, color) - end - elseif control.type == "action_item" then - local label_text = control.label - if i == ConfigurationWindow.selected_control then - color = Config.colors.item - Print.text("<", x_start - 8, current_y, color) - Print.text(label_text, x_start, current_y, color) - Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color) - else - Print.text(label_text, x_start, current_y, color) - end - end - end - Print.text("Press B to go back", x_start, 120, Config.colors.light_grey) -end - ---- Updates configuration window logic. ---- @within ConfigurationWindow -function ConfigurationWindow.update() - if Input.menu_back() then - GameWindow.set_state("menu") - return - end - - if Input.up() then - ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1 - if ConfigurationWindow.selected_control < 1 then - ConfigurationWindow.selected_control = #ConfigurationWindow.controls - end - elseif Input.down() then - ConfigurationWindow.selected_control = ConfigurationWindow.selected_control + 1 - if ConfigurationWindow.selected_control > #ConfigurationWindow.controls then - ConfigurationWindow.selected_control = 1 - end - end - - local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control] - if control then - if control.type == "numeric_stepper" then - local current_value = control.get() - if Input.left() then - local new_value = math.max(control.min, current_value - control.step) - control.set(new_value) - elseif Input.right() then - local new_value = math.min(control.max, current_value + control.step) - control.set(new_value) - end - elseif control.type == "action_item" then - if Input.menu_confirm() then - control.action() - end - end - 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.controls.lua b/inc/window/window.controls.lua new file mode 100644 index 0000000..1a4cd89 --- /dev/null +++ b/inc/window/window.controls.lua @@ -0,0 +1,44 @@ +--- @section ControlsWindow +local _controls = { + { action = "Navigate", keyboard = "Arrow keys", gamepad = "D-pad" }, + { action = "Select / OK", keyboard = "Space", gamepad = "Z button" }, + { action = "Back", keyboard = "Backspace", gamepad = "B button" }, + { action = "Click", keyboard = "Mouse", gamepad = "" }, +} + +--- Draws the controls window. +--- @within ControlsWindow +function ControlsWindow.draw() + UI.draw_top_bar("Controls") + + local col_action = 4 + local col_keyboard = 80 + local col_gamepad = 170 + local row_h = 10 + local y_header = 18 + local y_start = 30 + + Print.text("Action", col_action, y_header, Config.colors.light_grey) + Print.text("Keyboard", col_keyboard, y_header, Config.colors.light_grey) + Print.text("Gamepad", col_gamepad, y_header, Config.colors.light_grey) + line(col_action, y_header + 8, Config.screen.width - 4, y_header + 8, Config.colors.dark_grey) + + for i, entry in ipairs(_controls) do + local y = y_start + (i - 1) * row_h + Print.text(entry.action, col_action, y, Config.colors.white) + Print.text(entry.keyboard, col_keyboard, y, Config.colors.light_blue) + if entry.gamepad ~= "" then + Print.text(entry.gamepad, col_gamepad, y, Config.colors.light_blue) + end + end + + Print.text("Space / Z button or click to go back", col_action, Config.screen.height - 10, Config.colors.light_grey) +end + +--- Updates the controls window logic. +--- @within ControlsWindow +function ControlsWindow.update() + if Input.back() or Input.select() then + Window.set_current("menu") + end +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 d1862dd..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 @@ -48,14 +48,6 @@ function GameWindow.update() if not screen or not screen.update then return end screen.update() - -- Handle current situation updates - if Context.game.current_situation then - local current_situation_obj = Situation.get_by_id(Context.game.current_situation) - if current_situation_obj and type(current_situation_obj.update) == "function" then - current_situation_obj.update() - end - end - if Context.stat_screen_active then return end -- Fetch and filter decisions locally @@ -68,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 ) @@ -77,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..3fd4378 100644 --- a/inc/window/window.intro.ttg.lua +++ b/inc/window/window.intro.ttg.lua @@ -27,13 +27,13 @@ function TTGIntroWindow.update() TTGIntroWindow.glitch_started = true end - -- Count menu_back presses during the intro - if Input.menu_back() then + -- Count enter presses during the intro + if Input.enter() 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..62afbf1 100644 --- a/inc/window/window.menu.lua +++ b/inc/window/window.menu.lua @@ -1,18 +1,62 @@ --- @section MenuWindow local _menu_items = {} +local _click_timer = 0 +local _anim = 0 +local _menu_max_w = 0 +local ANIM_SPEED = 2.5 +local HEADER_H = 28 + +--- Calculates the animated x position of the menu block. +--- @within MenuWindow +--- @return number x The left edge x coordinate for the menu. +function MenuWindow.calc_menu_x() + local center_start = Config.screen.width / 2 + local center_end = Config.screen.width * 0.72 + local center = center_start + _anim * (center_end - center_start) + return math.floor(center - _menu_max_w / 2) +end + +--- Draws the header with title and separator. +--- @within MenuWindow +function MenuWindow.draw_header() + rect(0, 0, Config.screen.width, HEADER_H, Config.colors.dark_grey) + rect(0, HEADER_H - 2, Config.screen.width, 2, Config.colors.light_blue) + + local cx = Config.screen.width / 2 + local subtitle = "Definitely not an" + if Context.test_mode then subtitle = subtitle .. " [TEST]" end + local sub_w = print(subtitle, 0, -6, 0, false, 1, true) + print(subtitle, math.floor(cx - sub_w / 2) + 1, 5, Config.colors.dark_grey, false, 1, true) + print(subtitle, math.floor(cx - sub_w / 2), 4, Config.colors.light_grey, false, 1, true) + + Print.text_center("IMPOSTOR", cx, 12, Config.colors.item, false, 2) +end + +--- Draws the 4x scaled Norman sprite on the left side of the screen. +--- @within MenuWindow +function MenuWindow.draw_norman() + local nx = math.floor(Config.screen.width * 0.45 / 2) - 32 + local ny = HEADER_H + math.floor((Config.screen.height - HEADER_H - 96) / 2) + spr(272, nx, ny, 0, 4) + spr(273, nx + 32, ny, 0, 4) + spr(288, nx, ny + 32, 0, 4) + spr(289, nx + 32, ny + 32, 0, 4) + spr(304, nx, ny + 64, 0, 4) + spr(305, nx + 32, ny + 64, 0, 4) +end --- Draws the menu window. --- @within MenuWindow function MenuWindow.draw() - local title = "Definitely not an Impostor" - if Context.test_mode then - title = title .. " (TEST MODE)" + MenuWindow.draw_header() + + if _anim > 0 then + MenuWindow.draw_norman() end - UI.draw_top_bar(title) local menu_h = #_menu_items * 10 - local y = 10 + (Config.screen.height - 10 - 10 - menu_h) / 2 - UI.draw_menu(_menu_items, Context.current_menu_item, 0, y, true) + local y = HEADER_H + math.floor((Config.screen.height - HEADER_H - 10 - menu_h) / 2) + UI.draw_menu(_menu_items, Context.current_menu_item, MenuWindow.calc_menu_x(), y, false) local ttg_text = "TTG" local ttg_w = print(ttg_text, 0, -10, 0, false, 1, false) @@ -22,9 +66,32 @@ end --- Updates the menu window logic. --- @within MenuWindow function MenuWindow.update() - Context.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item) + if _anim < 1 then + _anim = math.min(1, _anim + ANIM_SPEED * Context.delta_time) + end - if Input.menu_confirm() then + local menu_h = #_menu_items * 10 + local y = HEADER_H + math.floor((Config.screen.height - HEADER_H - 10 - menu_h) / 2) + + 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, MenuWindow.calc_menu_x(), y, false) + 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() @@ -64,11 +131,10 @@ function MenuWindow.exit() exit() end ---- Opens the configuration menu. +--- Opens the controls screen. --- @within MenuWindow -function MenuWindow.configuration() - ConfigurationWindow.init() - GameWindow.set_state("configuration") +function MenuWindow.controls() + Window.set_current("controls") end --- Opens the audio test menu. @@ -85,7 +151,7 @@ function MenuWindow.continued() GameWindow.set_state("continued") end ---- Opens the minigame ddr test menu. +--- Opens the DDR minigame test. --- @within MenuWindow function MenuWindow.ddr_test() AudioTestWindow.init() @@ -93,26 +159,34 @@ function MenuWindow.ddr_test() MinigameDDRWindow.start("menu", "generated", { special_mode = "only_nothing" }) end ---- Refreshes menu items. +--- Refreshes the list of menu items based on current game state. --- @within MenuWindow function MenuWindow.refresh_menu_items() _menu_items = {} if Context.game_in_progress then table.insert(_menu_items, {label = "Resume Game", decision = MenuWindow.resume_game}) - table.insert(_menu_items, {label = "Save Game", decision = MenuWindow.save_game}) + table.insert(_menu_items, {label = "Save Game", decision = MenuWindow.save_game}) end - table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game}) + table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game}) table.insert(_menu_items, {label = "Load Game", decision = MenuWindow.load_game}) - table.insert(_menu_items, {label = "Configuration", decision = MenuWindow.configuration}) + table.insert(_menu_items, {label = "Controls", decision = MenuWindow.controls}) if Context.test_mode then - table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test}) + table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test}) table.insert(_menu_items, {label = "To Be Continued...", decision = MenuWindow.continued}) - table.insert(_menu_items, {label = "DDR Test", decision = MenuWindow.ddr_test}) + table.insert(_menu_items, {label = "DDR Test", decision = MenuWindow.ddr_test}) end table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit}) + _menu_max_w = 0 + for _, item in ipairs(_menu_items) do + local w = print(item.label, 0, -10, 0, false, 1, false) + if w > _menu_max_w then _menu_max_w = w end + end + Context.current_menu_item = 1 + _click_timer = 0 + _anim = 0 end diff --git a/inc/window/window.minigame.ddr.lua b/inc/window/window.minigame.ddr.lua index 8db1962..5c92d1e 100644 --- a/inc/window/window.minigame.ddr.lua +++ b/inc/window/window.minigame.ddr.lua @@ -355,6 +355,12 @@ function MinigameDDRWindow.update() right = Input.right() } + for _, target in ipairs(mg.target_arrows) do + if Mouse.zone({ x = target.x, y = mg.target_y, w = mg.arrow_size, h = mg.arrow_size }) then + input_map[target.dir] = true + 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..ec52b9b 100644 --- a/inc/window/window.minigame.mash.lua +++ b/inc/window/window.minigame.mash.lua @@ -83,7 +83,9 @@ function MinigameButtonMashWindow.update() return end - if Input.select() then + local mouse_on_button = Mouse.zone_circle({ x = mg.button_x, y = mg.button_y, r = mg.button_size }) + + 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..15a893c 100644 --- a/inc/window/window.minigame.rhythm.lua +++ b/inc/window/window.minigame.rhythm.lua @@ -95,7 +95,9 @@ 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 = Mouse.zone_circle({ x = mg.button_x, y = mg.button_y, r = mg.button_size }) + + 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 diff --git a/inc/window/window.register.lua b/inc/window/window.register.lua index 2e9407c..a3388ac 100644 --- a/inc/window/window.register.lua +++ b/inc/window/window.register.lua @@ -16,8 +16,8 @@ Window.register("game", GameWindow) PopupWindow = {} Window.register("popup", PopupWindow) -ConfigurationWindow = {} -Window.register("configuration", ConfigurationWindow) +ControlsWindow = {} +Window.register("controls", ControlsWindow) AudioTestWindow = {} Window.register("audiotest", AudioTestWindow)