From b349ded281872a6b328f40c162e0479fc6c7a541 Mon Sep 17 00:00:00 2001 From: Zoltan Timar Date: Thu, 12 Mar 2026 18:10:50 +0100 Subject: [PATCH] feat: added intro sequence, fixed norman's sprite, placed him in various places --- inc/audio/audio.manager.lua | 3 + inc/decision/decision.go_to_sleep.lua | 4 +- inc/decision/decision.play_button_mash.lua | 4 +- inc/decision/decision.play_rhythm.lua | 4 +- inc/init/init.context.lua | 29 ++++++++ inc/screen/screen.home.lua | 7 +- inc/screen/screen.toilet.lua | 4 ++ inc/sprite/sprite.manager.lua | 79 +++++++++++++++------- inc/sprite/sprite.norman.lua | 30 ++++---- inc/window/window.game.lua | 23 +++++-- inc/window/window.menu.lua | 1 - inc/window/window.minigame.mash.lua | 29 +++++--- inc/window/window.minigame.rhythm.lua | 10 +-- inc/window/window.mysterious_man.lua | 64 +++++++++++++----- 14 files changed, 211 insertions(+), 80 deletions(-) diff --git a/inc/audio/audio.manager.lua b/inc/audio/audio.manager.lua index b302fc7..08cd186 100644 --- a/inc/audio/audio.manager.lua +++ b/inc/audio/audio.manager.lua @@ -40,3 +40,6 @@ function Audio.sfx_success() sfx(16, 'C-7', 60) end --- Plays bloop sound effect. --- @within Audio function Audio.sfx_bloop() sfx(21, 'C-3', 60) end +--- Plays alarm sound effect. +--- @within Audio +function Audio.sfx_alarm() sfx(61) end diff --git a/inc/decision/decision.go_to_sleep.lua b/inc/decision/decision.go_to_sleep.lua index bcc606d..7c369a5 100644 --- a/inc/decision/decision.go_to_sleep.lua +++ b/inc/decision/decision.go_to_sleep.lua @@ -5,8 +5,8 @@ Decision.register({ Meter.hide() Day.increase() MinigameRhythmWindow.start("game", { - focus_center_x = Config.screen.width / 2, - focus_center_y = Config.screen.height / 2, + focus_center_x = (Config.screen.width / 2) - 22, + focus_center_y = (Config.screen.height / 2) - 18, focus_initial_radius = 0, on_win = function() MysteriousManWindow.start() diff --git a/inc/decision/decision.play_button_mash.lua b/inc/decision/decision.play_button_mash.lua index b111cdf..3ac8995 100644 --- a/inc/decision/decision.play_button_mash.lua +++ b/inc/decision/decision.play_button_mash.lua @@ -4,8 +4,8 @@ Decision.register({ handle = function() Meter.hide() MinigameButtonMashWindow.start("game", { - focus_center_x = Config.screen.width / 2, - focus_center_y = Config.screen.height / 2, + focus_center_x = (Config.screen.width / 2) - 22, + focus_center_y = (Config.screen.height / 2) - 18, focus_initial_radius = 0, }) end, diff --git a/inc/decision/decision.play_rhythm.lua b/inc/decision/decision.play_rhythm.lua index e8cff8e..111c56b 100644 --- a/inc/decision/decision.play_rhythm.lua +++ b/inc/decision/decision.play_rhythm.lua @@ -4,8 +4,8 @@ Decision.register({ handle = function() Meter.hide() MinigameRhythmWindow.start("game", { - focus_center_x = Config.screen.width / 2, - focus_center_y = Config.screen.height / 2, + focus_center_x = (Config.screen.width / 2) - 22, + focus_center_y = (Config.screen.height / 2) - 18, focus_initial_radius = 0, }) end, diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua index 93a6609..5f41a31 100644 --- a/inc/init/init.context.lua +++ b/inc/init/init.context.lua @@ -37,6 +37,7 @@ function Context.initial_data() meters = Meter.get_initial(), timer = Timer.get_initial(), triggers = {}, + home_norman_visible = false, game = { current_screen = "home", current_situation = nil, @@ -86,6 +87,34 @@ function Context.new_game() Context.game_in_progress = true MenuWindow.refresh_menu_items() Screen.get_by_id(Context.game.current_screen).init() + MysteriousManWindow.start({ + text = [[ + Norman was never a bad + simulation engineer, but + we need to be careful in + letting him improve. We + need to distract him. + ]], + on_text_complete = function() + Audio.sfx_alarm() + Context.home_norman_visible = false + Util.go_to_screen_by_id("home") + MinigameButtonMashWindow.start("game", { + focus_center_x = (Config.screen.width / 2) - 22, + focus_center_y = (Config.screen.height / 2) - 18, + focus_initial_radius = 0, + target_points = 100, + instruction_text = "Wake up Norman!", + show_progress_text = false, + on_win = function() + Audio.music_play_wakingup() + Context.home_norman_visible = true + Meter.show() + Window.set_current("game") + end, + }) + end, + }) end --- Saves the current game state. diff --git a/inc/screen/screen.home.lua b/inc/screen/screen.home.lua index e5104ea..41321b6 100644 --- a/inc/screen/screen.home.lua +++ b/inc/screen/screen.home.lua @@ -7,5 +7,10 @@ Screen.register({ "go_to_sleep", "go_to_end", }, - background = "bedroom" + background = "bedroom", + draw = function() + if Context.home_norman_visible and Window.get_current_id() == "game" then + Sprite.draw_at("norman", 100, 80) + end + end }) diff --git a/inc/screen/screen.toilet.lua b/inc/screen/screen.toilet.lua index f388400..fdeccef 100644 --- a/inc/screen/screen.toilet.lua +++ b/inc/screen/screen.toilet.lua @@ -26,10 +26,14 @@ Screen.register({ local sw = Config.screen.width local cx = sw / 2 + local norman_x = math.floor(sw * 0.75) + local norman_y = math.floor(Config.screen.height * 0.75) local bar_w = math.floor(sw * 0.75) local bar_x = math.floor((sw - bar_w) / 2) local bar_h = 4 + Sprite.draw_at("norman", norman_x, norman_y) + Print.text_center("day " .. Context.day_count, cx, 10, Config.colors.white) local narrative = "reflecting on my past and present\n...\nboth eventually flushed." diff --git a/inc/sprite/sprite.manager.lua b/inc/sprite/sprite.manager.lua index d3b1250..9754b4f 100644 --- a/inc/sprite/sprite.manager.lua +++ b/inc/sprite/sprite.manager.lua @@ -2,6 +2,32 @@ local _sprites = {} local _active_sprites = {} +local function draw_sprite_instance(sprite_data, params) + local colorkey = params.colorkey or sprite_data.colorkey or 0 + local scale = params.scale or sprite_data.scale or 1 + local flip_x = params.flip_x or sprite_data.flip_x or 0 + local flip_y = params.flip_y or sprite_data.flip_y or 0 + local rot = params.rot or sprite_data.rot or 0 + + if sprite_data.sprites then + for i = 1, #sprite_data.sprites do + local sub_sprite = sprite_data.sprites[i] + spr( + sub_sprite.s, + params.x + (sub_sprite.x_offset or 0), + params.y + (sub_sprite.y_offset or 0), + sub_sprite.colorkey or colorkey, + sub_sprite.scale or scale, + sub_sprite.flip_x or flip_x, + sub_sprite.flip_y or flip_y, + sub_sprite.rot or rot + ) + end + else + spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot) + end +end + --- Registers a sprite definition. --- @within Sprite --- @param sprite_data table A table containing the sprite definition. @@ -59,6 +85,34 @@ function Sprite.hide(id) _active_sprites[id] = nil end +--- Draws a sprite immediately without scheduling it. +--- @within Sprite +--- @param id string The unique identifier of the sprite.
+--- @param x number The x-coordinate.
+--- @param y number The y-coordinate.
+--- @param[opt] colorkey number The color index for transparency.
+--- @param[opt] scale number The scaling factor.
+--- @param[opt] flip_x number Set to 1 to flip horizontally.
+--- @param[opt] flip_y number Set to 1 to flip vertically.
+--- @param[opt] rot number The rotation in degrees.
+function Sprite.draw_at(id, x, y, colorkey, scale, flip_x, flip_y, rot) + local sprite_data = _sprites[id] + if not sprite_data then + trace("Error: Attempted to draw non-registered sprite with id: " .. id) + return + end + + draw_sprite_instance(sprite_data, { + x = x, + y = y, + colorkey = colorkey, + scale = scale, + flip_x = flip_x, + flip_y = flip_y, + rot = rot, + }) +end + --- Draws all scheduled sprites. --- @within Sprite function Sprite.draw() @@ -68,29 +122,8 @@ function Sprite.draw() trace("Error: Sprite id " .. id .. " in _active_sprites is not registered.") _active_sprites[id] = nil end - - local colorkey = params.colorkey or sprite_data.colorkey or 0 - local scale = params.scale or sprite_data.scale or 1 - local flip_x = params.flip_x or sprite_data.flip_x or 0 - local flip_y = params.flip_y or sprite_data.flip_y or 0 - local rot = params.rot or sprite_data.rot or 0 - - if sprite_data.sprites then - for i = 1, #sprite_data.sprites do - local sub_sprite = sprite_data.sprites[i] - spr( - sub_sprite.s, - params.x + (sub_sprite.x_offset or 0), - params.y + (sub_sprite.y_offset or 0), - sub_sprite.colorkey or colorkey, - sub_sprite.scale or scale, - sub_sprite.flip_x or flip_x, - sub_sprite.flip_y or flip_y, - sub_sprite.rot or rot - ) - end - else - spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot) + if sprite_data then + draw_sprite_instance(sprite_data, params) end end end diff --git a/inc/sprite/sprite.norman.lua b/inc/sprite/sprite.norman.lua index d56eaaf..f355967 100644 --- a/inc/sprite/sprite.norman.lua +++ b/inc/sprite/sprite.norman.lua @@ -1,17 +1,23 @@ Sprite.register({ id = "norman", sprites = { - -- Body (sprite index 0) - { s = 0, x_offset = 0, y_offset = 0 }, - -- Head (sprite index 1) - { s = 1, x_offset = 0, y_offset = -8 }, - -- Left Arm (sprite index 2) - { s = 2, x_offset = -4, y_offset = 4 }, - -- Right Arm (sprite index 3, flipped) - { s = 3, x_offset = 4, y_offset = 4, flip_x = 1 }, -- Flipped arm - -- Left Leg (sprite index 4) - { s = 4, x_offset = -2, y_offset = 8 }, - -- Right Leg (sprite index 5, flipped) - { s = 5, x_offset = 2, y_offset = 8, flip_x = 1 } -- Flipped leg + { s = 272, x_offset = -4, y_offset = -4 }, + { s = 273, x_offset = 4, y_offset = -4 }, + { s = 288, x_offset = -4, y_offset = 4 }, + { s = 289, x_offset = 4, y_offset = 4 }, + { s = 304, x_offset = -4, y_offset = 12 }, + { s = 305, x_offset = 4, y_offset = 12 } } +}) + +Sprite.register({ + id = "sleeping_norman", + sprites = { + { s = 272, x_offset = 12, y_offset = -4, flip_y = 1 }, + { s = 273, x_offset = 12, y_offset = 4, flip_y = 1 }, + { s = 288, x_offset = 4, y_offset = -4, flip_y = 1 }, + { s = 289, x_offset = 4, y_offset = 4, flip_y = 1 }, + { s = 304, x_offset = -4, y_offset = -4, flip_y = 1 }, + { s = 305, x_offset = -4, y_offset = 4, flip_y = 1 } + } }) \ No newline at end of file diff --git a/inc/window/window.game.lua b/inc/window/window.game.lua index 5abc065..d1862dd 100644 --- a/inc/window/window.game.lua +++ b/inc/window/window.game.lua @@ -2,9 +2,7 @@ local _available_decisions = {} local _selected_decision_index = 1 ---- Draws the game window. ---- @within GameWindow -function GameWindow.draw() +local function draw_game_scene(underlay_draw) local screen = Screen.get_by_id(Context.game.current_screen) if not screen then return end if screen.background then @@ -12,6 +10,9 @@ function GameWindow.draw() elseif screen.background_color then rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color) end + if underlay_draw then + underlay_draw() + end if not Context.stat_screen_active and #_available_decisions > 0 then Decision.draw(_available_decisions, _selected_decision_index) end @@ -20,6 +21,19 @@ function GameWindow.draw() screen.draw() end +--- Draws the game window. +--- @within GameWindow +function GameWindow.draw() + draw_game_scene() +end + +--- Draws the game window with a custom underlay. +--- @within GameWindow +--- @param underlay_draw function A draw callback rendered after the background but before overlays.
+function GameWindow.draw_with_underlay(underlay_draw) + draw_game_scene(underlay_draw) +end + --- Updates the game window logic. --- @within GameWindow function GameWindow.update() @@ -31,12 +45,13 @@ function GameWindow.update() end local screen = Screen.get_by_id(Context.game.current_screen) + 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 current_situation_obj.update then + if current_situation_obj and type(current_situation_obj.update) == "function" then current_situation_obj.update() end end diff --git a/inc/window/window.menu.lua b/inc/window/window.menu.lua index 656b2ec..dce2118 100644 --- a/inc/window/window.menu.lua +++ b/inc/window/window.menu.lua @@ -26,7 +26,6 @@ end --- @within MenuWindow function MenuWindow.new_game() Context.new_game() - GameWindow.set_state("game") end --- Loads a game from the menu. diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua index 5d1fa11..d09b2bb 100644 --- a/inc/window/window.minigame.mash.lua +++ b/inc/window/window.minigame.mash.lua @@ -4,12 +4,14 @@ function MinigameButtonMashWindow.init_context() return { bar_fill = 0, - max_fill = 100, + target_points = 100, fill_per_press = 8, base_degradation = 0.15, degradation_multiplier = 0.006, button_pressed_timer = 0, button_press_duration = 8, + instruction_text = "MASH Z!", + show_progress_text = true, return_window = nil, bar_x = 20, bar_y = 10, @@ -35,6 +37,9 @@ function MinigameButtonMashWindow.init(params) for k, v in pairs(params) do defaults[k] = v end + if params.max_fill and not params.target_points then + defaults.target_points = params.max_fill + end end Context.minigame_button_mash = defaults end @@ -78,11 +83,11 @@ function MinigameButtonMashWindow.update() if Input.select() then mg.bar_fill = mg.bar_fill + mg.fill_per_press mg.button_pressed_timer = mg.button_press_duration - if mg.bar_fill > mg.max_fill then - mg.bar_fill = mg.max_fill + if mg.bar_fill > mg.target_points then + mg.bar_fill = mg.target_points end end - if mg.bar_fill >= mg.max_fill then + if mg.bar_fill >= mg.target_points then mg.win_timer = Config.timing.minigame_win_duration return end @@ -95,7 +100,7 @@ function MinigameButtonMashWindow.update() mg.button_pressed_timer = mg.button_pressed_timer - 1 end if mg.focus_center_x then - Focus.set_percentage(mg.bar_fill / mg.max_fill) + Focus.set_percentage(mg.bar_fill / mg.target_points) end end @@ -104,14 +109,16 @@ end function MinigameButtonMashWindow.draw() local mg = Context.minigame_button_mash if mg.return_window == "game" then - GameWindow.draw() + GameWindow.draw_with_underlay(function() + Sprite.draw_at("sleeping_norman", (Config.screen.width / 2) - 30, (Config.screen.height / 2) - 22) + end) end if not mg.focus_center_x then rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) end rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) - local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width + local fill_width = (mg.bar_fill / mg.target_points) * mg.bar_width if fill_width > 0 then local bar_color = Config.colors.light_blue if mg.bar_fill > 66 then @@ -130,9 +137,11 @@ function MinigameButtonMashWindow.draw() circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color) end Print.text_center("Z", mg.button_x, mg.button_y - 3, button_color) - Print.text_center("MASH Z!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey) - local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100) - Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black) + Print.text_center(mg.instruction_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey) + if mg.show_progress_text then + local points_text = math.floor(mg.bar_fill) .. "/" .. mg.target_points + Print.text_center(points_text, mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black) + end if mg.win_timer > 0 then Minigame.draw_win_overlay() diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua index 5dfc3d6..a874b18 100644 --- a/inc/window/window.minigame.rhythm.lua +++ b/inc/window/window.minigame.rhythm.lua @@ -130,7 +130,9 @@ end function MinigameRhythmWindow.draw() local mg = Context.minigame_rhythm if mg.return_window == "game" then - GameWindow.draw() + GameWindow.draw_with_underlay(function() + Sprite.draw_at("sleeping_norman", (Config.screen.width / 2) - 30, (Config.screen.height / 2) - 22) + end) end if not mg.focus_center_x then rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) @@ -144,12 +146,10 @@ function MinigameRhythmWindow.draw() rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.light_blue) local line_x = mg.bar_x + (mg.line_position * mg.bar_width) rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.item) - local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score - Print.text_center(score_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 8, Config.colors.light_grey) Print.text_center( - "Press Z when line is in green!", + "Sleep Norman ... Sleep!", Config.screen.width / 2, - mg.bar_y + mg.bar_height + 20, + mg.bar_y + mg.bar_height + 14, Config.colors.light_grey ) local button_color = Config.colors.light_grey diff --git a/inc/window/window.mysterious_man.lua b/inc/window/window.mysterious_man.lua index e4c5a90..7c4492c 100644 --- a/inc/window/window.mysterious_man.lua +++ b/inc/window/window.mysterious_man.lua @@ -4,12 +4,25 @@ local STATE_TEXT = "text" local STATE_DAY = "day" local STATE_CHOICE = "choice" +local DEFAULT_TEXT = [[ +Misterious man appears +during your sleep. + +He says nothing. +He doesn't need to. + +He says nothing. +]] + local state = STATE_TEXT local text_y = Config.screen.height local text_speed = 0.4 local day_timer = 0 local day_display_frames = 120 local selected_choice = 1 +local text = DEFAULT_TEXT +local day_text_override = nil +local on_text_complete = nil local choices = { { @@ -20,16 +33,6 @@ local choices = { }, } -local text = [[ -Misterious man appears -during your sleep. - -He says nothing. -He doesn't need to. - -He says nothing. -]] - --- Sets the scrolling text content. --- @within MysteriousManWindow --- @param new_text string The text to display. @@ -39,22 +42,49 @@ end --- Starts the mysterious man window. --- @within MysteriousManWindow -function MysteriousManWindow.start() +--- @param[opt] options table Optional window configuration.
+--- Fields:
+--- * text (string) Override for the scrolling text.
+--- * day_text (string) Override for the centered day label.
+--- * on_text_complete (function) Callback fired once when the text phase ends.
+function MysteriousManWindow.start(options) + options = options or {} state = STATE_TEXT text_y = Config.screen.height day_timer = 0 selected_choice = 1 + text = options.text or DEFAULT_TEXT + day_text_override = options.day_text + on_text_complete = options.on_text_complete Meter.hide() Window.set_current("mysterious_man") end +local function go_to_day_state() + if on_text_complete then + on_text_complete() + on_text_complete = nil + end + if Window.get_current_id() ~= "mysterious_man" then + return + end + state = STATE_DAY + day_timer = day_display_frames +end + local function wake_up() + Context.home_norman_visible = false Util.go_to_screen_by_id("home") MinigameButtonMashWindow.start("game", { - focus_center_x = Config.screen.width / 2, - focus_center_y = Config.screen.height / 2, + focus_center_x = (Config.screen.width / 2) - 22, + focus_center_y = (Config.screen.height / 2) - 18, focus_initial_radius = 0, + target_points = 100, + instruction_text = "Wake up Norman!", + show_progress_text = false, on_win = function() + Audio.music_play_wakingup() + Context.home_norman_visible = true Meter.show() Window.set_current("game") end, @@ -79,13 +109,11 @@ function MysteriousManWindow.update() end if text_y < -lines * 8 then - state = STATE_DAY - day_timer = day_display_frames + go_to_day_state() end if Input.select() then - state = STATE_DAY - day_timer = day_display_frames + go_to_day_state() end elseif state == STATE_DAY then day_timer = day_timer - 1 @@ -117,7 +145,7 @@ function MysteriousManWindow.draw() local x = (Config.screen.width - 132) / 2 Print.text(text, x, text_y, Config.colors.light_grey) elseif state == STATE_DAY then - local day_text = "Day " .. Context.day_count + local day_text = day_text_override or ("Day " .. Context.day_count) Print.text_center( day_text, Config.screen.width / 2,