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,