From b4dcdaba58746bde95e22a6b5454cef6b176e31b Mon Sep 17 00:00:00 2001 From: Zoltan Timar Date: Thu, 19 Mar 2026 18:22:06 +0100 Subject: [PATCH] feat: added ascension meter, done 0-1 asc logic, fixed mysterious man behaviours --- .luacheckrc | 1 + .vscode/tasks.json | 9 +- impostor.inc | 1 + inc/decision/decision.go_to_end.lua | 3 + inc/decision/decision.go_to_sleep.lua | 3 +- inc/decision/decision.start_discussion.lua | 10 +- inc/discussion/discussion.sumphore.lua | 4 +- inc/init/init.ascension.lua | 169 +++++++++++++++++++++ inc/init/init.context.lua | 4 + inc/init/init.module.lua | 1 + inc/logic/logic.day.lua | 6 + inc/logic/logic.discussion.lua | 7 +- inc/logic/logic.meter.lua | 3 + inc/logic/logic.timer.lua | 1 - inc/screen/screen.home.lua | 1 + inc/screen/screen.toilet.lua | 9 ++ inc/system/system.main.lua | 2 + inc/window/window.mysterious_man.lua | 75 ++++++--- 18 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 inc/init/init.ascension.lua diff --git a/.luacheckrc b/.luacheckrc index 4e5aa70..3908b83 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -18,6 +18,7 @@ globals = { "Input", "Audio", "AsciiArt", + "Ascension", "Config", "Context", "Meter", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8669db6..c360369 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,12 +6,17 @@ { "label": "Run TIC80", "type": "shell", - "command": "tic80 --fs=. impostor.lua" + "command": "tic80 --fs=. impostor.lua", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } }, { "label": "Build & Run TIC80", "type": "shell", - "command": "make build && tic80 --fs=. impostor.lua", + "command": "make clean && make build && tic80 --fs=. impostor.lua", "problemMatcher": [] }, { diff --git a/impostor.inc b/impostor.inc index d31a0e7..f469ba9 100644 --- a/impostor.inc +++ b/impostor.inc @@ -1,6 +1,7 @@ meta/meta.header.lua init/init.module.lua init/init.config.lua +init/init.ascension.lua init/init.context.lua system/system.util.lua system/system.print.lua diff --git a/inc/decision/decision.go_to_end.lua b/inc/decision/decision.go_to_end.lua index 9e59099..7e86146 100644 --- a/inc/decision/decision.go_to_end.lua +++ b/inc/decision/decision.go_to_end.lua @@ -1,6 +1,9 @@ Decision.register({ id = "go_to_end", label = "Break the cycle", + condition = function() + return Ascension.is_complete() + end, handle = function() Window.set_current("end") end, diff --git a/inc/decision/decision.go_to_sleep.lua b/inc/decision/decision.go_to_sleep.lua index 7c369a5..197b878 100644 --- a/inc/decision/decision.go_to_sleep.lua +++ b/inc/decision/decision.go_to_sleep.lua @@ -9,7 +9,8 @@ Decision.register({ focus_center_y = (Config.screen.height / 2) - 18, focus_initial_radius = 0, on_win = function() - MysteriousManWindow.start() + local ascended = Ascension.consume_increase() + MysteriousManWindow.start({ skip_text = not ascended }) end, }) end, diff --git a/inc/decision/decision.start_discussion.lua b/inc/decision/decision.start_discussion.lua index e7e4159..0c06afc 100644 --- a/inc/decision/decision.start_discussion.lua +++ b/inc/decision/decision.start_discussion.lua @@ -1,18 +1,18 @@ Decision.register({ id = "start_discussion", label = function() - if Context.day_count >= 3 then + if Context.have_met_sumphore then return "Talk to Sumphore" end return "Talk to the homeless guy" end, handle = function() - if Context.day_count < 3 then + if not Context.have_met_sumphore then Discussion.start("homeless_guy", "game") - end - if Context.day_count >= 3 then + elseif Ascension.get_level() == 0 then + Discussion.start("homeless_guy", "game", 4) + else Discussion.start("sumphore_day_3", "game") - return end end, }) diff --git a/inc/discussion/discussion.sumphore.lua b/inc/discussion/discussion.sumphore.lua index fc04a8c..6025653 100644 --- a/inc/discussion/discussion.sumphore.lua +++ b/inc/discussion/discussion.sumphore.lua @@ -46,7 +46,9 @@ Discussion.register({ { question = "My name is Sumphore, nice to meet you.", answers = { - { label = "Nice to meet you, Sumphore.", next_step = 5 }, + { label = "Nice to meet you, Sumphore.", next_step = 5, on_select = function() + Context.have_met_sumphore = true + end }, }, }, { diff --git a/inc/init/init.ascension.lua b/inc/init/init.ascension.lua new file mode 100644 index 0000000..4dae906 --- /dev/null +++ b/inc/init/init.ascension.lua @@ -0,0 +1,169 @@ +--- @section Ascension +local ASCENSION_MAX_LEVEL = 9 +local ASCENSION_WORD = "ASCENSION" +local _increased_this_cycle = false + +local _flash_active = false +local _flash_timer = 0 +local _flash_total = 0 +local FLASH_DURATION = 120 + +local _fade_active = false +local _fade_timer = 0 +local FADE_DURATION = 120 +local FADE_COLORS = nil + +--- Gets initial ascension state. +--- @within Ascension +--- @return result table Initial ascension state.
+--- Fields:
+--- * level (number) Current ascension level (0-9). +function Ascension.get_initial() + _increased_this_cycle = false + return { + level = 0, + } +end + +--- Gets the current ascension level. +--- @within Ascension +--- @return number The current ascension level (0-9). +function Ascension.get_level() + if not Context or not Context.ascension then return 0 end + return Context.ascension.level +end + +--- Gets the maximum ascension level. +--- @within Ascension +--- @return number The maximum ascension level. +function Ascension.get_max_level() + return ASCENSION_MAX_LEVEL +end + +--- Increases the ascension level by 1, clamped to the max. +--- @within Ascension +function Ascension.increase() + if not Context or not Context.ascension then return end + Context.ascension.level = math.min(ASCENSION_MAX_LEVEL, Context.ascension.level + 1) + _increased_this_cycle = true +end + +--- Returns true if ascension was incremented since the last consume call. +--- @within Ascension +--- @return boolean Whether ascension increased this cycle. +function Ascension.did_increase() + return _increased_this_cycle +end + +--- Consumes the increase flag, returning its value and resetting it. +--- @within Ascension +--- @return boolean Whether ascension had increased this cycle. +function Ascension.consume_increase() + local result = _increased_this_cycle + _increased_this_cycle = false + return result +end + +--- Returns true when the ascension meter is fully complete (level 10). +--- @within Ascension +--- @return boolean Whether the cycle can be broken. +function Ascension.is_complete() + return Ascension.get_level() >= ASCENSION_MAX_LEVEL +end + +--- Draws the ascension meter as individual letters of "ASCENSION". +--- Each letter lights up per level. Drawn beneath existing meter bars. +--- @within Ascension +--- @param x number Left x position. +--- @param y number Top y position. +--- @param options table Optional overrides: lit_color, dim_color, spacing. +function Ascension.draw(x, y, options) + if not Context or not Context.ascension then return end + options = options or {} + local level = Context.ascension.level + if level < 1 then return end + local lit_color = options.lit_color or Config.colors.white + local spacing = options.spacing or 5 + + for i = 1, level do + local ch = ASCENSION_WORD:sub(i, i) + local color + if i == level and _fade_active then + color = Ascension.get_fade_color() + else + color = lit_color + end + print(ch, x + (i - 1) * spacing, y, color, false, 1, true) + end +end + +--- Returns the current fade-in color based on progress through the palette. +--- @within Ascension +--- @return number The palette color index for the current fade step. +function Ascension.get_fade_color() + if not FADE_COLORS then + FADE_COLORS = { + Config.colors.black, + Config.colors.dark_grey, + Config.colors.light_grey, + Config.colors.white, + } + end + if not _fade_active then return Config.colors.white end + local progress = math.min(_fade_timer / FADE_DURATION, 1) + local idx = math.floor(progress * (#FADE_COLORS - 1)) + 1 + return FADE_COLORS[idx] +end + +--- Starts the fade-in effect for the most recently gained letter. +--- @within Ascension +function Ascension.start_fade() + _fade_active = true + _fade_timer = 0 +end + +--- Starts the ascension flash effect. +--- @within Ascension +function Ascension.start_flash() + _flash_active = true + _flash_timer = 0 + _flash_total = FLASH_DURATION +end + +--- Updates and draws the ascension flash overlay. +--- Call once per frame from the main loop. +--- @within Ascension +function Ascension.draw_flash() + if not _flash_active then return end + _flash_timer = _flash_timer + 1 + + local sw = Config.screen.width + local sh = Config.screen.height + + local progress = _flash_timer / FLASH_DURATION + local pulse = math.abs(math.sin(progress * math.pi * 6)) + local flash_color = (pulse > 0.5) and Config.colors.white or Config.colors.light_grey + rect(0, 0, sw, sh, flash_color) + + if _flash_timer >= _flash_total then + _flash_active = false + Ascension.start_fade() + end +end + +--- Updates the fade-in timer. Call once per frame from the main loop. +--- @within Ascension +function Ascension.update_fade() + if not _fade_active then return end + _fade_timer = _fade_timer + 1 + if _fade_timer >= FADE_DURATION then + _fade_active = false + end +end + +--- Returns whether a flash effect is currently active. +--- @within Ascension +--- @return boolean Whether the flash is playing. +function Ascension.is_flashing() + return _flash_active +end diff --git a/inc/init/init.context.lua b/inc/init/init.context.lua index 6b35e6e..e406279 100644 --- a/inc/init/init.context.lua +++ b/inc/init/init.context.lua @@ -17,8 +17,10 @@ Context = {} --- * minigame_button_mash (table) Button mash minigame state (see Minigame.get_default_button_mash).
--- * minigame_rhythm (table) Rhythm minigame state (see Minigame.get_default_rhythm).
--- * meters (table) Meter values (see Meter.get_initial).
+--- * ascension (table) Ascension meter state (see Ascension.get_initial).
--- * triggers (table) Active trigger runtime state, keyed by trigger ID.
--- * stat_screen_active (boolean) Whether the stat screen overlay is currently shown.
+--- * have_met_sumphore (boolean) Whether the player has talked to the homeless guy.
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.
function Context.initial_data() return { @@ -33,9 +35,11 @@ function Context.initial_data() minigame_button_mash = {}, minigame_rhythm = {}, meters = Meter.get_initial(), + ascension = Ascension.get_initial(), timer = Timer.get_initial(), triggers = {}, home_norman_visible = false, + have_met_sumphore = false, game = { current_screen = "home", current_situation = nil, diff --git a/inc/init/init.module.lua b/inc/init/init.module.lua index 6d9c9fb..1113e20 100644 --- a/inc/init/init.module.lua +++ b/inc/init/init.module.lua @@ -17,3 +17,4 @@ Timer = {} Trigger = {} Discussion = {} AsciiArt = {} +Ascension = {} diff --git a/inc/logic/logic.day.lua b/inc/logic/logic.day.lua index a3c450e..f5a5987 100644 --- a/inc/logic/logic.day.lua +++ b/inc/logic/logic.day.lua @@ -22,4 +22,10 @@ Day.register_handler(function() m.ism = math.max(0, m.ism - METER_DECAY_PER_DAY) m.wpm = math.max(0, m.wpm - METER_DECAY_PER_DAY) m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY) +end) + +Day.register_handler(function() + if Context.day_count == 3 then + Ascension.increase() + end end) \ No newline at end of file diff --git a/inc/logic/logic.discussion.lua b/inc/logic/logic.discussion.lua index 6729d71..617b960 100644 --- a/inc/logic/logic.discussion.lua +++ b/inc/logic/logic.discussion.lua @@ -37,15 +37,18 @@ end --- @within Discussion --- @param id string The discussion ID to start. --- @param return_window string The window ID to return to after the discussion. -function Discussion.start(id, return_window) +--- @param[opt] start_step number The step index to start from (defaults to 1). +function Discussion.start(id, return_window, start_step) local discussion = _discussions[id] if not discussion then trace("Error: Discussion not found: " .. tostring(id)) return end + local step = start_step or 1 + if not discussion.steps[step] then step = 1 end Context.discussion.active = true Context.discussion.id = id - Context.discussion.step = 1 + Context.discussion.step = step Context.discussion.selected_answer = 1 Context.discussion.scroll_y = 0 Context.discussion.scroll_timer = 0 diff --git a/inc/logic/logic.meter.lua b/inc/logic/logic.meter.lua index ad64d67..001ddaa 100644 --- a/inc/logic/logic.meter.lua +++ b/inc/logic/logic.meter.lua @@ -152,4 +152,7 @@ function Meter.draw() end print(meter.label, label_x, label_y, meter.color, false, 1, true) end + + local ascension_y = start_y + 3 * line_h + 1 + Ascension.draw(bar_x, ascension_y, { spacing = 5 }) end diff --git a/inc/logic/logic.timer.lua b/inc/logic/logic.timer.lua index 3493aec..7345749 100644 --- a/inc/logic/logic.timer.lua +++ b/inc/logic/logic.timer.lua @@ -30,7 +30,6 @@ function Timer.update() if not in_minigame then t.progress = t.progress + (1 / timer_duration) if t.progress >= 1 then - Day.increase() t.progress = t.progress - 1 end end diff --git a/inc/screen/screen.home.lua b/inc/screen/screen.home.lua index b8de6e0..41321b6 100644 --- a/inc/screen/screen.home.lua +++ b/inc/screen/screen.home.lua @@ -5,6 +5,7 @@ Screen.register({ "go_to_toilet", "go_to_walking_to_office", "go_to_sleep", + "go_to_end", }, background = "bedroom", draw = function() diff --git a/inc/screen/screen.toilet.lua b/inc/screen/screen.toilet.lua index fdeccef..4c31593 100644 --- a/inc/screen/screen.toilet.lua +++ b/inc/screen/screen.toilet.lua @@ -75,5 +75,14 @@ Screen.register({ 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 + + if Ascension.get_level() > 0 then + local asc_y = meter_start_y + #meter_list * 20 + local asc_letter_y = asc_y + 10 + local asc_spacing = 8 + local asc_total_w = Ascension.get_level() * asc_spacing + local asc_x = math.floor((sw - asc_total_w) / 2) + Ascension.draw(asc_x, asc_letter_y, { spacing = asc_spacing }) + end end, }) diff --git a/inc/system/system.main.lua b/inc/system/system.main.lua index 8407975..5b39be2 100644 --- a/inc/system/system.main.lua +++ b/inc/system/system.main.lua @@ -26,8 +26,10 @@ function TIC() Timer.update() Trigger.update() Glitch.draw() + Ascension.update_fade() if Context.game_in_progress then Meter.draw() Timer.draw() end + Ascension.draw_flash() end diff --git a/inc/window/window.mysterious_man.lua b/inc/window/window.mysterious_man.lua index 9fb9269..71f99db 100644 --- a/inc/window/window.mysterious_man.lua +++ b/inc/window/window.mysterious_man.lua @@ -16,13 +16,18 @@ He says nothing. local state = STATE_TEXT local text_y = Config.screen.height -local text_speed = 0.4 +local text_speed = 0.2 local day_timer = 0 local day_display_frames = 120 +local text_done = false +local text_done_timer = 0 +local TEXT_DONE_HOLD_FRAMES = 120 local selected_choice = 1 local text = DEFAULT_TEXT local day_text_override = nil local on_text_complete = nil +local show_mysterious_screen = true +local trigger_flash_on_wake = false local choices = { { @@ -47,16 +52,30 @@ end --- * text (string) Override for the scrolling text.
--- * day_text (string) Override for the centered day label.
--- * on_text_complete (function) Callback fired once when the text phase ends.
+--- * skip_text (boolean) If true, skip the text phase and go straight to day display.
function MysteriousManWindow.start(options) options = options or {} - state = STATE_TEXT - text_y = Config.screen.height day_timer = 0 + text_done = false + text_done_timer = 0 selected_choice = 1 text = options.text or DEFAULT_TEXT + local line_count = 1 + for _ in string.gmatch(text, "\n") do line_count = line_count + 1 end + local text_block_h = line_count * 8 + text_y = math.floor((Config.screen.height - text_block_h) / 2) day_text_override = options.day_text on_text_complete = options.on_text_complete Meter.hide() + trigger_flash_on_wake = not options.skip_text + if options.skip_text then + show_mysterious_screen = false + state = STATE_DAY + day_timer = day_display_frames + else + show_mysterious_screen = true + state = STATE_TEXT + end Window.set_current("mysterious_man") end @@ -86,6 +105,10 @@ local function wake_up() Audio.music_play_wakingup() Context.home_norman_visible = true Meter.show() + if trigger_flash_on_wake then + trigger_flash_on_wake = false + Ascension.start_flash() + end Window.set_current("game") end, }) @@ -101,26 +124,34 @@ end --- @within MysteriousManWindow function MysteriousManWindow.update() if state == STATE_TEXT then - text_y = text_y - text_speed + if not text_done then + text_y = text_y - text_speed - local lines = 1 - for _ in string.gmatch(text, "\n") do - lines = lines + 1 - end + local lines = 1 + for _ in string.gmatch(text, "\n") do + lines = lines + 1 + end - if text_y < -lines * 8 then - go_to_day_state() - end - - if Input.select() then - go_to_day_state() + if text_y < -lines * 8 then + text_done = true + text_done_timer = TEXT_DONE_HOLD_FRAMES + end + else + text_done_timer = text_done_timer - 1 + if text_done_timer <= 0 then + go_to_day_state() + end end elseif state == STATE_DAY then day_timer = day_timer - 1 if day_timer <= 0 or Input.select() then - state = STATE_CHOICE - selected_choice = 1 + if trigger_flash_on_wake or Ascension.get_level() < 1 then + wake_up() + else + state = STATE_CHOICE + selected_choice = 1 + end end elseif state == STATE_CHOICE then selected_choice = UI.update_menu(choices, selected_choice) @@ -140,11 +171,17 @@ end --- @within MysteriousManWindow function MysteriousManWindow.draw() rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) - Screen.draw_the_mysterious_screen() + if show_mysterious_screen then + Screen.draw_the_mysterious_screen() + end if state == STATE_TEXT then - local x = (Config.screen.width - 132) / 2 - Print.text(text, x, text_y, Config.colors.light_grey) + local cx = Config.screen.width / 2 + local line_y = text_y + for line in (text .. "\n"):gmatch("(.-)\n") do + Print.text_center(line, cx, line_y, Config.colors.light_grey) + line_y = line_y + 8 + end elseif state == STATE_DAY then local day_text = day_text_override or ("Day " .. Context.day_count) Print.text_center(