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(