diff --git a/.luacheckrc b/.luacheckrc
index e71c179..0a71b27 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -24,6 +24,7 @@ globals = {
"Minigame",
"Window",
"ContinuedWindow",
+ "CreditsWindow",
"TTGIntroWindow",
"BriefIntroWindow",
"TitleIntroWindow",
@@ -38,10 +39,14 @@ globals = {
"MysteriousManScreen",
"DiscussionWindow",
"EndWindow",
+<<<<<<< HEAD
"PlayerNameWindow",
"TextInput",
"CodeGenerator",
"CreditsWindow",
+=======
+ "GameOverWindow",
+>>>>>>> develop
"mset",
"mget",
"btnp",
diff --git a/impostor.inc b/impostor.inc
index ddefee6..cc44218 100644
--- a/impostor.inc
+++ b/impostor.inc
@@ -68,6 +68,7 @@ screen/screen.work.lua
screen/screen.mysterious_man.lua
window/window.manager.lua
window/window.register.lua
+window/window.gameover.lua
window/window.end.lua
window/window.intro.title.lua
window/window.intro.ttg.lua
diff --git a/inc/decision/decision.go_to_toilet.lua b/inc/decision/decision.go_to_toilet.lua
index 1a6d8b9..c28f6ec 100644
--- a/inc/decision/decision.go_to_toilet.lua
+++ b/inc/decision/decision.go_to_toilet.lua
@@ -1,7 +1,23 @@
+local function apply_home_toilet_meter_delta()
+ local max = Meter.get_max()
+ Meter.add("bm", -math.floor(max * 0.15))
+ Meter.add("ism", -math.floor(max * 0.10))
+ Meter.add("wpm", math.floor(max * 0.05))
+end
+
Decision.register({
id = "go_to_toilet",
label = "Go to Toilet",
handle = function()
+ if not Context.have_done_work_today and not Context.toilet_meters_today_morning then
+ apply_home_toilet_meter_delta()
+ Context.toilet_meters_today_morning = true
+ elseif Context.have_been_to_office
+ and Context.have_done_work_today
+ and not Context.toilet_meters_today_evening then
+ apply_home_toilet_meter_delta()
+ Context.toilet_meters_today_evening = true
+ end
Util.go_to_screen_by_id("toilet")
end,
})
diff --git a/inc/discussion/discussion.coworker.lua b/inc/discussion/discussion.coworker.lua
index 344e04c..a7c4c1c 100644
--- a/inc/discussion/discussion.coworker.lua
+++ b/inc/discussion/discussion.coworker.lua
@@ -1,5 +1,6 @@
Discussion.register({
id = "coworker_disc_0",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Good morning Normal, enjoying your coffee as usual, huh?",
@@ -18,6 +19,7 @@ Discussion.register({
Discussion.register({
id = "coworker_disc_1",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Norman, you look confused, what's up?",
@@ -36,6 +38,7 @@ Discussion.register({
Discussion.register({
id = "coworker_disc_asc_1",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Normann you look weird and unfocused. You are usually locked in and not like this, what's up?",
@@ -54,6 +57,7 @@ Discussion.register({
Discussion.register({
id = "coworker_disc_2",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Hey Norman, do you have new socks on? That's a weird color!",
@@ -79,6 +83,7 @@ Discussion.register({
Discussion.register({
id = "coworker_disc_asc_2",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Normann, are you ok? You were doing weird things while typing?",
@@ -97,6 +102,7 @@ Discussion.register({
Discussion.register({
id = "coworker_disc_3",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "You look so happy, did you catch a bull or something?",
@@ -120,6 +126,7 @@ Discussion.register({
})
Discussion.register({
id = "coworker_disc_asc_3",
+ on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Normal, you should take a break, you don't live up to your name today",
diff --git a/inc/discussion/discussion.sumphore.lua b/inc/discussion/discussion.sumphore.lua
index adc604b..340c8d1 100644
--- a/inc/discussion/discussion.sumphore.lua
+++ b/inc/discussion/discussion.sumphore.lua
@@ -1,5 +1,6 @@
Discussion.register({
id = "sumphore_disc_asc_1",
+ on_end = Meter.apply_sumphore_discussion_reward,
steps = {
{
question = "Are you still seeking the ox?",
@@ -19,6 +20,7 @@ Discussion.register({
Discussion.register({
id = "sumphore_disc_asc_2",
+ on_end = Meter.apply_sumphore_discussion_reward,
steps = {
{
question = "How's work? Your face looks strange",
@@ -61,6 +63,7 @@ Discussion.register({
Discussion.register({
id = "sumphore_disc_asc_3",
+ on_end = Meter.apply_sumphore_discussion_reward,
steps = {
{
question = "Do you think it's work you're doing?",
@@ -88,6 +91,7 @@ Discussion.register({
Discussion.register({
id = "homeless_guy",
+ on_end = Meter.apply_sumphore_discussion_reward,
steps = {
{
question = "Sup bro, how are you?",
diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua
index 53d4d99..d13c6fb 100644
--- a/inc/init/init.context.lua
+++ b/inc/init/init.context.lua
@@ -23,6 +23,10 @@ Context = {}
--- * have_met_sumphore (boolean) Whether the player has talked to the homeless guy.
--- * have_been_to_office (boolean) Whether the player has been to the office.
--- * have_done_work_today (boolean) Whether the player has done work today.
+--- * toilet_meters_today_morning (boolean) Whether the home (before work) toilet meter delta was already applied this day.
+--- * toilet_meters_today_evening (boolean) Whether the home (after work) toilet meter delta was already applied this day.
+--- * coworker_discussion_meter_applied_today (boolean) Whether the daily coworker discussion meter roll was already applied this day.
+--- * sumphore_discussion_meter_applied_today (boolean) Whether the daily sumphore discussion meter roll was already applied this day.
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID
function Context.initial_data()
return {
@@ -45,6 +49,10 @@ function Context.initial_data()
home_norman_visible = false,
have_been_to_office = false,
have_done_work_today = false,
+ toilet_meters_today_morning = false,
+ toilet_meters_today_evening = false,
+ coworker_discussion_meter_applied_today = false,
+ sumphore_discussion_meter_applied_today = false,
should_ascend = false,
have_met_sumphore = false,
office_sprites = {},
@@ -124,6 +132,7 @@ function Context.new_game()
target_points = 100,
instruction_text = "Wake up Norman!",
show_progress_text = false,
+ meter_on_complete = Meter.apply_wakeup_reward,
on_win = function()
Audio.music_play_room_work()
Meter.show()
diff --git a/inc/logic/logic.day.lua b/inc/logic/logic.day.lua
index ae9d760..1be8991 100644
--- a/inc/logic/logic.day.lua
+++ b/inc/logic/logic.day.lua
@@ -8,6 +8,10 @@ function Day.increase()
if Context.day_count == 3 then
Context.should_ascend = true
end
+ if Context.day_count >= 100 and not Ascension.is_complete() then
+ GameOverWindow.show("days")
+ return
+ end
for _, handler in ipairs(_day_increase_handlers) do
handler()
end
@@ -27,6 +31,13 @@ Day.register_handler(function()
m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY)
end)
+Day.register_handler(function()
+ Context.toilet_meters_today_morning = false
+ Context.toilet_meters_today_evening = false
+ Context.coworker_discussion_meter_applied_today = false
+ Context.sumphore_discussion_meter_applied_today = false
+end)
+
Day.register_handler(function()
if Context.should_ascend then
Ascension.increase()
diff --git a/inc/logic/logic.meter.lua b/inc/logic/logic.meter.lua
index 2607582..552ddd0 100644
--- a/inc/logic/logic.meter.lua
+++ b/inc/logic/logic.meter.lua
@@ -1,6 +1,8 @@
--- @section Meter
local METER_MAX = 1000
-local METER_DEFAULT = 500
+local BM_METER_DEFAULT = 200
+local ISM_METER_DEFAULT = 500
+local WPM_METER_DEFAULT = 200
local METER_GAIN_PER_CHORE = 100
local METER_DECAY_PER_DAY = 20
local COMBO_BASE_BONUS = 0.02
@@ -26,9 +28,9 @@ Meter.COLOR_CONTOUR = Config.colors.white
--- * hidden (boolean) Whether meters are hidden.
function Meter.get_initial()
return {
- ism = METER_DEFAULT,
- wpm = METER_DEFAULT,
- bm = METER_DEFAULT,
+ ism = ISM_METER_DEFAULT,
+ wpm = WPM_METER_DEFAULT,
+ bm = BM_METER_DEFAULT,
combo = 0,
combo_timer = 0,
hidden = false,
@@ -103,22 +105,129 @@ function Meter.add(key, amount)
if not Context or not Context.meters then return end
local m = Context.meters
if m[key] ~= nil then
- m[key] = math.min(METER_MAX, m[key] + amount)
+ if amount > 0 and (key == "ism" or key == "bm") and m[key] >= METER_MAX then
+ GameOverWindow.show(key)
+ return
+ end
+ m[key] = math.max(0, math.min(METER_MAX, m[key] + amount))
end
end
--- Called on minigame completion.
--- @within Meter
-function Meter.on_minigame_complete()
+--- @param is_work boolean If true (work-style minigame), apply combo to WPM/ISM/BM and advance combo. DDR uses `Meter.apply_ddr_reward` instead. Otherwise flat equal gain, combo unchanged.
+function Meter.on_minigame_complete(is_work)
local m = Context.meters
- local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier())
- Meter.add("wpm", gain)
- Meter.add("ism", gain)
- Meter.add("bm", gain)
+ if is_work then
+ local mult = Meter.get_combo_multiplier()
+ local wpm_delta = math.floor(METER_GAIN_PER_CHORE / mult)
+ local ism_bm_delta = math.floor(METER_GAIN_PER_CHORE * mult)
+ Meter.add("wpm", wpm_delta)
+ Meter.add("ism", ism_bm_delta)
+ Meter.add("bm", ism_bm_delta)
+ m.combo = m.combo + 1
+ m.combo_timer = 0
+ else
+ local flat = METER_GAIN_PER_CHORE
+ Meter.add("wpm", flat)
+ Meter.add("ism", flat)
+ Meter.add("bm", flat)
+ end
+end
+
+--- Meter changes after DDR: uses max-meter percentages; combo advances like other work minigames.
+--- 0 mistakes: WPM −10%, ISM +5%, BM +5%. 1–3: WPM −5%, ISM +10%, BM +10%. More than 3: WPM unchanged, ISM +10%, BM +10%.
+--- @within Meter
+--- @param mistake_count number Total mistakes (missed arrows, wrong inputs, and special-mode rule violations).
+function Meter.apply_ddr_reward(mistake_count)
+ if not Context or not Context.meters then return end
+ local max = Meter.get_max()
+ local m = Context.meters
+ local wpm_pct, ism_pct, bm_pct
+ if mistake_count == 0 then
+ wpm_pct, ism_pct, bm_pct = -0.10, 0.05, 0.05
+ elseif mistake_count <= 3 then
+ wpm_pct, ism_pct, bm_pct = -0.05, 0.10, 0.10
+ else
+ wpm_pct, ism_pct, bm_pct = 0, 0.10, 0.10
+ end
+ if wpm_pct ~= 0 then
+ Meter.add("wpm", math.floor(max * wpm_pct))
+ end
+ if ism_pct ~= 0 then
+ Meter.add("ism", math.floor(max * ism_pct))
+ end
+ if bm_pct ~= 0 then
+ Meter.add("bm", math.floor(max * bm_pct))
+ end
m.combo = m.combo + 1
m.combo_timer = 0
end
+--- Meter changes for the wake-up button mash: faster completion is better for WPM.
+--- Perfect: under 2s — WPM +20%. Good: 2–3s — WPM +10%, ISM +5%, BM +5%. Bad: over 3s — WPM −5%, ISM +10%, BM +10%.
+--- @within Meter
+--- @param elapsed_sec number Seconds from minigame start until the bar was filled.
+function Meter.apply_wakeup_reward(elapsed_sec)
+ if not Context or not Context.meters then return end
+ local max = Meter.get_max()
+ local wpm_pct, ism_pct, bm_pct
+ if elapsed_sec < 2 then
+ wpm_pct, ism_pct, bm_pct = 0.20, 0, 0
+ elseif elapsed_sec <= 3 then
+ wpm_pct, ism_pct, bm_pct = 0.10, 0.05, 0.05
+ else
+ wpm_pct, ism_pct, bm_pct = -0.05, 0.10, 0.10
+ end
+ if wpm_pct ~= 0 then
+ Meter.add("wpm", math.floor(max * wpm_pct))
+ end
+ if ism_pct ~= 0 then
+ Meter.add("ism", math.floor(max * ism_pct))
+ end
+ if bm_pct ~= 0 then
+ Meter.add("bm", math.floor(max * bm_pct))
+ end
+end
+
+--- Random single meter shift after finishing a coworker discussion: ISM +10%, WPM −10%, or BM +10%.
+--- @within Meter
+function Meter.apply_coworker_discussion_reward()
+ if not Context or not Context.meters then return end
+ if Context.coworker_discussion_meter_applied_today then return end
+ local max = Meter.get_max()
+ local delta = math.floor(max * 0.10)
+ local roll = math.random(1, 3)
+ if roll == 1 then
+ Meter.add("ism", delta)
+ elseif roll == 2 then
+ Meter.add("wpm", -delta)
+ else
+ Meter.add("bm", delta)
+ end
+ Context.coworker_discussion_meter_applied_today = true
+end
+
+--- After finishing a sumphore discussion: reduce whichever of ISM / WPM / BM is highest by 10% of max (stable tie to ISM, then WPM, then BM).
+--- @within Meter
+function Meter.apply_sumphore_discussion_reward()
+ if not Context or not Context.meters then return end
+ if Context.sumphore_discussion_meter_applied_today then return end
+ local m = Context.meters
+ local max = Meter.get_max()
+ local delta = math.floor(max * 0.10)
+ local biggest_val_key = "ism"
+ local biggest_val = m.ism
+ for _, key in ipairs({ "wpm", "bm" }) do
+ if m[key] > biggest_val then
+ biggest_val = m[key]
+ biggest_val_key = key
+ end
+ end
+ Meter.add(biggest_val_key, -delta)
+ Context.sumphore_discussion_meter_applied_today = true
+end
+
--- Draws meters.
--- @within Meter
function Meter.draw()
diff --git a/inc/screen/screen.mysterious_man.lua b/inc/screen/screen.mysterious_man.lua
index 5a07a4c..e55d11f 100644
--- a/inc/screen/screen.mysterious_man.lua
+++ b/inc/screen/screen.mysterious_man.lua
@@ -132,6 +132,7 @@ function MysteriousManScreen.wake_up()
target_points = 100,
instruction_text = "Wake up Norman!",
show_progress_text = false,
+ meter_on_complete = Meter.apply_wakeup_reward,
on_win = function()
Audio.music_play_wakingup()
Meter.show()
diff --git a/inc/screen/screen.toilet.lua b/inc/screen/screen.toilet.lua
index 908787f..664ac66 100644
--- a/inc/screen/screen.toilet.lua
+++ b/inc/screen/screen.toilet.lua
@@ -51,8 +51,8 @@ Screen.register({
local decay_pct = Meter.get_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 ism_bm_combo_pct = math.floor((combo_mult - 1) * 100)
+ local wpm_combo_pct = math.floor((1 / combo_mult - 1) * 100 + 0.5)
local meter_start_y = text_y + 10
local meter_list = {
@@ -73,6 +73,12 @@ Screen.register({
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
end
+ local mult_text
+ if meter.key == "wpm" then
+ mult_text = string.format("%+d%%", wpm_combo_pct)
+ else
+ mult_text = string.format("+%d%%", ism_bm_combo_pct)
+ end
local decay_w = print(decay_text, 0, -6, 0, false, 1)
Print.text_contour(decay_text, bar_x - decay_w - 4, bar_y, Config.colors.light_blue, false, 1, Config.colors.white)
Print.text_contour(mult_text, bar_x + bar_w + 4, bar_y, Config.colors.light_blue, false, 1, Config.colors.white)
diff --git a/inc/window/window.credits.lua b/inc/window/window.credits.lua
new file mode 100644
index 0000000..174dbd8
--- /dev/null
+++ b/inc/window/window.credits.lua
@@ -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
diff --git a/inc/window/window.gameover.lua b/inc/window/window.gameover.lua
new file mode 100644
index 0000000..f62d1f4
--- /dev/null
+++ b/inc/window/window.gameover.lua
@@ -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
diff --git a/inc/window/window.menu.lua b/inc/window/window.menu.lua
index 8d159f9..6c2b524 100644
--- a/inc/window/window.menu.lua
+++ b/inc/window/window.menu.lua
@@ -149,7 +149,6 @@ function MenuWindow.credits()
Window.set_current("credits")
end
-
--- Opens the audio test menu.
--- @within MenuWindow
function MenuWindow.audio_test()
diff --git a/inc/window/window.minigame.ddr.lua b/inc/window/window.minigame.ddr.lua
index 6a0167c..1231206 100644
--- a/inc/window/window.minigame.ddr.lua
+++ b/inc/window/window.minigame.ddr.lua
@@ -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
diff --git a/inc/window/window.minigame.mash.lua b/inc/window/window.minigame.mash.lua
index 467eaf1..7213b59 100644
--- a/inc/window/window.minigame.mash.lua
+++ b/inc/window/window.minigame.mash.lua
@@ -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()
diff --git a/inc/window/window.minigame.rhythm.lua b/inc/window/window.minigame.rhythm.lua
index d3ba312..dd2eee6 100644
--- a/inc/window/window.minigame.rhythm.lua
+++ b/inc/window/window.minigame.rhythm.lua
@@ -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()
diff --git a/inc/window/window.register.lua b/inc/window/window.register.lua
index afc6fd6..a969b59 100644
--- a/inc/window/window.register.lua
+++ b/inc/window/window.register.lua
@@ -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)