From c9db82cce733ca2afc4d366a7fcc64d3151b3580 Mon Sep 17 00:00:00 2001 From: Zoltan Timar Date: Thu, 12 Feb 2026 16:31:03 +0100 Subject: [PATCH 1/2] feat: added minigames (button_mash, rhythm, ddr), correction in makefiles readline, placed games in init.context --- .gitignore | 2 + Makefile | 4 +- impostor.inc | 4 + inc/data/data.songs.lua | 107 ++++++++ inc/init/init.context.lua | 74 +++++- inc/init/init.modules.lua | 3 + inc/init/init.windows.lua | 3 + inc/system/system.main.lua | 12 + inc/system/system.print.lua | 7 + inc/window/window.game.lua | 7 + inc/window/window.minigame.ddr.lua | 347 ++++++++++++++++++++++++++ inc/window/window.minigame.mash.lua | 113 +++++++++ inc/window/window.minigame.rhythm.lua | 154 ++++++++++++ inc/window/window.popup.lua | 38 ++- 14 files changed, 857 insertions(+), 18 deletions(-) create mode 100644 inc/data/data.songs.lua create mode 100644 inc/window/window.minigame.ddr.lua create mode 100644 inc/window/window.minigame.mash.lua create mode 100644 inc/window/window.minigame.rhythm.lua diff --git a/.gitignore b/.gitignore index a5b9ea1..12099f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .local impostor.lua +prompts +docs \ No newline at end of file diff --git a/Makefile b/Makefile index 383f4f4..a58f48d 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,10 @@ build: $(OUTPUT) $(OUTPUT): $(SRC) $(ORDER) @rm -f $(OUTPUT) - @while read f; do \ + @sed 's/\r$$//' $(ORDER) | while read f; do \ cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \ echo "" >> $(OUTPUT); \ - done < $(ORDER) + done export: build @if [ -z "$(VERSION)" ]; then \ diff --git a/impostor.inc b/impostor.inc index ddfac6d..59128af 100644 --- a/impostor.inc +++ b/impostor.inc @@ -3,6 +3,7 @@ init/init.modules.lua init/init.config.lua init/init.windows.lua init/init.context.lua +data/data.songs.lua system/system.print.lua entity/entity.npc.lua entity/entity.item.lua @@ -14,6 +15,9 @@ window/window.intro.lua window/window.menu.lua window/window.configuration.lua window/window.popup.lua +window/window.minigame.mash.lua +window/window.minigame.rhythm.lua +window/window.minigame.ddr.lua window/window.game.lua system/system.main.lua meta/meta.assets.lua diff --git a/inc/data/data.songs.lua b/inc/data/data.songs.lua new file mode 100644 index 0000000..90fcca3 --- /dev/null +++ b/inc/data/data.songs.lua @@ -0,0 +1,107 @@ +-- DDR Arrow Spawn Patterns +-- Each song defines when arrows should spawn, synced to music beats + +Songs = { + -- Example song pattern + test_song = { + name = "Test Song", + bpm = 120, -- Beats per minute (for reference) + fps = 60, -- Frames per second (TIC-80 default) + + -- Arrow spawn pattern + -- Each entry defines when (in frames) and which direction arrow spawns + -- Formula: frame = (beat / bpm) * 60 * fps + -- For 120 BPM: 1 beat = 30 frames, 2 beats = 60 frames, etc. + pattern = { + -- Beat 1-4 (intro) + {frame = 30, dir = "left"}, + {frame = 60, dir = "down"}, + {frame = 90, dir = "up"}, + {frame = 120, dir = "right"}, + + -- Beat 5-8 (faster) + {frame = 135, dir = "left"}, + {frame = 150, dir = "right"}, + {frame = 165, dir = "left"}, + {frame = 180, dir = "right"}, + + -- Beat 9-12 (complex pattern) + {frame = 210, dir = "left"}, + {frame = 210, dir = "right"}, -- simultaneous + {frame = 240, dir = "up"}, + {frame = 240, dir = "down"}, -- simultaneous + {frame = 270, dir = "left"}, + {frame = 300, dir = "right"}, + + -- Beat 13-16 (rapid sequence) + {frame = 330, dir = "left"}, + {frame = 345, dir = "down"}, + {frame = 360, dir = "up"}, + {frame = 375, dir = "right"}, + {frame = 390, dir = "left"}, + {frame = 405, dir = "down"}, + {frame = 420, dir = "up"}, + {frame = 435, dir = "right"}, + + -- Beat 17-20 (finale) + {frame = 465, dir = "up"}, + {frame = 465, dir = "down"}, + {frame = 495, dir = "left"}, + {frame = 495, dir = "right"}, + {frame = 525, dir = "up"}, + {frame = 540, dir = "down"}, + {frame = 555, dir = "left"}, + {frame = 570, dir = "right"} + } + }, + + -- Random mode (no predefined pattern, spawns randomly) + random = { + name = "Random Mode", + bpm = 0, -- Not applicable for random mode + fps = 60, + pattern = {} -- Empty, will spawn randomly in game + } +} + +-- Helper function to calculate frame from beat +-- Usage: frame_from_beat(beat_number, bpm, fps) +function frame_from_beat(beat, bpm, fps) + fps = fps or 60 + local seconds_per_beat = 60 / bpm + local frames_per_beat = seconds_per_beat * fps + return math.floor(beat * frames_per_beat) +end + +-- Helper function to convert simple beat notation to frame pattern +-- Usage: beats_to_pattern({{1, "left"}, {2, "down"}}, 120) +function beats_to_pattern(beats, bpm, fps) + fps = fps or 60 + local pattern = {} + for _, beat_data in ipairs(beats) do + local beat = beat_data[1] + local dir = beat_data[2] + table.insert(pattern, { + frame = frame_from_beat(beat, bpm, fps), + dir = dir + }) + end + return pattern +end + +-- Example of creating a song using beat notation: +--[[ +Songs.custom_song = { + name = "Custom Song", + bpm = 130, + fps = 60, + pattern = beats_to_pattern({ + {1, "left"}, + {2, "down"}, + {3, "up"}, + {4, "right"}, + {4.5, "left"}, + {5, "right"} + }, 130) +} +]] diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua index cfb31ba..5612d42 100644 --- a/inc/init/init.context.lua +++ b/inc/init/init.context.lua @@ -2,15 +2,8 @@ local SAVE_GAME_BANK = 6 local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0 local SAVE_GAME_MAGIC_VALUE = 0xCA -local SAVE_GAME_PLAYER_X_ADDRESS = 1 -local SAVE_GAME_PLAYER_Y_ADDRESS = 2 -local SAVE_GAME_PLAYER_VX_ADDRESS = 3 -local SAVE_GAME_PLAYER_VY_ADDRESS = 4 -local SAVE_GAME_selectS_ADDRESS = 5 local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6 -local VX_VY_OFFSET = 128 -- Offset for negative velocities - -- Helper for deep copying tables local function clone_table(t) local copy = {} @@ -57,9 +50,25 @@ local function get_initial_data() game_in_progress = false, -- New flag screens = clone_table({ { - -- Screen 1 name = "Screen 1", npcs = { + { + name = "Button Mash Minigame", + sprite_id = 4, + dialog = { + start = { + text = "Ready to test your reflexes? Prove your speed!", + options = { + {label = "Let's do it!", next_node = "__MINIGAME_BUTTON_MASH__"}, + {label = "Not now.", next_node = "dialog_end"} + } + }, + dialog_end = { + text = "Come back when you're ready.", + options = {} + } + } + }, { name = "Trinity", sprite_id = 2, @@ -171,7 +180,7 @@ local function get_initial_data() options = {} } } - } + }, }, items = { { @@ -185,6 +194,23 @@ local function get_initial_data() -- Screen 2 name = "Screen 2", npcs = { + { + name = "Rhythm Master", + sprite_id = 4, + dialog = { + start = { + text = "Test your timing! Hit the mark when the moment is right.", + options = { + {label = "Let's go!", next_node = "__MINIGAME_RHYTHM__"}, + {label = "Not now.", next_node = "dialog_end"} + } + }, + dialog_end = { + text = "Come back when you're ready to test your reflexes.", + options = {} + } + } + }, { name = "Morpheus", sprite_id = 5, @@ -275,6 +301,36 @@ local function get_initial_data() -- Screen 3 name = "Screen 3", npcs = { + { + name = "DDR Rhythm Master", + sprite_id = 4, + dialog = { + start = { + text = "Test your reflexes! Hit the arrows in time with the music. Choose your difficulty:", + options = { + {label = "Test Song", next_node = "test"}, + {label = "Random Mode", next_node = "random"}, + {label = "Not now.", next_node = "dialog_end"} + } + }, + test = { + text = "Test song selected. Show me what you got!", + options = { + {label = "Start!", next_node = "__MINIGAME_DDR:test_song__"} + } + }, + random = { + text = "Random arrows! No pattern, just react!", + options = { + {label = "Start!", next_node = "__MINIGAME_DDR__"} + } + }, + dialog_end = { + text = "Come back when you're ready to dance!", + options = {} + } + } + }, { name = "Agent Smith", sprite_id = 8, diff --git a/inc/init/init.modules.lua b/inc/init/init.modules.lua index 3a80d6e..c73a004 100644 --- a/inc/init/init.modules.lua +++ b/inc/init/init.modules.lua @@ -4,6 +4,9 @@ local MenuWindow = {} local GameWindow = {} local PopupWindow = {} local ConfigurationWindow = {} +local MinigameButtonMashWindow = {} +local MinigameRhythmWindow = {} +local MinigameDDRWindow = {} local UI = {} local Print = {} diff --git a/inc/init/init.windows.lua b/inc/init/init.windows.lua index 8571d39..d3a907e 100644 --- a/inc/init/init.windows.lua +++ b/inc/init/init.windows.lua @@ -4,3 +4,6 @@ local WINDOW_MENU = 2 local WINDOW_GAME = 3 local WINDOW_POPUP = 4 local WINDOW_CONFIGURATION = 7 +local WINDOW_MINIGAME_BUTTON_MASH = 8 +local WINDOW_MINIGAME_RHYTHM = 9 +local WINDOW_MINIGAME_DDR = 10 diff --git a/inc/system/system.main.lua b/inc/system/system.main.lua index abfcb74..b61c17f 100644 --- a/inc/system/system.main.lua +++ b/inc/system/system.main.lua @@ -24,6 +24,18 @@ local STATE_HANDLERS = { ConfigurationWindow.update() ConfigurationWindow.draw() end, + [WINDOW_MINIGAME_BUTTON_MASH] = function() + MinigameButtonMashWindow.update() + MinigameButtonMashWindow.draw() + end, + [WINDOW_MINIGAME_RHYTHM] = function() + MinigameRhythmWindow.update() + MinigameRhythmWindow.draw() + end, + [WINDOW_MINIGAME_DDR] = function() + MinigameDDRWindow.update() + MinigameDDRWindow.draw() + end, } local initialized_game = false diff --git a/inc/system/system.print.lua b/inc/system/system.print.lua index 9fdbb13..f39d13f 100644 --- a/inc/system/system.print.lua +++ b/inc/system/system.print.lua @@ -5,4 +5,11 @@ function Print.text(text, x, y, color, fixed, scale) scale = scale or 1 print(text, x + 1, y + 1, shadow_color, fixed, scale) print(text, x, y, color, fixed, scale) +end + +function Print.text_center(text, x, y, color, fixed, scale) + scale = scale or 1 + local text_width = print(text, 0, -6, 0, fixed, scale) + local centered_x = x - (text_width / 2) + Print.text(text, centered_x, y, color, fixed, scale) end \ No newline at end of file diff --git a/inc/window/window.game.lua b/inc/window/window.game.lua index dbcf351..8b116b3 100644 --- a/inc/window/window.game.lua +++ b/inc/window/window.game.lua @@ -19,11 +19,18 @@ function GameWindow.update() end if Input.player_interact() then + -- Get the current screen's NPCs + local currentScreenData = Context.screens[Context.current_screen] + if currentScreenData and currentScreenData.npcs and #currentScreenData.npcs > 0 then + -- For now, interact with the first NPC on the screen + -- TODO: Add proximity detection to find nearest NPC + local npc = currentScreenData.npcs[1] PopupWindow.show_menu_dialog(npc, { {label = "Talk to", action = NPC.talk_to}, {label = "Fight", action = NPC.fight}, {label = "Go back", action = NPC.go_back} }, WINDOW_POPUP) + end end end diff --git a/inc/window/window.minigame.ddr.lua b/inc/window/window.minigame.ddr.lua new file mode 100644 index 0000000..1673ed2 --- /dev/null +++ b/inc/window/window.minigame.ddr.lua @@ -0,0 +1,347 @@ +function MinigameDDRWindow.init() + -- Calculate evenly spaced arrow positions + local arrow_size = 12 + local arrow_spacing = 30 + local total_width = (4 * arrow_size) + (3 * arrow_spacing) + local start_x = (Config.screen.width - total_width) / 2 + + Context.minigame_ddr = { + -- Progress bar (matching button mash style) + bar_fill = 0, -- 0 to 100 + max_fill = 100, + fill_per_hit = 10, -- Points gained per perfect hit + miss_penalty = 5, -- Points lost per miss + bar_x = 20, + bar_y = 10, + bar_width = 200, + bar_height = 12, + + -- Arrow settings + arrow_size = arrow_size, + arrow_spawn_timer = 0, + arrow_spawn_interval = 45, -- Frames between arrow spawns (for random mode) + arrow_fall_speed = 1.5, -- Pixels per frame + arrows = {}, -- Active falling arrows {dir, x, y} + + -- Target arrows at bottom (evenly spaced, centered on screen) + target_y = 115, -- Y position of target arrows + target_arrows = { + {dir = "left", x = start_x}, + {dir = "down", x = start_x + arrow_size + arrow_spacing}, + {dir = "up", x = start_x + (arrow_size + arrow_spacing) * 2}, + {dir = "right", x = start_x + (arrow_size + arrow_spacing) * 3} + }, + + -- Hit detection + hit_threshold = 8, -- Pixels of tolerance for perfect hit + button_pressed_timers = {}, -- Visual feedback per arrow + button_press_duration = 8, + + -- Input cooldown per direction + input_cooldowns = { + left = 0, + down = 0, + up = 0, + right = 0 + }, + input_cooldown_duration = 10, + + -- Song/Pattern system + frame_counter = 0, -- Tracks frames since start + current_song = nil, -- Current song data + pattern_index = 1, -- Current position in pattern + use_pattern = false, -- If true, use song pattern; if false, use random spawning + + return_window = WINDOW_GAME + } +end + +function MinigameDDRWindow.start(return_window, song_key) + MinigameDDRWindow.init() + Context.minigame_ddr.return_window = return_window or WINDOW_GAME + + -- Debug: Store song_key for display + Context.minigame_ddr.debug_song_key = song_key + + -- Load song pattern if specified + if song_key and Songs and Songs[song_key] then + Context.minigame_ddr.current_song = Songs[song_key] + Context.minigame_ddr.use_pattern = true + Context.minigame_ddr.pattern_index = 1 + Context.minigame_ddr.debug_status = "Pattern loaded: " .. song_key + else + -- Default to random spawning + Context.minigame_ddr.use_pattern = false + if song_key then + Context.minigame_ddr.debug_status = "Song not found: " .. tostring(song_key) + else + Context.minigame_ddr.debug_status = "Random mode" + end + end + + Context.active_window = WINDOW_MINIGAME_DDR +end + +-- Spawn a new arrow (random direction) +local function spawn_arrow() + local mg = Context.minigame_ddr + local target = mg.target_arrows[math.random(1, 4)] + table.insert(mg.arrows, { + dir = target.dir, + x = target.x, + y = mg.bar_y + mg.bar_height + 10 -- Start below progress bar + }) +end + +-- Spawn an arrow with specific direction +local function spawn_arrow_dir(direction) + local mg = Context.minigame_ddr + -- Find the target arrow for this direction + for _, target in ipairs(mg.target_arrows) do + if target.dir == direction then + table.insert(mg.arrows, { + dir = direction, + x = target.x, + y = mg.bar_y + mg.bar_height + 10 + }) + break + end + end +end + +-- Check if arrow is close enough for a hit +local function check_hit(arrow) + local mg = Context.minigame_ddr + local distance = math.abs(arrow.y - mg.target_y) + return distance <= mg.hit_threshold +end + +-- Check if arrow has passed the target +local function check_miss(arrow) + local mg = Context.minigame_ddr + return arrow.y > mg.target_y + mg.hit_threshold +end + +-- Draw a single arrow sprite +local function draw_arrow(x, y, direction, color) + local size = 12 + local half = size / 2 + + -- Draw arrow shape based on direction + if direction == "left" then + -- Triangle pointing left + tri(x + half, y, x, y + half, x + half, y + size, color) + rect(x + half, y + half - 2, half, 4, color) + elseif direction == "right" then + -- Triangle pointing right + tri(x + half, y, x + size, y + half, x + half, y + size, color) + rect(x, y + half - 2, half, 4, color) + elseif direction == "up" then + -- Triangle pointing up + tri(x, y + half, x + half, y, x + size, y + half, color) + rect(x + half - 2, y + half, 4, half, color) + elseif direction == "down" then + -- Triangle pointing down + tri(x, y + half, x + half, y + size, x + size, y + half, color) + rect(x + half - 2, y, 4, half, color) + end +end + +function MinigameDDRWindow.update() + local mg = Context.minigame_ddr + + -- Check for completion + if mg.bar_fill >= mg.max_fill then + Context.active_window = mg.return_window + return + end + + -- Increment frame counter + mg.frame_counter = mg.frame_counter + 1 + + -- Spawn arrows based on mode (pattern or random) + if mg.use_pattern and mg.current_song and mg.current_song.pattern then + -- Pattern-based spawning (synced to song) + local pattern = mg.current_song.pattern + + -- Check if current frame matches any pattern entry + while mg.pattern_index <= #pattern do + local spawn_entry = pattern[mg.pattern_index] + + if mg.frame_counter >= spawn_entry.frame then + -- Time to spawn this arrow! + spawn_arrow_dir(spawn_entry.dir) + mg.pattern_index = mg.pattern_index + 1 + else + -- Not time yet, break the loop + break + end + end + + -- If we've finished the pattern, check if we should loop or end + if mg.pattern_index > #pattern then + -- Pattern complete - could loop or switch to random mode + -- For now, keep playing but stop spawning new arrows + -- Optionally: mg.pattern_index = 1 to loop + end + else + -- Random spawning mode (original behavior) + mg.arrow_spawn_timer = mg.arrow_spawn_timer + 1 + if mg.arrow_spawn_timer >= mg.arrow_spawn_interval then + spawn_arrow() + mg.arrow_spawn_timer = 0 + end + end + + -- Update falling arrows + local arrows_to_remove = {} + for i, arrow in ipairs(mg.arrows) do + arrow.y = arrow.y + mg.arrow_fall_speed + + -- Check if arrow went off-screen (miss) + if check_miss(arrow) then + table.insert(arrows_to_remove, i) + -- Penalty for missing + mg.bar_fill = mg.bar_fill - mg.miss_penalty + if mg.bar_fill < 0 then + mg.bar_fill = 0 + end + end + end + + -- Remove off-screen arrows (iterate backwards to avoid index issues) + for i = #arrows_to_remove, 1, -1 do + table.remove(mg.arrows, arrows_to_remove[i]) + end + + -- Update input cooldowns + for dir, _ in pairs(mg.input_cooldowns) do + if mg.input_cooldowns[dir] > 0 then + mg.input_cooldowns[dir] = mg.input_cooldowns[dir] - 1 + end + end + + -- Update button press timers + for dir, _ in pairs(mg.button_pressed_timers) do + if mg.button_pressed_timers[dir] > 0 then + mg.button_pressed_timers[dir] = mg.button_pressed_timers[dir] - 1 + end + end + + -- Check for arrow key inputs + local input_map = { + left = Input.left(), + down = Input.down(), + up = Input.up(), + right = Input.right() + } + + for dir, pressed in pairs(input_map) do + if pressed and mg.input_cooldowns[dir] == 0 then + mg.input_cooldowns[dir] = mg.input_cooldown_duration + mg.button_pressed_timers[dir] = mg.button_press_duration + + -- Check if any arrow matches this direction and is in hit range + local hit = false + for i, arrow in ipairs(mg.arrows) do + if arrow.dir == dir and check_hit(arrow) then + -- Perfect hit! + mg.bar_fill = mg.bar_fill + mg.fill_per_hit + if mg.bar_fill > mg.max_fill then + mg.bar_fill = mg.max_fill + end + table.remove(mg.arrows, i) + hit = true + break + end + end + + -- If pressed but no arrow to hit, apply small penalty + if not hit then + mg.bar_fill = mg.bar_fill - 2 + if mg.bar_fill < 0 then + mg.bar_fill = 0 + end + end + end + end +end + +function MinigameDDRWindow.draw() + local mg = Context.minigame_ddr + + -- Safety check + if not mg then + cls(0) + print("DDR ERROR: Context not initialized", 10, 10, 12) + print("Press Z to return", 10, 20, 12) + if Input.select() then + Context.active_window = WINDOW_GAME + end + return + end + + -- Draw the underlying window first (for overlay effect) + if mg.return_window == WINDOW_GAME then + GameWindow.draw() + end + + -- Draw semi-transparent overlay background + rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + + -- Draw progress bar background + 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) + + -- Draw progress bar fill + local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width + if fill_width > 0 then + -- Color changes as bar fills + local bar_color = Config.colors.green + if mg.bar_fill > 66 then + bar_color = Config.colors.item -- yellow + elseif mg.bar_fill > 33 then + bar_color = Config.colors.npc + end + + rect(mg.bar_x, mg.bar_y, fill_width, mg.bar_height, bar_color) + end + + -- Draw progress percentage + local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100) + Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black) + + -- Draw target arrows at bottom (light grey when not pressed) + if mg.target_arrows then + for _, target in ipairs(mg.target_arrows) do + local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0 + local color = is_pressed and Config.colors.green or Config.colors.light_grey + draw_arrow(target.x, mg.target_y, target.dir, color) + end + end + + -- Draw falling arrows (blue) + if mg.arrows then + for _, arrow in ipairs(mg.arrows) do + draw_arrow(arrow.x, arrow.y, arrow.dir, Config.colors.npc) -- blue color + end + end + + -- Draw instruction text + Print.text_center("Hit the arrows!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey) + + -- Debug info (large and visible) + local debug_y = 60 + if mg.debug_status then + Print.text_center(mg.debug_status, Config.screen.width / 2, debug_y, Config.colors.item) + debug_y = debug_y + 10 + end + if mg.use_pattern then + Print.text_center("PATTERN MODE - Frame:" .. mg.frame_counter, Config.screen.width / 2, debug_y, Config.colors.green) + if mg.current_song and mg.current_song.pattern then + Print.text_center("Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index, Config.screen.width / 2, debug_y + 10, Config.colors.green) + end + else + Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.npc) + end +end diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua new file mode 100644 index 0000000..ecd2765 --- /dev/null +++ b/inc/window/window.minigame.mash.lua @@ -0,0 +1,113 @@ +function MinigameButtonMashWindow.init() + Context.minigame_button_mash = { + bar_fill = 0, -- 0 to 100 + max_fill = 100, + fill_per_press = 8, + base_degradation = 0.15, -- Base degradation per frame + degradation_multiplier = 0.006, -- Increases with bar fill + button_pressed_timer = 0, -- Visual feedback timer + button_press_duration = 8, -- Frames to show button press + return_window = WINDOW_GAME, -- Window to return to after completion + bar_x = 20, + bar_y = 10, + bar_width = 200, + bar_height = 12, + button_x = 20, + button_y = 110, + button_size = 12 + } +end + +function MinigameButtonMashWindow.start(return_window) + MinigameButtonMashWindow.init() + Context.minigame_button_mash.return_window = return_window or WINDOW_GAME + Context.active_window = WINDOW_MINIGAME_BUTTON_MASH +end + +function MinigameButtonMashWindow.update() + local mg = Context.minigame_button_mash + + -- Check for Z button press + if Input.select() then + mg.bar_fill = mg.bar_fill + mg.fill_per_press + mg.button_pressed_timer = mg.button_press_duration + + -- Clamp to max + if mg.bar_fill > mg.max_fill then + mg.bar_fill = mg.max_fill + end + end + + -- Check if bar is full (completed) + if mg.bar_fill >= mg.max_fill then + Context.active_window = mg.return_window + return + end + + -- Automatic degradation (increases with bar fill level) + local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier) + mg.bar_fill = mg.bar_fill - degradation + + -- Clamp to minimum + if mg.bar_fill < 0 then + mg.bar_fill = 0 + end + + -- Update button press timer + if mg.button_pressed_timer > 0 then + mg.button_pressed_timer = mg.button_pressed_timer - 1 + end +end + +function MinigameButtonMashWindow.draw() + local mg = Context.minigame_button_mash + + -- Draw the underlying window first (for overlay effect) + if mg.return_window == WINDOW_GAME then + GameWindow.draw() + end + + -- Draw semi-transparent overlay background + -- Draw darker rectangles to create overlay effect + rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + + -- Draw progress bar background + 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) + + -- Draw progress bar fill + local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width + if fill_width > 0 then + -- Color changes as bar fills (green -> yellow -> red analogy using available colors) + local bar_color = Config.colors.green + if mg.bar_fill > 66 then + bar_color = Config.colors.item -- yellow + elseif mg.bar_fill > 33 then + bar_color = Config.colors.npc -- medium color + end + + rect(mg.bar_x, mg.bar_y, fill_width, mg.bar_height, bar_color) + end + + -- Draw button indicator + local button_color = Config.colors.light_grey + if mg.button_pressed_timer > 0 then + button_color = Config.colors.green -- Highlight when pressed + end + + -- Draw button as circle (approximated with rect for TIC-80) + circb(mg.button_x, mg.button_y, mg.button_size, button_color) + if mg.button_pressed_timer > 0 then + circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color) + end + + -- Draw Z text in the button + Print.text_center(" Z", mg.button_x - 2, mg.button_y - 3, Config.colors.light_grey) + + -- Draw instruction text + Print.text_center("MASH Z!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey) + + -- Draw progress percentage + local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100) + Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black) +end diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua new file mode 100644 index 0000000..91f965a --- /dev/null +++ b/inc/window/window.minigame.rhythm.lua @@ -0,0 +1,154 @@ +function MinigameRhythmWindow.init() + Context.minigame_rhythm = { + line_position = 0, -- Normalized position (0 to 1) + line_speed = 0.015, -- Movement speed per frame + line_direction = 1, -- 1 for left-to-right, -1 for right-to-left + target_center = 0.5, -- Center of target area (middle of bar) + target_width = 0.3, -- Width of target area (normalized) + initial_target_width = 0.3, + min_target_width = 0.08, -- Minimum width to keep game possible + target_shrink_rate = 0.9, -- Multiplier per successful hit (0.9 = 10% shrink) + score = 0, + max_score = 10, + button_pressed_timer = 0, + button_press_duration = 10, + return_window = WINDOW_GAME, + + -- Visual layout (match button mash minigame dimensions) + bar_x = 20, + bar_y = 10, + bar_width = 200, + bar_height = 12, + + -- Button indicator + button_x = 210, + button_y = 110, + button_size = 10, + + -- Cooldown to prevent multiple presses in one frame + press_cooldown = 0, + press_cooldown_duration = 15 + } +end + +function MinigameRhythmWindow.start(return_window) + MinigameRhythmWindow.init() + Context.minigame_rhythm.return_window = return_window or WINDOW_GAME + Context.active_window = WINDOW_MINIGAME_RHYTHM +end + +function MinigameRhythmWindow.update() + local mg = Context.minigame_rhythm + + -- Move the line across the bar (bidirectional) + mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction) + + -- Reverse direction when reaching either end + if mg.line_position > 1 then + mg.line_position = 1 + mg.line_direction = -1 + elseif mg.line_position < 0 then + mg.line_position = 0 + mg.line_direction = 1 + end + + -- Decrease cooldown timer + if mg.press_cooldown > 0 then + mg.press_cooldown = mg.press_cooldown - 1 + end + + -- Check for Z button press (only if cooldown expired) + if Input.select() and mg.press_cooldown == 0 then + mg.button_pressed_timer = mg.button_press_duration + mg.press_cooldown = mg.press_cooldown_duration + + -- Calculate if line is within target area + local target_left = mg.target_center - (mg.target_width / 2) + local target_right = mg.target_center + (mg.target_width / 2) + + if mg.line_position >= target_left and mg.line_position <= target_right then + -- HIT! Award point + mg.score = mg.score + 1 + else + -- MISS! Deduct point (but not below 0) + mg.score = mg.score - 1 + if mg.score < 0 then + mg.score = 0 + end + end + + -- Calculate target width dynamically based on score + -- Each point shrinks by 10%, so reverse the formula + mg.target_width = mg.initial_target_width * (mg.target_shrink_rate ^ mg.score) + if mg.target_width < mg.min_target_width then + mg.target_width = mg.min_target_width + end + end + + -- Check win condition + if mg.score >= mg.max_score then + Context.active_window = mg.return_window + return + end + + -- Update button press timer + if mg.button_pressed_timer > 0 then + mg.button_pressed_timer = mg.button_pressed_timer - 1 + end +end + +function MinigameRhythmWindow.draw() + local mg = Context.minigame_rhythm + + -- Draw the underlying window first (for overlay effect) + if mg.return_window == WINDOW_GAME then + GameWindow.draw() + end + + -- Draw semi-transparent overlay background + rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) + + -- Calculate actual pixel positions + local bar_center_x = mg.bar_x + mg.bar_width / 2 + + -- Draw bar container background + 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) + + -- Draw bar background (empty area) + rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey) + + -- Draw target area (highlighted section in middle) + local target_left = mg.target_center - (mg.target_width / 2) + local target_right = mg.target_center + (mg.target_width / 2) + local target_x = mg.bar_x + (target_left * mg.bar_width) + local target_width_pixels = mg.target_width * mg.bar_width + + rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.green) + + -- Draw the moving line + 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) -- Yellow line + + -- Draw score text + local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score + Print.text_center(score_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 8, Config.colors.light_grey) + + -- Draw instruction text + Print.text_center("Press Z when line is in green!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 20, Config.colors.light_grey) + + -- Draw button indicator in bottom-right corner + local button_color = Config.colors.light_grey + if mg.button_pressed_timer > 0 then + button_color = Config.colors.green -- Highlight when pressed + end + + -- Draw button circle + circb(mg.button_x, mg.button_y, mg.button_size, button_color) + if mg.button_pressed_timer > 0 then + circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color) + end + + -- Draw Z text in the button + Print.text_center("Z", mg.button_x - 2, mg.button_y - 3, button_color) +end diff --git a/inc/window/window.popup.lua b/inc/window/window.popup.lua index b974870..4e1a2dc 100644 --- a/inc/window/window.popup.lua +++ b/inc/window/window.popup.lua @@ -1,4 +1,28 @@ function PopupWindow.set_dialog_node(node_key) + -- Special handling for minigame trigger + if node_key == "__MINIGAME_BUTTON_MASH__" then + MinigameButtonMashWindow.start(WINDOW_GAME) + return + end + + -- Special handling for rhythm minigame trigger + if node_key == "__MINIGAME_RHYTHM__" then + MinigameRhythmWindow.start(WINDOW_GAME) + return + end + + -- Special handling for DDR minigame trigger + -- Format: __MINIGAME_DDR__ or __MINIGAME_DDR:song_key__ + if node_key == "__MINIGAME_DDR__" then + MinigameDDRWindow.start(WINDOW_GAME, nil) + return + end + if node_key:sub(1, 16) == "__MINIGAME_DDR:" then + -- Extract song key from the node (format: __MINIGAME_DDR:test_song__) + local song_key = node_key:sub(17, -3) -- Remove prefix "__MINIGAME_DDR:" and trailing "__" + MinigameDDRWindow.start(WINDOW_GAME, song_key) + return + end local npc = Context.dialog.active_entity local node = npc.dialog[node_key] @@ -24,10 +48,10 @@ function PopupWindow.set_dialog_node(node_key) -- if no options, it's the end of this branch. if #menu_items == 0 then - table.insert(menu_items, { - label = "Go back", - action = function() GameWindow.set_state(WINDOW_GAME) end - }) + table.insert(menu_items, { + label = "Go back", + action = function() GameWindow.set_state(WINDOW_GAME) end + }) end Context.dialog.menu_items = menu_items @@ -52,7 +76,7 @@ function PopupWindow.update() selected_item.action() end end - + if Input.menu_back() then GameWindow.set_state(WINDOW_GAME) end @@ -87,12 +111,12 @@ function PopupWindow.draw() -- Display the dialog content (description for "look at", or initial name/dialog for others) local wrapped_lines = UI.word_wrap(Context.dialog.text, 25) -- Max 25 chars per line - local current_y = 55 -- Starting Y position for the first line of content + local current_y = 55 -- Starting Y position for the first line of content for _, line in ipairs(wrapped_lines) do Print.text(line, 50, current_y, Config.colors.light_grey) current_y = current_y + 8 -- Move to the next line (8 pixels for default font height + padding) end - + -- Adjust menu position based on the number of wrapped lines if not Context.dialog.showing_description then UI.draw_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item, 50, current_y + 2) -- 2.49.1 From cd279803ac92d7acbcc066ec56bc3156f90f00a1 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Thu, 12 Feb 2026 20:12:25 +0100 Subject: [PATCH 2/2] Makefile update --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a58f48d..de83213 100644 --- a/Makefile +++ b/Makefile @@ -95,8 +95,9 @@ clean: # CI/CD Targets ci-version: - @VERSION=$$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]"); \ + @VERSION=$$(sed -n "s/^-- version: //p" inc-meta-meta.header.lua | head -n 1 | tr -d "[:space:]"); \ BRANCH=$${CI_COMMIT_BRANCH:-$${WOODPECKER_BRANCH}}; \ + BRANCH=$$(echo "$$BRANCH" | tr '/' '-'); \ if [ "$$BRANCH" != "main" ] && [ "$$BRANCH" != "master" ] && [ -n "$$BRANCH" ]; then \ VERSION=dev-$$VERSION-$$BRANCH; \ fi; \ -- 2.49.1