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..4f7d075 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
@@ -50,6 +48,7 @@ decision/decision.go_to_end.lua
decision/decision.go_to_walking_to_home.lua
decision/decision.go_to_sleep.lua
decision/decision.do_work.lua
+decision/decision.have_a_coffee.lua
decision/decision.sumphore_discussion.lua
discussion/discussion.sumphore.lua
discussion/discussion.coworker.lua
@@ -72,7 +71,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
index a7c5212..985f3c9 100644
--- a/inc/decision/decision.have_a_coffee.lua
+++ b/inc/decision/decision.have_a_coffee.lua
@@ -2,7 +2,6 @@ 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
@@ -11,6 +10,5 @@ Decision.register({
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..3dcbb86 100644
--- a/inc/decision/decision.manager.lua
+++ b/inc/decision/decision.manager.lua
@@ -123,9 +123,13 @@ function Decision.draw(decisions, selected_decision_index)
local selected_decision = decisions[selected_decision_index]
local decision_label = Decision.get_label(selected_decision)
local text_y = bar_y + 4
- Print.text("<", 2, text_y, Config.colors.light_blue)
- Print.text_center(decision_label, Config.screen.width / 2, text_y, Config.colors.item)
- Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
+ local left_arrow_color = Input.left() and Config.colors.white or Config.colors.orange
+ local right_arrow_color = Input.right() and Config.colors.white or Config.colors.orange
+ local left_arrow_contour_color = Input.left() and Config.colors.white or Config.colors.black
+ local right_arrow_contour_color = Input.right() and Config.colors.white or Config.colors.black
+ Print.text_center_contour("<", 6, text_y, left_arrow_color, false, 1, left_arrow_contour_color)
+ Print.text_center_contour(decision_label, Config.screen.width / 2, text_y, Config.colors.orange)
+ Print.text_center_contour(">", Config.screen.width - 6, text_y, right_arrow_color, false, 1, right_arrow_contour_color)
end
end
@@ -134,6 +138,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 +147,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/decision/decision.play_button_mash.lua b/inc/decision/decision.play_button_mash.lua
index 3ac8995..22d5f9c 100644
--- a/inc/decision/decision.play_button_mash.lua
+++ b/inc/decision/decision.play_button_mash.lua
@@ -7,6 +7,9 @@ Decision.register({
focus_center_x = (Config.screen.width / 2) - 22,
focus_center_y = (Config.screen.height / 2) - 18,
focus_initial_radius = 0,
+ on_win = function()
+ Audio.music_play_room_work()
+ end
})
end,
})
diff --git a/inc/discussion/discussion.coworker.lua b/inc/discussion/discussion.coworker.lua
index 61529a8..344e04c 100644
--- a/inc/discussion/discussion.coworker.lua
+++ b/inc/discussion/discussion.coworker.lua
@@ -40,7 +40,7 @@ Discussion.register({
{
question = "Normann you look weird and unfocused. You are usually locked in and not like this, what's up?",
answers = {
- { label = "Nothing it's just, I noticed some bugs in the simulation, maybe.", next_step = 2 },
+ { label = "Some bugs I noticed, maybe...", next_step = 2 },
},
},
{
diff --git a/inc/init/init.ascension.lua b/inc/init/init.ascension.lua
index 4dae906..e343b90 100644
--- a/inc/init/init.ascension.lua
+++ b/inc/init/init.ascension.lua
@@ -93,7 +93,7 @@ function Ascension.draw(x, y, options)
else
color = lit_color
end
- print(ch, x + (i - 1) * spacing, y, color, false, 1, true)
+ Print.text_contour(ch, x + (i - 1) * spacing, y, color, false, 1)
end
end
diff --git a/inc/init/init.config.lua b/inc/init/init.config.lua
index 815daae..64dc372 100644
--- a/inc/init/init.config.lua
+++ b/inc/init/init.config.lua
@@ -18,7 +18,8 @@ function Config.initial_data()
white = 4,
item = 7,
meter_bg = 1,
- transparent = 12
+ transparent = 12,
+ orange = 7
},
timing = {
minigame_win_duration = 180
diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua
index a3d9100..53d4d99 100644
--- a/inc/init/init.context.lua
+++ b/inc/init/init.context.lua
@@ -23,11 +23,12 @@ 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,
test_mode = false,
+ mouse_trace = false,
popup = {
show = false,
content = {}
@@ -46,9 +47,10 @@ function Context.initial_data()
have_done_work_today = false,
should_ascend = false,
have_met_sumphore = false,
+ office_sprites = {},
+ walking_to_office_sprites = {},
game = {
current_screen = "home",
- current_situation = nil,
},
day_count = 1,
delta_time = 0,
@@ -100,15 +102,15 @@ function Context.new_game()
MysteriousManScreen.start({
text = [[
Norman was never a bad
- ...
+
simulation engineer,
- ...
+
but
- ...
+
we need to be careful
- ...
+
letting him improve.
- ...
+
We need to distract him.
]],
on_text_complete = function()
@@ -123,7 +125,7 @@ function Context.new_game()
instruction_text = "Wake up Norman!",
show_progress_text = false,
on_win = function()
- Audio.music_play_wakingup()
+ Audio.music_play_room_work()
Meter.show()
Window.set_current("game")
end,
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/logic/logic.meter.lua b/inc/logic/logic.meter.lua
index 001ddaa..2607582 100644
--- a/inc/logic/logic.meter.lua
+++ b/inc/logic/logic.meter.lua
@@ -8,10 +8,11 @@ local COMBO_MAX_BONUS = 0.16
local COMBO_TIMEOUT_FRAMES = 600
-- Internal meters for tracking game progress and player stats.
-Meter.COLOR_ISM = Config.colors.red
+Meter.COLOR_ISM = Config.colors.orange
Meter.COLOR_WPM = Config.colors.blue
-Meter.COLOR_BM = Config.colors.black
+Meter.COLOR_BM = Config.colors.red
Meter.COLOR_BG = Config.colors.meter_bg
+Meter.COLOR_CONTOUR = Config.colors.white
--- Gets initial meter values.
--- @within Meter
@@ -126,16 +127,16 @@ function Meter.draw()
local m = Context.meters
local max = Meter.get_max()
- local bar_w = 44
+ local screen_w = Config.screen.width
+ local screen_h = Config.screen.height
+ local bar_w = screen_w * 0.25
local bar_h = 2
- local bar_x = 182
- local label_x = 228
- local line_h = 5
- local start_y = 1
+ local edge = math.max(2, math.floor(screen_w * 0.03))
+ local bar_x = screen_w - bar_w - edge
+ local line_h = 3
+ local start_y = screen_h * 0.05
- local bar_offset = math.floor((line_h - bar_h) / 2)
-
local meter_list = {
{ key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 },
{ key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 },
@@ -144,15 +145,16 @@ function Meter.draw()
for _, meter in ipairs(meter_list) do
local label_y = start_y + meter.row * line_h
- local bar_y = label_y + bar_offset
+ local bar_y = label_y
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
+ rect(bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2, Meter.COLOR_CONTOUR)
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
if fill_w > 0 then
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
end
- print(meter.label, label_x, label_y, meter.color, false, 1, true)
+ ---print(meter.label, label_x, label_y, meter.color, false, 1, true)
end
local ascension_y = start_y + 3 * line_h + 1
- Ascension.draw(bar_x, ascension_y, { spacing = 5 })
+ Ascension.draw(bar_x, ascension_y, { spacing = 8 })
end
diff --git a/inc/logic/logic.minigame.lua b/inc/logic/logic.minigame.lua
index 47a687c..13ccb99 100644
--- a/inc/logic/logic.minigame.lua
+++ b/inc/logic/logic.minigame.lua
@@ -12,8 +12,9 @@ function Minigame.draw_win_overlay(win_text)
local box_h = th + padding * 2
local box_x = (Config.screen.width - box_w) / 2
local box_y = (Config.screen.height - box_h) / 2
-
+ local text_x = Config.screen.width / 2
rect(box_x, box_y, box_w, box_h, Config.colors.dark_grey)
rectb(box_x, box_y, box_w, box_h, Config.colors.white)
- Print.text_center(text, Config.screen.width / 2, box_y + padding, Config.colors.white)
+ Print.text_center_contour(text, text_x, box_y + padding, Config.colors.black, false, 1, Config.colors.white)
end
+
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..5a07a4c 100644
--- a/inc/screen/screen.mysterious_man.lua
+++ b/inc/screen/screen.mysterious_man.lua
@@ -6,52 +6,52 @@ local STATE_CHOICE = "choice"
local ASC_01_TEXT = [[
Normann seems to be in line,
- ...
+
and stays seeking for oxes
- ...
+
within the confines.
- ...
+
Very good.
]]
local ASC_12_TEXT = [[
We have a problem!
- ...
+
Normann formed his first thought.
- ...
+
He saw the tracks.
]]
local ASC_23_TEXT = [[
Not good, not terrible.
- ...
+
Normann caught his glimpse
- ...
+
of another way
- ...
+
- quite literally -
- ...
+
if this continues,
- ...
+
we will lose control.
]]
local ASC_34_TEXT = [[
There is no turning back now for Norman.
- ...
+
He caught on.
- ...
+
I hoped it would never come to this...
]]
--[[ Norman speaks for the first time during MM screen ]]
local ASC_45_TEXT = [[
Wait, who are you?
- ...
+
*silence*
- ...
+
Why am I seeing this?
- ...
+
*silence*
- ...
+
]]
local ascension_texts = {
@@ -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()
@@ -258,16 +261,18 @@ Screen.register({
end
if state == STATE_TEXT then
- local cx = Config.screen.width / 2
+ local screen_w = Config.screen.width
local line_y = text_y
for line in (text .. "\n"):gmatch("(.-)\n") do
- Print.text_center(line, cx, line_y, Config.colors.light_grey)
+ local line_w = print(line, 0, -6, 0, false, 1)
+ local line_x = math.floor((screen_w - line_w) / 2)
+ Print.text_contour(line, (line_x - 8), line_y, Config.colors.black, false, 1)
line_y = line_y + 8
end
elseif state == STATE_DAY then
MysteriousManScreen.draw_day_switch_background()
- local day_text = day_text_override or ("Day " .. Context.day_count)
- Print.text_center(
+ local day_text = day_text_override or ("day " .. Context.day_count)
+ Print.text_center_contour(
day_text,
Config.screen.width / 2,
Config.screen.height / 2 - 3,
diff --git a/inc/screen/screen.office.lua b/inc/screen/screen.office.lua
index 917f953..aca55b8 100644
--- a/inc/screen/screen.office.lua
+++ b/inc/screen/screen.office.lua
@@ -6,26 +6,45 @@ Screen.register({
"go_to_walking_to_home",
"have_a_coffee",
},
- situations = {
- "drink_coffee",
- },
init = function()
Audio.music_play_room_work()
+ Context.have_been_to_office = true
+
+ local possible_sprites = {
+ "dev_project_manager",
+ "dev_hr_girl",
+ "dev_introvert",
+ "dev_extrovert",
+ "dev_guru",
+ "dev_operator",
+ {id="dev_buddy", y_correct=1 * 8},
+ {id="dev_boy", y_correct=1 * 8},
+ {id="dev_girl", y_correct=1 * 8}
+ }
+
+ local possible_positions = {
+ {x = 6 * 8, y = 4 * 8},
+ {x = 10 * 8, y = 11 * 8 + 4},
+ {x = 12 * 8, y = 4 * 8},
+ {x = 15 * 8, y = 9 * 8},
+ {x = 16 * 8, y = 4 * 8},
+ {x = 17 * 8, y = 8 * 8},
+ {x = 17 * 8, y = 11 * 8},
+ {x = 20 * 8, y = 4 * 8},
+ {x = 23 * 8, y = 5 * 8},
+ {x = 22 * 8, y = 10 * 8 + 4},
+ {x = 27 * 8, y = 10 * 8 + 4},
+ {x = -4 + 5 * 8, y = 9 * 8}
+ }
+
+ Context.office_sprites = Sprite.list_randomize(possible_sprites, possible_positions)
end,
background = "office",
draw = function()
if Window.get_current_id() == "game" then
Sprite.draw_at("norman", 13 * 8, 9 * 8)
- Sprite.draw_at("dev_buddy", 15 * 8, 9 * 8)
- Sprite.draw_at("dev_project_manager", 6 * 8, 4 * 8)
- Sprite.draw_at("dev_hr_girl", 12 * 8, 4 * 8)
- Sprite.draw_at("dev_introvert", -4 + 5 * 8, 9 * 8)
- Sprite.draw_at("dev_extrovert", 20 * 8, 4 * 8)
- Sprite.draw_at("dev_girl", 23 * 8, 5 * 8)
- Sprite.draw_at("dev_boy", 10 * 8, 11 * 8 + 4)
- Sprite.draw_at("dev_guru", 22 * 8, 10 * 8 + 4)
- Sprite.draw_at("dev_operator", 27 * 8, 10 * 8 + 4)
+
+ Sprite.draw_list(Context.office_sprites)
end
- Context.have_been_to_office = true
end
})
diff --git a/inc/screen/screen.toilet.lua b/inc/screen/screen.toilet.lua
index 1d7be30..908787f 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()
@@ -36,13 +36,13 @@ Screen.register({
Sprite.draw_at("norman", norman_x, norman_y)
- Print.text_center("day " .. Context.day_count, cx, 10, Config.colors.white)
+ Print.text_center_contour("day " .. Context.day_count, cx, 10, Config.colors.black)
- local narrative = "reflecting on my past and present\n...\nboth eventually flushed."
+ local narrative = "reflecting on my past and present...\nboth eventually flushed..."
local wrapped = UI.word_wrap(narrative, 38)
local text_y = 24
for _, line in ipairs(wrapped) do
- Print.text_center(line, cx, text_y, Config.colors.light_grey)
+ Print.text_center_contour(line, cx, text_y, Config.colors.black)
text_y = text_y + 8
end
@@ -56,26 +56,26 @@ Screen.register({
local meter_start_y = text_y + 10
local meter_list = {
- { key = "wpm", label = "Work Productivity Meter" },
- { key = "ism", label = "Impostor Syndrome Meter" },
- { key = "bm", label = "Burnout Meter" },
+ { key = "wpm", label = "Work Productivity Meter", color = Meter.COLOR_WPM },
+ { key = "ism", label = "Impostor Syndrome Meter", color = Meter.COLOR_ISM },
+ { key = "bm", label = "Burnout Meter", color = Meter.COLOR_BM },
}
for i, meter in ipairs(meter_list) do
local y = meter_start_y + (i - 1) * 20
- Print.text_center(meter.label, cx, y, Config.colors.white)
+ Print.text_center_contour(meter.label, cx, y, meter.color, false, 1, Config.colors.white)
local bar_y = y + 8
local fill_w = math.max(0, math.floor((m[meter.key] / max_val) * bar_w))
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
if fill_w > 0 then
- rect(bar_x, bar_y, fill_w, bar_h, Config.colors.blue)
+ rect(bar_x, bar_y, fill_w, bar_h, meter.color)
end
local decay_w = print(decay_text, 0, -6, 0, false, 1)
- Print.text(decay_text, bar_x - decay_w - 4, bar_y, Config.colors.light_blue)
- Print.text(mult_text, bar_x + bar_w + 4, bar_y, Config.colors.light_blue)
+ Print.text_contour(decay_text, bar_x - decay_w - 4, bar_y, Config.colors.light_blue, false, 1, Config.colors.white)
+ Print.text_contour(mult_text, bar_x + bar_w + 4, bar_y, Config.colors.light_blue, false, 1, Config.colors.white)
end
if Ascension.get_level() > 0 then
diff --git a/inc/screen/screen.walking_to_office.lua b/inc/screen/screen.walking_to_office.lua
index 4e78252..af578d4 100644
--- a/inc/screen/screen.walking_to_office.lua
+++ b/inc/screen/screen.walking_to_office.lua
@@ -8,6 +8,28 @@ Screen.register({
},
init = function()
Audio.music_play_room_work()
+
+ local possible_sprites = {
+ "matrix_trinity",
+ "matrix_neo",
+ {id="matrix_oraculum", y_correct=1 * 8},
+ "matrix_architect"
+ }
+
+ local possible_positions = {
+ {x = 5 * 8, y = 11 * 8},
+ {x = 7 * 8, y = 11 * 8},
+ {x = 9 * 8, y = 11 * 8},
+ {x = 11 * 8, y = 11 * 8},
+ {x = 13 * 8, y = 11 * 8},
+ {x = 15 * 8, y = 11 * 8},
+ {x = 18 * 8, y = 11 * 8},
+ {x = 21 * 8, y = 11 * 8},
+ {x = 24 * 8, y = 11 * 8},
+ {x = 27 * 8, y = 11 * 8},
+ }
+
+ Context.walking_to_office_sprites = Sprite.list_randomize(possible_sprites, possible_positions)
end,
background = "street",
draw = function()
@@ -16,10 +38,8 @@ Screen.register({
Sprite.draw_at("sumphore", 9 * 8, 2 * 8)
Sprite.draw_at("pizza_vendor", 19 * 8, 1 * 8)
Sprite.draw_at("dev_guard", 22 * 8, 2 * 8)
- Sprite.draw_at("matrix_trinity", 5 * 8, 11 * 8)
- Sprite.draw_at("matrix_neo", 7 * 8, 11 * 8)
- Sprite.draw_at("matrix_oraculum", 9 * 8, 12 * 8)
- Sprite.draw_at("matrix_architect", 11 * 8, 11 * 8)
+
+ Sprite.draw_list(Context.walking_to_office_sprites)
end
end
})
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/sprite/sprite.manager.lua b/inc/sprite/sprite.manager.lua
index 8453484..2e7350e 100644
--- a/inc/sprite/sprite.manager.lua
+++ b/inc/sprite/sprite.manager.lua
@@ -73,6 +73,70 @@ function Sprite.generate_table(width, height, starting_s, x_base, y_base, x_step
return sprites
end
+--- Immediately draws a list of sprites
+--- @within Sprite
+--- @param sprite_list table An array of tables, each containing: `id` (string) sprite identifier, `x` (number) x-coordinate, `y` (number) y-coordinate, and optional `colorkey`, `scale`, `flip_x`, `flip_y`, `rot` parameters.
+function Sprite.draw_list(sprite_list)
+ for _, sprite_info in ipairs(sprite_list) do
+ local sprite_data = _sprites[sprite_info.id]
+ if not sprite_data then
+ trace("Error: Attempted to draw non-registered sprite with id: " .. sprite_info.id)
+ else
+ draw_sprite_instance(sprite_data, sprite_info)
+ end
+ end
+end
+
+--- Given a list of sprite IDs (or sprite entries with correction offsets) and a list of possible positions, randomly assigns each sprite to a unique position and returns a drawable list.
+--- @within Sprite
+--- @param sprite_ids table An array of sprite identifier values or tables.
+--- Each entry may be either:
+--- - string: sprite ID to draw.
+--- - table: { sprite_id = string, x_correct = number, y_correct = number }.
+--- @param positions table An array of tables, each containing `x` and `y` fields for possible sprite positions.
+function Sprite.list_randomize(sprite_ids, positions)
+ if #sprite_ids > #positions then
+ trace("Error: More sprite IDs than available positions in Sprite.draw_randomized")
+ return
+ end
+
+ local shuffled_positions = {}
+ for i, pos in ipairs(positions) do
+ shuffled_positions[i] = pos
+ end
+ for i = #shuffled_positions, 2, -1 do
+ local j = math.random(i)
+ shuffled_positions[i], shuffled_positions[j] = shuffled_positions[j], shuffled_positions[i]
+ end
+
+ local drawable_list = {}
+ for i, sprite_entry in ipairs(sprite_ids) do
+ local sprite_id = sprite_entry
+ local x_correct = 0
+ local y_correct = 0
+
+ if type(sprite_entry) == "table" then
+ sprite_id = sprite_entry.sprite_id or sprite_entry.id
+ x_correct = sprite_entry.x_correct or 0
+ y_correct = sprite_entry.y_correct or 0
+ end
+
+ local sprite_data = _sprites[sprite_id]
+ if not sprite_data then
+ trace("Error: Attempted to draw non-registered sprite with id: " .. tostring(sprite_id))
+ else
+ local pos = shuffled_positions[i]
+ table.insert(drawable_list, {
+ id = sprite_id,
+ x = pos.x + x_correct,
+ y = pos.y + y_correct
+ })
+ end
+ end
+
+ return drawable_list
+end
+
--- Schedules a sprite for drawing.
--- @within Sprite
--- @param id string The unique identifier of the sprite.
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..3470ed5
--- /dev/null
+++ b/inc/system/system.mouse.lua
@@ -0,0 +1,81 @@
+--- @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]
+
+ -- trace mouse position and tile for testing purposes
+ if Context.test_mode and Context.mouse_trace then
+ trace("Mouse: (" .. _mx .. "," .. _my .. "), tile: (" .. math.floor(_mx / 8) .. "," .. math.floor(_my / 8) .. ")")
+ end
+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..fdfdab8 100644
--- a/inc/system/system.print.lua
+++ b/inc/system/system.print.lua
@@ -10,7 +10,29 @@ 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
+
+--- Prints text with a contour (outline) instead of shadow.
+--- @within Print
+--- @param text string The text to print.
+--- @param x number The x-coordinate.
+--- @param y number The y-coordinate.
+--- @param color number The color of the text.
+--- @param[opt] fixed boolean If true, uses fixed-width font.
+--- @param[opt] scale number The scaling factor (also used for outline thickness).
+--- @param[opt] contour_color number Outline color; defaults to black; if equal to text color, uses white.
+function Print.text_contour(text, x, y, color, fixed, scale, contour_color)
+ scale = scale or 1
+ local cc = contour_color
+ if cc == nil then cc = Config.colors.black end
+ if color == cc then cc = Config.colors.white end
+ local ox = { -scale, scale, 0, 0, -scale, scale, -scale, scale }
+ local oy = { 0, 0, -scale, scale, -scale, -scale, scale, scale }
+ for i = 1, 8 do
+ print(text, x + ox[i], y + oy[i], cc, fixed, scale)
+ end
print(text, x, y, color, fixed, scale)
end
@@ -24,7 +46,23 @@ 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
+
+--- Prints centered text with contour instead of shadow.
+--- @within Print
+--- @param text string The text to print.
+--- @param x number The x-coordinate for centering.
+--- @param y number The y-coordinate.
+--- @param color number The color of the text.
+--- @param[opt] fixed boolean If true, uses fixed-width font.
+--- @param[opt] scale number The scaling factor.
+--- @param[opt] contour_color number Outline color; defaults to black; if equal to text color, uses white.
+function Print.text_center_contour(text, x, y, color, fixed, scale, contour_color)
+ scale = scale or 1
+ local text_width = print(text, 0, -6 * scale, 0, fixed, scale)
+ local centered_x = x - (text_width / 2)
+ Print.text_contour(text, centered_x, y, color, fixed, scale, contour_color)
+end
diff --git a/inc/system/system.ui.lua b/inc/system/system.ui.lua
index e9f0cd4..87f70c3 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.
@@ -68,12 +90,54 @@ end
--- @param[opt] bg_color number The background fill color (default: Config.colors.dark_grey).
--- @param[opt] border_color number The border color (default: Config.colors.white).
--- @param[opt] center_text boolean Whether to center each line inside the box. Defaults to false.
+local function draw_rounded_rect_fill(x, y, w, h, corner_radius, color)
+ local inner_w = w - corner_radius * 2
+ local inner_h = h - corner_radius * 2
+ if inner_w > 0 and inner_h > 0 then
+ rect(x + corner_radius, y + corner_radius, inner_w, inner_h, color)
+ end
+ if inner_w > 0 and corner_radius > 0 then
+ rect(x + corner_radius, y, inner_w, corner_radius, color)
+ rect(x + corner_radius, y + h - corner_radius, inner_w, corner_radius, color)
+ end
+ if corner_radius > 0 and inner_h > 0 then
+ rect(x, y + corner_radius, corner_radius, inner_h, color)
+ rect(x + w - corner_radius, y + corner_radius, corner_radius, inner_h, color)
+ end
+ for row = 0, corner_radius - 1 do
+ for col = 0, corner_radius - 1 do
+ if col + row >= corner_radius - 1 then
+ pix(x + corner_radius - 1 - col, y + corner_radius - 1 - row, color)
+ pix(x + w - corner_radius + col, y + corner_radius - 1 - row, color)
+ pix(x + corner_radius - 1 - col, y + h - corner_radius + row, color)
+ pix(x + w - corner_radius + col, y + h - corner_radius + row, color)
+ end
+ end
+ end
+end
+
+local function draw_rounded_rect_border(x, y, w, h, corner_radius, color)
+ line(x + corner_radius, y, x + w - corner_radius - 1, y, color)
+ line(x + corner_radius, y + h - 1, x + w - corner_radius - 1, y + h - 1, color)
+ line(x, y + corner_radius, x, y + h - corner_radius - 1, color)
+ line(x + w - 1, y + corner_radius, x + w - 1, y + h - corner_radius - 1, color)
+ pix(x + corner_radius - 1, y + 1, color)
+ pix(x + 1, y + corner_radius - 1, color)
+ pix(x + w - corner_radius, y + 1, color)
+ pix(x + w - 2, y + corner_radius - 1, color)
+ pix(x + corner_radius - 1, y + h - 2, color)
+ pix(x + 1, y + h - corner_radius, color)
+ pix(x + w - corner_radius, y + h - 2, color)
+ pix(x + w - 2, y + h - corner_radius, color)
+end
+
function UI.draw_textbox(text, box_x, box_y, box_w, box_h, scroll_y, color, bg_color, border_color, center_text)
color = color or Config.colors.white
- bg_color = bg_color or Config.colors.dark_grey
+ bg_color = bg_color or Config.colors.black
border_color = border_color or Config.colors.white
center_text = center_text or false
+ local r = 3
local padding = 4
local line_height = 8
local inner_x = box_x + padding
@@ -88,7 +152,7 @@ function UI.draw_textbox(text, box_x, box_y, box_w, box_h, scroll_y, color, bg_c
base_y = inner_y + math.floor((visible_height - text_height) / 2)
end
- rect(box_x, box_y, box_w, box_h, bg_color)
+ draw_rounded_rect_fill(box_x, box_y, box_w, box_h, r, bg_color)
for i, line in ipairs(lines) do
local ly = base_y + (i - 1) * line_height - scroll_y
@@ -101,7 +165,7 @@ function UI.draw_textbox(text, box_x, box_y, box_w, box_h, scroll_y, color, bg_c
end
end
- rectb(box_x, box_y, box_w, box_h, border_color)
+ draw_rounded_rect_border(box_x, box_y, box_w, box_h, r, border_color)
end
--- Wraps 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.discussion.lua b/inc/window/window.discussion.lua
index 69dd7ca..e51db29 100644
--- a/inc/window/window.discussion.lua
+++ b/inc/window/window.discussion.lua
@@ -24,7 +24,7 @@ function DiscussionWindow.draw()
TEXTBOX_W, TEXTBOX_H,
Context.discussion.scroll_y,
Config.colors.white,
- Config.colors.dark_grey,
+ Config.colors.black,
Config.colors.light_blue,
true
)
@@ -37,9 +37,13 @@ function DiscussionWindow.draw()
local selected = answers[Context.discussion.selected_answer]
local label = selected.label
local answer_text_y = bar_y + 4
- Print.text("<", 2, answer_text_y, Config.colors.light_blue)
- Print.text_center(label, Config.screen.width / 2, answer_text_y, Config.colors.item)
- Print.text(">", Config.screen.width - 6, answer_text_y, Config.colors.light_blue)
+ local left_arrow_color = Input.left() and Config.colors.white or Config.colors.orange
+ local right_arrow_color = Input.right() and Config.colors.white or Config.colors.orange
+ local left_arrow_contour_color = Input.left() and Config.colors.white or Config.colors.black
+ local right_arrow_contour_color = Input.right() and Config.colors.white or Config.colors.black
+ Print.text_center_contour("<", 6, answer_text_y, left_arrow_color, false, 1, left_arrow_contour_color)
+ Print.text_center(label, Config.screen.width / 2, answer_text_y, Config.colors.orange)
+ Print.text_center_contour(">", Config.screen.width - 6, answer_text_y, right_arrow_color, false, 1, right_arrow_contour_color)
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..6a0167c 100644
--- a/inc/window/window.minigame.ddr.lua
+++ b/inc/window/window.minigame.ddr.lua
@@ -1,7 +1,44 @@
--- @section MinigameDDRWindow
+---@class MinigameDDRState
+---@field special_mode string
+---@field bar_fill number
+---@field max_fill number
+---@field fill_per_hit number
+---@field miss_penalty number
+---@field bar_x number
+---@field bar_y number
+---@field bar_width number
+---@field bar_height number
+---@field arrow_size number
+---@field arrow_spawn_timer number
+---@field arrow_spawn_interval number
+---@field arrow_fall_speed number
+---@field arrows table
+---@field target_y number
+---@field target_arrows table
+---@field hit_threshold number
+---@field button_pressed_timers table
+---@field button_press_duration number
+---@field input_cooldowns table
+---@field input_cooldown_duration number
+---@field frame_counter number
+---@field current_song table?
+---@field pattern_index number
+---@field use_pattern boolean
+---@field generated_length number
+---@field return_window string?
+---@field win_timer number
+---@field on_win fun(ctx: MinigameDDRState)|nil
+---@field total_misses number
+---@field total_hits number
+---@field special_mode_condition boolean
+---@field special_mode_counter number
+---@field debug_song_key string|nil
+---@field debug_status string|nil
+
--- Background drawing for DDR minigame.
---- @witin MinigameDDRWindow
+--- @within MinigameDDRWindow
function MinigameDDRWindow.draw_background()
local img_values = {1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1}
local img_runs = {809,40,5,26,178,42,4,7,30,10,127,103,11,1,116,124,116,124,115,18,60,47,115,10,105,10,115,9,108,9,114,9,108,9,114,9,108,9,114,9,33,31,44,9,114,9,34,28,46,9,114,9,108,9,114,9,108,9,114,9,108,9,114,9,108,9,114,9,109,8,114,9,109,8,114,9,109,9,112,10,105,1,3,9,111,11,104,2,3,9,111,11,101,5,3,9,111,11,101,5,3,9,111,9,103,5,3,9,111,9,99,1,2,6,3,9,111,9,99,1,2,8,1,9,111,9,3,1,88,1,3,1,2,11,1,9,111,9,3,1,88,1,3,14,1,9,111,9,3,1,88,1,3,1,2,11,1,9,111,9,3,1,88,1,5,12,1,9,111,9,3,1,88,1,3,14,1,9,111,9,3,1,88,3,1,1,2,11,1,9,111,9,3,1,88,3,1,14,1,9,111,9,3,1,88,3,2,13,2,8,111,9,3,1,88,3,3,12,3,8,110,9,3,1,88,3,3,12,3,9,109,9,3,1,88,3,1,14,3,9,109,9,3,1,90,1,1,7,3,4,3,9,109,9,3,1,90,1,1,5,6,3,3,9,108,10,3,1,44,1,4,1,40,1,1,5,7,2,3,9,108,10,3,1,44,1,4,1,38,3,1,5,7,3,2,9,108,10,3,1,48,1,39,3,1,5,7,2,3,9,108,10,3,1,88,3,1,5,6,3,3,9,108,10,3,1,32,5,4,4,3,2,38,3,1,15,2,9,108,10,3,1,41,5,2,2,2,4,32,3,1,15,2,9,108,10,2,2,41,9,2,5,3,1,3,1,23,3,1,9,1,5,2,9,108,10,2,2,41,12,35,3,1,7,4,4,2,9,108,10,2,2,32,26,30,3,1,5,7,3,2,9,108,10,2,2,36,21,31,9,7,3,2,9,108,10,2,2,35,23,30,10,6,3,2,9,108,10,2,2,35,23,30,11,4,4,2,9,108,10,2,2,34,24,30,19,2,9,108,10,2,2,33,25,30,19,2,9,108,10,2,2,32,28,28,19,2,9,108,10,2,2,32,30,26,19,2,9,108,10,2,2,33,31,24,19,2,9,108,10,2,4,37,17,32,19,2,10,107,10,2,5,85,19,2,10,107,10,2,109,2,10,107,12,1,107,3,10,107,133,107,133,107,133,107,133,107,133,107,133,107,133,118,111,129,6,63,3,28,10,121,13,98,6,142,7,76,6,129,10,13,5,77,15,109,4,31,4,78,4,24,7,75,173,66,176,64,177,62,178,62,178,62,56,31,2,7,2,2,3,2,1,2,1,61,8,62,56,114,8,62,56,114,8,62,56,114,8,62,56,114,8,62,9,8,39,114,9,61,8,9,39,114,9,61,8,9,39,114,9,61,8,9,39,114,9,61,8,9,39,114,9,61,8,9,39,114,9,61,8,9,39,115,8,61,8,8,40,115,8,61,56,115,8,61,13,2,1,1,1,1,16,1,1,2,17,115,8,61,13,2,1,1,1,2,1,1,10,2,1,1,1,2,1,2,7,1,6,115,8,60,14,2,1,1,1,5,9,2,1,1,1,2,1,2,6,2,6,115,8,59,15,2,1,1,1,5,9,2,1,1,1,2,1,2,6,2,6,115,8,59,15,2,1,1,1,6,8,2,1,1,1,2,1,2,14,115,8,59,6,1,11,1,1,7,10,1,1,2,1,2,14,115,8,59,6,1,11,1,1,7,10,1,1,2,1,2,1,1,12,115,9,58,6,1,1,2,10,9,8,1,1,2,1,2,1,1,12,115,9,58,6,1,1,2,8,1,1,9,8,1,1,2,1,2,1,1,11,116,9,58,6,1,1,2,1,2,7,9,8,1,1,2,1,2,1,1,11,116,9,58,6,1,1,2,1,2,7,9,10,2,1,2,1,1,11,116,9,58,57,116,9,58,58,115,9,102,13,116,9,58,48,2,10,1,3,22,84,5,7,58,158,7,5,6,6,75,3,7,2,7,1,7,2,6,1,6,2,6,2,4,4,6,2,6,1,6,2,6,3,6,1,6,5,7,8,7,9,11,1,9,2,50,1,22,1,14,5,3,5,3,4,3,6,3,5,2,16,2,5,3,14,2,6,2,6,5,5,11,4,30,1,50,1,37,6,3,5,3,5,2,6,3,5,3,6,1,7,2,6,2,15,2,6,2,5,6,5,12,1,15,1,106,93,2,8,10,5,2,6,2,5,4,4,100,7,3,13,2,65,3,8,3,5,2,5,4,4,3,5,4,4,100,94,2,5,3,5,4,4,6,5,2,5,105,86,2,13,3,5,4,4,6,5,2,1,1,3,102,4,2,24,2,62,1,1,2,7,3,5,4,5,4,5,2,8,4,2,92,5,2,24,2,61,5,5,7,2,5,6,3,4,3,7,102,88,6,7,12,8,6,1,3,7,103,87,6,7,11,9,6,1,3,7,110,1,9,9,5,1,2,1,3,47,162,1,10,6,27,34,162,1,10,6,27,34,97,3,57,14,2,7,26,66,9,33,5,2,17,223,17,223,18,222,47,193,53,1,3,163,19,1,85,109,14,29,36,151,784}
@@ -12,7 +49,7 @@ end
--- Gets initial DDR minigame configuration.
--- @within MinigameDDRWindow
---- @return result table The default DDR minigame configuration.
+---@return MinigameDDRState
function MinigameDDRWindow.init_context()
local arrow_size = 12
local arrow_spacing = 30
@@ -33,7 +70,7 @@ function MinigameDDRWindow.init_context()
arrow_spawn_interval = 45,
arrow_fall_speed = 1.5,
arrows = {},
- target_y = 115,
+ target_y = 120,
target_arrows = {
{ dir = "left", x = start_x },
{ dir = "down", x = start_x + arrow_size + arrow_spacing },
@@ -60,25 +97,29 @@ function MinigameDDRWindow.init_context()
}
end
+--- Builds song data (and optional generated pattern) for the minigame.
+--- @within MinigameDDRWindow
function MinigameDDRWindow.prepareSong(song, generated_length, special_mode)
- local current_song = Util.deepcopy(song)
+ local current_song = Util.deepcopy(song)
+ if current_song.generated then
+ local pattern = musicator_generate_pattern(generated_length, current_song.bpm, current_song.spd * 4)
+ current_song.pattern = pattern
+ current_song.end_frame = pattern[#pattern].frame
- if current_song.generated then
- local pattern = musicator_generate_pattern(generated_length, current_song.bpm, current_song.spd * 4)
- current_song.pattern = pattern
- current_song.end_frame = pattern[#pattern].frame
-
- if special_mode == "only_special" then
- for i, _ in ipairs(current_song.pattern) do
- current_song.pattern[i].special = (i % 5 == 0)
- end
+ if special_mode == "only_special" then
+ for i, _ in ipairs(current_song.pattern) do
+ current_song.pattern[i].special = (i % 5 == 0)
end
end
+ end
- return current_song
+ return current_song
end
+--- Handles hit feedback and special-mode scoring for one arrow.
+--- @within MinigameDDRWindow
+---@param game_context MinigameDDRState
function MinigameDDRWindow.on_arrow_hit_special(arrow, game_context)
local special_mode = game_context.special_mode
@@ -109,13 +150,15 @@ function MinigameDDRWindow.on_arrow_hit_special(arrow, game_context)
end
end
+--- Ends the minigame: win timer, sfx, and special-mode pass/fail bookkeeping.
+--- @within MinigameDDRWindow
function MinigameDDRWindow.on_end(game_context)
Audio.sfx_select()
game_context.win_timer = Config.timing.minigame_win_duration
local num_special = 0
- for _,v in ipairs(game_context.current_song.pattern) do
+ for _, v in ipairs(game_context.current_song.pattern) do
if game_context.special_mode == "only_left" then
num_special = num_special + ((v.dir == "left" and 1) or 0)
else
@@ -123,12 +166,9 @@ function MinigameDDRWindow.on_end(game_context)
end
end
+ local sm = game_context.special_mode
local was_ok = true
- if game_context.special_mode == "normal" then
- was_ok = game_context.special_mode_counter == num_special
- elseif game_context.special_mode == "only_special" then
- was_ok = game_context.special_mode_counter == num_special
- elseif game_context.special_mode == "only_left" then
+ if sm == "normal" or sm == "only_special" or sm == "only_left" then
was_ok = game_context.special_mode_counter == num_special
end
@@ -192,12 +232,14 @@ end
local function spawn_arrow()
trace("random arrow")
+ ---@type MinigameDDRState
local mg = Context.minigame_ddr
+ local y0 = mg.bar_y + mg.bar_height + 10
local target = mg.target_arrows[math.random(1, 4)]
table.insert(mg.arrows, {
dir = target.dir,
x = target.x,
- y = mg.bar_y + mg.bar_height + 10
+ y = y0
})
end
@@ -205,13 +247,15 @@ end
--- @within MinigameDDRWindow
--- @param direction string The direction of the arrow ("left", "down", "up", "right").
local function spawn_arrow_dir(direction, note, special)
+ ---@type MinigameDDRState
local mg = Context.minigame_ddr
+ local y0 = mg.bar_y + mg.bar_height + 10
for _, target in ipairs(mg.target_arrows) do
if target.dir == direction then
table.insert(mg.arrows, {
dir = direction,
x = target.x,
- y = mg.bar_y + mg.bar_height + 10,
+ y = y0,
note = note,
special = special
})
@@ -225,6 +269,7 @@ end
--- @param arrow table The arrow data.
--- @return boolean True if the arrow is hit, false otherwise.
local function check_hit(arrow)
+ ---@type MinigameDDRState
local mg = Context.minigame_ddr
local distance = math.abs(arrow.y - mg.target_y)
return distance <= mg.hit_threshold
@@ -235,34 +280,53 @@ end
--- @param arrow table The arrow data.
--- @return boolean True if the arrow is missed, false otherwise.
local function check_miss(arrow)
+ ---@type MinigameDDRState
local mg = Context.minigame_ddr
return arrow.y > mg.target_y + mg.hit_threshold
end
---- Draws an arrow.
+--- Rotates a point (px, py) around (cx, cy) by a number of 90-degree CW steps.
+--- @within MinigameDDRWindow
+local function rotate(px, py, cx, cy, steps)
+ local dx, dy = px - cx, py - cy
+ for _ = 1, steps % 4 do
+ dx, dy = dy, -dx
+ end
+ return cx + dx, cy + dy
+end
+
+local arrow_rotations = { down = 0, right = 1, up = 2, left = 3 }
+
+--- Draws an arrow by rotating the "down" arrow shape.
--- @within MinigameDDRWindow
--- @param x number The x-coordinate.
--- @param y number The y-coordinate.
--- @param direction string The direction of the arrow.
--- @param color number The color of the arrow.
local function draw_arrow(x, y, direction, color)
- local size = 12
+ local size = 14
local half = size / 2
- if direction == "left" then
- tri(x + half, y, x, y + half, x + half, y + size, color)
- rectb(x + half, y + half - 2, half, 4, color)
- elseif direction == "right" then
- tri(x + half, y, x + size, y + half, x + half, y + size, color)
- rectb(x, y + half - 2, half, 4, color)
- elseif direction == "up" then
- tri(x, y + half, x + half, y, x + size, y + half, color)
- rectb(x + half - 2, y + half, 4, half, color)
- elseif direction == "down" then
- tri(x, y + half, x + half, y + size, x + size, y + half, color)
- rectb(x + half - 2, y, 4, half, color)
- end
+ local pivot_x, pivot_y = x + half, y + half
+ local steps = arrow_rotations[direction] or 0
+
+ local head_left_x, head_left_y = rotate(x, y + half, pivot_x, pivot_y, steps)
+ local head_tip_x, head_tip_y = rotate(x + half, y + size, pivot_x, pivot_y, steps)
+ local head_right_x, head_right_y = rotate(x + size, y + half, pivot_x, pivot_y, steps)
+
+ tri(head_left_x, head_left_y,
+ head_tip_x, head_tip_y,
+ head_right_x, head_right_y, color)
+
+ local stem_top_x, stem_top_y = rotate(x + half - 3, y, pivot_x, pivot_y, steps)
+ local stem_bot_x, stem_bot_y = rotate(x + half + 3, y + half, pivot_x, pivot_y, steps)
+ local stem_x = math.min(stem_top_x, stem_bot_x)
+ local stem_y = math.min(stem_top_y, stem_bot_y)
+ local stem_w = math.abs(stem_bot_x - stem_top_x)
+ local stem_h = math.abs(stem_bot_y - stem_top_y)
+ rectb(stem_x, stem_y, stem_w, stem_h, color)
end
+
--- Updates DDR minigame logic.
--- @within MinigameDDRWindow
function MinigameDDRWindow.update()
@@ -323,10 +387,7 @@ function MinigameDDRWindow.update()
arrow.y = arrow.y + mg.arrow_fall_speed
if check_miss(arrow) then
table.insert(arrows_to_remove, i)
- mg.bar_fill = mg.bar_fill - mg.miss_penalty
- if mg.bar_fill < 0 then
- mg.bar_fill = 0
- end
+ mg.bar_fill = math.max(0, mg.bar_fill - mg.miss_penalty)
mg.total_misses = mg.total_misses + 1
end
end
@@ -355,6 +416,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
@@ -364,20 +431,14 @@ function MinigameDDRWindow.update()
if arrow.dir == dir and check_hit(arrow) then
MinigameDDRWindow.on_arrow_hit_special(arrow, mg)
- mg.bar_fill = mg.bar_fill + mg.fill_per_hit
- if mg.bar_fill > mg.max_fill then
- mg.bar_fill = mg.max_fill
- end
+ mg.bar_fill = math.min(mg.max_fill, mg.bar_fill + mg.fill_per_hit)
table.remove(mg.arrows, i)
hit = true
break
end
end
if not hit then
- mg.bar_fill = mg.bar_fill - 2
- if mg.bar_fill < 0 then
- mg.bar_fill = 0
- end
+ mg.bar_fill = math.max(0, mg.bar_fill - 2)
mg.total_misses = mg.total_misses + 1
end
end
@@ -387,6 +448,7 @@ end
--- Draws DDR minigame.
--- @within MinigameDDRWindow
function MinigameDDRWindow.draw()
+ ---@type MinigameDDRState|nil
local mg = Context.minigame_ddr
if not mg then
cls(0)
@@ -414,8 +476,6 @@ function MinigameDDRWindow.draw()
end
rect(mg.bar_x, mg.bar_y, fill_width, mg.bar_height, bar_color)
end
- 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)
if mg.target_arrows then
for _, target in ipairs(mg.target_arrows) do
local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0
@@ -429,7 +489,7 @@ function MinigameDDRWindow.draw()
draw_arrow(arrow.x, arrow.y, arrow.dir, arrow_color)
end
end
- Print.text_center("Hit the arrows!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
+ Print.text_center_contour("Hit the arrows!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_blue)
local debug_y = 60
if mg.debug_status then
Print.text_center(mg.debug_status, Config.screen.width / 2, debug_y, Config.colors.item)
@@ -453,13 +513,14 @@ function MinigameDDRWindow.draw()
elseif Context.test_mode then
Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue)
end
-if mg.win_timer > 0 then
- if mg.special_mode_condition then
- Minigame.draw_win_overlay("SUCCESS...?")
- elseif mg.total_hits < 10 then
- Minigame.draw_win_overlay("MEH...")
- else
- Minigame.draw_win_overlay()
+
+ if mg.win_timer > 0 then
+ if mg.special_mode_condition then
+ Minigame.draw_win_overlay("SUCCESS...?")
+ elseif mg.total_hits < 10 then
+ Minigame.draw_win_overlay("MEH...")
+ else
+ Minigame.draw_win_overlay()
+ end
end
end
-end
diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua
index c39bfd9..467eaf1 100644
--- a/inc/window/window.minigame.mash.lua
+++ b/inc/window/window.minigame.mash.lua
@@ -49,6 +49,7 @@ end
--- @param return_window string The window ID to return to after the minigame.
--- @param[opt] params table Optional parameters for minigame configuration.
function MinigameButtonMashWindow.start(return_window, params)
+ Audio.music_stop()
MinigameButtonMashWindow.init(params)
local mg = Context.minigame_button_mash
mg.return_window = return_window or "game"
@@ -83,7 +84,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
@@ -143,7 +146,7 @@ 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(mg.instruction_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
+ Print.text_center_contour(mg.instruction_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_blue)
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)
diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua
index 8e13276..d3ba312 100644
--- a/inc/window/window.minigame.rhythm.lua
+++ b/inc/window/window.minigame.rhythm.lua
@@ -53,6 +53,7 @@ end
--- @param return_window string The window ID to return to after the minigame.
--- @param[opt] params table Optional parameters for minigame configuration.
function MinigameRhythmWindow.start(return_window, params)
+ Audio.music_stop()
MinigameRhythmWindow.init(params)
local mg = Context.minigame_rhythm
mg.return_window = return_window or "game"
@@ -95,7 +96,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)
@@ -144,14 +147,14 @@ function MinigameRhythmWindow.draw()
local target_left = mg.target_center - (mg.target_width / 2)
local target_x = mg.bar_x + (target_left * mg.bar_width)
local target_width_pixels = mg.target_width * mg.bar_width
- rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.light_blue)
+ rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.orange)
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)
- Print.text_center(
+ rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.light_blue)
+ Print.text_center_contour(
"Sleep Norman ... Sleep!",
Config.screen.width / 2,
mg.bar_y + mg.bar_height + 14,
- Config.colors.light_grey
+ Config.colors.light_blue
)
local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then
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)