Merge branch 'develop' into codegenerator
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-04-27 22:57:18 +02:00
17 changed files with 493 additions and 18 deletions

View File

@@ -0,0 +1,197 @@
--- @section CreditsWindow
local _time = 0.0
local _scroll_x = 0.0
local _scroll_total_w = 0
local _scroll_chars = {}
local _title_chars = {}
local _title_total_w = 0
local _stars = {}
local TITLE = "TELETYPE GAMES"
local SCROLL_PARTS = {
"WEB: GAMES.TELETYPE.HU",
"BBS: GAMES.TELETYPE.HU:2323",
"IRC: LIBERA.CHAT #TELETYPEGAMES",
"YOUTUBE.COM/@TELETYPEGAMES",
}
local SCROLL_SEP = " * "
local SCROLL_SPEED = 55.0
local SCROLL_Y = 129
local SCROLL_ZONE_COLS = { 7, 4, 9 }
local TITLE_Y = 4
local TITLE_FALL_DUR = 0.45
local TITLE_DELAY_STEP = 0.18
local RASTER_COLS = { 1, 3, 9, 10, 11, 4, 11, 10, 9, 3, 1 }
local RASTER_Y_TOP = 26
local RASTER_Y_BOT = 110
local AUTHORS = {
"Mr. Zero - Zsolt Tasnadi",
"Mr. One - Balazs Tari",
"Mr. Two - Zoltan Timar",
"Mr. Three - Bela Mezo",
}
local AUTHORS_BASE_Y = 56
local AUTHORS_LINE_H = 12
local AUTHORS_ENTRY_DT = 0.65
local AUTHORS_ENTRY_V = 2.5
local RAINBOW = { 4, 9, 3, 7, 13, 2, 9, 4 }
local NUM_STARS = 40
--- Initialises credits state and pre-computes character metrics.
--- @within CreditsWindow
function CreditsWindow.init()
_time = 0.0
_scroll_x = Config.screen.width + 4.0
_title_chars = {}
_title_total_w = 0
for i = 1, #TITLE do
local ch = TITLE:sub(i, i)
local w = print(ch, 0, -100, 0, false, 2)
_title_chars[i] = { ch = ch, ox = _title_total_w, w = w }
_title_total_w = _title_total_w + w
end
_scroll_chars = {}
_scroll_total_w = 0
local function append_str(str, col)
for i = 1, #str do
local ch = str:sub(i, i)
local w = print(ch, 0, -100, 0, false, 1)
_scroll_chars[#_scroll_chars + 1] = { ch = ch, ox = _scroll_total_w, w = w, col = col }
_scroll_total_w = _scroll_total_w + w
end
end
for _, part in ipairs(SCROLL_PARTS) do
append_str(part, RAINBOW[math.random(#RAINBOW)])
append_str(SCROLL_SEP, Config.colors.white)
end
_stars = {}
for i = 1, NUM_STARS do
_stars[i] = {
x = math.random(0, Config.screen.width - 1) + 0.0,
y = math.random(0, Config.screen.height - 1),
spd = (i % 3 + 1) * 10.0,
col = ({ 1, 2, 4, 4 })[(i % 4) + 1],
}
end
end
local function draw_stars()
for _, s in ipairs(_stars) do
pix(math.floor(s.x), s.y, s.col)
end
end
local function draw_rasters()
local cy = RASTER_Y_TOP + math.floor(math.sin(_time * 1.3) * 6)
for i, col in ipairs(RASTER_COLS) do
local y = cy + i - 1
if y >= 0 and y < Config.screen.height then
line(0, y, Config.screen.width - 1, y, col)
end
end
local cy2 = RASTER_Y_BOT + math.floor(math.sin(_time * 1.7 + 1.5) * 5)
for i, col in ipairs(RASTER_COLS) do
local y = cy2 + i - 1
if y >= 0 and y < Config.screen.height then
line(0, y, Config.screen.width - 1, y, col)
end
end
end
local function bounce_out(p)
local n1, d1 = 7.5625, 2.75
if p < 1 / d1 then
return n1 * p * p
elseif p < 2 / d1 then
p = p - 1.5 / d1; return n1 * p * p + 0.75
elseif p < 2.5 / d1 then
p = p - 2.25 / d1; return n1 * p * p + 0.9375
else
p = p - 2.625 / d1; return n1 * p * p + 0.984375
end
end
local function draw_title()
local sx = math.floor((Config.screen.width - _title_total_w) / 2)
local n = #_title_chars
local max_dist = (n - 1) / 2.0
for i, tc in ipairs(_title_chars) do
local dist = math.abs(i - (n + 1) / 2.0)
local delay = (max_dist - dist) * TITLE_DELAY_STEP
local t = math.max(0, _time - delay)
local p = math.min(1, t / TITLE_FALL_DUR)
local y = math.floor(-14 + bounce_out(p) * (TITLE_Y + 14))
print(tc.ch, sx + tc.ox + 1, y + 1, 0, false, 2)
print(tc.ch, sx + tc.ox, y, Config.colors.light_blue, false, 2)
end
end
local function draw_authors()
local col = Config.colors.light_blue
for i, lbl in ipairs(AUTHORS) do
local enter_t = math.max(0, _time - (i - 1) * AUTHORS_ENTRY_DT)
local slide = math.max(0, 1 - enter_t * AUTHORS_ENTRY_V)
local x_off = math.floor(slide * (Config.screen.width + 40))
local yo = (slide < 0.01) and math.floor(math.sin(_time * 2.0 + i * 1.1) * 2) or 0
Print.text(lbl, 12 + x_off, AUTHORS_BASE_Y + (i - 1) * AUTHORS_LINE_H + yo, col)
end
end
local function draw_scroller()
local third = Config.screen.width / 3
for pass = 0, 1 do
local base = _scroll_x + pass * _scroll_total_w
for _, sc in ipairs(_scroll_chars) do
local x = math.floor(base + sc.ox)
if x >= Config.screen.width then break end
if x + sc.w > 0 then
local zone = math.max(1, math.min(3, math.floor(x / third) + 1))
print(sc.ch, x, SCROLL_Y, SCROLL_ZONE_COLS[zone], false, 1)
end
end
end
end
--- Draws the credits window.
--- @within CreditsWindow
function CreditsWindow.draw()
cls(Config.colors.black)
draw_stars()
draw_rasters()
draw_title()
Print.text_center("Authors", Config.screen.width / 2, 47, Config.colors.light_grey)
draw_authors()
draw_scroller()
end
--- Updates credits window logic.
--- @within CreditsWindow
function CreditsWindow.update()
_time = _time + Context.delta_time
for _, s in ipairs(_stars) do
s.x = s.x + s.spd * Context.delta_time
if s.x >= Config.screen.width then s.x = s.x - Config.screen.width end
end
_scroll_x = _scroll_x - SCROLL_SPEED * Context.delta_time
if _scroll_x <= -_scroll_total_w then
_scroll_x = _scroll_x + _scroll_total_w
end
if Input.back() or Input.select() then
Window.set_current("menu")
end
end

View File

@@ -0,0 +1,59 @@
--- @section GameOverWindow
local GAME_OVER_ART = [[
_###_ __#__ #___# #####
#____ _#_#_ ##_## #____
#_### ##### #_#_# ####_
#___# #___# #___# #____
_###_ #___# #___# #####
_###_ #___# ##### ####_
#___# #___# #____ #___#
#___# _#_#_ ####_ ####_
#___# __#__ #____ #_#__
_###_ __#__ ##### #__##
]]
local REASON_MESSAGES = {
ism = "Your impostor syndrome consumed you.",
bm = "You burned out like a cheap candle.",
days = "100 days passed. The cycle never broke.",
}
--- Shows the game over screen.
--- @within GameOverWindow
--- @param reason string One of "ism", "bm", "days".
function GameOverWindow.show(reason)
GameOverWindow.reason = reason
Context.game_in_progress = false
Glitch.show()
Window.set_current("game_over")
end
--- Draws the game over screen.
--- @within GameOverWindow
function GameOverWindow.draw()
cls(Config.colors.black)
local cx = Config.screen.width / 2
local bounds = AsciiArt.draw(GAME_OVER_ART, {
char_w = 4,
char_h = 6,
line_gap = 1,
word_gap = 10,
color = Config.colors.red,
})
local msg = REASON_MESSAGES[GameOverWindow.reason] or ""
Print.text_center(msg, cx, bounds.bottom + 8, Config.colors.white)
Print.text_center("Press Z to restart", cx, Config.screen.height - 10, Config.colors.light_grey)
end
--- Updates the game over screen logic.
--- @within GameOverWindow
function GameOverWindow.update()
if Input.select() then
Context.reset()
MenuWindow.refresh_menu_items()
Window.set_current("menu")
end
end

View File

@@ -149,7 +149,6 @@ function MenuWindow.credits()
Window.set_current("credits")
end
--- Opens the audio test menu.
--- @within MenuWindow
function MenuWindow.audio_test()

View File

@@ -130,6 +130,7 @@ function MinigameDDRWindow.on_arrow_hit_special(arrow, game_context)
Audio.sfx_arrowhit(arrow.note)
game_context.special_mode_counter = game_context.special_mode_counter + 1
else
game_context.total_misses = game_context.total_misses + 1
if game_context.special_mode_condition then Audio.sfx_bloop() end
game_context.special_mode_condition = false
end
@@ -141,10 +142,12 @@ function MinigameDDRWindow.on_arrow_hit_special(arrow, game_context)
game_context.bar_fill = game_context.bar_fill - game_context.fill_per_hit
end
else
game_context.total_misses = game_context.total_misses + 1
if game_context.special_mode_condition then Audio.sfx_bloop() end
game_context.special_mode_condition = false
end
elseif special_mode == "only_nothing" then
game_context.total_misses = game_context.total_misses + 1
if game_context.special_mode_condition then Audio.sfx_bloop() end
game_context.special_mode_condition = false
end
@@ -173,6 +176,9 @@ function MinigameDDRWindow.on_end(game_context)
end
game_context.special_mode_condition = game_context.special_mode_condition and was_ok
if game_context.special_mode_condition and sm ~= "normal" then
game_context.bar_fill = game_context.max_fill
end
end
--- Initializes DDR minigame state.
@@ -336,7 +342,8 @@ function MinigameDDRWindow.update()
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Audio.music_stop()
Meter.on_minigame_complete()
Meter.apply_ddr_reward(mg.total_misses)
if not Context.game_in_progress then return end
if mg.on_win then
mg.on_win(mg)
else

View File

@@ -1,6 +1,35 @@
--- @section MinigameButtonMashWindow
---@class MinigameButtonMashState
---@field bar_fill number
---@field target_points number
---@field fill_per_press number
---@field base_degradation number
---@field degradation_multiplier number
---@field button_pressed_timer number
---@field button_press_duration number
---@field instruction_text string
---@field show_progress_text boolean
---@field return_window string?
---@field bar_x number
---@field bar_y number
---@field bar_width number
---@field bar_height number
---@field button_x number
---@field button_y number
---@field button_size number
---@field focus_center_x number?
---@field focus_center_y number?
---@field focus_initial_radius number
---@field win_timer number
---@field on_win (fun())?
---@field meter_on_complete (fun(elapsed_sec: number))?
---@field start_ms number?
---@field elapsed_sec number?
--- Gets initial button mash minigame configuration.
--- @within MinigameButtonMashWindow
--- @return result table The default button mash minigame configuration.
---@return MinigameButtonMashState
function MinigameButtonMashWindow.init_context()
return {
bar_fill = 0,
@@ -24,7 +53,11 @@ function MinigameButtonMashWindow.init_context()
focus_center_y = nil,
focus_initial_radius = 0,
win_timer = 0,
on_win = nil
on_win = nil,
--- If set, called with elapsed_sec instead of Meter.on_minigame_complete()
meter_on_complete = nil,
start_ms = nil,
elapsed_sec = nil,
}
end
@@ -51,8 +84,10 @@ end
function MinigameButtonMashWindow.start(return_window, params)
Audio.music_stop()
MinigameButtonMashWindow.init(params)
---@type MinigameButtonMashState
local mg = Context.minigame_button_mash
mg.return_window = return_window or "game"
mg.start_ms = time()
if mg.focus_center_x then
Focus.start_driven(mg.focus_center_x, mg.focus_center_y, {
initial_radius = mg.focus_initial_radius
@@ -64,12 +99,18 @@ end
--- Updates button mash minigame logic.
--- @within MinigameButtonMashWindow
function MinigameButtonMashWindow.update()
---@type MinigameButtonMashState
local mg = Context.minigame_button_mash
if mg.win_timer > 0 then
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Meter.on_minigame_complete()
if mg.meter_on_complete then
mg.meter_on_complete(mg.elapsed_sec or 0)
else
Meter.on_minigame_complete(false)
end
if not Context.game_in_progress then return end
if mg.focus_center_x then Focus.stop() end
Context.home_norman_visible = true
Context.have_done_work_today = false
@@ -97,6 +138,7 @@ function MinigameButtonMashWindow.update()
end
if mg.bar_fill >= mg.target_points then
Audio.sfx_select()
mg.elapsed_sec = (time() - mg.start_ms) / 1000
mg.win_timer = Config.timing.minigame_win_duration
return
end
@@ -116,6 +158,7 @@ end
--- Draws button mash minigame.
--- @within MinigameButtonMashWindow
function MinigameButtonMashWindow.draw()
---@type MinigameButtonMashState
local mg = Context.minigame_button_mash
if mg.return_window == "game" then
GameWindow.draw_with_underlay(function()

View File

@@ -73,7 +73,8 @@ function MinigameRhythmWindow.update()
if mg.win_timer > 0 then
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Meter.on_minigame_complete()
Meter.on_minigame_complete(false)
if not Context.game_in_progress then return end
if mg.focus_center_x then Focus.stop() end
if mg.on_win then
mg.on_win()

View File

@@ -31,6 +31,9 @@ Window.register("minigame_rhythm", MinigameRhythmWindow)
MinigameDDRWindow = {}
Window.register("minigame_ddr", MinigameDDRWindow)
GameOverWindow = {}
Window.register("game_over", GameOverWindow)
EndWindow = {}
Window.register("end", EndWindow)