Compare commits

...

21 Commits

Author SHA1 Message Date
1e6fd5a6da tic80pro image pull always
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2026-02-27 00:13:00 +01:00
99ace8a1e8 add minify to export
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline failed
2026-02-26 23:39:00 +01:00
15bf66f1ca minify.lua from github
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-26 23:27:41 +01:00
Zoltan Timar
41f75da8c3 feat: added minifier, added minify step to makefile, added unminify step to makefile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-26 22:41:58 +01:00
e05018d637 Merge pull request 'feature/ldoc-return-fixes' (#20) from feature/ldoc-return-fixes into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #20
2026-02-26 16:50:14 +00:00
8e104b1ff9 Merge branch 'master' into feature/ldoc-return-fixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 17:49:21 +01:00
e07eeb466b vscode settings update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-26 17:44:41 +01:00
337f1fc132 ldoc return fixes 2026-02-26 17:41:06 +01:00
e14160114b Merge pull request 'feature/imp-30-stat-screen' (#19) from feature/imp-30-stat-screen into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #19
2026-02-26 16:06:54 +00:00
Zoltan Timar
1b64fd2392 Merge branch 'master' into feature/imp-30-stat-screen
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 16:54:24 +01:00
Zoltan Timar
aaf1479a78 feat: stat screen on toilet with Focus overlay, screen draw callback added to manager, meter decay now only on timer revolution, timer visible on stat screen not minigames, Meter.get_timer_decay_percentage() added, Context.stat_screen_active flag added
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 16:54:00 +01:00
e56662f6ad linter and doc fix for Focus
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 16:21:25 +01:00
2d25537abb Merge pull request 'feature/imp-42-time-indicator' (#18) from feature/imp-42-time-indicator into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #18
2026-02-26 14:49:00 +00:00
Zoltan Timar
66af47c483 feat: ring timer drawn at top-left of screen, Meter.set_timer_duration(f) controls speed, Meter.set_timer_decay(a) controls decay amount, all decay pauses during any minigame window
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 15:43:39 +01:00
8f9e044a17 Merge pull request 'feature/imp-62-focus-handling' (#17) from feature/imp-62-focus-handling into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #17
2026-02-26 13:58:51 +00:00
Zoltan Timar
954a39aef1 feat: added new functionality with focus, added base background to screens, created Focus.close(), Focus.start(), Focus.driven() methods for different use-cases, added focus to screens
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 14:53:22 +01:00
Zsolt Tasnadi
226d75d905 return table details in docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 11:25:20 +01:00
Zsolt Tasnadi
8f34cbf875 docs for table properties
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 10:21:48 +01:00
64de41a940 Merge pull request 'section and within annotations for ldoc' (#16) from feature/ldoc-sections into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #16
2026-02-25 22:25:12 +00:00
777c27aa54 section and within annotations for ldoc
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-25 23:24:53 +01:00
297ee8b622 Merge pull request 'refact by claude' (#14) from claude-refact into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #14
2026-02-23 09:44:07 +00:00
41 changed files with 1030 additions and 186 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
.local .local
impostor.lua impostor.lua
impostor.original.lua
prompts prompts
docs docs
minify.lua

View File

@@ -2,6 +2,7 @@
-- Configuration for luacheck -- Configuration for luacheck
globals = { globals = {
"Focus",
"Util", "Util",
"Decision", "Decision",
"Situation", "Situation",
@@ -39,6 +40,8 @@ globals = {
"circb", "circb",
"cls", "cls",
"tri", "tri",
"pix",
"line",
"Songs", "Songs",
"frame_from_beat", "frame_from_beat",
"beats_to_pattern", "beats_to_pattern",

View File

@@ -14,7 +14,10 @@
"inc/?.lua" "inc/?.lua"
], ],
"Lua.diagnostics.disable": [ "Lua.diagnostics.disable": [
"undefined-global" "undefined-global",
"undefined-doc-param",
"undefined-doc-name",
"luadoc-miss-param-name"
], ],
"python.autoComplete.extraPaths": [ "python.autoComplete.extraPaths": [
"${workspaceFolder}/sources/poky/bitbake/lib", "${workspaceFolder}/sources/poky/bitbake/lib",

View File

@@ -7,6 +7,7 @@ steps:
- name: build - name: build
image: git.teletype.hu/internal/tic80pro:latest image: git.teletype.hu/internal/tic80pro:latest
pull: true
environment: environment:
XDG_RUNTIME_DIR: /tmp XDG_RUNTIME_DIR: /tmp
commands: commands:

View File

@@ -6,9 +6,13 @@ PROJECT = impostor
ORDER = $(PROJECT).inc ORDER = $(PROJECT).inc
OUTPUT = $(PROJECT).lua OUTPUT = $(PROJECT).lua
OUTPUT_ORIGINAL = $(PROJECT).original.lua
OUTPUT_ZIP = $(PROJECT).html.zip OUTPUT_ZIP = $(PROJECT).html.zip
OUTPUT_TIC = $(PROJECT).tic OUTPUT_TIC = $(PROJECT).tic
MINIFY = minify.lua
MINIFY_URL = https://raw.githubusercontent.com/ztimar31/lua-minify-tic80/refs/heads/master/minify.lua
SRC_DIR = inc SRC_DIR = inc
SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER)) SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
@@ -39,7 +43,19 @@ $(OUTPUT): $(SRC) $(ORDER)
echo "" >> $(OUTPUT); \ echo "" >> $(OUTPUT); \
done done
export: build $(MINIFY):
@echo "==> Downloading $(MINIFY)"
@curl -fsSL $(MINIFY_URL) -o $(MINIFY)
minify: $(OUTPUT_ORIGINAL)
$(OUTPUT_ORIGINAL): $(SRC) $(ORDER) $(MINIFY)
@$(MAKE) build
@echo "==> Minifying $(OUTPUT)"
@cp $(OUTPUT) $(OUTPUT_ORIGINAL)
@lua $(MINIFY) minify $(OUTPUT_ORIGINAL) > $(OUTPUT)
export: build minify
@if [ -z "$(VERSION)" ]; then \ @if [ -z "$(VERSION)" ]; then \
echo "ERROR: VERSION not set!"; \ echo "ERROR: VERSION not set!"; \
exit 1; \ exit 1; \
@@ -156,7 +172,7 @@ export_assets:
@$(call f_export_asset_awk,WAVES,$(OUTPUT),$(ASSETS_LUA)) @$(call f_export_asset_awk,WAVES,$(OUTPUT),$(ASSETS_LUA))
clean: clean:
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(OUTPUT) @rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(OUTPUT) $(OUTPUT_MIN) $(PROJECT).original.lua
@echo "==> Cleaned build artifacts" @echo "==> Cleaned build artifacts"
# CI/CD Targets # CI/CD Targets
@@ -219,5 +235,4 @@ docs: build
@ldoc ${OUTPUT} -d docs @ldoc ${OUTPUT} -d docs
@echo "==> Documentation generated." @echo "==> Documentation generated."
.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 minify export watch import_assets export_assets clean lint ci-version ci-export ci-upload ci-update install_precommit_hook docs

View File

@@ -7,6 +7,7 @@ init/init.context.lua
system/system.util.lua system/system.util.lua
system/system.print.lua system/system.print.lua
system/system.input.lua system/system.input.lua
system/system.focus.lua
system/system.ui.lua system/system.ui.lua
audio/audio.manager.lua audio/audio.manager.lua
audio/audio.songs.lua audio/audio.songs.lua

View File

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

View File

@@ -1,6 +1,7 @@
--- @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,10 +109,11 @@ Songs = {
} }
--- Converts beats to frames. --- Converts beats to frames.
-- @param beat number The beat number. --- @within Songs
-- @param bpm number Beats per minute. --- @param beat number The beat number.
-- @param[opt] fps number Frames per second (default: 60). --- @param bpm number Beats per minute.
-- @return number The corresponding frame number. --- @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
@@ -120,10 +122,16 @@ function frame_from_beat(beat, bpm, fps)
end end
--- Converts beat notation to frame pattern. --- Converts beat notation to frame pattern.
-- @param beats table A table of beat data, e.g., {{1, "left"}, {2, "down"}}. --- @within Songs
-- @param bpm number Beats per minute. --- @param beats table A table of beat data, e.g., {{1, "left"}, {2, "down"}}.
-- @param[opt] fps number Frames per second (default: 60). --- @param beats.1 number The beat number.
-- @return table The generated pattern. --- @param beats.2 string Arrow direction ("left", "down", "up", or "right").
--- @param bpm number Beats per minute.
--- @param[opt] fps number Frames per second (default: 60).
--- @return result table The generated pattern or nil. </br>
--- Fields: </br>
--- * frame (number) The frame number when the arrow should spawn.<br/>
--- * dir (string) Arrow direction ("left", "down", "up", or "right").<br/>
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

@@ -1,7 +1,13 @@
--- @section Decision
local _decisions = {} local _decisions = {}
--- Registers a decision definition. --- Registers a decision definition.
-- @param decision table The decision data table. --- @within Decision
--- @param decision table The decision data table.
--- @param decision.id string Unique decision identifier.
--- @param decision.label string Display text for the decision.
--- @param[opt] decision.condition function Returns true if decision is available. Defaults to always true.
--- @param[opt] decision.handle function Called when the decision is selected. Defaults to noop.
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)!"})
@@ -25,21 +31,40 @@ function Decision.register(decision)
end end
--- Gets a decision by ID. --- Gets a decision by ID.
-- @param id string The ID of the decision. --- @within Decision
-- @return table The decision table or nil. --- @param id string The ID of the decision.
--- @return table|nil result The decision table or nil. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.
function Decision.get_by_id(id) function Decision.get_by_id(id)
return _decisions[id] return _decisions[id]
end end
--- Gets all registered decisions. --- Gets all registered decisions.
-- @return table A table of all registered decisions. --- @within Decision
--- @return result table A table of all registered decisions, indexed by their IDs. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.
function Decision.get_all() function Decision.get_all()
return _decisions return _decisions
end end
--- Gets decision objects based on a screen's data. --- Gets decision objects based on a screen's data.
-- @param screen_data table The data for the screen, containing a 'decisions' field (list of decision IDs). --- @within Decision
-- @return table A table containing decision objects relevant to the screen. --- @param screen_data table The data for the screen.
--- @param screen_data.decisions table Array of decision ID strings.
--- @return result table An array of decision objects relevant to the screen or nil. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.<br/>
function Decision.get_for_screen(screen_data) function Decision.get_for_screen(screen_data)
if not screen_data or not screen_data.decisions then if not screen_data or not screen_data.decisions then
return {} return {}
@@ -56,8 +81,14 @@ function Decision.get_for_screen(screen_data)
end end
--- Filters a list of decision objects based on their condition function. --- Filters a list of decision objects based on their condition function.
-- @param decisions_list table A table of decision objects. --- @within Decision
-- @return table A new table containing only the decisions for which condition() is true. --- @param decisions_list table A table of decision objects.
--- @return result table An array of decisions for which condition() is true or nil. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.<br/>
function Decision.filter_available(decisions_list) function Decision.filter_available(decisions_list)
local available = {} local available = {}
for _, decision in ipairs(decisions_list) do for _, decision in ipairs(decisions_list) do

View File

@@ -1,5 +1,12 @@
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()
Meter.hide()
MinigameButtonMashWindow.start("game", {
focus_center_x = Config.screen.width / 2,
focus_center_y = Config.screen.height / 2,
focus_initial_radius = 0,
})
end,
}) })

View File

@@ -1,5 +1,12 @@
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()
Meter.hide()
MinigameRhythmWindow.start("game", {
focus_center_x = Config.screen.width / 2,
focus_center_y = Config.screen.height / 2,
focus_initial_radius = 0,
})
end,
}) })

View File

@@ -1,5 +1,7 @@
Config = {} Config = {}
--- Return initial data for Config
--- @within Config
function Config.initial_data() function Config.initial_data()
return { return {
screen = { screen = {
@@ -11,7 +13,7 @@ function Config.initial_data()
light_grey = 13, light_grey = 13,
dark_grey = 14, dark_grey = 14,
red = 0, red = 0,
green = 7, light_blue = 7,
blue = 9, blue = 9,
white = 12, white = 12,
item = 12, item = 12,
@@ -24,6 +26,7 @@ function Config.initial_data()
end end
--- Restores default configuration settings. --- Restores default configuration settings.
--- @within Config
function Config.reset() function Config.reset()
local initial = Config.initial_data() local initial = Config.initial_data()
Config.screen = initial.screen Config.screen = initial.screen
@@ -37,12 +40,14 @@ local CONFIG_SPLASH_DURATION_ADDRESS = 3
local CONFIG_MAGIC_VALUE = 0xDE local CONFIG_MAGIC_VALUE = 0xDE
--- Saves the current configuration. --- 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) mset(Config.timing.splash_duration, CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
end end
--- Loads saved configuration. --- 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)

View File

@@ -3,10 +3,23 @@ local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
local SAVE_GAME_MAGIC_VALUE = 0xCA local SAVE_GAME_MAGIC_VALUE = 0xCA
--- Global game context. --- Global game context.
--- @section Context
Context = {} Context = {}
--- Gets initial data for Context. --- Gets initial data for Context.
-- @return table Initial context data. --- @within Context
--- @return result table Initial context data or nil. </br>
--- Fields: </br>
--- * current_menu_item (number) Index of the currently selected menu item.<br/>
--- * splash_timer (number) Remaining frames for the splash screen timer.<br/>
--- * popup (table) Popup window state. Contains: `show` (boolean) whether popup is visible, `content` (table) array of strings to display.<br/>
--- * game_in_progress (boolean) Whether a game is currently active.<br/>
--- * minigame_ddr (table) DDR minigame state (see Minigame.get_default_ddr).<br/>
--- * minigame_button_mash (table) Button mash minigame state (see Minigame.get_default_button_mash).<br/>
--- * minigame_rhythm (table) Rhythm minigame state (see Minigame.get_default_rhythm).<br/>
--- * meters (table) Meter values (see Meter.get_initial).<br/>
--- * stat_screen_active (boolean) Whether the stat screen overlay is currently shown.<br/>
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.<br/>
function Context.initial_data() function Context.initial_data()
return { return {
current_menu_item = 1, current_menu_item = 1,
@@ -16,6 +29,7 @@ function Context.initial_data()
content = {} content = {}
}, },
game_in_progress = false, game_in_progress = false,
stat_screen_active = false,
minigame_ddr = Minigame.get_default_ddr(), minigame_ddr = Minigame.get_default_ddr(),
minigame_button_mash = Minigame.get_default_button_mash(), minigame_button_mash = Minigame.get_default_button_mash(),
minigame_rhythm = Minigame.get_default_rhythm(), minigame_rhythm = Minigame.get_default_rhythm(),
@@ -28,6 +42,7 @@ function Context.initial_data()
end end
--- Resets game context to initial state. --- Resets game context to initial state.
--- @within Context
function Context.reset() function Context.reset()
local initial_data = Context.initial_data() local initial_data = Context.initial_data()
for k in pairs(Context) do for k in pairs(Context) do
@@ -42,6 +57,7 @@ function Context.reset()
end end
--- Starts a new game. --- Starts a new game.
--- @within Context
function Context.new_game() function Context.new_game()
Context.reset() Context.reset()
Context.game_in_progress = true Context.game_in_progress = true
@@ -50,12 +66,14 @@ function Context.new_game()
end end
--- Saves the current game state. --- 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)
end end
--- Loads a saved game state. --- 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()

View File

@@ -1,19 +1,46 @@
--- @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_GAIN_PER_CHORE = 100 local METER_GAIN_PER_CHORE = 100
local COMBO_BASE_BONUS = 0.02 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
-- 1800 frames = 30 seconds (1800 ÷ 60 = 30)
local meter_timer_duration = 1800
local meter_timer_decay_per_revolution = 20
-- Internal meters for tracking game progress and player stats. -- Internal meters for tracking game progress and player stats.
Meter.COLOR_ISM = Config.colors.red Meter.COLOR_ISM = Config.colors.red
Meter.COLOR_WPM = Config.colors.blue Meter.COLOR_WPM = Config.colors.blue
Meter.COLOR_BM = Config.colors.black Meter.COLOR_BM = Config.colors.black
Meter.COLOR_BG = Config.colors.meter_bg Meter.COLOR_BG = Config.colors.meter_bg
--- Sets the number of frames for one full timer revolution.
--- @within Meter
--- @param frames number Frames per revolution (controls degradation speed).
function Meter.set_timer_duration(frames)
meter_timer_duration = frames
end
--- Sets the degradation amount applied to all meters per revolution.
--- @within Meter
--- @param amount number Amount to subtract from each meter per revolution.
function Meter.set_timer_decay(amount)
meter_timer_decay_per_revolution = amount
end
--- Gets initial meter values. --- Gets initial meter values.
-- @return table A table of initial meter values. --- @within Meter
--- @return result table Initial meter values. </br>
--- Fields: </br>
--- * ism (number) Initial ISM meter value.<br/>
--- * wpm (number) Initial WPM meter value.<br/>
--- * bm (number) Initial BM meter value.<br/>
--- * combo (number) Current combo count.<br/>
--- * combo_timer (number) Frames since last combo action.<br/>
--- * hidden (boolean) Whether meters are hidden.<br/>
--- * timer_progress (number) Clock timer revolution progress (0 to 1).
function Meter.get_initial() function Meter.get_initial()
return { return {
ism = METER_DEFAULT, ism = METER_DEFAULT,
@@ -22,27 +49,32 @@ function Meter.get_initial()
combo = 0, combo = 0,
combo_timer = 0, combo_timer = 0,
hidden = false, hidden = false,
timer_progress = 0,
} }
end end
--- Hides meters. --- Hides meters.
--- @within Meter
function Meter.hide() 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. --- Shows meters.
--- @within Meter
function Meter.show() 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. --- Gets max meter value.
-- @return number The maximum meter value. --- @within Meter
--- @return number The maximum meter value.
function Meter.get_max() function Meter.get_max()
return METER_MAX return METER_MAX
end end
--- Gets combo multiplier. --- Gets combo multiplier.
-- @return number The current combo multiplier. --- @within Meter
--- @return number The current combo multiplier.
function Meter.get_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
@@ -51,24 +83,33 @@ function Meter.get_combo_multiplier()
end end
--- Updates all meters. --- Updates all meters.
--- @within Meter
function Meter.update() 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) local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_FRAME) if not in_minigame then
m.bm = math.max(0, m.bm - METER_DECAY_PER_FRAME) if m.combo > 0 then
if m.combo > 0 then m.combo_timer = m.combo_timer + 1
m.combo_timer = m.combo_timer + 1 if m.combo_timer >= COMBO_TIMEOUT_FRAMES then
if m.combo_timer >= COMBO_TIMEOUT_FRAMES then m.combo = 0
m.combo = 0 m.combo_timer = 0
m.combo_timer = 0 end
end
m.timer_progress = m.timer_progress + (1 / meter_timer_duration)
if m.timer_progress >= 1 then
m.timer_progress = m.timer_progress - 1
m.ism = math.max(0, m.ism - meter_timer_decay_per_revolution)
m.wpm = math.max(0, m.wpm - meter_timer_decay_per_revolution)
m.bm = math.max(0, m.bm - meter_timer_decay_per_revolution)
end end
end end
end end
--- Adds amount to a meter. --- Adds amount to a meter.
-- @param key string The meter key (e.g., "wpm", "ism", "bm"). --- @within Meter
-- @param amount number The amount to add. --- @param key string The meter key (e.g., "wpm", "ism", "bm").
--- @param amount number The amount to add.
function Meter.add(key, amount) 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
@@ -77,7 +118,15 @@ function Meter.add(key, amount)
end end
end end
--- Gets the timer decay as a percentage of the max meter value.
--- @within Meter
--- @return number The decay percentage per revolution (e.g. 2 means -2%).
function Meter.get_timer_decay_percentage()
return math.floor(meter_timer_decay_per_revolution / METER_MAX * 100)
end
--- Called on minigame completion. --- Called on minigame completion.
--- @within Meter
function Meter.on_minigame_complete() 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 * Meter.get_combo_multiplier())

View File

@@ -1,9 +1,11 @@
-- Manages minigame configurations and initial states. -- Manages minigame configurations and initial states.
--- @section Minigame
--- Applies parameters to defaults. --- Applies parameters to defaults
-- @param defaults table The default configuration table. --- @within Minigame
-- @param params table The parameters to apply. --- @param defaults table The default configuration table.
-- @return table The updated 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
@@ -13,7 +15,34 @@ local function apply_params(defaults, params)
end end
--- Gets default DDR minigame configuration. --- Gets default DDR minigame configuration.
-- @return table The default DDR minigame configuration. --- @within Minigame
--- @return result table The default DDR minigame configuration. </br>
--- Fields: </br>
--- * bar_fill (number) Current fill level of the progress bar.<br/>
--- * max_fill (number) Maximum fill value to win.<br/>
--- * fill_per_hit (number) Fill gained per successful hit.<br/>
--- * miss_penalty (number) Fill lost per miss.<br/>
--- * bar_x (number) Progress bar X position.<br/>
--- * bar_y (number) Progress bar Y position.<br/>
--- * bar_width (number) Progress bar width.<br/>
--- * bar_height (number) Progress bar height.<br/>
--- * arrow_size (number) Size of arrow sprites.<br/>
--- * arrow_spawn_timer (number) Timer for arrow spawning.<br/>
--- * arrow_spawn_interval (number) Frames between arrow spawns.<br/>
--- * arrow_fall_speed (number) Speed of falling arrows.<br/>
--- * arrows (table) Array of active arrow objects.<br/>
--- * target_y (number) Y position of the target line.<br/>
--- * target_arrows (table) Array of target arrow positions. Each entry has: `dir` (string) arrow direction, `x` (number) X position.<br/>
--- * hit_threshold (number) Pixel distance for a valid hit.<br/>
--- * button_pressed_timers (table) Per-button press animation timers.<br/>
--- * button_press_duration (number) Duration of button press animation.<br/>
--- * input_cooldowns (table) Per-direction cooldown timers (left, down, up, right).<br/>
--- * input_cooldown_duration (number) Frames of input cooldown.<br/>
--- * frame_counter (number) Global frame counter.<br/>
--- * current_song (table) Currently playing song data.<br/>
--- * pattern_index (number) Current index in song pattern.<br/>
--- * use_pattern (boolean) Whether to use song pattern for spawning.<br/>
--- * return_window (string) Window ID to return to after minigame.
function Minigame.get_default_ddr() function Minigame.get_default_ddr()
local arrow_size = 12 local arrow_size = 12
local arrow_spacing = 30 local arrow_spacing = 30
@@ -54,7 +83,24 @@ function Minigame.get_default_ddr()
end end
--- Gets default button mash minigame configuration. --- Gets default button mash minigame configuration.
-- @return table The default button mash minigame configuration. --- @within Minigame
--- @return result table The default button mash minigame configuration. </br>
--- Fields: </br>
--- * bar_fill (number) Current fill level of the progress bar.<br/>
--- * max_fill (number) Maximum fill value to win.<br/>
--- * fill_per_press (number) Fill gained per button press.<br/>
--- * base_degradation (number) Base rate of bar degradation per frame.<br/>
--- * degradation_multiplier (number) Multiplier for degradation scaling.<br/>
--- * button_pressed_timer (number) Button press animation timer.<br/>
--- * button_press_duration (number) Duration of button press animation.<br/>
--- * return_window (string) Window ID to return to after minigame.<br/>
--- * bar_x (number) Progress bar X position.<br/>
--- * bar_y (number) Progress bar Y position.<br/>
--- * bar_width (number) Progress bar width.<br/>
--- * bar_height (number) Progress bar height.<br/>
--- * button_x (number) Button indicator X position.<br/>
--- * button_y (number) Button indicator Y position.<br/>
--- * button_size (number) Button indicator size.<br/>
function Minigame.get_default_button_mash() function Minigame.get_default_button_mash()
return { return {
bar_fill = 0, bar_fill = 0,
@@ -71,12 +117,39 @@ function Minigame.get_default_button_mash()
bar_height = 12, bar_height = 12,
button_x = 20, button_x = 20,
button_y = 110, button_y = 110,
button_size = 12 button_size = 12,
focus_center_x = nil,
focus_center_y = nil,
focus_initial_radius = 0
} }
end end
--- Gets default rhythm minigame configuration. --- Gets default rhythm minigame configuration.
-- @return table The default rhythm minigame configuration. --- @within Minigame
--- @return result table The default rhythm minigame configuration. </br>
--- Fields: </br>
--- * line_position (number) Current position of the moving line (0-1).<br/>
--- * line_speed (number) Speed of the moving line per frame.<br/>
--- * line_direction (number) Direction of line movement (1 or -1).<br/>
--- * target_center (number) Center of the target zone (0-1).<br/>
--- * target_width (number) Current width of the target zone.<br/>
--- * initial_target_width (number) Starting width of the target zone.<br/>
--- * min_target_width (number) Minimum width the target zone can shrink to.<br/>
--- * target_shrink_rate (number) Multiplier applied to target width after each hit.<br/>
--- * score (number) Current score.<br/>
--- * max_score (number) Score needed to win.<br/>
--- * button_pressed_timer (number) Button press animation timer.<br/>
--- * button_press_duration (number) Duration of button press animation.<br/>
--- * return_window (string) Window ID to return to after minigame.<br/>
--- * bar_x (number) Progress bar X position.<br/>
--- * bar_y (number) Progress bar Y position.<br/>
--- * bar_width (number) Progress bar width.<br/>
--- * bar_height (number) Progress bar height.<br/>
--- * button_x (number) Button indicator X position.<br/>
--- * button_y (number) Button indicator Y position.<br/>
--- * button_size (number) Button indicator size.<br/>
--- * press_cooldown (number) Current cooldown timer.<br/>
--- * press_cooldown_duration (number) Frames of press cooldown.<br/>
function Minigame.get_default_rhythm() function Minigame.get_default_rhythm()
return { return {
line_position = 0, line_position = 0,
@@ -100,27 +173,95 @@ function Minigame.get_default_rhythm()
button_y = 110, button_y = 110,
button_size = 10, button_size = 10,
press_cooldown = 0, press_cooldown = 0,
press_cooldown_duration = 15 press_cooldown_duration = 15,
focus_center_x = nil,
focus_center_y = nil,
focus_initial_radius = 0
} }
end end
--- Configures DDR minigame. --- Configures DDR minigame.
-- @param params table Optional parameters to override defaults. --- @within Minigame
-- @return table The configured DDR minigame state. --- @param params table Optional parameters to override defaults (see Minigame.get_default_ddr).
--- @param[opt] params.bar_fill number Current fill level of the progress bar.
--- @param[opt] params.max_fill number Maximum fill value to win.
--- @param[opt] params.fill_per_hit number Fill gained per successful hit.
--- @param[opt] params.miss_penalty number Fill lost per miss.
--- @param[opt] params.bar_x number Progress bar X position.
--- @param[opt] params.bar_y number Progress bar Y position.
--- @param[opt] params.bar_width number Progress bar width.
--- @param[opt] params.bar_height number Progress bar height.
--- @param[opt] params.arrow_size number Size of arrow sprites.
--- @param[opt] params.arrow_spawn_timer number Timer for arrow spawning.
--- @param[opt] params.arrow_spawn_interval number Frames between arrow spawns.
--- @param[opt] params.arrow_fall_speed number Speed of falling arrows.
--- @param[opt] params.arrows table Array of active arrow objects.
--- @param[opt] params.target_y number Y position of the target line.
--- @param[opt] params.target_arrows table Array of target arrow positions with dir and x fields.
--- @param[opt] params.hit_threshold number Pixel distance for a valid hit.
--- @param[opt] params.button_pressed_timers table Per-button press animation timers.
--- @param[opt] params.button_press_duration number Duration of button press animation.
--- @param[opt] params.input_cooldowns table Per-direction cooldown timers (left, down, up, right).
--- @param[opt] params.input_cooldown_duration number Frames of input cooldown.
--- @param[opt] params.frame_counter number Global frame counter.
--- @param[opt] params.current_song table Currently playing song data.
--- @param[opt] params.pattern_index number Current index in song pattern.
--- @param[opt] params.use_pattern boolean Whether to use song pattern for spawning.
--- @param[opt] params.return_window string Window ID to return to after minigame.
--- @return result table The configured DDR minigame state (see Minigame.get_default_ddr for fields).
function Minigame.configure_ddr(params) function Minigame.configure_ddr(params)
return apply_params(Minigame.get_default_ddr(), params) return apply_params(Minigame.get_default_ddr(), params)
end end
--- Configures button mash minigame. --- Configures button mash minigame.
-- @param params table Optional parameters to override defaults. --- @within Minigame
-- @return table The configured button mash minigame state. --- @param params table Optional parameters to override defaults (see Minigame.get_default_button_mash).
--- @param[opt] params.bar_fill number Current fill level of the progress bar.
--- @param[opt] params.max_fill number Maximum fill value to win.
--- @param[opt] params.fill_per_press number Fill gained per button press.
--- @param[opt] params.base_degradation number Base rate of bar degradation per frame.
--- @param[opt] params.degradation_multiplier number Multiplier for degradation scaling.
--- @param[opt] params.button_pressed_timer number Button press animation timer.
--- @param[opt] params.button_press_duration number Duration of button press animation.
--- @param[opt] params.return_window string Window ID to return to after minigame.
--- @param[opt] params.bar_x number Progress bar X position.
--- @param[opt] params.bar_y number Progress bar Y position.
--- @param[opt] params.bar_width number Progress bar width.
--- @param[opt] params.bar_height number Progress bar height.
--- @param[opt] params.button_x number Button indicator X position.
--- @param[opt] params.button_y number Button indicator Y position.
--- @param[opt] params.button_size number Button indicator size.
--- @return result table The configured button mash minigame state (see Minigame.get_default_button_mash for fields).
function Minigame.configure_button_mash(params) function Minigame.configure_button_mash(params)
return apply_params(Minigame.get_default_button_mash(), params) return apply_params(Minigame.get_default_button_mash(), params)
end end
--- Configures rhythm minigame. --- Configures rhythm minigame.
-- @param params table Optional parameters to override defaults. --- @within Minigame
-- @return table The configured rhythm minigame state. --- @param params table Optional parameters to override defaults (see Minigame.get_default_rhythm).
--- @param[opt] params.line_position number Current position of the moving line (0-1).
--- @param[opt] params.line_speed number Speed of the moving line per frame.
--- @param[opt] params.line_direction number Direction of line movement (1 or -1).
--- @param[opt] params.target_center number Center of the target zone (0-1).
--- @param[opt] params.target_width number Current width of the target zone.
--- @param[opt] params.initial_target_width number Starting width of the target zone.
--- @param[opt] params.min_target_width number Minimum width the target zone can shrink to.
--- @param[opt] params.target_shrink_rate number Multiplier applied to target width after each hit.
--- @param[opt] params.score number Current score.
--- @param[opt] params.max_score number Score needed to win.
--- @param[opt] params.button_pressed_timer number Button press animation timer.
--- @param[opt] params.button_press_duration number Duration of button press animation.
--- @param[opt] params.return_window string Window ID to return to after minigame.
--- @param[opt] params.bar_x number Progress bar X position.
--- @param[opt] params.bar_y number Progress bar Y position.
--- @param[opt] params.bar_width number Progress bar width.
--- @param[opt] params.bar_height number Progress bar height.
--- @param[opt] params.button_x number Button indicator X position.
--- @param[opt] params.button_y number Button indicator Y position.
--- @param[opt] params.button_size number Button indicator size.
--- @param[opt] params.press_cooldown number Current cooldown timer.
--- @param[opt] params.press_cooldown_duration number Frames of press cooldown.
--- @return result table The configured rhythm minigame state (see Minigame.get_default_rhythm for fields).
function Minigame.configure_rhythm(params) function Minigame.configure_rhythm(params)
return apply_params(Minigame.get_default_rhythm(), params) return apply_params(Minigame.get_default_rhythm(), params)
end end

View File

@@ -11,3 +11,4 @@ Print = {}
Input = {} Input = {}
Sprite = {} Sprite = {}
Audio = {} Audio = {}
Focus = {}

View File

@@ -1,9 +1,20 @@
-- Manages game maps. -- Manages game maps.
--- @section Map
local _maps = {} local _maps = {}
--- Gets all registered maps as an array. --- Gets all registered maps as an array.
-- @return table An array of registered map data. --- @within Map
--- @return result table An array of registered map data. </br>
--- Fields: </br>
--- * id (string) Unique map identifier.<br/>
--- * from_x (number) Source tile X coordinate in the map sheet.<br/>
--- * from_y (number) Source tile Y coordinate in the map sheet.<br/>
--- * width (number) Width in tiles.<br/>
--- * height (number) Height in tiles.<br/>
--- * to_x (number) Destination X coordinate on screen.<br/>
--- * to_y (number) Destination Y coordinate on screen.<br/>
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
@@ -13,7 +24,15 @@ function Map.get_maps_array()
end end
--- Registers a map definition. --- Registers a map definition.
-- @param map_data table The map data table. --- @within Map
--- @param map_data table The map data table.
--- @param map_data.id string Unique map identifier.<br/>
--- @param map_data.from_x number Source tile X coordinate in the map sheet.<br/>
--- @param map_data.from_y number Source tile Y coordinate in the map sheet.<br/>
--- @param map_data.width number Width in tiles.<br/>
--- @param map_data.height number Height in tiles.<br/>
--- @param map_data.to_x number Destination X coordinate on screen.<br/>
--- @param map_data.to_y number Destination Y coordinate on screen.<br/>
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)
@@ -22,14 +41,24 @@ function Map.register(map_data)
end end
--- Gets a map by ID. --- Gets a map by ID.
-- @param map_id string The ID of the map. --- @within Map
-- @return table The map data table or nil. --- @param map_id string The ID of the map.
--- @return result table The map data table or nil. </br>
--- Fields: </br>
--- * id (string) Unique map identifier.<br/>
--- * from_x (number) Source tile X coordinate in the map sheet.<br/>
--- * from_y (number) Source tile Y coordinate in the map sheet.<br/>
--- * width (number) Width in tiles.<br/>
--- * height (number) Height in tiles.<br/>
--- * to_x (number) Destination X coordinate on screen.<br/>
--- * to_y (number) Destination Y coordinate on screen.<br/>
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. --- Draws a map.
-- @param map_id string The ID of the map to draw. --- @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

View File

@@ -1,7 +1,17 @@
--- @section Screen
local _screens = {} local _screens = {}
--- Registers a screen definition. --- Registers a screen definition.
-- @param screen_data table The screen data table. --- @within Screen
--- @param screen_data table The screen data table.
--- @param screen_data.id string Unique screen identifier.
--- @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.
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)
@@ -15,18 +25,39 @@ function Screen.register(screen_data)
if not screen_data.update then if not screen_data.update then
screen_data.update = function() end screen_data.update = function() end
end end
if not screen_data.draw then
screen_data.draw = function() end
end
_screens[screen_data.id] = screen_data _screens[screen_data.id] = screen_data
end end
--- Gets a screen by ID. --- Gets a screen by ID.
-- @param screen_id string The ID of the screen. --- @within Screen
-- @return table The screen table or nil. --- @param screen_id string The ID of the screen.
--- @return table|nil screen The screen table or nil. </br>
--- Fields: </br>
--- * id (string) Unique screen identifier.<br/>
--- * name (string) Display name.<br/>
--- * decisions (table) Array of decision ID strings.<br/>
--- * background (string) Map ID used as background.<br/>
--- * situations (table) Array of situation ID strings.<br/>
--- * init (function) Called when the screen is entered.<br/>
--- * update (function) Called each frame while screen is active.
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. --- Gets all registered screens.
-- @return table A table containing all registered screen data, indexed by their IDs. --- @within Screen
--- @return result table A table containing all registered screen data, indexed by their IDs or nil. </br>
--- Fields: </br>
--- * id (string) Unique screen identifier.<br/>
--- * name (string) Display name of the screen.<br/>
--- * decisions (table) Array of decision ID strings available on this screen.<br/>
--- * background (string) Map ID used as background.<br/>
--- * situations (table) Array of situation ID strings.<br/>
--- * init (function) Called when the screen is entered.<br/>
--- * update (function) Called each frame while screen is active.<br/>
function Screen.get_all() function Screen.get_all()
return _screens return _screens
end end

View File

@@ -1,6 +1,7 @@
Screen.register({ Screen.register({
id = "office", id = "office",
name = "Office", name = "Office",
background_color = Config.colors.dark_grey,
decisions = { decisions = {
"play_button_mash", "play_button_mash",
"play_rhythm", "play_rhythm",

View File

@@ -3,5 +3,74 @@ Screen.register({
name = "Toilet", name = "Toilet",
decisions = { decisions = {
"go_to_home", "go_to_home",
} },
background = "bedroom",
init = function()
Context.stat_screen_active = true
Meter.hide()
local cx = Config.screen.width * 0.75
local cy = Config.screen.height * 0.75
Focus.start_driven(cx, cy)
Focus.set_percentage(0.15)
end,
update = function()
if not Context.stat_screen_active then return end
if Input.select() or Input.player_interact() then
Focus.stop()
Context.stat_screen_active = false
Meter.show()
end
end,
draw = function()
if not Context.stat_screen_active then return end
local sw = Config.screen.width
local cx = sw / 2
local bar_w = math.floor(sw * 0.75)
local bar_x = math.floor((sw - bar_w) / 2)
local bar_h = 4
-- TODO: Add day counter
Print.text_center("day 1", cx, 10, Config.colors.white)
local narrative = "reflecting on my past and present\n...\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)
text_y = text_y + 8
end
local m = Context.meters
local max_val = Meter.get_max()
local decay_pct = Meter.get_timer_decay_percentage()
local decay_text = string.format("-%d%%", decay_pct)
local combo_mult = Meter.get_combo_multiplier()
local combo_pct = math.floor((combo_mult - 1) * 100)
local mult_text = string.format("+%d%%", combo_pct)
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" },
}
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)
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)
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)
end
end,
}) })

View File

@@ -1,6 +1,7 @@
Screen.register({ Screen.register({
id = "walking_to_home", id = "walking_to_home",
name = "Walking to home", name = "Walking to home",
background_color = Config.colors.dark_grey,
decisions = { decisions = {
"go_to_home", "go_to_home",
"go_to_office", "go_to_office",

View File

@@ -1,6 +1,7 @@
Screen.register({ Screen.register({
id = "walking_to_office", id = "walking_to_office",
name = "Walking to office", name = "Walking to office",
background_color = Config.colors.dark_grey,
decisions = { decisions = {
"go_to_home", "go_to_home",
"go_to_office", "go_to_office",

View File

@@ -1,7 +1,13 @@
--- @section Situation
local _situations = {} local _situations = {}
--- Registers a situation definition. --- Registers a situation definition.
-- @param situation table The situation data table. --- @within Situation
--- @param situation table The situation data table.
--- @param situation.id string Unique situation identifier.<br/>
--- @param[opt] situation.screen_id string ID of the screen this situation belongs to.<br/>
--- @param[opt] situation.handle function Called when the situation is applied. Defaults to noop.<br/>
--- @param[opt] situation.update function Called each frame while situation is active. Defaults to noop.<br/>
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)!"})
@@ -20,15 +26,27 @@ function Situation.register(situation)
end end
--- Gets a situation by ID. --- Gets a situation by ID.
-- @param id string The situation ID. --- @within Situation
-- @return table The situation table or nil. --- @param id string The situation ID.
--- @return result table The situation table or nil. </br>
--- Fields: </br>
--- * id (string) Unique situation identifier.<br/>
--- * screen_id (string) ID of the screen this situation belongs to.<br/>
--- * handle (function) Called when the situation is applied.<br/>
--- * update (function) Called each frame while situation is active.<br/>
function Situation.get_by_id(id) function Situation.get_by_id(id)
return _situations[id] return _situations[id]
end end
--- Gets all registered situations, optionally filtered by screen ID. --- Gets all registered situations, optionally filtered by screen ID.
-- @param screen_id string Optional. If provided, returns situations associated with this screen ID. --- @within Situation
-- @return table A table containing all registered situation data, indexed by their IDs, or filtered by screen_id. --- @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. </br>
--- Fields: </br>
--- * id (string) Unique situation identifier.<br/>
--- * screen_id (string) ID of the screen this situation belongs to.<br/>
--- * handle (function) Called when the situation is applied.<br/>
--- * update (function) Called each frame while situation is active.<br/>
function Situation.get_all(screen_id) function Situation.get_all(screen_id)
if screen_id then if screen_id then
local filtered_situations = {} local filtered_situations = {}
@@ -43,9 +61,10 @@ function Situation.get_all(screen_id)
end end
--- Applies a situation, checking screen compatibility and returning the new situation ID if successful. --- Applies a situation, checking screen compatibility and returning the new situation ID if successful.
-- @param id string The situation ID to apply. --- @within Situation
-- @param current_screen_id string The ID of the currently active screen. --- @param id string The situation ID to apply.
-- @return string|nil The ID of the applied situation if successful, otherwise nil. --- @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) function Situation.apply(id, current_screen_id)
local situation = Situation.get_by_id(id) local situation = Situation.get_by_id(id)
local screen = Screen.get_by_id(current_screen_id) local screen = Screen.get_by_id(current_screen_id)

View File

@@ -1,8 +1,18 @@
--- @section Sprite
local _sprites = {} local _sprites = {}
local _active_sprites = {} local _active_sprites = {}
--- Registers a sprite definition. --- Registers a sprite definition.
-- @param sprite_data table A table containing the sprite definition. --- @within Sprite
--- @param sprite_data table A table containing the sprite definition.
--- @param sprite_data.id string Unique sprite identifier.<br/>
--- @param[opt] sprite_data.s number Sprite index for single-sprite mode.<br/>
--- @param[opt] sprite_data.colorkey number Default color index for transparency.<br/>
--- @param[opt] sprite_data.scale number Default scaling factor.<br/>
--- @param[opt] sprite_data.flip_x number Set to 1 to flip horizontally by default.<br/>
--- @param[opt] sprite_data.flip_y number Set to 1 to flip vertically by default.<br/>
--- @param[opt] sprite_data.rot number Default rotation in degrees.<br/>
--- @param[opt] sprite_data.sprites table Array of sub-sprite tables for composite sprites. Each entry has: `s` (number) sprite index, `x_offset` (number) horizontal offset, `y_offset` (number) vertical offset, and optional `colorkey`, `scale`, `flip_x`, `flip_y`, `rot` overrides.<br/>
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)!")
@@ -15,14 +25,15 @@ function Sprite.register(sprite_data)
end end
--- Schedules a sprite for drawing. --- Schedules a sprite for drawing.
-- @param id string The unique identifier of the sprite. --- @within Sprite
-- @param x number The x-coordinate. --- @param id string The unique identifier of the sprite.<br/>
-- @param y number The y-coordinate. --- @param x number The x-coordinate.<br/>
-- @param[opt] colorkey number The color index for transparency. --- @param y number The y-coordinate.<br/>
-- @param[opt] scale number The scaling factor. --- @param[opt] colorkey number The color index for transparency.<br/>
-- @param[opt] flip_x number Set to 1 to flip horizontally. --- @param[opt] scale number The scaling factor.<br/>
-- @param[opt] flip_y number Set to 1 to flip vertically. --- @param[opt] flip_x number Set to 1 to flip horizontally.<br/>
-- @param[opt] rot number The rotation in degrees. --- @param[opt] flip_y number Set to 1 to flip vertically.<br/>
--- @param[opt] rot number The rotation in degrees.<br/>
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)
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)
@@ -42,12 +53,14 @@ function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot)
end end
--- Hides a displayed sprite. --- Hides a displayed sprite.
-- @param id string The unique identifier of the sprite. --- @within Sprite
--- @param id string The unique identifier of the sprite.<br/>
function Sprite.hide(id) function Sprite.hide(id)
_active_sprites[id] = nil _active_sprites[id] = nil
end end
--- Draws all scheduled sprites. --- Draws all scheduled sprites.
--- @within Sprite
function Sprite.draw() function Sprite.draw()
for id, params in pairs(_active_sprites) do for id, params in pairs(_active_sprites) do
local sprite_data = _sprites[id] local sprite_data = _sprites[id]

166
inc/system/system.focus.lua Normal file
View File

@@ -0,0 +1,166 @@
--- @section Focus
local FOCUS_DEFAULT_SPEED = 5
local active = false
local closing = false
local driven = false
local center_x = 0
local center_y = 0
local radius = 0
local speed = FOCUS_DEFAULT_SPEED
local on_complete = nil
local driven_initial_r = 0
local driven_max_r = 0
local function max_radius(cx, cy)
local dx = math.max(cx, Config.screen.width - cx)
local dy = math.max(cy, Config.screen.height - cy)
return math.sqrt(dx * dx + dy * dy)
end
--- Starts a focus overlay that reveals content through an expanding circle.
--- @within Focus
--- @param cx number The x-coordinate of the circle center.
--- @param cy number The y-coordinate of the circle center.
--- @param[opt] params table Optional parameters: `speed` (number) expansion rate in pixels/frame, `initial_radius` (number) starting radius in pixels (default 0), `on_complete` (function) callback when overlay disperses.
function Focus.start(cx, cy, params)
params = params or {}
active = true
closing = false
driven = false
center_x = cx
center_y = cy
radius = params.initial_radius or 0
speed = params.speed or FOCUS_DEFAULT_SPEED
on_complete = params.on_complete
end
--- Starts a closing focus overlay that hides content by shrinking the visible circle.
--- @within Focus
--- @param cx number The x-coordinate of the circle center.
--- @param cy number The y-coordinate of the circle center.
--- @param[opt] params table Optional parameters: `speed` (number) shrink rate in pixels/frame, `on_complete` (function) callback when screen is fully covered.
function Focus.close(cx, cy, params)
params = params or {}
active = true
closing = true
driven = false
center_x = cx
center_y = cy
radius = max_radius(cx, cy)
speed = params.speed or FOCUS_DEFAULT_SPEED
on_complete = params.on_complete
end
--- Starts a driven focus overlay whose radius is controlled externally via Focus.set_percentage().
--- The radius maps linearly from initial_radius (at 0%) to the screen corner distance (at 100%).
--- @within Focus
--- @param cx number The x-coordinate of the circle center.
--- @param cy number The y-coordinate of the circle center.
--- @param[opt] params table Optional parameters: `initial_radius` (number) radius at 0% (default 0).
function Focus.start_driven(cx, cy, params)
params = params or {}
active = true
closing = false
driven = true
center_x = cx
center_y = cy
driven_initial_r = params.initial_radius or 0
driven_max_r = max_radius(cx, cy)
radius = driven_initial_r
on_complete = nil
end
--- Sets the visible radius as a percentage of the full screen extent.
--- Only has effect when the overlay is in driven mode (started via Focus.start_driven).
--- @within Focus
--- @param pct number A value from 0 to 1 (0 = initial_radius, 1 = full screen).
function Focus.set_percentage(pct)
if not driven then return end
radius = driven_initial_r + pct * (driven_max_r - driven_initial_r)
end
--- Checks whether the focus overlay is currently active.
--- @within Focus
--- @return boolean Whether the focus overlay is active.
function Focus.is_active()
return active
end
--- Stops the focus overlay immediately.
--- @within Focus
function Focus.stop()
active = false
closing = false
driven = false
radius = 0
on_complete = nil
end
--- Updates the focus overlay animation. No-op in driven mode.
--- @within Focus
function Focus.update()
if not active then return end
if driven then return end
if closing then
radius = radius - speed
if radius <= 0 then
local cb = on_complete
Focus.stop()
if cb then cb() end
end
else
radius = radius + speed
if radius >= max_radius(center_x, center_y) then
local cb = on_complete
Focus.stop()
if cb then cb() end
end
end
end
--- Draws the focus overlay (black screen with circular cutout).
--- Must be called after all other drawing to appear on top of every visual layer.
--- @within Focus
function Focus.draw()
if not active then return end
local cx = center_x
local cy = center_y
local r = radius
local w = Config.screen.width
local h = Config.screen.height
local color = Config.colors.black
if closing and r <= 0 then
rect(0, 0, w, h, color)
return
end
local top = math.max(0, math.floor(cy - r))
local bottom = math.min(h - 1, math.ceil(cy + r))
if top > 0 then
rect(0, 0, w, top, color)
end
if bottom < h - 1 then
rect(0, bottom + 1, w, h - bottom - 1, color)
end
for y = top, bottom do
local dy = y - cy
local half_w = math.sqrt(math.max(0, r * r - dy * dy))
local left = math.floor(cx - half_w)
local right = math.ceil(cx + half_w)
if left > 0 then
rect(0, y, left, 1, color)
end
if right < w then
rect(right, y, w - right, 1, color)
end
end
end

View File

@@ -1,3 +1,4 @@
--- @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
@@ -10,20 +11,29 @@ local INPUT_KEY_BACKSPACE = 51
local INPUT_KEY_ENTER = 50 local INPUT_KEY_ENTER = 50
--- Checks if Up is pressed. --- 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. --- 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. --- 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. --- 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. --- 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. --- 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. --- Checks if Player Interact is pressed.
--- @within Input
function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end
--- Checks if Menu Back is pressed. --- Checks if Menu Back is pressed.
--- @within Input
function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end
--- Checks if Toggle Popup is pressed. --- 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,6 +1,9 @@
--- @section Main
local initialized_game = false local initialized_game = false
--- Initializes game state. --- Initializes game state.
--- @within Main
--- @return boolean initialized_game True if game has been initialized, false otherwise.
local function init_game() local function init_game()
if initialized_game then return end if initialized_game then return end
Context.reset() Context.reset()
@@ -10,6 +13,7 @@ local function init_game()
end end
--- Main game loop (TIC-80 callback). --- Main game loop (TIC-80 callback).
--- @within Main
function TIC() function TIC()
init_game() init_game()
cls(Config.colors.black) cls(Config.colors.black)
@@ -20,5 +24,6 @@ function TIC()
Meter.update() Meter.update()
if Context.game_in_progress then if Context.game_in_progress then
UI.draw_meters() UI.draw_meters()
UI.draw_timer()
end end
end end

View File

@@ -1,10 +1,11 @@
--- Prints text with shadow. --- Prints text with shadow.
-- @param text string The text to print. --- @within Print
-- @param x number The x-coordinate. --- @param text string The text to print.<br/>
-- @param y number The y-coordinate. --- @param x number The x-coordinate.<br/>
-- @param color number The color of the text. --- @param y number The y-coordinate.<br/>
-- @param[opt] fixed boolean If true, uses fixed-width font. --- @param color number The color of the text.<br/>
-- @param[opt] scale number The scaling factor. --- @param[opt] fixed boolean If true, uses fixed-width font.<br/>
--- @param[opt] scale number The scaling factor.<br/>
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
@@ -14,12 +15,13 @@ function Print.text(text, x, y, color, fixed, scale)
end end
--- Prints centered text with shadow. --- Prints centered text with shadow.
-- @param text string The text to print. --- @within Print
-- @param x number The x-coordinate for centering. --- @param text string The text to print.<br/>
-- @param y number The y-coordinate. --- @param x number The x-coordinate for centering.<br/>
-- @param color number The color of the text. --- @param y number The y-coordinate.<br/>
-- @param[opt] fixed boolean If true, uses fixed-width font. --- @param color number The color of the text.<br/>
-- @param[opt] scale number The scaling factor. --- @param[opt] fixed boolean If true, uses fixed-width font.<br/>
--- @param[opt] scale number The scaling factor.<br/>
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,34 +1,40 @@
--- @section UI
--- Draws the top bar. --- Draws the top bar.
-- @param title string The title text to display. --- @within UI
--- @param title string The title text to display.<br/>
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.light_blue)
end end
--- Draws dialog window. --- Draws dialog window.
--- @within UI
function UI.draw_dialog() function UI.draw_dialog()
PopupWindow.draw() PopupWindow.draw()
end end
--- Draws a menu. --- Draws a menu.
-- @param items table A table of menu items. --- @within UI
-- @param selected_item number The index of the currently selected item. --- @param items table A table of menu items.<br/>
-- @param x number The x-coordinate for the menu. --- @param selected_item number The index of the currently selected item.<br/>
-- @param y number The y-coordinate for the menu. --- @param x number The x-coordinate for the menu.<br/>
--- @param y number The y-coordinate for the menu.<br/>
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
if i == selected_item then if i == selected_item then
Print.text(">", x - 8, current_y, Config.colors.green) Print.text(">", x - 8, current_y, Config.colors.light_blue)
end end
Print.text(item.label, x, current_y, Config.colors.green) Print.text(item.label, x, current_y, Config.colors.light_blue)
end end
end end
--- Updates menu selection. --- Updates menu selection.
-- @param items table A table of menu items. --- @within UI
-- @param selected_item number The current index of the selected item. --- @param items table A table of menu items.<br/>
-- @return number The updated index of the selected item. --- @param selected_item number The current index of the selected item.<br/>
--- @return number selected_item 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()
@@ -47,9 +53,10 @@ function UI.update_menu(items, selected_item)
end end
--- Wraps text. --- Wraps text.
-- @param text string The text to wrap. --- @within UI
-- @param max_chars_per_line number The maximum characters per line. --- @param text string The text to wrap.<br/>
-- @return table A table of wrapped lines. --- @param max_chars_per_line number The maximum characters per line.<br/>
--- @return result 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 = {}
@@ -80,14 +87,24 @@ function UI.word_wrap(text, max_chars_per_line)
end end
--- Creates a numeric stepper. --- Creates a numeric stepper.
-- @param label string The label for the stepper. --- @within UI
-- @param value_getter function Function to get the current value. --- @param label string The label for the stepper.<br/>
-- @param value_setter function Function to set the current value. --- @param value_getter function Function to get the current value.<br/>
-- @param min number The minimum value. --- @param value_setter function Function to set the current value.<br/>
-- @param max number The maximum value. --- @param min number The minimum value.<br/>
-- @param step number The step increment. --- @param max number The maximum value.<br/>
-- @param[opt] format string The format string for displaying the value. --- @param step number The step increment.<br/>
-- @return table A numeric stepper control definition. --- @param[opt] format string The format string for displaying the value.<br/>
--- @return result table A numeric stepper control definition or nil. </br>
--- Fields: </br>
--- * label (string) The label for the stepper.<br/>
--- * get (function) Function to get the current value.<br/>
--- * set (function) Function to set the current value.<br/>
--- * min (number) The minimum value.<br/>
--- * max (number) The maximum value.<br/>
--- * step (number) The step increment.<br/>
--- * format (string) The format string for displaying the value.<br/>
--- * type (string) Control type identifier ("numeric_stepper").<br/>
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,
@@ -102,9 +119,14 @@ function UI.create_numeric_stepper(label, value_getter, value_setter, min, max,
end end
--- Creates an action item. --- Creates an action item.
-- @param label string The label for the action item. --- @within UI
-- @param action function The function to execute when the item is selected. --- @param label string The label for the action item.<br/>
-- @return table An action item control definition. --- @param action function The function to execute when the item is selected.<br/>
--- @return result table An action item control definition or nil. </br>
--- Fields: </br>
--- * label (string) The label for the action item.<br/>
--- * action (function) The function to execute when the item is selected.<br/>
--- * type (string) Control type identifier ("action_item").<br/>
function UI.create_action_item(label, action) function UI.create_action_item(label, action)
return { return {
label = label, label = label,
@@ -114,8 +136,9 @@ function UI.create_action_item(label, action)
end end
--- Draws decision selector. --- Draws decision selector.
-- @param decisions table A table of decision items. --- @within UI
-- @param selected_decision_index number The index of the selected decision. --- @param decisions table A table of decision items.<br/>
--- @param selected_decision_index number The index of the selected decision.<br/>
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
@@ -126,13 +149,68 @@ function UI.draw_decision_selector(decisions, selected_decision_index)
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.light_blue)
Print.text(decision_label, text_x, text_y, Config.colors.item) Print.text(decision_label, text_x, text_y, Config.colors.item)
Print.text(">", Config.screen.width - 6, text_y, Config.colors.green) Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
end end
end end
--- Draws the clock timer indicator as a circular progress bar in the top-left area.
--- Color transitions: white (0-50%), yellow (50-75%), red (75-100%).
--- @within UI
function UI.draw_timer()
if not Context or not Context.game_in_progress or not Context.meters then return end
if Context.meters.hidden and not Context.stat_screen_active then return end
local m = Context.meters
local cx = 10
local cy = 20
local r_outer = 5
local r_inner = 3
local progress = m.timer_progress
local fg_color
if progress <= 0.25 then
fg_color = Config.colors.white
elseif progress <= 0.5 then
fg_color = Config.colors.light_blue
elseif progress <= 0.75 then
fg_color = Config.colors.blue
elseif progress <= 1 then
fg_color = Config.colors.red
end
local bg_color = Config.colors.dark_grey
local start_angle = -math.pi * 0.5
local progress_angle = progress * 2 * math.pi
local r_outer_sq = r_outer * r_outer
local r_inner_sq = r_inner * r_inner
for dy = -r_outer, r_outer do
for dx = -r_outer, r_outer do
local dist_sq = dx * dx + dy * dy
if dist_sq <= r_outer_sq and dist_sq > r_inner_sq then
local angle = math.atan(dy, dx)
local relative = angle - start_angle
if relative < 0 then relative = relative + 2 * math.pi end
if relative <= progress_angle then
pix(cx + dx, cy + dy, fg_color)
else
pix(cx + dx, cy + dy, bg_color)
end
end
end
end
local hand_angle = start_angle + progress_angle
local hand_x = math.floor(cx + math.cos(hand_angle) * (r_inner - 1) + 0.5)
local hand_y = math.floor(cy + math.sin(hand_angle) * (r_inner - 1) + 0.5)
line(cx, cy, hand_x, hand_y, Config.colors.white)
end
--- Draws meters. --- 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
@@ -166,9 +244,10 @@ function UI.draw_meters()
end end
--- Updates decision selector. --- Updates decision selector.
-- @param decisions table A table of decision items. --- @within UI
-- @param selected_decision_index number The current index of the selected decision. --- @param decisions table A table of decision items.<br/>
-- @return number The updated index of the selected decision. --- @param selected_decision_index number The current index of the selected decision.<br/>
--- @return number selected_decision_index 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()
@@ -178,4 +257,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,14 +1,18 @@
--- @section Util
--- Safely wraps an index for an array. --- Safely wraps an index for an array.
-- @param array table The array to index. --- @within Util
-- @param index number The desired index (can be out of bounds). --- @param array table The array to index.
-- @return number The wrapped index within the array's bounds. --- @param index number The desired index (can be out of bounds).
--- @return number index 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. --- Navigates to a screen by its ID.
-- @param screen_id string The ID of the screen to go to. --- @within Util
--- @param screen_id string The ID of the screen to go to.<br/>
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 = Screen.get_by_id(screen_id)
if screen then if screen then
@@ -20,8 +24,10 @@ function Util.go_to_screen_by_id(screen_id)
end end
--- Checks if a table contains a specific value. --- Checks if a table contains a specific value.
-- @param t table The table to check. --- @within Util
-- @param value any The value to look for. --- @param t table The table to check.
--- @param value any The value to look for.<br/>
--- @return boolean true if the value is found, false otherwise.
function Util.contains(t, value) function Util.contains(t, value)
for i = 1, #t do for i = 1, #t do
if t[i] == value then if t[i] == value then

View File

@@ -1,3 +1,4 @@
--- @section AudioTestWindow
AudioTestWindow = { AudioTestWindow = {
index_menu = 1, index_menu = 1,
index_func = 1, index_func = 1,
@@ -7,9 +8,13 @@ AudioTestWindow = {
} }
--- Generates menu items for audio test. --- Generates menu items for audio test.
-- @param list_func table List of audio functions. --- @within AudioTestWindow
-- @param index_func number Current index of selected function. --- @param list_func table List of audio functions.<br/>
-- @return table Generated menu items. --- @param index_func number Current index of selected function.<br/>
--- @return result table Generated menu items, an array of menu item tables or nil. </br>
--- Fields: </br>
--- * label (string) Display text for the menu item.<br/>
--- * decision (function) Called when the menu item is selected.<br/>
function AudioTestWindow.generate_menuitems(list_func, index_func) function AudioTestWindow.generate_menuitems(list_func, index_func)
return { return {
{ {
@@ -39,7 +44,8 @@ function AudioTestWindow.generate_menuitems(list_func, index_func)
end end
--- Generates list of audio functions. --- Generates list of audio functions.
-- @return table A sorted list of audio function names. --- @within AudioTestWindow
--- @return result table A sorted list of audio function names.
function AudioTestWindow.generate_listfunc() function AudioTestWindow.generate_listfunc()
local result = {} local result = {}
@@ -55,12 +61,14 @@ function AudioTestWindow.generate_listfunc()
end end
--- Navigates back from audio test window. --- 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("menu")
end end
--- Initializes audio test window. --- 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
@@ -72,12 +80,14 @@ function AudioTestWindow.init()
end end
--- Draws audio test window. --- 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. --- 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,9 +1,11 @@
--- @section ConfigurationWindow
ConfigurationWindow = { ConfigurationWindow = {
controls = {}, controls = {},
selected_control = 1, selected_control = 1,
} }
--- Initializes configuration window. --- Initializes configuration window.
--- @within ConfigurationWindow
function ConfigurationWindow.init() function ConfigurationWindow.init()
ConfigurationWindow.controls = { ConfigurationWindow.controls = {
UI.create_action_item( UI.create_action_item(
@@ -18,6 +20,7 @@ function ConfigurationWindow.init()
end end
--- Draws configuration window. --- Draws configuration window.
--- @within ConfigurationWindow
function ConfigurationWindow.draw() function ConfigurationWindow.draw()
UI.draw_top_bar("Configuration") UI.draw_top_bar("Configuration")
@@ -27,7 +30,7 @@ function ConfigurationWindow.draw()
local char_width = 4 local char_width = 4
for i, control in ipairs(ConfigurationWindow.controls) do for i, control in ipairs(ConfigurationWindow.controls) do
local current_y = y_start + (i - 1) * 12 local current_y = y_start + (i - 1) * 12
local color = Config.colors.green local color = Config.colors.light_blue
if control.type == "numeric_stepper" then if control.type == "numeric_stepper" then
local value = control.get() local value = control.get()
local label_text = control.label local label_text = control.label
@@ -60,6 +63,7 @@ function ConfigurationWindow.draw()
end end
--- Updates configuration window logic. --- 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("menu")

View File

@@ -1,19 +1,29 @@
--- @section GameWindow
local _available_decisions = {} local _available_decisions = {}
local _selected_decision_index = 1 local _selected_decision_index = 1
--- Draws the game window. --- Draws the game window.
--- @within GameWindow
function GameWindow.draw() function GameWindow.draw()
local screen = Screen.get_by_id(Context.game.current_screen) local screen = Screen.get_by_id(Context.game.current_screen)
if screen.background then Map.draw(screen.background) end if screen.background then
Map.draw(screen.background)
elseif screen.background_color then
rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color)
end
UI.draw_top_bar(screen.name) UI.draw_top_bar(screen.name)
if #_available_decisions > 0 then if not Context.stat_screen_active and #_available_decisions > 0 then
UI.draw_decision_selector(_available_decisions, _selected_decision_index) UI.draw_decision_selector(_available_decisions, _selected_decision_index)
end end
Sprite.draw() Sprite.draw()
Focus.draw()
screen.draw()
end end
--- Updates the game window logic. --- Updates the game window logic.
--- @within GameWindow
function GameWindow.update() function GameWindow.update()
Focus.update()
if Input.menu_back() then if Input.menu_back() then
Window.set_current("menu") Window.set_current("menu")
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
@@ -31,6 +41,8 @@ function GameWindow.update()
end end
end end
if Context.stat_screen_active then return end
-- Fetch and filter decisions locally -- Fetch and filter decisions locally
local all_decisions_for_screen = Decision.get_for_screen(screen) local all_decisions_for_screen = Decision.get_for_screen(screen)
_available_decisions = Decision.filter_available(all_decisions_for_screen) _available_decisions = Decision.filter_available(all_decisions_for_screen)
@@ -57,10 +69,13 @@ function GameWindow.update()
selected_decision.handle() selected_decision.handle()
end end
end end
end end
--- Sets the active window. --- Sets the active window.
-- @param new_state string The ID of the new active window. --- @within GameWindow
--- @param new_state string The ID of the new active window.</br>
function GameWindow.set_state(new_state) function GameWindow.set_state(new_state)
Window.set_current(new_state) Window.set_current(new_state)
end end

View File

@@ -1,3 +1,4 @@
--- @section IntroWindow
IntroWindow.y = Config.screen.height IntroWindow.y = Config.screen.height
IntroWindow.speed = 0.5 IntroWindow.speed = 0.5
IntroWindow.text = [[ IntroWindow.text = [[
@@ -14,12 +15,14 @@ on than meets the eye.
]] ]]
--- Draws the intro window. --- 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(IntroWindow.text, x, IntroWindow.y, Config.colors.light_blue)
end end
--- Updates the intro window logic. --- Updates the intro window logic.
--- @within IntroWindow
function IntroWindow.update() function IntroWindow.update()
IntroWindow.y = IntroWindow.y - IntroWindow.speed IntroWindow.y = IntroWindow.y - IntroWindow.speed

View File

@@ -1,34 +1,44 @@
--- @section Window
local _windows = {} local _windows = {}
--- Registers a window table. --- Registers a window table.
-- @param id string The ID of the window (e.g., "splash", "menu"). --- @within Window
-- @param window_table table The actual window module table (e.g., SplashWindow). --- @param id string The ID of the window (e.g., "splash", "menu").</br>
--- @param window_table table The actual window module table (e.g., SplashWindow).</br>
function Window.register(id, window_table) function Window.register(id, window_table)
_windows[id] = window_table _windows[id] = window_table
end end
--- Retrieves a registered window table by its ID. --- Retrieves a registered window table by its ID.
-- @param id string The ID of the window. --- @within Window
-- @return table The window module table. --- @param id string The ID of the window.
--- @return result table The window module table or nil. </br>
--- Fields: </br>
--- * update (function) Called each frame to update window logic.<br/>
--- * draw (function) Called each frame to draw the window.<br/>
function Window.get(id) function Window.get(id)
return _windows[id] return _windows[id]
end end
--- Sets the currently active window. --- Sets the currently active window.
-- @param id string The ID of the window to activate. --- @within Window
--- @param id string The ID of the window to activate.</br>
function Window.set_current(id) function Window.set_current(id)
Context.current_window = id Context.current_window = id
end end
--- Gets the ID of the currently active window. --- Gets the ID of the currently active window.
-- @return string The ID of the active window. --- This function is used by the main game loop to update and draw the active window.
--- @within Window
--- @return string The ID of the active window.
function Window.get_current_id() function Window.get_current_id()
return Context.current_window return Context.current_window
end end
--- Gets the handler function for the currently active window. --- 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. -- This function is used by the main game loop to update and draw the active window.
-- @return function A function that updates and draws the current window. --- @within Window
--- @return function A function that updates and draws the current window.
function Window.get_current_handler() function Window.get_current_handler()
local window_table = Window.get(Context.current_window) local window_table = Window.get(Context.current_window)
if window_table and window_table.update and window_table.draw then if window_table and window_table.update and window_table.draw then

View File

@@ -1,12 +1,15 @@
--- @section MenuWindow
local _menu_items = {} local _menu_items = {}
--- Draws the menu window. --- 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(_menu_items, Context.current_menu_item, 108, 70)
end end
--- Updates the menu window logic. --- 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.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item)
@@ -20,45 +23,53 @@ function MenuWindow.update()
end end
--- Starts a new game from the menu. --- 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("game") GameWindow.set_state("game")
end end
--- Loads a game from the menu. --- 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("game") GameWindow.set_state("game")
end end
--- Saves the current game from the menu. --- 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. --- Resumes the game from the menu.
--- @within MenuWindow
function MenuWindow.resume_game() function MenuWindow.resume_game()
GameWindow.set_state("game") GameWindow.set_state("game")
end end
--- Exits the game. --- Exits the game.
--- @within MenuWindow
function MenuWindow.exit() function MenuWindow.exit()
exit() exit()
end end
--- Opens the configuration menu. --- Opens the configuration menu.
--- @within MenuWindow
function MenuWindow.configuration() function MenuWindow.configuration()
ConfigurationWindow.init() ConfigurationWindow.init()
GameWindow.set_state("configuration") GameWindow.set_state("configuration")
end end
--- Opens the audio test menu. --- 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("audiotest")
end end
--- Refreshes menu items. --- Refreshes menu items.
--- @within MenuWindow
function MenuWindow.refresh_menu_items() function MenuWindow.refresh_menu_items()
_menu_items = {} _menu_items = {}
if Context.game_in_progress then if Context.game_in_progress then

View File

@@ -1,13 +1,17 @@
--- @section MinigameDDRWindow
--- Initializes DDR minigame state. --- Initializes DDR minigame state.
-- @param params table Optional parameters for configuration. --- @within MinigameDDRWindow
--- @param params table Optional parameters for configuration.<br/>
function MinigameDDRWindow.init(params) function MinigameDDRWindow.init(params)
Context.minigame_ddr = Minigame.configure_ddr(params) Context.minigame_ddr = Minigame.configure_ddr(params)
end end
--- Starts the DDR minigame. --- Starts the DDR minigame.
-- @param return_window string The window ID to return to after the minigame. --- @within MinigameDDRWindow
-- @param[opt] song_key string The key of the song to play. --- @param return_window string The window ID to return to after the minigame.</br>
-- @param[opt] params table Optional parameters for minigame configuration. --- @param[opt] song_key string The key of the song to play.</br>
--- @param[opt] params table Optional parameters for minigame configuration.</br>
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 "game"
@@ -29,6 +33,7 @@ function MinigameDDRWindow.start(return_window, song_key, params)
end end
--- Spawns a random arrow. --- 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)]
@@ -40,7 +45,8 @@ local function spawn_arrow()
end end
--- Spawns an arrow in a specific direction. --- Spawns an arrow in a specific direction.
-- @param direction string The direction of the arrow ("left", "down", "up", "right"). --- @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
@@ -56,8 +62,9 @@ local function spawn_arrow_dir(direction)
end end
--- Checks if an arrow is hit. --- Checks if an arrow is hit.
-- @param arrow table The arrow data. --- @within MinigameDDRWindow
-- @return boolean True if the arrow is hit, false otherwise. --- @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)
@@ -65,18 +72,20 @@ local function check_hit(arrow)
end end
--- Checks if an arrow is missed. --- Checks if an arrow is missed.
-- @param arrow table The arrow data. --- @within MinigameDDRWindow
-- @return boolean True if the arrow is missed, false otherwise. --- @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. --- Draws an arrow.
-- @param x number The x-coordinate. --- @within MinigameDDRWindow
-- @param y number The y-coordinate. --- @param x number The x-coordinate.
-- @param direction string The direction of the arrow. --- @param y number The y-coordinate.
-- @param color number The color of the arrow. --- @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
@@ -96,6 +105,7 @@ local function draw_arrow(x, y, direction, color)
end end
--- Updates DDR minigame logic. --- 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
@@ -189,6 +199,7 @@ function MinigameDDRWindow.update()
end end
--- Draws DDR minigame. --- 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
@@ -208,7 +219,7 @@ function MinigameDDRWindow.draw()
rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey)
local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width
if fill_width > 0 then if fill_width > 0 then
local bar_color = Config.colors.green local bar_color = Config.colors.light_blue
if mg.bar_fill > 66 then if mg.bar_fill > 66 then
bar_color = Config.colors.item bar_color = Config.colors.item
elseif mg.bar_fill > 33 then elseif mg.bar_fill > 33 then
@@ -221,7 +232,7 @@ function MinigameDDRWindow.draw()
if mg.target_arrows then if mg.target_arrows then
for _, target in ipairs(mg.target_arrows) do 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 local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0
local color = is_pressed and Config.colors.green or Config.colors.light_grey local color = is_pressed and Config.colors.light_blue or Config.colors.light_grey
draw_arrow(target.x, mg.target_y, target.dir, color) draw_arrow(target.x, mg.target_y, target.dir, color)
end end
end end
@@ -241,17 +252,17 @@ function MinigameDDRWindow.draw()
"PATTERN MODE - Frame:" .. mg.frame_counter, "PATTERN MODE - Frame:" .. mg.frame_counter,
Config.screen.width / 2, Config.screen.width / 2,
debug_y, debug_y,
Config.colors.green Config.colors.light_blue
) )
if mg.current_song and mg.current_song.pattern then if mg.current_song and mg.current_song.pattern then
Print.text_center( Print.text_center(
"Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index, "Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index,
Config.screen.width / 2, Config.screen.width / 2,
debug_y + 10, debug_y + 10,
Config.colors.green Config.colors.light_blue
) )
end end
else else
Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue) Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue)
end end
end end

View File

@@ -1,19 +1,28 @@
--- Initializes button mash minigame state. --- Initializes button mash minigame state.
-- @param params table Optional parameters for configuration. --- @within MinigameButtonMashWindow
--- @param params table Optional parameters for configuration.<br/>
function MinigameButtonMashWindow.init(params) function MinigameButtonMashWindow.init(params)
Context.minigame_button_mash = Minigame.configure_button_mash(params) Context.minigame_button_mash = Minigame.configure_button_mash(params)
end end
--- Starts the button mash minigame. --- Starts the button mash minigame.
-- @param return_window string The window ID to return to after the minigame. --- @within MinigameButtonMashWindow
-- @param[opt] params table Optional parameters for minigame configuration. --- @param return_window string The window ID to return to after the minigame.<br/>
--- @param[opt] params table Optional parameters for minigame configuration.<br/>
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" local mg = Context.minigame_button_mash
mg.return_window = return_window or "game"
if mg.focus_center_x then
Focus.start_driven(mg.focus_center_x, mg.focus_center_y, {
initial_radius = mg.focus_initial_radius
})
end
Window.set_current("minigame_button_mash") Window.set_current("minigame_button_mash")
end end
--- Updates button mash minigame logic. --- 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
@@ -26,6 +35,7 @@ function MinigameButtonMashWindow.update()
if mg.bar_fill >= mg.max_fill then if mg.bar_fill >= mg.max_fill then
Meter.on_minigame_complete() Meter.on_minigame_complete()
Meter.show() Meter.show()
if mg.focus_center_x then Focus.stop() end
Window.set_current(mg.return_window) Window.set_current(mg.return_window)
return return
end end
@@ -37,20 +47,26 @@ function MinigameButtonMashWindow.update()
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
mg.button_pressed_timer = mg.button_pressed_timer - 1 mg.button_pressed_timer = mg.button_pressed_timer - 1
end end
if mg.focus_center_x then
Focus.set_percentage(mg.bar_fill / mg.max_fill)
end
end end
--- Draws button mash minigame. --- 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 == "game" then
GameWindow.draw() GameWindow.draw()
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) if not mg.focus_center_x then
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
end
rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey)
rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey)
local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width
if fill_width > 0 then if fill_width > 0 then
local bar_color = Config.colors.green local bar_color = Config.colors.light_blue
if mg.bar_fill > 66 then if mg.bar_fill > 66 then
bar_color = Config.colors.item bar_color = Config.colors.item
elseif mg.bar_fill > 33 then elseif mg.bar_fill > 33 then
@@ -60,7 +76,7 @@ function MinigameButtonMashWindow.draw()
end end
local button_color = Config.colors.light_grey local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
button_color = Config.colors.green button_color = Config.colors.light_blue
end end
circb(mg.button_x, mg.button_y, mg.button_size, button_color) circb(mg.button_x, mg.button_y, mg.button_size, button_color)
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then

View File

@@ -1,19 +1,30 @@
--- @section MinigameRhythmWindow
--- Initializes rhythm minigame state. --- Initializes rhythm minigame state.
-- @param params table Optional parameters for configuration. --- @within MinigameRhythmWindow
--- @param params table Optional parameters for configuration.<br/>
function MinigameRhythmWindow.init(params) function MinigameRhythmWindow.init(params)
Context.minigame_rhythm = Minigame.configure_rhythm(params) Context.minigame_rhythm = Minigame.configure_rhythm(params)
end end
--- Starts the rhythm minigame. --- Starts the rhythm minigame.
-- @param return_window string The window ID to return to after the minigame. --- @within MinigameRhythmWindow
-- @param[opt] params table Optional parameters for minigame configuration. --- @param return_window string The window ID to return to after the minigame.<br/>
--- @param[opt] params table Optional parameters for minigame configuration.<br/>
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" local mg = Context.minigame_rhythm
mg.return_window = return_window or "game"
if mg.focus_center_x then
Focus.start_driven(mg.focus_center_x, mg.focus_center_y, {
initial_radius = mg.focus_initial_radius
})
end
Window.set_current("minigame_rhythm") Window.set_current("minigame_rhythm")
end end
--- Updates rhythm minigame logic. --- 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)
@@ -48,28 +59,35 @@ function MinigameRhythmWindow.update()
if mg.score >= mg.max_score then if mg.score >= mg.max_score then
Meter.on_minigame_complete() Meter.on_minigame_complete()
Meter.show() Meter.show()
if mg.focus_center_x then Focus.stop() end
Window.set_current(mg.return_window) Window.set_current(mg.return_window)
return return
end end
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
mg.button_pressed_timer = mg.button_pressed_timer - 1 mg.button_pressed_timer = mg.button_pressed_timer - 1
end end
if mg.focus_center_x then
Focus.set_percentage(1 - mg.score / mg.max_score)
end
end end
--- Draws rhythm minigame. --- 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 == "game" then
GameWindow.draw() GameWindow.draw()
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) if not mg.focus_center_x then
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
end
rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey)
rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey)
rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey) rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey)
local target_left = mg.target_center - (mg.target_width / 2) local target_left = mg.target_center - (mg.target_width / 2)
local target_x = mg.bar_x + (target_left * mg.bar_width) local target_x = mg.bar_x + (target_left * mg.bar_width)
local target_width_pixels = mg.target_width * 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.green) rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.light_blue)
local line_x = mg.bar_x + (mg.line_position * mg.bar_width) 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) rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.item)
local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score
@@ -82,7 +100,7 @@ function MinigameRhythmWindow.draw()
) )
local button_color = Config.colors.light_grey local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
button_color = Config.colors.green button_color = Config.colors.light_blue
end end
circb(mg.button_x, mg.button_y, mg.button_size, button_color) circb(mg.button_x, mg.button_y, mg.button_size, button_color)
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then

View File

@@ -1,3 +1,4 @@
--- @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
@@ -7,7 +8,8 @@ local TEXT_MARGIN_Y = POPUP_Y + 10
local LINE_HEIGHT = 8 local LINE_HEIGHT = 8
--- Displays a popup window. --- Displays a popup window.
-- @param content_strings table A table of strings to display in the popup. --- @within PopupWindow
--- @param content_strings table A table of strings to display in the popup.</br>
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 {}
@@ -15,6 +17,7 @@ function PopupWindow.show(content_strings)
end end
--- Hides the popup window. --- 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 = {}
@@ -22,6 +25,7 @@ function PopupWindow.hide()
end end
--- Updates popup window logic. --- 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
@@ -31,10 +35,11 @@ function PopupWindow.update()
end end
--- Draws the popup window. --- 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)
rectb(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.green) rectb(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.light_blue)
local current_y = TEXT_MARGIN_Y local current_y = TEXT_MARGIN_Y
for _, line in ipairs(Context.popup.content) do for _, line in ipairs(Context.popup.content) do
@@ -42,6 +47,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.light_blue)
end end
end end

View File

@@ -1,4 +1,5 @@
--- Draws the splash window. --- 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 y = (Config.screen.height - 6) / 2
@@ -6,6 +7,7 @@ function SplashWindow.draw()
end end
--- Updates the splash window logic. --- 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