Compare commits

..

1 Commits

Author SHA1 Message Date
787b6656b0 docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-21 23:53:36 +01:00
45 changed files with 505 additions and 882 deletions

View File

@@ -11,11 +11,9 @@ globals = {
"Print", "Print",
"Input", "Input",
"Audio", "Audio",
"Config",
"Context", "Context",
"Meter", "Meters",
"Minigame", "Minigames",
"Window",
"SplashWindow", "SplashWindow",
"IntroWindow", "IntroWindow",
"MenuWindow", "MenuWindow",

View File

@@ -55,7 +55,7 @@ Based on the analysis of `impostor.lua`, the following regularities and conventi
--- ---
# Impostor Minigame Documentation # Impostor Minigames Documentation
This document provides comprehensive documentation for all three minigames implemented in the Impostor game: Button Mash, Rhythm, and DDR (Dance Dance Revolution). This document provides comprehensive documentation for all three minigames implemented in the Impostor game: Button Mash, Rhythm, and DDR (Dance Dance Revolution).
@@ -74,7 +74,7 @@ This document provides comprehensive documentation for all three minigames imple
The Impostor game includes three interactive minigames that can be triggered during gameplay. Each minigame presents a unique challenge that the player must complete to progress. All minigames feature: The Impostor game includes three interactive minigames that can be triggered during gameplay. Each minigame presents a unique challenge that the player must complete to progress. All minigames feature:
- **Overlay rendering** - Minigame render over the current game window - **Overlay rendering** - Minigames render over the current game window
- **Progress tracking** - Visual indicators show completion status - **Progress tracking** - Visual indicators show completion status
- **Return mechanism** - Automatic return to the calling window upon completion - **Return mechanism** - Automatic return to the calling window upon completion
- **Visual feedback** - Button presses and hits are visually indicated - **Visual feedback** - Button presses and hits are visually indicated

View File

@@ -58,8 +58,8 @@ export: build
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true @ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
watch: watch:
$(MAKE) build make build
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do $(MAKE) build; done fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
import_assets: $(OUTPUT) import_assets: $(OUTPUT)
@TIC_CMD="load $(OUTPUT) &"; \ @TIC_CMD="load $(OUTPUT) &"; \
@@ -221,3 +221,10 @@ docs: build
.PHONY: all build export watch import_assets export_assets clean lint ci-version ci-export ci-upload ci-update install_precommit_hook docs .PHONY: all build export watch import_assets export_assets clean lint ci-version ci-export ci-upload ci-update install_precommit_hook docs
#-- <WAVES>
#-- 000:224578acdeeeeddcba95434567653100
#-- </WAVES>
#
#-- <SFX>
#-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000
#-- </SFX>

View File

@@ -1,15 +1,10 @@
meta/meta.header.lua meta/meta.header.lua
init/init.module.lua init/init.modules.lua
init/init.config.lua init/init.config.lua
init/init.minigame.lua init/init.minigames.lua
init/init.meter.lua init/init.meters.lua
init/init.context.lua
system/system.util.lua system/system.util.lua
system/system.print.lua init/init.windows.lua
system/system.input.lua
system/system.ui.lua
audio/audio.manager.lua
audio/audio.songs.lua
sprite/sprite.manager.lua sprite/sprite.manager.lua
sprite/sprite.norman.lua sprite/sprite.norman.lua
situation/situation.manager.lua situation/situation.manager.lua
@@ -32,8 +27,12 @@ screen/screen.toilet.lua
screen/screen.walking_to_office.lua screen/screen.walking_to_office.lua
screen/screen.office.lua screen/screen.office.lua
screen/screen.walking_to_home.lua screen/screen.walking_to_home.lua
window/window.manager.lua init/init.context.lua
window/window.register.lua data/data.songs.lua
system/system.print.lua
system/system.input.lua
system/system.audio.lua
system/system.ui.lua
window/window.splash.lua window/window.splash.lua
window/window.intro.lua window/window.intro.lua
window/window.menu.lua window/window.menu.lua

View File

@@ -1,43 +0,0 @@
--- @section Audio
--- Stops current music.
--- @within Audio
function Audio.music_stop() music() end
--- Plays main menu music.
--- @within Audio
function Audio.music_play_mainmenu() end
--- Plays waking up music.
--- @within Audio
function Audio.music_play_wakingup() end
--- Plays room morning music.
--- @within Audio
function Audio.music_play_room_morning() end
--- Plays room street 1 music.
--- @within Audio
function Audio.music_play_room_street_1() end
--- Plays room street 2 music.
--- @within Audio
function Audio.music_play_room_street_2() end
--- Plays room music.
-- TODO: function name is incomplete, determine the correct room identifier
--- @within Audio
function Audio.music_play_room_() end
--- Plays room work music.
--- @within Audio
function Audio.music_play_room_work() end
--- Plays select sound effect.
--- @within Audio
function Audio.sfx_select() sfx(17, 'C-7', 30) end
--- Plays deselect sound effect.
--- @within Audio
function Audio.sfx_deselect() sfx(18, 'C-7', 30) end
--- Plays beep sound effect.
--- @within Audio
function Audio.sfx_beep() sfx(19, 'C-6', 30) end
--- Plays success sound effect.
--- @within Audio
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

View File

@@ -1,7 +1,6 @@
--- @section Songs
-- DDR Arrow Spawn Patterns -- DDR Arrow Spawn Patterns
-- Each song defines when arrows should spawn, synced to music beats -- Each song defines when arrows should spawn, synced to music beats
Songs = { Songs = {
-- Example song pattern -- Example song pattern
test_song = { test_song = {
@@ -108,12 +107,8 @@ Songs = {
} }
} }
--- Converts beats to frames. -- Helper function to calculate frame from beat
--- @within Songs -- Usage: frame_from_beat(beat_number, bpm, fps)
-- @param beat number The beat number.
-- @param bpm number Beats per minute.
-- @param[opt] fps number Frames per second (default: 60).
-- @return number The corresponding frame number.
function frame_from_beat(beat, bpm, fps) function frame_from_beat(beat, bpm, fps)
fps = fps or 60 fps = fps or 60
local seconds_per_beat = 60 / bpm local seconds_per_beat = 60 / bpm
@@ -121,12 +116,8 @@ function frame_from_beat(beat, bpm, fps)
return math.floor(beat * frames_per_beat) return math.floor(beat * frames_per_beat)
end end
--- Converts beat notation to frame pattern. -- Helper function to convert simple beat notation to frame pattern
--- @within Songs -- Usage: beats_to_pattern({{1, "left"}, {2, "down"}}, 120)
-- @param beats table A table of beat data, e.g., {{1, "left"}, {2, "down"}}.
-- @param bpm number Beats per minute.
-- @param[opt] fps number Frames per second (default: 60).
-- @return table The generated pattern.
function beats_to_pattern(beats, bpm, fps) function beats_to_pattern(beats, bpm, fps)
fps = fps or 60 fps = fps or 60
local pattern = {} local pattern = {}

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("home") Util.go_to_screen_by_id("home")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("office") Util.go_to_screen_by_id("office")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("toilet") Util.go_to_screen_by_id("toilet")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("walking_to_home") Util.go_to_screen_by_id("walking_to_home")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("walking_to_office") Util.go_to_screen_by_id("walking_to_office")
end, end,
condition = function() return true end
}) })

View File

@@ -2,7 +2,7 @@ Decision.register({
id = "have_a_coffee", id = "have_a_coffee",
label = "Have a Coffee", label = "Have a Coffee",
handle = function() handle = function()
local new_situation_id = Situation.apply("drink_coffee", Context.game.current_screen) Situation.apply("drink_coffee")
Context.game.current_situation = new_situation_id
end, end,
condition = function() return true end
}) })

View File

@@ -1,9 +1,5 @@
--- @section Decision
local _decisions = {} local _decisions = {}
--- Registers a decision definition.
--- @within Decision
-- @param decision table The decision data table.
function Decision.register(decision) function Decision.register(decision)
if not decision or not decision.id then if not decision or not decision.id then
PopupWindow.show({"Error: Invalid decision object registered (missing id)!"}) PopupWindow.show({"Error: Invalid decision object registered (missing id)!"})
@@ -26,50 +22,10 @@ function Decision.register(decision)
_decisions[decision.id] = decision _decisions[decision.id] = decision
end end
--- Gets a decision by ID. function Decision.get(id)
--- @within Decision
-- @param id string The ID of the decision.
-- @return table The decision table or nil.
function Decision.get_by_id(id)
return _decisions[id] return _decisions[id]
end end
--- Gets all registered decisions.
--- @within Decision
-- @return table A table of all registered decisions.
function Decision.get_all() function Decision.get_all()
return _decisions return _decisions
end end
--- Gets decision objects based on a screen's data.
--- @within Decision
-- @param screen_data table The data for the screen, containing a 'decisions' field (list of decision IDs).
-- @return table A table containing decision objects relevant to the screen.
function Decision.get_for_screen(screen_data)
if not screen_data or not screen_data.decisions then
return {}
end
local screen_decisions = {}
for _, decision_id in ipairs(screen_data.decisions) do
local decision = Decision.get_by_id(decision_id)
if decision then
table.insert(screen_decisions, decision)
end
end
return screen_decisions
end
--- Filters a list of decision objects based on their condition function.
--- @within Decision
-- @param decisions_list table A table of decision objects.
-- @return table A new table containing only the decisions for which condition() is true.
function Decision.filter_available(decisions_list)
local available = {}
for _, decision in ipairs(decisions_list) do
if decision and decision.condition() then
table.insert(available, decision)
end
end
return available
end

View File

@@ -1,5 +1,6 @@
Decision.register({ Decision.register({
id = "play_button_mash", id = "play_button_mash",
label = "Play Button Mash", label = "Play Button Mash",
handle = function() Meter.hide() MinigameButtonMashWindow.start("game") end, handle = function() Meters.hide() MinigameButtonMashWindow.start(WINDOW_GAME) end,
condition = function() return true end
}) })

View File

@@ -1,5 +1,6 @@
Decision.register({ Decision.register({
id = "play_ddr", id = "play_ddr",
label = "Play DDR (Random)", label = "Play DDR (Random)",
handle = function() Meter.hide() MinigameDDRWindow.start("game", nil) end, handle = function() Meters.hide() MinigameDDRWindow.start(WINDOW_GAME, nil) end,
condition = function() return true end
}) })

View File

@@ -1,5 +1,6 @@
Decision.register({ Decision.register({
id = "play_rhythm", id = "play_rhythm",
label = "Play Rhythm Game", label = "Play Rhythm Game",
handle = function() Meter.hide() MinigameRhythmWindow.start("game") end, handle = function() Meters.hide() MinigameRhythmWindow.start(WINDOW_GAME) end,
condition = function() return true end
}) })

View File

@@ -1,59 +1,53 @@
Config = {} local DEFAULT_CONFIG = {
screen = {
--- Return initial data for Config width = 240,
--- @within Config height = 136
function Config.initial_data() },
return { colors = {
screen = { black = 0,
width = 240, light_grey = 13,
height = 136 dark_grey = 14,
}, red = 2,
colors = { green = 6,
black = 2, blue = 9,
light_grey = 13, white = 12,
dark_grey = 14, item = 12,
red = 0, meter_bg = 12
green = 7, },
blue = 9, player = {
white = 12, sprite_id = 1
item = 12, },
meter_bg = 12 timing = {
}, splash_duration = 120
timing = {
splash_duration = 120
}
} }
end }
--- Restores default configuration settings. local Config = {
--- @within Config screen = DEFAULT_CONFIG.screen,
function Config.reset() colors = DEFAULT_CONFIG.colors,
local initial = Config.initial_data() player = DEFAULT_CONFIG.player,
Config.screen = initial.screen timing = DEFAULT_CONFIG.timing,
Config.colors = initial.colors }
Config.timing = initial.timing
end
local CONFIG_SAVE_BANK = 7 local CONFIG_SAVE_BANK = 7
local CONFIG_MAGIC_VALUE_ADDRESS = 2 local CONFIG_MAGIC_VALUE_ADDRESS = 2
local CONFIG_SPLASH_DURATION_ADDRESS = 3 local CONFIG_SPLASH_DURATION_ADDRESS = 3
local CONFIG_MAGIC_VALUE = 0xDE local CONFIG_MAGIC_VALUE = 0xDE
--- Saves the current configuration.
--- @within Config
function Config.save() function Config.save()
mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK)
mset(Config.timing.splash_duration, CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
end end
--- Loads saved configuration.
--- @within Config
function Config.load() function Config.load()
if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then
Config.timing.splash_duration = mget(CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK) Config.timing.splash_duration = mget(CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
else else
Config.reset() Config.restore_defaults()
end end
end end
function Config.restore_defaults()
Config.timing.splash_duration = DEFAULT_CONFIG.timing.splash_duration
end
Config.load() Config.load()

View File

@@ -2,73 +2,111 @@ local SAVE_GAME_BANK = 6
local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0 local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
local SAVE_GAME_MAGIC_VALUE = 0xCA local SAVE_GAME_MAGIC_VALUE = 0xCA
--- Global game context. local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6
--- @section Context
Context = {}
--- Gets initial data for Context. local function get_initial_data()
--- @within Context
-- @return table Initial context data.
function Context.initial_data()
return { return {
current_menu_item = 1, active_window = WINDOW_SPLASH,
intro = {
y = Config.screen.height,
speed = 0.5,
text = [[Norman Reds everyday life
seems ordinary: work,
meetings, coffee, and
endless notifications.
But beneath the surface
— within him, or around
him — something is
constantly building, and
it soon becomes clear
that there is more going
on than meets the eye.]]
},
current_screen = 1,
splash_timer = Config.timing.splash_duration, splash_timer = Config.timing.splash_duration,
popup = { popup = {
show = false, show = false,
content = {} content = {}
}, },
player = {
sprite_id = Config.player.sprite_id
},
ground = {
x = 0,
y = Config.screen.height,
w = Config.screen.width,
h = 8
},
menu_items = {},
selected_menu_item = 1,
selected_decision_index = 1,
game_in_progress = false, game_in_progress = false,
minigame_ddr = Minigame.get_default_ddr(), screens = {},
minigame_button_mash = Minigame.get_default_button_mash(), minigame_ddr = Minigames.get_default_ddr(),
minigame_rhythm = Minigame.get_default_rhythm(), minigame_button_mash = Minigames.get_default_button_mash(),
meters = Meter.get_initial(), minigame_rhythm = Minigames.get_default_rhythm(),
game = { meters = Meters.get_initial(),
current_screen = "home", --- Table storing currently active sprites to be drawn.
current_situation = nil, -- Each entry is a table with `id`, `x`, `y`, and other drawing parameters.
} sprites = {},
--- The ID of the currently active situation.
-- Set by `Situation.apply()` and `nil` if no situation is active.
current_situation = nil,
} }
end end
--- Resets game context to initial state. Context = {}
--- @within Context
function Context.reset() local function reset_context_to_initial_state()
local initial_data = Context.initial_data() local initial_data = get_initial_data()
for k in pairs(Context) do for k in pairs(Context) do
if type(Context[k]) ~= "function" then if type(Context[k]) ~= "function" then Context[k] = nil
Context[k] = nil
end end
end end
for k, v in pairs(initial_data) do for k, v in pairs(initial_data) do
Context[k] = v Context[k] = v
end end
Context.screens = {}
Context.screen_indices_by_id = {}
local screen_order = {"home", "toilet", "walking_to_office", "office", "walking_to_home"}
for i, screen_id in ipairs(screen_order) do
local screen_data = Screen.get_by_id(screen_id)
if screen_data then
table.insert(Context.screens, screen_data)
Context.screen_indices_by_id[screen_id] = i
else
PopupWindow.show({"Error: Screen '" .. screen_id .. "' not registered!"})
end
end
end end
--- Starts a new game. reset_context_to_initial_state()
--- @within Context
function Context.new_game() function Context.new_game()
Context.reset() reset_context_to_initial_state()
Context.game_in_progress = true Context.game_in_progress = true
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
Screen.get_by_id(Context.game.current_screen).init()
end end
--- Saves the current game state.
--- @within Context
function Context.save_game() function Context.save_game()
if not Context.game_in_progress then return end if not Context.game_in_progress then return end
mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK)
mset(Context.current_screen, SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK)
end end
--- Loads a saved game state.
--- @within Context
function Context.load_game() function Context.load_game()
if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then
Context.new_game() Context.new_game()
return return
end end
Context.reset()
reset_context_to_initial_state()
Context.current_screen = mget(SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK)
Context.game_in_progress = true Context.game_in_progress = true
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
Screen.get_by_id(Context.game.current_screen).init()
end end

View File

@@ -1,4 +1,3 @@
--- @section Meter
local METER_MAX = 1000 local METER_MAX = 1000
local METER_DEFAULT = 500 local METER_DEFAULT = 500
local METER_DECAY_PER_FRAME = 0.02 local METER_DECAY_PER_FRAME = 0.02
@@ -7,16 +6,12 @@ local COMBO_BASE_BONUS = 0.02
local COMBO_MAX_BONUS = 0.16 local COMBO_MAX_BONUS = 0.16
local COMBO_TIMEOUT_FRAMES = 600 local COMBO_TIMEOUT_FRAMES = 600
-- Internal meters for tracking game progress and player stats. Meters.COLOR_ISM = Config.colors.red
Meter.COLOR_ISM = Config.colors.red Meters.COLOR_WPM = Config.colors.blue
Meter.COLOR_WPM = Config.colors.blue Meters.COLOR_BM = Config.colors.black
Meter.COLOR_BM = Config.colors.black Meters.COLOR_BG = Config.colors.meter_bg
Meter.COLOR_BG = Config.colors.meter_bg
--- Gets initial meter values. function Meters.get_initial()
--- @within Meter
-- @return table A table of initial meter values.
function Meter.get_initial()
return { return {
ism = METER_DEFAULT, ism = METER_DEFAULT,
wpm = METER_DEFAULT, wpm = METER_DEFAULT,
@@ -27,38 +22,26 @@ function Meter.get_initial()
} }
end end
--- Hides meters. function Meters.hide()
--- @within Meter
function Meter.hide()
if Context and Context.meters then Context.meters.hidden = true end if Context and Context.meters then Context.meters.hidden = true end
end end
--- Shows meters. function Meters.show()
--- @within Meter
function Meter.show()
if Context and Context.meters then Context.meters.hidden = false end if Context and Context.meters then Context.meters.hidden = false end
end end
--- Gets max meter value. function Meters.get_max()
--- @within Meter
-- @return number The maximum meter value.
function Meter.get_max()
return METER_MAX return METER_MAX
end end
--- Gets combo multiplier. function Meters.get_combo_multiplier()
--- @within Meter
-- @return number The current combo multiplier.
function Meter.get_combo_multiplier()
if not Context or not Context.meters then return 1 end if not Context or not Context.meters then return 1 end
local combo = Context.meters.combo local combo = Context.meters.combo
if combo == 0 then return 1 end if combo == 0 then return 1 end
return 1 + math.min(COMBO_MAX_BONUS, COMBO_BASE_BONUS * (2 ^ (combo - 1))) return 1 + math.min(COMBO_MAX_BONUS, COMBO_BASE_BONUS * (2 ^ (combo - 1)))
end end
--- Updates all meters. function Meters.update()
--- @within Meter
function Meter.update()
if not Context or not Context.game_in_progress or not Context.meters then return end if not Context or not Context.game_in_progress or not Context.meters then return end
local m = Context.meters local m = Context.meters
m.ism = math.max(0, m.ism - METER_DECAY_PER_FRAME) m.ism = math.max(0, m.ism - METER_DECAY_PER_FRAME)
@@ -73,11 +56,7 @@ function Meter.update()
end end
end end
--- Adds amount to a meter. function Meters.add(key, amount)
--- @within Meter
-- @param key string The meter key (e.g., "wpm", "ism", "bm").
-- @param amount number The amount to add.
function Meter.add(key, amount)
if not Context or not Context.meters then return end if not Context or not Context.meters then return end
local m = Context.meters local m = Context.meters
if m[key] ~= nil then if m[key] ~= nil then
@@ -85,14 +64,12 @@ function Meter.add(key, amount)
end end
end end
--- Called on minigame completion. function Meters.on_minigame_complete()
--- @within Meter
function Meter.on_minigame_complete()
local m = Context.meters local m = Context.meters
local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier()) local gain = math.floor(METER_GAIN_PER_CHORE * Meters.get_combo_multiplier())
Meter.add("wpm", gain) Meters.add("wpm", gain)
Meter.add("ism", gain) Meters.add("ism", gain)
Meter.add("bm", gain) Meters.add("bm", gain)
m.combo = m.combo + 1 m.combo = m.combo + 1
m.combo_timer = 0 m.combo_timer = 0
end end

View File

@@ -1,11 +1,5 @@
-- Manages minigame configurations and initial states. Minigames = {}
--- @section Minigame
--- Applies parameters to defaults
--- @within Minigame
-- @param defaults table The default configuration table.
-- @param params table The parameters to apply.
-- @return table The updated configuration table.
local function apply_params(defaults, params) local function apply_params(defaults, params)
if not params then return defaults end if not params then return defaults end
for k, v in pairs(params) do for k, v in pairs(params) do
@@ -14,10 +8,7 @@ local function apply_params(defaults, params)
return defaults return defaults
end end
--- Gets default DDR minigame configuration. function Minigames.get_default_ddr()
--- @within Minigame
-- @return table The default DDR minigame configuration.
function Minigame.get_default_ddr()
local arrow_size = 12 local arrow_size = 12
local arrow_spacing = 30 local arrow_spacing = 30
local total_width = (4 * arrow_size) + (3 * arrow_spacing) local total_width = (4 * arrow_size) + (3 * arrow_spacing)
@@ -56,10 +47,7 @@ function Minigame.get_default_ddr()
} }
end end
--- Gets default button mash minigame configuration. function Minigames.get_default_button_mash()
--- @within Minigame
-- @return table The default button mash minigame configuration.
function Minigame.get_default_button_mash()
return { return {
bar_fill = 0, bar_fill = 0,
max_fill = 100, max_fill = 100,
@@ -79,10 +67,7 @@ function Minigame.get_default_button_mash()
} }
end end
--- Gets default rhythm minigame configuration. function Minigames.get_default_rhythm()
--- @within Minigame
-- @return table The default rhythm minigame configuration.
function Minigame.get_default_rhythm()
return { return {
line_position = 0, line_position = 0,
line_speed = 0.015, line_speed = 0.015,
@@ -109,26 +94,14 @@ function Minigame.get_default_rhythm()
} }
end end
--- Configures DDR minigame. function Minigames.configure_ddr(params)
--- @within Minigame return apply_params(Minigames.get_default_ddr(), params)
-- @param params table Optional parameters to override defaults.
-- @return table The configured DDR minigame state.
function Minigame.configure_ddr(params)
return apply_params(Minigame.get_default_ddr(), params)
end end
--- Configures button mash minigame. function Minigames.configure_button_mash(params)
--- @within Minigame return apply_params(Minigames.get_default_button_mash(), params)
-- @param params table Optional parameters to override defaults.
-- @return table The configured button mash minigame state.
function Minigame.configure_button_mash(params)
return apply_params(Minigame.get_default_button_mash(), params)
end end
--- Configures rhythm minigame. function Minigames.configure_rhythm(params)
--- @within Minigame return apply_params(Minigames.get_default_rhythm(), params)
-- @param params table Optional parameters to override defaults.
-- @return table The configured rhythm minigame state.
function Minigame.configure_rhythm(params)
return apply_params(Minigame.get_default_rhythm(), params)
end end

View File

@@ -1,13 +0,0 @@
Window = {}
Util = {}
Meter = {}
Minigame = {}
Decision = {}
Situation = {}
Screen = {}
Map = {}
UI = {}
Print = {}
Input = {}
Sprite = {}
Audio = {}

22
inc/init/init.modules.lua Normal file
View File

@@ -0,0 +1,22 @@
SplashWindow = {}
IntroWindow = {}
MenuWindow = {}
GameWindow = {}
PopupWindow = {}
ConfigurationWindow = {}
AudioTestWindow = {}
MinigameButtonMashWindow = {}
MinigameRhythmWindow = {}
MinigameDDRWindow = {}
Util = {}
Meters = {}
Minigames = {}
Decision = {}
Situation = {}
Screen = {}
Map = {}
UI = {}
Print = {}
Input = {}
Sprite = {}
Audio = {}

10
inc/init/init.windows.lua Normal file
View File

@@ -0,0 +1,10 @@
local WINDOW_SPLASH = 0
local WINDOW_INTRO = 1
local WINDOW_MENU = 2
local WINDOW_GAME = 3
local WINDOW_POPUP = 4
local WINDOW_CONFIGURATION = 7
local WINDOW_MINIGAME_BUTTON_MASH = 8
local WINDOW_MINIGAME_RHYTHM = 9
local WINDOW_MINIGAME_DDR = 10
local WINDOW_AUDIOTEST = 9001

View File

@@ -1,12 +1,5 @@
-- Manages game maps.
--- @section Map
local _maps = {} local _maps = {}
--- Gets all registered maps as an array.
--- @within Map
-- @return table An array of registered map data.
function Map.get_maps_array() function Map.get_maps_array()
local maps_array = {} local maps_array = {}
for _, map_data in pairs(_maps) do for _, map_data in pairs(_maps) do
@@ -15,9 +8,6 @@ function Map.get_maps_array()
return maps_array return maps_array
end end
--- Registers a map definition.
--- @within Map
-- @param map_data table The map data table.
function Map.register(map_data) function Map.register(map_data)
if _maps[map_data.id] then if _maps[map_data.id] then
trace("Warning: Overwriting map with id: " .. map_data.id) trace("Warning: Overwriting map with id: " .. map_data.id)
@@ -25,17 +15,10 @@ function Map.register(map_data)
_maps[map_data.id] = map_data _maps[map_data.id] = map_data
end end
--- Gets a map by ID.
--- @within Map
-- @param map_id string The ID of the map.
-- @return table The map data table or nil.
function Map.get_by_id(map_id) function Map.get_by_id(map_id)
return _maps[map_id] return _maps[map_id]
end end
--- Draws a map.
--- @within Map
-- @param map_id string The ID of the map to draw.
function Map.draw(map_id) function Map.draw(map_id)
local map_data = Map.get_by_id(map_id) local map_data = Map.get_by_id(map_id)
if not map_data then if not map_data then
@@ -49,4 +32,4 @@ function Map.draw(map_id)
map_data.to_x, map_data.to_x,
map_data.to_y map_data.to_y
) )
end end

View File

@@ -1,9 +1,13 @@
--- @section Screen
local _screens = {} local _screens = {}
--- Registers a screen definition. --- Registers a new screen definition with the Screen manager.
--- @within Screen -- Overwrites existing screen if an ID conflict occurs.
-- @param screen_data table The screen data table. -- @param screen_data table A table containing the screen definition.
-- Must include an `id` field (string).
-- Optional fields:
-- - `situations` (table): A table of situation IDs allowed on this screen. Defaults to an empty table.
-- - `init` (function): Function to execute once when the screen becomes active. Defaults to an empty function.
-- - `update` (function): Function to execute each frame while the screen is active. Defaults to an empty function.
function Screen.register(screen_data) function Screen.register(screen_data)
if _screens[screen_data.id] then if _screens[screen_data.id] then
trace("Warning: Overwriting screen with id: " .. screen_data.id) trace("Warning: Overwriting screen with id: " .. screen_data.id)
@@ -20,17 +24,9 @@ function Screen.register(screen_data)
_screens[screen_data.id] = screen_data _screens[screen_data.id] = screen_data
end end
--- Gets a screen by ID. --- Retrieves a registered screen by its ID.
--- @within Screen -- @param screen_id string The ID of the screen to retrieve.
-- @param screen_id string The ID of the screen. -- @return table The screen table, or `nil` if not found.
-- @return table The screen table or nil.
function Screen.get_by_id(screen_id) function Screen.get_by_id(screen_id)
return _screens[screen_id] return _screens[screen_id]
end end
--- Gets all registered screens.
--- @within Screen
-- @return table A table containing all registered screen data, indexed by their IDs.
function Screen.get_all()
return _screens
end

View File

@@ -1,9 +1,12 @@
--- @section Situation
local _situations = {} local _situations = {}
--- Registers a situation definition. --- Registers a new situation with the Situation manager.
--- @within Situation -- Overwrites existing situation if an ID conflict occurs.
-- @param situation table The situation data table. -- @param situation table A table containing the situation definition.
-- Must include an `id` field (string).
-- Optional fields:
-- - `handle` (function): Function to execute when the situation is applied. Defaults to an empty function.
-- - `update` (function): Function to execute each frame while the situation is active. Defaults to an empty function.
function Situation.register(situation) function Situation.register(situation)
if not situation or not situation.id then if not situation or not situation.id then
PopupWindow.show({"Error: Invalid situation object registered (missing id)!"}) PopupWindow.show({"Error: Invalid situation object registered (missing id)!"})
@@ -21,50 +24,32 @@ function Situation.register(situation)
_situations[situation.id] = situation _situations[situation.id] = situation
end end
--- Gets a situation by ID. --- Retrieves a registered situation by its ID.
--- @within Situation -- @param id string The ID of the situation to retrieve.
-- @param id string The situation ID. -- @return table The situation table, or `nil` if not found.
-- @return table The situation table or nil. function Situation.get(id)
function Situation.get_by_id(id)
return _situations[id] return _situations[id]
end end
--- Gets all registered situations, optionally filtered by screen ID. --- Applies a situation, making it the current active situation.
--- @within Situation -- The situation's `handle` function is executed.
-- @param screen_id string Optional. If provided, returns situations associated with this screen ID. -- This function first checks if the situation is valid and if it's allowed
-- @return table A table containing all registered situation data, indexed by their IDs, or filtered by screen_id. -- on the current screen (as defined in `Context.screens[Context.current_screen].situations`).
function Situation.get_all(screen_id) -- If successful, `Context.current_situation` is updated with the ID of the applied situation.
if screen_id then -- @param id string The ID of the situation to apply.
local filtered_situations = {} function Situation.apply(id)
for _, situation in pairs(_situations) do local situation = Situation.get(id)
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 if not situation then
trace("Error: No situation found with id: " .. id) trace("Error: No situation found with id: " .. id)
return nil return
end end
if Util.contains(screen.situations, id) then local current_screen_obj = Screen.get_by_id(Context.current_screen)
situation.handle() if current_screen_obj and not current_screen_obj.situations[id] then
return id trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. Context.current_screen .. ").")
else return
trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. current_screen_id .. ").")
return nil
end end
Context.current_situation = id
situation.handle()
end end

View File

@@ -1,10 +1,15 @@
--- @section Sprite
local _sprites = {} local _sprites = {}
local _active_sprites = {}
--- Registers a sprite definition. --- Registers a new sprite or complex sprite definition with the Sprite manager.
--- @within Sprite -- Overwrites existing sprite if an ID conflict occurs.
-- @param sprite_data table A table containing the sprite definition. -- @param sprite_data table A table containing the sprite definition.
-- Must include an `id` field (string) and either an `s` field (number, for simple sprites)
-- or a `sprites` field (table, for complex sprites).
-- For complex sprites, `sprites` is a table of sub-sprite definitions, each with:
-- - `s` (number): Sprite index.
-- - `x_offset` (number): X-offset relative to the parent sprite's position.
-- - `y_offset` (number): Y-offset relative to the parent sprite's position.
-- - Optional `colorkey`, `scale`, `flip_x`, `flip_y`, `rot` for individual sub-sprites.
function Sprite.register(sprite_data) function Sprite.register(sprite_data)
if not sprite_data or not sprite_data.id then if not sprite_data or not sprite_data.id then
trace("Error: Invalid sprite object registered (missing id)!") trace("Error: Invalid sprite object registered (missing id)!")
@@ -16,23 +21,25 @@ function Sprite.register(sprite_data)
_sprites[sprite_data.id] = sprite_data _sprites[sprite_data.id] = sprite_data
end end
--- Schedules a sprite for drawing. --- Schedules a registered sprite to be drawn at a specific position with optional transformations.
--- @within Sprite -- The sprite's parameters are stored in `Context.sprites` for deferred rendering by `Sprite.draw()`.
-- @param id string The unique identifier of the sprite. -- If the sprite with the given `id` is already scheduled, its parameters will be updated.
-- @param x number The x-coordinate. -- @param id string The unique identifier of the sprite to show. Must be registered via `Sprite.register`.
-- @param y number The y-coordinate. -- @param x number The x-coordinate on the screen where the sprite will be drawn.
-- @param[opt] colorkey number The color index for transparency. -- @param y number The y-coordinate on the screen where the sprite will be drawn.
-- @param[opt] scale number The scaling factor. -- @param[opt] colorkey number The color index to be treated as transparent (default: 0).
-- @param[opt] flip_x number Set to 1 to flip horizontally. -- @param[opt] scale number The scaling factor for the sprite (default: 1).
-- @param[opt] flip_y number Set to 1 to flip vertically. -- @param[opt] flip_x number Set to 1 to flip the sprite horizontally (default: 0).
-- @param[opt] rot number The rotation in degrees. -- @param[opt] flip_y number Set to 1 to flip the sprite vertically (default: 0).
-- @param[opt] rot number The rotation of the sprite in degrees (default: 0).
function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot) function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot)
-- Ensure the sprite exists before attempting to show it
if not _sprites[id] then if not _sprites[id] then
trace("Error: Attempted to show non-registered sprite with id: " .. id) trace("Error: Attempted to show non-registered sprite with id: " .. id)
return return
end end
_active_sprites[id] = { Context.sprites[id] = {
id = id, id = id,
x = x, x = x,
y = y, y = y,
@@ -44,30 +51,34 @@ function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot)
} }
end end
--- Hides a displayed sprite. --- Hides a currently displayed sprite by removing it from the `Context.sprites` table.
--- @within Sprite -- The sprite will no longer be drawn in subsequent frames.
-- @param id string The unique identifier of the sprite. -- @param id string The unique identifier of the sprite to hide.
function Sprite.hide(id) function Sprite.hide(id)
_active_sprites[id] = nil Context.sprites[id] = nil
end end
--- Draws all scheduled sprites. --- Draws all sprites currently scheduled in `Context.sprites`.
--- @within Sprite -- This function retrieves the registered sprite definitions and applies the stored
-- position and transformation parameters. It handles both simple and complex sprites.
function Sprite.draw() function Sprite.draw()
for id, params in pairs(_active_sprites) do for id, params in pairs(Context.sprites) do
local sprite_data = _sprites[id] local sprite_data = _sprites[id]
if not sprite_data then if not sprite_data then
trace("Error: Sprite id " .. id .. " in _active_sprites is not registered.") trace("Error: Sprite id " .. id .. " in Context.sprites is not registered.")
_active_sprites[id] = nil Context.sprites[id] = nil -- Clean up invalid entry
-- We should probably continue to the next sprite instead of returning
-- so that other valid sprites can still be drawn.
end end
-- Use parameters from Context.sprites, or fall back to sprite_data, then to defaults
local colorkey = params.colorkey or sprite_data.colorkey or 0 local colorkey = params.colorkey or sprite_data.colorkey or 0
local scale = params.scale or sprite_data.scale or 1 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_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 flip_y = params.flip_y or sprite_data.flip_y or 0
local rot = params.rot or sprite_data.rot or 0 local rot = params.rot or sprite_data.rot or 0
if sprite_data.sprites then if sprite_data.sprites then -- Complex sprite
for i = 1, #sprite_data.sprites do for i = 1, #sprite_data.sprites do
local sub_sprite = sprite_data.sprites[i] local sub_sprite = sprite_data.sprites[i]
spr( spr(
@@ -81,7 +92,7 @@ function Sprite.draw()
sub_sprite.rot or rot sub_sprite.rot or rot
) )
end end
else else -- Simple sprite
spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot) spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot)
end end
end end

View File

@@ -0,0 +1,14 @@
function Audio.music_stop() music() end
function Audio.music_play_mainmenu() end
function Audio.music_play_wakingup() end
function Audio.music_play_room_morning() end
function Audio.music_play_room_street_1() end
function Audio.music_play_room_street_2() end
function Audio.music_play_room_() end
function Audio.music_play_room_work() end
function Audio.sfx_select() sfx(17, 'C-7', 30) end
function Audio.sfx_deselect() sfx(18, 'C-7', 30) end
function Audio.sfx_beep() sfx(19, 'C-6', 30) end
function Audio.sfx_success() sfx(16, 'C-7', 60) end
function Audio.sfx_bloop() sfx(21, 'C-3', 60) end

View File

@@ -1,39 +1,17 @@
--- @section Input
local INPUT_KEY_UP = 0 local INPUT_KEY_UP = 0
local INPUT_KEY_DOWN = 1 local INPUT_KEY_DOWN = 1
local INPUT_KEY_LEFT = 2 local INPUT_KEY_LEFT = 2
local INPUT_KEY_RIGHT = 3 local INPUT_KEY_RIGHT = 3
local INPUT_KEY_A = 4 local INPUT_KEY_A = 4 local INPUT_KEY_B = 5 local INPUT_KEY_Y = 7
local INPUT_KEY_B = 5
local INPUT_KEY_Y = 7
local INPUT_KEY_SPACE = 48 local INPUT_KEY_SPACE = 48
local INPUT_KEY_BACKSPACE = 51 local INPUT_KEY_BACKSPACE = 51
local INPUT_KEY_ENTER = 50 local INPUT_KEY_ENTER = 50
--- Checks if Up is pressed.
--- @within Input
function Input.up() return btnp(INPUT_KEY_UP) end function Input.up() return btnp(INPUT_KEY_UP) end
--- Checks if Down is pressed.
--- @within Input
function Input.down() return btnp(INPUT_KEY_DOWN) end function Input.down() return btnp(INPUT_KEY_DOWN) end
--- Checks if Left is pressed.
--- @within Input
function Input.left() return btnp(INPUT_KEY_LEFT) end 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 function Input.right() return btnp(INPUT_KEY_RIGHT) end
--- Checks if Select is pressed.
--- @within Input
function Input.select() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_SPACE) end function Input.select() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_SPACE) end
--- Checks if Menu Confirm is pressed.
--- @within Input
function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end
--- Checks if Player Interact is pressed. function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end
--- @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.toggle_popup() return keyp(INPUT_KEY_ENTER) end

View File

@@ -1,26 +1,64 @@
--- @section Main local STATE_HANDLERS = {
[WINDOW_SPLASH] = function()
SplashWindow.update()
SplashWindow.draw()
end,
[WINDOW_INTRO] = function()
IntroWindow.update()
IntroWindow.draw()
end,
[WINDOW_MENU] = function()
MenuWindow.update()
MenuWindow.draw()
end,
[WINDOW_GAME] = function()
GameWindow.update()
GameWindow.draw()
end,
[WINDOW_POPUP] = function()
GameWindow.draw()
PopupWindow.update()
PopupWindow.draw()
end,
[WINDOW_CONFIGURATION] = function()
ConfigurationWindow.update()
ConfigurationWindow.draw()
end,
[WINDOW_AUDIOTEST] = function()
AudioTestWindow.update()
AudioTestWindow.draw()
end,
[WINDOW_MINIGAME_BUTTON_MASH] = function()
MinigameButtonMashWindow.update()
MinigameButtonMashWindow.draw()
end,
[WINDOW_MINIGAME_RHYTHM] = function()
MinigameRhythmWindow.update()
MinigameRhythmWindow.draw()
end,
[WINDOW_MINIGAME_DDR] = function()
MinigameDDRWindow.update()
MinigameDDRWindow.draw()
end,
}
local initialized_game = false local initialized_game = false
--- Initializes game state.
--- @within Main
local function init_game() local function init_game()
if initialized_game then return end if initialized_game then return end
Context.reset()
Window.set_current("splash") -- Set initial window using new manager
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
initialized_game = true initialized_game = true
end end
--- Main game loop (TIC-80 callback).
--- @within Main
function TIC() function TIC()
init_game() init_game()
cls(Config.colors.black) cls(Config.colors.black)
local handler = Window.get_current_handler() -- Get handler from Window manager local handler = STATE_HANDLERS[Context.active_window]
if handler then if handler then
handler() handler()
end end
Meter.update() Meters.update()
if Context.game_in_progress then if Context.game_in_progress then
UI.draw_meters() UI.draw_meters()
end end

View File

@@ -1,13 +1,4 @@
--- @section Print
--- Prints text with 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.
function Print.text(text, x, y, color, fixed, scale) function Print.text(text, x, y, color, fixed, scale)
local shadow_color = Config.colors.black local shadow_color = Config.colors.black
if color == shadow_color then shadow_color = Config.colors.light_grey end if color == shadow_color then shadow_color = Config.colors.light_grey end
@@ -16,14 +7,6 @@ function Print.text(text, x, y, color, fixed, scale)
print(text, x, y, color, fixed, scale) print(text, x, y, color, fixed, scale)
end end
--- Prints centered text with 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.
function Print.text_center(text, x, y, color, fixed, scale) function Print.text_center(text, x, y, color, fixed, scale)
scale = scale or 1 scale = scale or 1
local text_width = print(text, 0, -6, 0, fixed, scale) local text_width = print(text, 0, -6, 0, fixed, scale)

View File

@@ -1,25 +1,12 @@
--- @section UI
--- Draws the top bar.
--- @within UI
-- @param title string The title text to display.
function UI.draw_top_bar(title) function UI.draw_top_bar(title)
rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey) rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
Print.text(title, 3, 2, Config.colors.green) Print.text(title, 3, 2, Config.colors.green)
end end
--- Draws dialog window.
--- @within UI
function UI.draw_dialog() function UI.draw_dialog()
PopupWindow.draw() PopupWindow.draw()
end end
--- Draws a menu.
--- @within UI
-- @param items table A table of menu items.
-- @param selected_item number The index of the currently selected item.
-- @param x number The x-coordinate for the menu.
-- @param y number The y-coordinate for the menu.
function UI.draw_menu(items, selected_item, x, y) function UI.draw_menu(items, selected_item, x, y)
for i, item in ipairs(items) do for i, item in ipairs(items) do
local current_y = y + (i-1)*10 local current_y = y + (i-1)*10
@@ -30,11 +17,6 @@ function UI.draw_menu(items, selected_item, x, y)
end end
end end
--- Updates menu selection.
--- @within UI
-- @param items table A table of menu items.
-- @param selected_item number The current index of the selected item.
-- @return number The updated index of the selected item.
function UI.update_menu(items, selected_item) function UI.update_menu(items, selected_item)
if Input.up() then if Input.up() then
Audio.sfx_beep() Audio.sfx_beep()
@@ -52,11 +34,6 @@ function UI.update_menu(items, selected_item)
return selected_item return selected_item
end end
--- Wraps text.
--- @within UI
-- @param text string The text to wrap.
-- @param max_chars_per_line number The maximum characters per line.
-- @return table A table of wrapped lines.
function UI.word_wrap(text, max_chars_per_line) function UI.word_wrap(text, max_chars_per_line)
if text == nil then return {""} end if text == nil then return {""} end
local lines = {} local lines = {}
@@ -86,16 +63,6 @@ function UI.word_wrap(text, max_chars_per_line)
return lines return lines
end end
--- Creates a numeric stepper.
--- @within UI
-- @param label string The label for the stepper.
-- @param value_getter function Function to get the current value.
-- @param value_setter function Function to set the current value.
-- @param min number The minimum value.
-- @param max number The maximum value.
-- @param step number The step increment.
-- @param[opt] format string The format string for displaying the value.
-- @return table A numeric stepper control definition.
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format) function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
return { return {
label = label, label = label,
@@ -109,11 +76,6 @@ function UI.create_numeric_stepper(label, value_getter, value_setter, min, max,
} }
end end
--- Creates an action item.
--- @within UI
-- @param label string The label for the action item.
-- @param action function The function to execute when the item is selected.
-- @return table An action item control definition.
function UI.create_action_item(label, action) function UI.create_action_item(label, action)
return { return {
label = label, label = label,
@@ -122,10 +84,6 @@ function UI.create_action_item(label, action)
} }
end end
--- Draws decision selector.
--- @within UI
-- @param decisions table A table of decision items.
-- @param selected_decision_index number The index of the selected decision.
function UI.draw_decision_selector(decisions, selected_decision_index) function UI.draw_decision_selector(decisions, selected_decision_index)
local bar_height = 16 local bar_height = 16
local bar_y = Config.screen.height - bar_height local bar_y = Config.screen.height - bar_height
@@ -133,23 +91,18 @@ function UI.draw_decision_selector(decisions, selected_decision_index)
if #decisions > 0 then if #decisions > 0 then
local selected_decision = decisions[selected_decision_index] local selected_decision = decisions[selected_decision_index]
local decision_label = selected_decision.label local decision_label = selected_decision.label
local text_width = #decision_label * 4 local text_width = #decision_label * 4 local text_y = bar_y + 4
local text_y = bar_y + 4 local text_x = (Config.screen.width - text_width) / 2
local text_x = (Config.screen.width - text_width) / 2 Print.text("<", 2, text_y, Config.colors.green)
Print.text("<", 2, text_y, Config.colors.green) Print.text(decision_label, text_x, text_y, Config.colors.item) Print.text(">", Config.screen.width - 6, text_y, Config.colors.green) end
Print.text(decision_label, text_x, text_y, Config.colors.item)
Print.text(">", Config.screen.width - 6, text_y, Config.colors.green)
end
end end
--- Draws meters.
--- @within UI
function UI.draw_meters() function UI.draw_meters()
if not Context or not Context.game_in_progress or not Context.meters then return end if not Context or not Context.game_in_progress or not Context.meters then return end
if Context.meters.hidden then return end if Context.meters.hidden then return end
local m = Context.meters local m = Context.meters
local max = Meter.get_max() local max = Meters.get_max()
local bar_w = 44 local bar_w = 44
local bar_h = 2 local bar_h = 2
local bar_x = 182 local bar_x = 182
@@ -159,16 +112,16 @@ function UI.draw_meters()
local bar_offset = math.floor((line_h - bar_h) / 2) local bar_offset = math.floor((line_h - bar_h) / 2)
local meter_list = { local meter_list = {
{ key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 }, { key = "wpm", label = "WPM", color = Meters.COLOR_WPM, row = 0 },
{ key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 }, { key = "ism", label = "ISM", color = Meters.COLOR_ISM, row = 1 },
{ key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 }, { key = "bm", label = "BM", color = Meters.COLOR_BM, row = 2 },
} }
for _, meter in ipairs(meter_list) do for _, meter in ipairs(meter_list) do
local label_y = start_y + meter.row * line_h local label_y = start_y + meter.row * line_h
local bar_y = label_y + bar_offset local bar_y = label_y + bar_offset
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w)) local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG) rect(bar_x, bar_y, bar_w, bar_h, Meters.COLOR_BG)
if fill_w > 0 then if fill_w > 0 then
rect(bar_x, bar_y, fill_w, bar_h, meter.color) rect(bar_x, bar_y, fill_w, bar_h, meter.color)
end end
@@ -176,11 +129,6 @@ function UI.draw_meters()
end end
end end
--- Updates decision selector.
--- @within UI
-- @param decisions table A table of decision items.
-- @param selected_decision_index number The current index of the selected decision.
-- @return number The updated index of the selected decision.
function UI.update_decision_selector(decisions, selected_decision_index) function UI.update_decision_selector(decisions, selected_decision_index)
if Input.left() then if Input.left() then
Audio.sfx_beep() Audio.sfx_beep()
@@ -190,4 +138,4 @@ function UI.update_decision_selector(decisions, selected_decision_index)
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1) selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
end end
return selected_decision_index return selected_decision_index
end end

View File

@@ -1,37 +1,14 @@
Util = {}
--- @section Util
--- Safely wraps an index for an array.
--- @within Util
-- @param array table The array to index.
-- @param index number The desired index (can be out of bounds).
-- @return number The wrapped index within the array's bounds.
function Util.safeindex(array, index) function Util.safeindex(array, index)
return ((index - 1 + #array) % #array) + 1 return ((index - 1 + #array) % #array) + 1
end end
--- Navigates to a screen by its ID.
--- @within Util
-- @param screen_id string The ID of the screen to go to.
function Util.go_to_screen_by_id(screen_id) function Util.go_to_screen_by_id(screen_id)
local screen = Screen.get_by_id(screen_id) local screen_index = Context.screen_indices_by_id[screen_id]
if screen then if screen_index then
Context.game.current_screen = screen_id Context.current_screen = screen_index
screen.init() Context.selected_decision_index = 1 else
else PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found or not indexed!"})
PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found!"})
end end
end
--- Checks if a table contains a specific value.
--- @within Util
-- @param t table The table to check.
-- @param value any The value to look for.
function Util.contains(t, value)
for i = 1, #t do
if t[i] == value then
return true
end
end
return false
end end

View File

@@ -1,4 +1,3 @@
--- @section AudioTestWindow
AudioTestWindow = { AudioTestWindow = {
index_menu = 1, index_menu = 1,
index_func = 1, index_func = 1,
@@ -7,11 +6,6 @@ AudioTestWindow = {
last_pressed = false last_pressed = false
} }
--- Generates menu items for audio test.
--- @within AudioTestWindow
-- @param list_func table List of audio functions.
-- @param index_func number Current index of selected function.
-- @return table Generated menu items.
function AudioTestWindow.generate_menuitems(list_func, index_func) function AudioTestWindow.generate_menuitems(list_func, index_func)
return { return {
{ {
@@ -21,7 +15,7 @@ function AudioTestWindow.generate_menuitems(list_func, index_func)
if current_func then if current_func then
current_func() current_func()
else else
trace("Invalid Audio function: " .. list_func[index_func]) trace("Invalid Audio function: " .. list_func[index_menu])
end end
end end
}, },
@@ -40,9 +34,6 @@ function AudioTestWindow.generate_menuitems(list_func, index_func)
} }
end end
--- Generates list of audio functions.
--- @within AudioTestWindow
-- @return table A sorted list of audio function names.
function AudioTestWindow.generate_listfunc() function AudioTestWindow.generate_listfunc()
local result = {} local result = {}
@@ -57,15 +48,11 @@ function AudioTestWindow.generate_listfunc()
return result return result
end end
--- Navigates back from audio test window.
--- @within AudioTestWindow
function AudioTestWindow.back() function AudioTestWindow.back()
Audio.sfx_deselect() Audio.sfx_deselect()
GameWindow.set_state("menu") GameWindow.set_state(WINDOW_MENU)
end end
--- Initializes audio test window.
--- @within AudioTestWindow
function AudioTestWindow.init() function AudioTestWindow.init()
AudioTestWindow.last_pressed = false AudioTestWindow.last_pressed = false
AudioTestWindow.index_menu = 1 AudioTestWindow.index_menu = 1
@@ -76,15 +63,11 @@ function AudioTestWindow.init()
) )
end end
--- Draws audio test window.
--- @within AudioTestWindow
function AudioTestWindow.draw() function AudioTestWindow.draw()
UI.draw_top_bar("Audio test") UI.draw_top_bar("Audio test")
UI.draw_menu(AudioTestWindow.menuitems, AudioTestWindow.index_menu, 20, 50) UI.draw_menu(AudioTestWindow.menuitems, AudioTestWindow.index_menu, 20, 50)
end end
--- Updates audio test window logic.
--- @within AudioTestWindow
function AudioTestWindow.update() function AudioTestWindow.update()
if Input.up() then if Input.up() then
AudioTestWindow.index_menu = Util.safeindex(AudioTestWindow.menuitems, AudioTestWindow.index_menu - 1) AudioTestWindow.index_menu = Util.safeindex(AudioTestWindow.menuitems, AudioTestWindow.index_menu - 1)

View File

@@ -1,31 +1,25 @@
--- @section ConfigurationWindow
ConfigurationWindow = { ConfigurationWindow = {
controls = {}, controls = {},
selected_control = 1, selected_control = 1,
} }
--- Initializes configuration window.
--- @within ConfigurationWindow
function ConfigurationWindow.init() function ConfigurationWindow.init()
ConfigurationWindow.controls = { ConfigurationWindow.controls = {
UI.create_action_item( UI.create_decision_item(
"Save", "Save",
function() Config.save() end function() Config.save() end
), ),
UI.create_action_item( UI.create_decision_item(
"Restore Defaults", "Restore Defaults",
function() Config.reset() end function() Config.restore_defaults() end
), ),
} }
end end
--- Draws configuration window.
--- @within ConfigurationWindow
function ConfigurationWindow.draw() function ConfigurationWindow.draw()
UI.draw_top_bar("Configuration") UI.draw_top_bar("Configuration")
local x_start = 10 local x_start = 10 local y_start = 40
local y_start = 40
local x_value_right_align = Config.screen.width - 10 local x_value_right_align = Config.screen.width - 10
local char_width = 4 local char_width = 4
for i, control in ipairs(ConfigurationWindow.controls) do for i, control in ipairs(ConfigurationWindow.controls) do
@@ -35,23 +29,22 @@ function ConfigurationWindow.draw()
local value = control.get() local value = control.get()
local label_text = control.label local label_text = control.label
local value_text = string.format(control.format, value) local value_text = string.format(control.format, value)
local value_x = x_value_right_align - (#value_text * char_width)
local value_x = x_value_right_align - (#value_text * char_width)
if i == ConfigurationWindow.selected_control then if i == ConfigurationWindow.selected_control then
color = Config.colors.item color = Config.colors.item
Print.text("<", x_start - 8, current_y, color) Print.text("<", x_start -8, current_y, color)
Print.text(label_text, x_start, current_y, color) Print.text(label_text, x_start, current_y, color) Print.text(value_text, value_x, 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(">", x_value_right_align + 4, current_y, color)
else
Print.text(label_text, x_start, current_y, color) Print.text(label_text, x_start, current_y, color)
Print.text(value_text, value_x, current_y, color) Print.text(value_text, value_x, current_y, color)
end end
elseif control.type == "action_item" then elseif control.type == "decision_item" then
local label_text = control.label local label_text = control.label
if i == ConfigurationWindow.selected_control then if i == ConfigurationWindow.selected_control then
color = Config.colors.item color = Config.colors.item
Print.text("<", x_start - 8, current_y, color) Print.text("<", x_start -8, current_y, color)
Print.text(label_text, x_start, 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) Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color)
else else
@@ -62,11 +55,9 @@ function ConfigurationWindow.draw()
Print.text("Press B to go back", x_start, 120, Config.colors.light_grey) Print.text("Press B to go back", x_start, 120, Config.colors.light_grey)
end end
--- Updates configuration window logic.
--- @within ConfigurationWindow
function ConfigurationWindow.update() function ConfigurationWindow.update()
if Input.menu_back() then if Input.menu_back() then
GameWindow.set_state("menu") GameWindow.set_state(WINDOW_MENU)
return return
end end
@@ -86,17 +77,15 @@ function ConfigurationWindow.update()
if control then if control then
if control.type == "numeric_stepper" then if control.type == "numeric_stepper" then
local current_value = control.get() local current_value = control.get()
if Input.left() then if btnp(2) then local new_value = math.max(control.min, current_value - control.step)
local new_value = math.max(control.min, current_value - control.step)
control.set(new_value) control.set(new_value)
elseif Input.right() then elseif btnp(3) then local new_value = math.min(control.max, current_value + control.step)
local new_value = math.min(control.max, current_value + control.step)
control.set(new_value) control.set(new_value)
end end
elseif control.type == "action_item" then elseif control.type == "decision_item" then
if Input.menu_confirm() then if Input.menu_confirm() then
control.action() control.decision()
end end
end end
end end
end end

View File

@@ -1,70 +1,95 @@
--- @section GameWindow --- Draws the main game window content.
local _available_decisions = {} -- This includes the current screen's background, top bar, decisions, and all active sprites.
local _selected_decision_index = 1 -- @function GameWindow.draw
--- Draws the game window.
--- @within GameWindow
function GameWindow.draw() function GameWindow.draw()
local screen = Screen.get_by_id(Context.game.current_screen) local screen = Context.screens[Context.current_screen]
if screen.background then Map.draw(screen.background) end Map.draw(screen.background)
UI.draw_top_bar(screen.name) UI.draw_top_bar(screen.name)
if #_available_decisions > 0 then if screen and screen.decisions and #screen.decisions > 0 then
UI.draw_decision_selector(_available_decisions, _selected_decision_index) local available_decisions = {}
for _, decision_id in ipairs(screen.decisions) do
local decision = Decision.get(decision_id)
if decision and decision.condition() then
table.insert(available_decisions, decision)
end
end
if #available_decisions > 0 then
UI.draw_decision_selector(available_decisions, Context.selected_decision_index)
end
end end
Sprite.draw() Sprite.draw()
end end
--- Updates the game window logic. --- Updates the logic for the main game window.
--- @within GameWindow -- Handles input, navigates between screens, calls the current screen's and situation's update functions,
-- and processes player decisions.
-- @function GameWindow.update
function GameWindow.update() function GameWindow.update()
local previous_screen_index = Context.current_screen
if Input.menu_back() then if Input.menu_back() then
Window.set_current("menu") Context.active_window = WINDOW_MENU
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
return return
end end
local screen = Screen.get_by_id(Context.game.current_screen) if Input.up() then
Context.current_screen = Context.current_screen - 1
if Context.current_screen < 1 then
Context.current_screen = #Context.screens
end
Context.selected_decision_index = 1 elseif Input.down() then
Context.current_screen = Context.current_screen + 1
if Context.current_screen > #Context.screens then
Context.current_screen = 1
end
Context.selected_decision_index = 1 end
local screen = Context.screens[Context.current_screen]
screen.update() screen.update()
-- Handle current situation updates if previous_screen_index ~= Context.current_screen then
if Context.game.current_situation then screen.init()
local current_situation_obj = Situation.get_by_id(Context.game.current_situation) end
if Context.current_situation then
local current_situation_obj = Situation.get(Context.current_situation)
if current_situation_obj and current_situation_obj.update then if current_situation_obj and current_situation_obj.update then
current_situation_obj.update() current_situation_obj.update()
end end
end end
-- Fetch and filter decisions locally if screen and screen.decisions and #screen.decisions > 0 then
local all_decisions_for_screen = Decision.get_for_screen(screen) local available_decisions = {}
_available_decisions = Decision.filter_available(all_decisions_for_screen) for _, decision_id in ipairs(screen.decisions) do
local decision = Decision.get(decision_id)
if decision and decision.condition() then table.insert(available_decisions, decision)
end
end
if #_available_decisions == 0 then return end if #available_decisions == 0 then return end
if _selected_decision_index > #_available_decisions then local new_selected_decision_index = UI.update_decision_selector(
_selected_decision_index = 1 available_decisions,
end Context.selected_decision_index
)
local new_selected_decision_index = UI.update_decision_selector( if new_selected_decision_index ~= Context.selected_decision_index then
_available_decisions, Context.selected_decision_index = new_selected_decision_index
_selected_decision_index end
)
if new_selected_decision_index ~= _selected_decision_index then if Input.select() then
_selected_decision_index = new_selected_decision_index local selected_decision = available_decisions[Context.selected_decision_index]
end if selected_decision and selected_decision.handle then Audio.sfx_select() selected_decision.handle()
end
if Input.select() then
local selected_decision = _available_decisions[_selected_decision_index]
if selected_decision and selected_decision.handle then
Audio.sfx_select()
selected_decision.handle()
end end
end end
end end
--- Sets the active window. --- Sets the active window state for the game.
--- @within GameWindow -- This function is typically called when transitioning between different game states (e.g., to a minigame).
-- @param new_state string The ID of the new active window. -- @param new_state number The ID of the new active window (e.g., `WINDOW_MENU`, `WINDOW_GAME`).
-- @function GameWindow.set_state
function GameWindow.set_state(new_state) function GameWindow.set_state(new_state)
Window.set_current(new_state) Context.active_window = new_state
end end

View File

@@ -1,41 +1,21 @@
--- @section IntroWindow
IntroWindow.y = Config.screen.height
IntroWindow.speed = 0.5
IntroWindow.text = [[
Norman Reds everyday life
seems ordinary: work,
meetings, coffee, and
endless notifications.
But beneath him, or around
him — something is
constantly building, and
it soon becomes clear
that there is more going
on than meets the eye.
]]
--- Draws the intro window.
--- @within IntroWindow
function IntroWindow.draw() function IntroWindow.draw()
local x = (Config.screen.width - 132) / 2 local x = (Config.screen.width - 132) / 2
Print.text(IntroWindow.text, x, IntroWindow.y, Config.colors.green) Print.text(Context.intro.text, x, Context.intro.y, Config.colors.green)
end end
--- Updates the intro window logic.
--- @within IntroWindow
function IntroWindow.update() function IntroWindow.update()
IntroWindow.y = IntroWindow.y - IntroWindow.speed Context.intro.y = Context.intro.y - Context.intro.speed
local lines = 1 local lines = 1
for _ in string.gmatch(IntroWindow.text, "\n") do for _ in string.gmatch(Context.intro.text, "\n") do
lines = lines + 1 lines = lines + 1
end end
if IntroWindow.y < -lines * 8 then if Context.intro.y < -lines * 8 then
Window.set_current("menu") GameWindow.set_state(WINDOW_MENU)
end end
if Input.menu_confirm() then if Input.menu_confirm() then
Window.set_current("menu") GameWindow.set_state(WINDOW_MENU)
end end
end end

View File

@@ -1,49 +0,0 @@
--- @section Window
local _windows = {}
--- Registers a window table.
--- @within Window
-- @param id string The ID of the window (e.g., "splash", "menu").
-- @param window_table table The actual window module table (e.g., SplashWindow).
function Window.register(id, window_table)
_windows[id] = window_table
end
--- Retrieves a registered window table by its ID.
--- @within Window
-- @param id string The ID of the window.
-- @return table The window module table.
function Window.get(id)
return _windows[id]
end
--- Sets the currently active window.
--- @within Window
-- @param id string The ID of the window to activate.
function Window.set_current(id)
Context.current_window = id
end
--- Gets the ID of the currently active window.
--- @within Window
-- @return string The ID of the active window.
function Window.get_current_id()
return Context.current_window
end
--- Gets the handler function for the currently active window.
-- This function is used by the main game loop to update and draw the active window.
--- @within Window
-- @return function A function that updates and draws the current window.
function Window.get_current_handler()
local window_table = Window.get(Context.current_window)
if window_table and window_table.update and window_table.draw then
return function()
window_table.update()
window_table.draw()
end
else
-- Fallback handler for unregistered or incomplete windows
return function() trace("Error: No handler for window: " .. tostring(Context.current_window)) end
end
end

View File

@@ -1,20 +1,13 @@
--- @section MenuWindow
local _menu_items = {}
--- Draws the menu window.
--- @within MenuWindow
function MenuWindow.draw() function MenuWindow.draw()
UI.draw_top_bar("Main Menu") UI.draw_top_bar("Main Menu")
UI.draw_menu(_menu_items, Context.current_menu_item, 108, 70) UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70)
end end
--- Updates the menu window logic.
--- @within MenuWindow
function MenuWindow.update() function MenuWindow.update()
Context.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item) Context.selected_menu_item = UI.update_menu(Context.menu_items, Context.selected_menu_item)
if Input.menu_confirm() then if Input.menu_confirm() then
local selected_item = _menu_items[Context.current_menu_item] local selected_item = Context.menu_items[Context.selected_menu_item]
if selected_item and selected_item.decision then if selected_item and selected_item.decision then
Audio.sfx_select() Audio.sfx_select()
selected_item.decision() selected_item.decision()
@@ -22,66 +15,46 @@ function MenuWindow.update()
end end
end end
--- Starts a new game from the menu.
--- @within MenuWindow
function MenuWindow.new_game() function MenuWindow.new_game()
Context.new_game() Context.new_game() GameWindow.set_state(WINDOW_GAME)
GameWindow.set_state("game")
end end
--- Loads a game from the menu.
--- @within MenuWindow
function MenuWindow.load_game() function MenuWindow.load_game()
Context.load_game() Context.load_game() GameWindow.set_state(WINDOW_GAME)
GameWindow.set_state("game")
end end
--- Saves the current game from the menu.
--- @within MenuWindow
function MenuWindow.save_game() function MenuWindow.save_game()
Context.save_game() Context.save_game() end
end
--- Resumes the game from the menu.
--- @within MenuWindow
function MenuWindow.resume_game() function MenuWindow.resume_game()
GameWindow.set_state("game") GameWindow.set_state(WINDOW_GAME)
end end
--- Exits the game.
--- @within MenuWindow
function MenuWindow.exit() function MenuWindow.exit()
exit() exit()
end end
--- Opens the configuration menu.
--- @within MenuWindow
function MenuWindow.configuration() function MenuWindow.configuration()
ConfigurationWindow.init() ConfigurationWindow.init()
GameWindow.set_state("configuration") GameWindow.set_state(WINDOW_CONFIGURATION)
end end
--- Opens the audio test menu.
--- @within MenuWindow
function MenuWindow.audio_test() function MenuWindow.audio_test()
AudioTestWindow.init() AudioTestWindow.init()
GameWindow.set_state("audiotest") GameWindow.set_state(WINDOW_AUDIOTEST)
end end
--- Refreshes menu items.
--- @within MenuWindow
function MenuWindow.refresh_menu_items() function MenuWindow.refresh_menu_items()
_menu_items = {} Context.menu_items = {}
if Context.game_in_progress then if Context.game_in_progress then
table.insert(_menu_items, {label = "Resume Game", decision = MenuWindow.resume_game}) table.insert(Context.menu_items, {label = "Resume Game", decision = MenuWindow.resume_game})
table.insert(_menu_items, {label = "Save Game", decision = MenuWindow.save_game}) table.insert(Context.menu_items, {label = "Save Game", decision = MenuWindow.save_game})
end end
table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game}) table.insert(Context.menu_items, {label = "New Game", decision = MenuWindow.new_game})
table.insert(_menu_items, {label = "Load Game", decision = MenuWindow.load_game}) table.insert(Context.menu_items, {label = "Load Game", decision = MenuWindow.load_game})
table.insert(_menu_items, {label = "Configuration", decision = MenuWindow.configuration}) table.insert(Context.menu_items, {label = "Configuration", decision = MenuWindow.configuration})
table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test}) table.insert(Context.menu_items, {label = "Audio Test", decision = MenuWindow.audio_test})
table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit}) table.insert(Context.menu_items, {label = "Exit", decision = MenuWindow.exit})
Context.current_menu_item = 1 Context.selected_menu_item = 1 end
end

View File

@@ -1,20 +1,10 @@
--- @section MinigameDDRWindow
--- Initializes DDR minigame state.
--- @within MinigameDDRWindow
-- @param params table Optional parameters for configuration.
function MinigameDDRWindow.init(params) function MinigameDDRWindow.init(params)
Context.minigame_ddr = Minigame.configure_ddr(params) Context.minigame_ddr = Minigames.configure_ddr(params)
end end
--- Starts the DDR minigame.
--- @within MinigameDDRWindow
-- @param return_window string The window ID to return to after the minigame.
-- @param[opt] song_key string The key of the song to play.
-- @param[opt] params table Optional parameters for minigame configuration.
function MinigameDDRWindow.start(return_window, song_key, params) function MinigameDDRWindow.start(return_window, song_key, params)
MinigameDDRWindow.init(params) MinigameDDRWindow.init(params)
Context.minigame_ddr.return_window = return_window or "game" Context.minigame_ddr.return_window = return_window or WINDOW_GAME
Context.minigame_ddr.debug_song_key = song_key Context.minigame_ddr.debug_song_key = song_key
if song_key and Songs and Songs[song_key] then if song_key and Songs and Songs[song_key] then
Context.minigame_ddr.current_song = Songs[song_key] Context.minigame_ddr.current_song = Songs[song_key]
@@ -29,11 +19,9 @@ function MinigameDDRWindow.start(return_window, song_key, params)
Context.minigame_ddr.debug_status = "Random mode" Context.minigame_ddr.debug_status = "Random mode"
end end
end end
Window.set_current("minigame_ddr") Context.active_window = WINDOW_MINIGAME_DDR
end end
--- Spawns a random arrow.
--- @within MinigameDDRWindow
local function spawn_arrow() local function spawn_arrow()
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
local target = mg.target_arrows[math.random(1, 4)] local target = mg.target_arrows[math.random(1, 4)]
@@ -44,9 +32,6 @@ local function spawn_arrow()
}) })
end end
--- Spawns an arrow in a specific direction.
--- @within MinigameDDRWindow
-- @param direction string The direction of the arrow ("left", "down", "up", "right").
local function spawn_arrow_dir(direction) local function spawn_arrow_dir(direction)
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
for _, target in ipairs(mg.target_arrows) do for _, target in ipairs(mg.target_arrows) do
@@ -61,31 +46,17 @@ local function spawn_arrow_dir(direction)
end end
end end
--- Checks if an arrow is hit.
--- @within MinigameDDRWindow
-- @param arrow table The arrow data.
-- @return boolean True if the arrow is hit, false otherwise.
local function check_hit(arrow) local function check_hit(arrow)
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
local distance = math.abs(arrow.y - mg.target_y) local distance = math.abs(arrow.y - mg.target_y)
return distance <= mg.hit_threshold return distance <= mg.hit_threshold
end end
--- Checks if an arrow is missed.
--- @within MinigameDDRWindow
-- @param arrow table The arrow data.
-- @return boolean True if the arrow is missed, false otherwise.
local function check_miss(arrow) local function check_miss(arrow)
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
return arrow.y > mg.target_y + mg.hit_threshold return arrow.y > mg.target_y + mg.hit_threshold
end end
--- Draws an arrow.
--- @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 function draw_arrow(x, y, direction, color)
local size = 12 local size = 12
local half = size / 2 local half = size / 2
@@ -104,22 +75,20 @@ local function draw_arrow(x, y, direction, color)
end end
end end
--- Updates DDR minigame logic.
--- @within MinigameDDRWindow
function MinigameDDRWindow.update() function MinigameDDRWindow.update()
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
if mg.bar_fill >= mg.max_fill then if mg.bar_fill >= mg.max_fill then
Meter.on_minigame_complete() Meters.on_minigame_complete()
Meter.show() Meters.show()
Window.set_current(mg.return_window) Context.active_window = mg.return_window
return return
end end
mg.frame_counter = mg.frame_counter + 1 mg.frame_counter = mg.frame_counter + 1
if mg.use_pattern and mg.current_song and mg.current_song.end_frame then if mg.use_pattern and mg.current_song and mg.current_song.end_frame then
if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then
Meter.on_minigame_complete() Meters.on_minigame_complete()
Meter.show() Meters.show()
Window.set_current(mg.return_window) Context.active_window = mg.return_window
return return
end end
end end
@@ -198,8 +167,6 @@ function MinigameDDRWindow.update()
end end
end end
--- Draws DDR minigame.
--- @within MinigameDDRWindow
function MinigameDDRWindow.draw() function MinigameDDRWindow.draw()
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
if not mg then if not mg then
@@ -207,11 +174,11 @@ function MinigameDDRWindow.draw()
print("DDR ERROR: Context not initialized", 10, 10, 12) print("DDR ERROR: Context not initialized", 10, 10, 12)
print("Press Z to return", 10, 20, 12) print("Press Z to return", 10, 20, 12)
if Input.select() then if Input.select() then
Window.set_current("game") Context.active_window = WINDOW_GAME
end end
return return
end end
if mg.return_window == "game" then if mg.return_window == WINDOW_GAME then
GameWindow.draw() GameWindow.draw()
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)

View File

@@ -1,24 +1,13 @@
--- @section MinigameButtonMashWindow
--- Initializes button mash minigame state.
--- @within MinigameButtonMashWindow
-- @param params table Optional parameters for configuration.
function MinigameButtonMashWindow.init(params) function MinigameButtonMashWindow.init(params)
Context.minigame_button_mash = Minigame.configure_button_mash(params) Context.minigame_button_mash = Minigames.configure_button_mash(params)
end end
--- Starts the button mash minigame.
--- @within MinigameButtonMashWindow
-- @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) function MinigameButtonMashWindow.start(return_window, params)
MinigameButtonMashWindow.init(params) MinigameButtonMashWindow.init(params)
Context.minigame_button_mash.return_window = return_window or "game" Context.minigame_button_mash.return_window = return_window or WINDOW_GAME
Window.set_current("minigame_button_mash") Context.active_window = WINDOW_MINIGAME_BUTTON_MASH
end end
--- Updates button mash minigame logic.
--- @within MinigameButtonMashWindow
function MinigameButtonMashWindow.update() function MinigameButtonMashWindow.update()
local mg = Context.minigame_button_mash local mg = Context.minigame_button_mash
if Input.select() then if Input.select() then
@@ -29,9 +18,9 @@ function MinigameButtonMashWindow.update()
end end
end end
if mg.bar_fill >= mg.max_fill then if mg.bar_fill >= mg.max_fill then
Meter.on_minigame_complete() Meters.on_minigame_complete()
Meter.show() Meters.show()
Window.set_current(mg.return_window) Context.active_window = mg.return_window
return return
end end
local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier) local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier)
@@ -44,11 +33,9 @@ function MinigameButtonMashWindow.update()
end end
end end
--- Draws button mash minigame.
--- @within MinigameButtonMashWindow
function MinigameButtonMashWindow.draw() function MinigameButtonMashWindow.draw()
local mg = Context.minigame_button_mash local mg = Context.minigame_button_mash
if mg.return_window == "game" then if mg.return_window == WINDOW_GAME then
GameWindow.draw() GameWindow.draw()
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)

View File

@@ -1,24 +1,13 @@
--- @section MinigameRhythmWindow
--- Initializes rhythm minigame state.
--- @within MinigameRhythmWindow
-- @param params table Optional parameters for configuration.
function MinigameRhythmWindow.init(params) function MinigameRhythmWindow.init(params)
Context.minigame_rhythm = Minigame.configure_rhythm(params) Context.minigame_rhythm = Minigames.configure_rhythm(params)
end end
--- Starts the rhythm minigame.
--- @within MinigameRhythmWindow
-- @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) function MinigameRhythmWindow.start(return_window, params)
MinigameRhythmWindow.init(params) MinigameRhythmWindow.init(params)
Context.minigame_rhythm.return_window = return_window or "game" Context.minigame_rhythm.return_window = return_window or WINDOW_GAME
Window.set_current("minigame_rhythm") Context.active_window = WINDOW_MINIGAME_RHYTHM
end end
--- Updates rhythm minigame logic.
--- @within MinigameRhythmWindow
function MinigameRhythmWindow.update() function MinigameRhythmWindow.update()
local mg = Context.minigame_rhythm local mg = Context.minigame_rhythm
mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction) mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction)
@@ -51,9 +40,9 @@ function MinigameRhythmWindow.update()
end end
end end
if mg.score >= mg.max_score then if mg.score >= mg.max_score then
Meter.on_minigame_complete() Meters.on_minigame_complete()
Meter.show() Meters.show()
Window.set_current(mg.return_window) Context.active_window = mg.return_window
return return
end end
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
@@ -61,11 +50,9 @@ function MinigameRhythmWindow.update()
end end
end end
--- Draws rhythm minigame.
--- @within MinigameRhythmWindow
function MinigameRhythmWindow.draw() function MinigameRhythmWindow.draw()
local mg = Context.minigame_rhythm local mg = Context.minigame_rhythm
if mg.return_window == "game" then if mg.return_window == WINDOW_GAME then
GameWindow.draw() GameWindow.draw()
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)

View File

@@ -1,4 +1,3 @@
--- @section PopupWindow
local POPUP_X = 40 local POPUP_X = 40
local POPUP_Y = 40 local POPUP_Y = 40
local POPUP_WIDTH = 160 local POPUP_WIDTH = 160
@@ -6,36 +5,21 @@ local POPUP_HEIGHT = 80
local TEXT_MARGIN_X = POPUP_X + 10 local TEXT_MARGIN_X = POPUP_X + 10
local TEXT_MARGIN_Y = POPUP_Y + 10 local TEXT_MARGIN_Y = POPUP_Y + 10
local LINE_HEIGHT = 8 local LINE_HEIGHT = 8
--- Displays a popup window.
--- @within PopupWindow
-- @param content_strings table A table of strings to display in the popup.
function PopupWindow.show(content_strings) function PopupWindow.show(content_strings)
Context.popup.show = true Context.popup.show = true
Context.popup.content = content_strings or {} Context.popup.content = content_strings or {} GameWindow.set_state(WINDOW_POPUP) end
GameWindow.set_state("popup")
end
--- Hides the popup window.
--- @within PopupWindow
function PopupWindow.hide() function PopupWindow.hide()
Context.popup.show = false Context.popup.show = false
Context.popup.content = {} Context.popup.content = {} GameWindow.set_state(WINDOW_GAME) end
GameWindow.set_state("game")
end
--- Updates popup window logic.
--- @within PopupWindow
function PopupWindow.update() function PopupWindow.update()
if Context.popup.show then if Context.popup.show then
if Input.menu_confirm() or Input.menu_back() then if Input.menu_confirm() or Input.menu_back() then PopupWindow.hide()
PopupWindow.hide()
end end
end end
end end
--- Draws the popup window.
--- @within PopupWindow
function PopupWindow.draw() function PopupWindow.draw()
if Context.popup.show then if Context.popup.show then
rect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.black) rect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.black)
@@ -47,6 +31,6 @@ function PopupWindow.draw()
current_y = current_y + LINE_HEIGHT current_y = current_y + LINE_HEIGHT
end end
Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.green) Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.green)
end end
end end

View File

@@ -1,29 +0,0 @@
SplashWindow = {}
Window.register("splash", SplashWindow)
IntroWindow = {}
Window.register("intro", IntroWindow)
MenuWindow = {}
Window.register("menu", MenuWindow)
GameWindow = {}
Window.register("game", GameWindow)
PopupWindow = {}
Window.register("popup", PopupWindow)
ConfigurationWindow = {}
Window.register("configuration", ConfigurationWindow)
AudioTestWindow = {}
Window.register("audiotest", AudioTestWindow)
MinigameButtonMashWindow = {}
Window.register("minigame_button_mash", MinigameButtonMashWindow)
MinigameRhythmWindow = {}
Window.register("minigame_rhythm", MinigameRhythmWindow)
MinigameDDRWindow = {}
Window.register("minigame_ddr", MinigameDDRWindow)

View File

@@ -1,18 +1,14 @@
--- @section SplashWindow
--- Draws the splash window.
--- @within SplashWindow
function SplashWindow.draw() function SplashWindow.draw()
local txt = "Definitely not an Impostor" local txt = "Definitely not an Impostor"
local y = (Config.screen.height - 6) / 2 local w = #txt * 6
Print.text_center(txt, Config.screen.width / 2, y, Config.colors.white) local x = (240 - w) / 2
local y = (136 - 6) / 2
print(txt, x, y, 12)
end end
--- Updates the splash window logic.
--- @within SplashWindow
function SplashWindow.update() function SplashWindow.update()
Context.splash_timer = Context.splash_timer - 1 Context.splash_timer = Context.splash_timer - 1
if Context.splash_timer <= 0 or Input.menu_confirm() then if Context.splash_timer <= 0 or Input.menu_confirm() then
Window.set_current("intro") GameWindow.set_state(WINDOW_INTRO)
end end
end end