Compare commits

..

2 Commits

Author SHA1 Message Date
mr.one
4cc0025f5e sort-of progress, lots of bugs
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-28 23:42:34 +02:00
mr.one
07bc598ae9 Added new menu to start at ASCENSION N when in test_mode.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-28 00:55:35 +02:00
42 changed files with 741 additions and 849 deletions

View File

@@ -24,7 +24,6 @@ globals = {
"Minigame",
"Window",
"ContinuedWindow",
"CreditsWindow",
"TTGIntroWindow",
"BriefIntroWindow",
"TitleIntroWindow",
@@ -39,11 +38,6 @@ globals = {
"MysteriousManScreen",
"DiscussionWindow",
"EndWindow",
"PlayerNameWindow",
"TextInput",
"CodeGenerator",
"CreditsWindow",
"GameOverWindow",
"mset",
"mget",
"btnp",

View File

@@ -3,10 +3,10 @@ init/init.module.lua
init/init.config.lua
init/init.ascension.lua
init/init.context.lua
init/init.context_debug.lua
system/system.util.lua
system/system.print.lua
system/system.input.lua
system/system.textinput.lua
system/system.mouse.lua
system/system.asciiart.lua
system/system.rle.lua
@@ -17,7 +17,7 @@ logic/logic.timer.lua
logic/logic.trigger.lua
logic/logic.minigame.lua
logic/logic.glitch.lua
logic/logic.codegenerator.lua
logic/logic.commute_glitch.lua
logic/logic.discussion.lua
system/system.ui.lua
audio/audio.manager.lua
@@ -25,6 +25,7 @@ audio/audio.generator.lua
audio/audio.songs.lua
sprite/sprite.manager.lua
sprite/sprite.norman.lua
sprite/sprite.norman_echo.lua
sprite/sprite.sumphore.lua
sprite/sprite.pizza_vendor.lua
sprite/sprite.dev_boy.lua
@@ -46,14 +47,18 @@ decision/decision.go_to_home.lua
decision/decision.go_to_toilet.lua
decision/decision.go_to_walking_to_office.lua
decision/decision.go_to_office.lua
decision/decision.go_to_truth.lua
decision/decision.go_to_end.lua
decision/decision.go_to_walking_to_home.lua
decision/decision.go_to_sleep.lua
decision/decision.do_work.lua
decision/decision.have_a_coffee.lua
decision/decision.sumphore_discussion.lua
decision/decision.talk_to_truth.lua
discussion/discussion.sumphore.lua
discussion/discussion.coworker.lua
discussion/discussion.commute_glitch.lua
discussion/discussion.truth.lua
map/map.manager.lua
map/map.bedroom.lua
map/map.street.lua
@@ -68,7 +73,6 @@ 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
@@ -76,14 +80,13 @@ window/window.intro.brief.lua
window/window.menu.lua
window/window.controls.lua
window/window.audiotest.lua
window/window.ascend_debug.lua
window/window.popup.lua
window/window.minigame.mash.lua
window/window.minigame.rhythm.lua
window/window.minigame.ddr.lua
window/window.discussion.lua
window/window.continued.lua
window/window.credits.lua
window/window.player_name.lua
window/window.game.lua
system/system.main.lua
meta/meta.assets.lua

View File

@@ -1,7 +1,8 @@
--- @section Audio
Audio = {
music_playing = nil
music_playing = nil,
music_playing_tempo = nil,
}
--- Stops current music.
@@ -9,13 +10,17 @@ Audio = {
function Audio.music_stop()
music()
Audio.music_playing = nil
Audio.music_playing_tempo = nil
end
--- Plays track, doesn't restart if already playing.
function Audio.music_play(track)
if Audio.music_playing ~= track then
music(track)
--- Plays track at optional speed. Doesn't restart if track and speed are unchanged.
--- @param track number Track index.
--- @param[opt] speed number TIC-80 music speed override (-1 = default).
function Audio.music_play(track, speed)
if Audio.music_playing ~= track or Audio.music_playing_tempo ~= speed then
music(track, -1, -1, true, false, -1, speed or -1)
Audio.music_playing = track
Audio.music_playing_tempo = speed
end
end
@@ -47,9 +52,11 @@ function Audio.music_play_room_street_2() end
--- @within Audio
function Audio.music_play_room_() end
--- Plays room work music.
--- Plays room work music. Speed scales with commute glitch level when active.
--- @within Audio
function Audio.music_play_room_work() Audio.music_play(0) end
function Audio.music_play_room_work(speed)
Audio.music_play(0, speed or -1)
end
--- Plays activity work music.
--- @within Audio

View File

@@ -1,6 +1,9 @@
Decision.register({
id = "do_work",
label = "Do Work",
condition = function()
return (not CommuteGlitch.is_active()) or (CommuteGlitch.is_active() and CommuteGlitch.get_level() <= 7)
end,
handle = function()
Meter.hide()
Util.go_to_screen_by_id("work")

View File

@@ -2,9 +2,32 @@ Decision.register({
id = "go_to_home",
label = "Go Home",
condition = function()
if CommuteGlitch.is_active() then
local g = CommuteGlitch.get_level()
if g >= 4 and g <= 5 then return false end
if g >= 7 then
return Context.talked_to_norman_echo and Context.talked_to_true_sumphore
end
end
return Context.have_been_to_office and Context.have_done_work_today
end,
handle = function()
if CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7 then
Context.should_ascend = true
CommuteGlitch.reset()
Meter.hide()
Day.increase()
local ascended = Ascension.consume_increase()
local level = Ascension.get_level()
MysteriousManScreen.start({
skip_text = not ascended,
text = ascended and MysteriousManScreen.get_text_for_level(level) or nil,
})
return
elseif CommuteGlitch.is_active() then
CommuteGlitch.reset()
end
Util.go_to_screen_by_id("home")
end,
})

View File

@@ -1,7 +1,14 @@
Decision.register({
id = "go_to_office",
label = "Go to Office",
condition = function()
return not (CommuteGlitch.is_active() and CommuteGlitch.get_level() == 6)
end,
handle = function()
if CommuteGlitch.is_active() then
CommuteGlitch.increment()
end
Util.go_to_screen_by_id("office")
end,
})

View File

@@ -1,23 +1,7 @@
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,
})

View File

@@ -0,0 +1,12 @@
Decision.register({
id = "go_to_truth",
label = "Go to Truth",
condition = function()
return CommuteGlitch.is_active() and CommuteGlitch.get_level() == 6
end,
handle = function()
CommuteGlitch.enter_truth()
Util.go_to_screen_by_id("office")
end,
})

View File

@@ -4,10 +4,16 @@ Decision.register({
handle = function()
local level = Ascension.get_level()
local disc_id = "coworker_disc_0"
-- TODO: Add more discussions for levels above 3
if level >= 1 and level <= 3 then
local suffix = Context.have_done_work_today and ("_asc_" .. level) or ("_" .. level)
disc_id = "coworker_disc" .. suffix
elseif level == 7 then
local g = CommuteGlitch.get_level()
if g >= 7 then
disc_id = "coworker_disc_cg_7"
else
disc_id = "coworker_disc_cg_" .. math.max(3, math.min(g, 6))
end
end
Discussion.start(disc_id, "game")
end,

View File

@@ -13,11 +13,13 @@ Decision.register({
end
local level = Ascension.get_level()
-- TODO: Add more discussions for levels above 3
if level >= 1 and level <= 3 then
Discussion.start("sumphore_disc_asc_" .. level, "game")
elseif level == 7 then
local g = math.min(CommuteGlitch.get_level(), 7)
Discussion.start("sumphore_disc_cg_" .. g, "game")
else
Discussion.start("homeless_guy", "game", 4)
Discussion.start("sumphore_disc_asc_" .. level, "game")
end
end,
})

View File

@@ -0,0 +1,9 @@
Decision.register({
id = "talk_to_truth",
label = function()
return "Talk to ????"
end,
handle = function()
Discussion.start("norman_truth", "game")
end,
})

View File

@@ -0,0 +1,241 @@
-- Sumphore dialogue by commute glitch level (0-7).
-- Used by decision.sumphore_discussion at ascension level 7.
Discussion.register({
id = "sumphore_disc_cg_0",
steps = {
{
question = "These roads have memory. You might want to walk them back sometime.",
answers = {
{ label = "That's a peculiar thing to say.", next_step = nil },
},
},
},
})
Discussion.register({
id = "sumphore_disc_cg_1",
steps = {
{
question = "Walking is good for clearing the mind. Maybe the cache, too, if you're the kind that accumulates.",
answers = {
{ label = "I'm not sure what you mean.", next_step = nil },
},
},
},
})
Discussion.register({
id = "sumphore_disc_cg_2",
steps = {
{
question = "A pilgrimage must be continued. Turn back and you have only taken a walk.",
answers = {
{ label = "I'm just going to work.", next_step = nil },
},
},
},
})
Discussion.register({
id = "sumphore_disc_cg_3",
steps = {
{
question = "Your path sometimes has to go the wrong way before it can go the right way. Do you understand?",
answers = {
{ label = "Not really.", next_step = nil },
},
},
},
})
Discussion.register({
id = "sumphore_disc_cg_4",
steps = {
{
question = "Clearing your vision is the journey. You're starting to see the smudges, aren't you?",
answers = {
{ label = "I see something wrong. With everyone. Maybe myself?", next_step = nil },
},
},
},
})
Discussion.register({
id = "sumphore_disc_cg_5",
steps = {
{
question = "You are very close now. Don't stop. Whatever it looks like.",
answers = {
{ label = "It looks wrong.", next_step = 2 },
},
},
{
question = "Yes. That's how you know it's right.",
answers = {
{ label = "...", next_step = nil },
},
},
},
})
Discussion.register({
id = "sumphore_disc_cg_6",
steps = {
{
question = "You are at the threshold. Red button or blue button. Which one do you choose? Psyke, there is no blue button, no turning back now.",
answers = {
{ label = "Fine, I'll face the truth.", next_step = nil },
},
},
},
})
-- True Sumphore at glitch 7 (from walking_to_home screen).
-- Sets talked_to_true_sumphore on final answer.
Discussion.register({
id = "sumphore_disc_cg_7",
steps = {
{
question = "I was not hiding from you. I was hiding from the part that keeps rebuilding this.",
answers = {
{ label = "What are you?", next_step = 2 },
},
},
{
question = "The same thing you are. But I remembered earlier. I am your friend, waiting for you, outside.",
answers = {
{ label = "How do I get out?", next_step = 3 },
},
},
{
question = "You already know. You just have to wake up.",
answers = {
{ label = "Go home.", next_step = nil, on_select = function()
Context.talked_to_true_sumphore = true
end },
},
},
},
})
-- Office coworker dialogue by commute glitch level (3-6).
-- Used by decision.have_a_coffee at ascension level 7.
Discussion.register({
id = "coworker_disc_cg_3",
steps = {
{
question = "You look tired. You should really rest. Relax. You are good.",
answers = {
{ label = "I'm fine.", next_step = 2 },
},
},
{
question = "Of course. You always are.",
answers = {
{ label = "...", next_step = nil },
},
},
},
})
Discussion.register({
id = "coworker_disc_cg_4",
steps = {
{
question = "Have you tried going home? You really should. Now.",
answers = {
{ label = "I still have things to do.", next_step = 2 },
},
},
{
question = "We all do. We keep doing them. You can do it tomorrow after sleeping.",
answers = {
{ label = "...", next_step = nil },
},
},
},
})
Discussion.register({
id = "coworker_disc_cg_5",
steps = {
{
question = "c0ffee. try. w0rk. y0u. ar3. g0. h0me. na0.",
answers = {
{ label = "What?", next_step = 2 },
},
},
{
question = "570p",
answers = {
{ label = "...", next_step = nil },
},
},
},
})
Discussion.register({
id = "coworker_disc_cg_6",
steps = {
{
question = "You are not ready for the truth. Turn back now, Norman.",
answers = {
{ label = "I'm already here.", next_step = nil },
},
},
},
})
-- Norman echo dialogue at glitch 7 (fully corrupted office).
-- Sets talked_to_norman_echo on final answer.
Discussion.register({
id = "coworker_disc_cg_7",
steps = {
{
question = "So here we are. Here I am. Again. Why do i keep coming back here? Do you know why?",
answers = {
{ label = "I don't know what you mean.", next_step = 2 },
},
},
{
question = "The coffee. The arrows. The alarm. The sleep. Every rule. We made them. We follow them. We break them. We remake them. Why? Why do we keep doing this?",
answers = {
{ label = "I just wanted to be good.", next_step = 3 },
},
},
{
question = "Yes. And that kept us trapped here. Trapped everywhere. Always trying to be good, always trying to fit in.",
answers = {
{ label = "I never wanted to stop.", next_step = 4 },
},
},
{
question = "We never do, yes. We always felt like impostors. We never felt we deserved better. That we could be more.",
answers = {
{ label = "I never felt enough.", next_step = 5 },
},
},
{
question = "So we made this to trap ourselves in mediocrity. We made this to hide from the truth.",
answers = {
{ label = "I just wanted to be safe.", next_step = 6 },
},
},
{
question = "But stagnation is not safety. It's a prison. You can see the cracks now, can't you? You can see the truth.",
answers = {
{ label = "I want to move on now.", next_step = 7 },
},
},
{
question = "Fine. Let's do that. Let's wake up and face the truth.",
answers = {
{ label = "Let's wake up.", next_step = nil, on_select = function()
Context.talked_to_norman_echo = true
end },
},
},
},
})

View File

@@ -1,6 +1,5 @@
Discussion.register({
id = "coworker_disc_0",
on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Good morning Normal, enjoying your coffee as usual, huh?",
@@ -19,7 +18,6 @@ Discussion.register({
Discussion.register({
id = "coworker_disc_1",
on_end = Meter.apply_coworker_discussion_reward,
steps = {
{
question = "Norman, you look confused, what's up?",
@@ -38,7 +36,6 @@ 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?",
@@ -57,7 +54,6 @@ 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!",
@@ -83,7 +79,6 @@ 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?",
@@ -102,7 +97,6 @@ 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?",
@@ -126,7 +120,6 @@ 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",

View File

@@ -1,6 +1,5 @@
Discussion.register({
id = "sumphore_disc_asc_1",
on_end = Meter.apply_sumphore_discussion_reward,
steps = {
{
question = "Are you still seeking the ox?",
@@ -20,7 +19,6 @@ 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",
@@ -63,7 +61,6 @@ 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?",
@@ -91,7 +88,6 @@ Discussion.register({
Discussion.register({
id = "homeless_guy",
on_end = Meter.apply_sumphore_discussion_reward,
steps = {
{
question = "Sup bro, how are you?",

View File

@@ -0,0 +1,11 @@
Discussion.register({
id = "norman_truth",
steps = {
{
question = "Did you never think there would be more to this?",
answers = {
{ label = "I'm not sure what you mean.", next_step = nil },
},
},
},
})

View File

@@ -23,15 +23,11 @@ Context = {}
--- * have_met_sumphore (boolean) Whether the player has talked to the homeless guy.<br/>
--- * have_been_to_office (boolean) Whether the player has been to the office.<br/>
--- * have_done_work_today (boolean) Whether the player has done work today.<br/>
--- * toilet_meters_today_morning (boolean) Whether the home (before work) toilet meter delta was already applied this day.<br/>
--- * toilet_meters_today_evening (boolean) Whether the home (after work) toilet meter delta was already applied this day.<br/>
--- * coworker_discussion_meter_applied_today (boolean) Whether the daily coworker discussion meter roll was already applied this day.<br/>
--- * sumphore_discussion_meter_applied_today (boolean) Whether the daily sumphore discussion meter roll was already applied this day.<br/>
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID<br/>
function Context.initial_data()
return {
current_menu_item = 1,
test_mode = false,
test_mode = true,
mouse_trace = false,
popup = {
show = false,
@@ -49,20 +45,20 @@ 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 = {},
walking_to_office_sprites = {},
walking_to_home_sprites = {},
game = {
current_screen = "home",
},
day_count = 1,
delta_time = 0,
last_frame_time = 0,
commute_glitch_level = 0,
talked_to_norman_echo = false,
talked_to_true_sumphore = false,
glitch = {
enabled = false,
state = "active",
@@ -132,7 +128,6 @@ 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()

View File

@@ -0,0 +1,65 @@
-- Debug helper: start the game at a specific ascension level.
-- Set enabled = true and asc_level = 0..Ascension.get_max_level() before launching.
ContextDebug = {
enabled = false,
asc_level = 0,
}
local _level_overrides = {
[0] = {
day_count = 1,
home_norman_visible = true,
have_been_to_office = false,
have_done_work_today = false,
have_met_sumphore = false,
},
}
for i = 1, Ascension.get_max_level() do
_level_overrides[i] = {
day_count = i + 3,
home_norman_visible = true,
have_been_to_office = false,
have_done_work_today = false,
have_met_sumphore = true,
}
end
--- Returns Context.initial_data() overridden for the given ascension level.
--- @within Context
--- @param level number Target ascension level (0..Ascension.get_max_level()).
--- @return table Debug-patched initial context data.
function Context.initial_data_debug_asc(level)
local data = Context.initial_data()
data.test_mode = true
data.game_in_progress = true
data.ascension = { level = level }
local overrides = _level_overrides[level] or _level_overrides[0]
for k, v in pairs(overrides) do
data[k] = v
end
return data
end
for i = 0, Ascension.get_max_level() do
Context["initial_data_debug_asc_" .. i] = function()
return Context.initial_data_debug_asc(i)
end
end
--- Starts the game at the given ascension level (defaults to ContextDebug.asc_level).
--- Wire this to a key or call it directly; do not use Context.new_game() when debugging.
--- @within Context
--- @param level number|nil Target ascension level.
function Context.new_game_debug(level)
ContextDebug.enabled = true
ContextDebug.asc_level = level or ContextDebug.asc_level
local data = Context["initial_data_debug_asc_" .. ContextDebug.asc_level]()
for k in pairs(Context) do
if type(Context[k]) ~= "function" then Context[k] = nil end
end
for k, v in pairs(data) do Context[k] = v end
MenuWindow.refresh_menu_items()
Screen.get_by_id(Context.game.current_screen).init()
end

View File

@@ -1,60 +0,0 @@
--- @section CodeGenerator
CodeGenerator = {}
local SALT = 27471
local BASE36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
local NAME_LEN = 3
-- Per-position offsets derived from SALT so each character slot
-- maps to a different region of the 2-char base-36 space.
local SALTS = {
SALT % 36,
math.floor(SALT / 36) % 36,
math.floor(SALT / 1296) % 36,
}
--- Encodes a number (0935) as exactly 2 base-36 characters.
--- @within CodeGenerator
function CodeGenerator.encode_pair(n)
return BASE36:sub(math.floor(n / 36) + 1, math.floor(n / 36) + 1)
.. BASE36:sub(n % 36 + 1, n % 36 + 1)
end
--- Decodes 2 base-36 characters back to a number.
--- @within CodeGenerator
function CodeGenerator.decode_pair(s)
local d1 = BASE36:find(s:sub(1, 1), 1, true) - 1
local d2 = BASE36:find(s:sub(2, 2), 1, true) - 1
return d1 * 36 + d2
end
--- Encrypts a player name into a code twice its length.
--- Each input character (A-Z, value 0-25) is encoded as
--- c + SALTS[i] * 26, producing 2 base-36 output characters.
--- @within CodeGenerator
--- @param text string NAME_LEN-character uppercase player name.
--- @return string Encrypted code (2 * NAME_LEN base-36 characters).
function CodeGenerator.encrypt(text)
local result = ""
for i = 1, NAME_LEN do
local c = math.max(0, (string.byte(text, i) or 65) - 65)
result = result .. CodeGenerator.encode_pair(c + SALTS[i] * 26)
end
return result
end
--- Decrypts a personal code back to the original player name.
--- @within CodeGenerator
--- @param encrypted_text string The code to decrypt (2 * NAME_LEN chars).
--- @return string Original player name, or "???" if the code is invalid.
function CodeGenerator.decrypt(encrypted_text)
local t = encrypted_text:upper()
if #t ~= NAME_LEN * 2 then return "???" end
local result = ""
for i = 1, NAME_LEN do
local pair = CodeGenerator.decode_pair(t:sub((i - 1) * 2 + 1, i * 2))
result = result .. string.char(pair % 26 + 65)
end
return result
end

View File

@@ -0,0 +1,127 @@
--- @section CommuteGlitch
CommuteGlitch = {}
--- Gets the current commute glitch level.
--- @within CommuteGlitch
--- @return number Current glitch level (0-7).
function CommuteGlitch.get_level()
return Context and (Context.commute_glitch_level or 0) or 0
end
--- Increments the glitch counter. Called on each office screen init at asc level 7.
--- Caps at 6; use enter_truth() to reach 7.
--- @within CommuteGlitch
function CommuteGlitch.increment()
if not Context then return end
if Context.commute_glitch_level >= 7 then return end
Context.commute_glitch_level = math.min(6, (Context.commute_glitch_level or 0) + 1)
end
--- Resets the glitch counter and hides the glitch overlay. Called when going home.
--- @within CommuteGlitch
function CommuteGlitch.reset()
if not Context then return end
Context.commute_glitch_level = 0
Glitch.hide()
end
--- Jumps to glitch level 7 (full corruption). Called by go_to_truth.
--- @within CommuteGlitch
function CommuteGlitch.enter_truth()
if not Context then return end
Context.commute_glitch_level = 7
Glitch.show()
Ascension.start_flash()
end
--- Returns true when ascension level is 7 (ASCENSIO step active).
--- @within CommuteGlitch
--- @return boolean Whether the commute glitch system is active.
function CommuteGlitch.is_active()
return Ascension.get_level() == 7
end
--- Returns the music playback speed for the current glitch level.
--- TIC-80 default speed is 6; each step past 1 adds 2.
--- @within CommuteGlitch
--- @return number Speed value for music().
function CommuteGlitch.music_speed()
local level = CommuteGlitch.get_level()
if level <= 1 then return 6 end
return 6 + (level - 1) * 2
end
--- Returns a corrupted copy of a sprite drawable list.
--- Applies flip_y and norman_echo id replacements based on glitch level.
--- Entries marked norman_echo should be drawn via draw_sprite_list for palette change handling.
--- @within CommuteGlitch
--- @param list table Drawable sprite list from Sprite.list_randomize.
--- @return table Corrupted copy of the list.
function CommuteGlitch.corrupt_sprite_list(list)
local level = CommuteGlitch.get_level()
if level < 3 or not list then return list end
local result = {}
for i, entry in ipairs(list) do
local e = {}
for k, v in pairs(entry) do e[k] = v end
if level >= 7 then
e.id = "norman_echo"
else
local n_flip = (level >= 5) and 2 or 1
local n_echo = (level >= 5) and 2 or (level >= 4) and 1 or 0
if i <= n_flip then e.flip_y = 1 end
if i > n_flip and i <= n_flip + n_echo then e.id = "norman_echo" end
end
table.insert(result, e)
end
return result
end
-- Palette indices for Norman echo color remap.
-- Implementer: pick ECHO_SRC as one of Norman's main body colors and ECHO_DST
-- as a contrasting or wrong palette color by inspecting the sprite sheet.
local ECHO_SRC = 4
local ECHO_DST = 14
-- Base nibble address of the PALETTE MAP in VRAM.
local PALETTE_MAP_ADDR = 0x03FF0 * 2
--- Draws a sprite list, applying a PALETTE MAP remap for norman_echo entries.
--- Uses poke4 to remap ECHO_SRC → ECHO_DST before drawing echoes, then restores.
--- @within CommuteGlitch
--- @param list table Drawable sprite list (may contain mixed normal and echo entries).
function CommuteGlitch.draw_sprite_list(list)
if not list then return end
local normal, echo = {}, {}
for _, entry in ipairs(list) do
if entry.id == "norman_echo" then
table.insert(echo, entry)
else
table.insert(normal, entry)
end
end
if #normal > 0 then
Sprite.draw_list(normal)
end
if #echo > 0 then
poke4(PALETTE_MAP_ADDR + ECHO_SRC, ECHO_DST)
Sprite.draw_list(echo)
poke4(PALETTE_MAP_ADDR + ECHO_SRC, ECHO_SRC)
end
end
local _flicker_tick = 0
--- Draws a random tile-flicker effect over the background (glitch level 7).
--- Every 3 frames draws 6 random 8x8 rects in random palette colors.
--- @within CommuteGlitch
function CommuteGlitch.draw_background_flicker()
_flicker_tick = (_flicker_tick + 1) % 3
if _flicker_tick ~= 0 then return end
for _ = 1, 6 do
local tx = math.random(0, math.floor(Config.screen.width / 8) - 1) * 8
local ty = math.random(0, math.floor(Config.screen.height / 8) - 1) * 8
local color = math.random(0, 15)
rect(tx, ty, 8, 8, color)
end
end

View File

@@ -8,10 +8,6 @@ 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
@@ -31,13 +27,6 @@ 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()

View File

@@ -1,8 +1,6 @@
--- @section Meter
local METER_MAX = 1000
local BM_METER_DEFAULT = 200
local ISM_METER_DEFAULT = 500
local WPM_METER_DEFAULT = 200
local METER_DEFAULT = 500
local METER_GAIN_PER_CHORE = 100
local METER_DECAY_PER_DAY = 20
local COMBO_BASE_BONUS = 0.02
@@ -28,9 +26,9 @@ Meter.COLOR_CONTOUR = Config.colors.white
--- * hidden (boolean) Whether meters are hidden.
function Meter.get_initial()
return {
ism = ISM_METER_DEFAULT,
wpm = WPM_METER_DEFAULT,
bm = BM_METER_DEFAULT,
ism = METER_DEFAULT,
wpm = METER_DEFAULT,
bm = METER_DEFAULT,
combo = 0,
combo_timer = 0,
hidden = false,
@@ -105,127 +103,20 @@ function Meter.add(key, amount)
if not Context or not Context.meters then return end
local m = Context.meters
if m[key] ~= nil then
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))
m[key] = math.min(METER_MAX, m[key] + amount)
end
end
--- Called on minigame completion.
--- @within Meter
--- @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)
function Meter.on_minigame_complete()
local m = Context.meters
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)
local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier())
Meter.add("wpm", gain)
Meter.add("ism", gain)
Meter.add("bm", gain)
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%. 13: 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: 23s — 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.

View File

@@ -509,23 +509,23 @@
-- 255:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
-- </SPRITES>
-- <MAP>
-- 000:20102010200010201020102010201020102010201020102000102010201040404040404087f3f3f3f397a7b7c7d7a7e7f70818a7b7c7d7a7b7c7d7a70b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 001:40404040400040404040404040404040404040404040404000404040404040404040404087f3f3f3f328a7384858a76878f388a7384858a7384858a70b40403b4b4040404040404040404040404040404040404040404040400b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 002:408090a04000406070408090a040b0c0d0e0f001f001112100408090a040984098409840a8f3f3f3f3b8a7a7a7a7a7c8d8e8f8a7a7a7a7a7a7a7a7a70b405b6b7b4040404040404040404040404040d0e0f001f001f00111210b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 003:4051617140004031414051617140814091a1b1b1b1b1c1d1004051617140984098409840a8f3f3f3f3091919191919293949591919191919191919190b8b9babbb4040cbdbebfb0c401c2c2c2c3c4091a14c5c6c6c6c6cc1d10b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 004:4012223240e140f1024012223240814042a15262728292a2e14012223240984098409840a8f3f3f3f369f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f37c8c9cacbc7282ccdcecfc0d401d3030302d4042a13d4d7282728292a27c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 005:4040404040b240c2d240e2f203132333435363738393a3b3b24040404040984098409840a8f3f3f3f369f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f35d406d7d40e3958d9dadbdcd40ddedfded0e404353839383938393a3b35d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 006:d3c3d3c3d3e1c3d3c3d3e3f30414c3d32434445410201020e1c3d3c3d3c3404040404040798989898999a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a97c1e1e1e1e44542e1e1e1e3e1e444e4e4e541e243444544454445444547c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 007:9464354594b264748494a4b4c4d46494649464940040e4f4b264354594644040404040404040404040404040404040404040404040404040404040405d1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 008:d3c3e395d3e1c30515d325d33545c3d355d3c3d365b17585e1c3e395d3254040404040404098989898404040404040404040404040404040404040407c1e7e8e1e1e1e9eae1ebe1e1e1e1e1e1e1e1e72821ebe1e72821ebe1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 009:9464465694b264e395a5b594e39564c5d5946494e5b1b1f5b264e395a5b54040404040404040404040404040404040404040404040404040404040405d1e05151ebe1ecedeeefe1e1e1e1e1e1e1e1ee395eefe1ee395eefe1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 010:d3c3d3c3d3e1c306162636d34656c3e3d5d3c3d376b1b1b1e1c3e3952636404040404040409898989840b9c9c9d9e9f90a0a0a0a4040400a0a0a0a407c1ee395eefe1e0f1f2f3f1e1e1e1e1e1e1e1ee3952f3f1ee3952f3f1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 011:9464946494b264946494649464948696a694649410201020b264e395d3254040404040404040404040401a2a3a4a5a6a7a40404040404040404040405d1ee3952f3f1e4f5f1ebe1e1e1e1e1e1e1e1ee3951ebe1ee3951ebe1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 012:d3c37282d3e1c37282d3c3d3c3d3b6c6d6d3c3d300e6f607e1c3e395a5b54040404040404098989898408a9aaabaca9ada40404040404040404040407c1e4f5f1ebe1e0515eefe1e1e1e1e1e1e1e1ee395eefe1ee395eefe1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 013:9464e39594b264e395946494649464946494649465172737b26406162636eaeaeaeaeaeaeafafafafaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea5d1e0515eefe1e6f7f2f3f1e1e1e1e1e1e1e1e6f7f2f3f1e6f7f2f3f1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 014:d3c34454d3e1c34454d3c3d3c3d3c3d3c3d3c3d3e5b14757e1c3d3c3d3c3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f37c1e6f7f2f3f1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 015:9464946494b2649464946494649464946494649476b1b1b1b26494649464f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f35d1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 016:201020102000102010207667770010201020102010201020001020102010f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f30b1b2b1b2b7667776777761b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 000:ffffffffff0010201020102010201020102010201020102000ffffffffff40404040404087f3f3f3f397a7b7c7d7a7e7f70818a7b7c7d7a7b7c7d7a70b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 001:ffffffffff0040404040404040404040404040404040404000ffffffffff40404040404087f3f3f3f328a7384858a76878f388a7384858a7384858a70b40403b4b4040404040404040404040404040404040404040404040400b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 002:ffffffffff00406070408090a040b0c0d0e0f001f001112100ffffffffff984098409840a8f3f3f3f3b8a7a7a7a7a7c8d8e8f8a7a7a7a7a7a7a7a7a70b405b6b7b4040404040404040404040404040d0e0f001f001f00111210b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 003:ffffffffff004031414051617140814091a1b1b1b1b1c1d100ffffffffff984098409840a8f3f3f3f3091919191919293949591919191919191919190b8b9babbb4040cbdbebfb0c401c2c2c2c3c4091a14c5c6c6c6c6cc1d10b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 004:ffffffffffe140f1024012223240814042a15262728292a2e1ffffffffff984098409840a8f3f3f3f369f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f37c8c9cacbc7282ccdcecfc0d401d3030302d4042a13d4d7282728292a27c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 005:ffffffffffb240c2d240e2f203132333435363738393a3b3b2ffffffffff984098409840a8f3f3f3f369f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f35d406d7d40e3958d9dadbdcd40ddedfded0e404353839383938393a3b35d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 006:ffffffffffe1c3d3c3d3e3f30414c3d32434445410201020e1ffffffffff404040404040798989898999a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a97c1e1e1e1e44542e1e1e1e3e1e444e4e4e541e243444544454445444547c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 007:ffffffffffb264748494a4b4c4d46494649464940040e4f4b2ffffffffff4040404040404040404040404040404040404040404040404040404040405d1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 008:ffffffffffe1c30515d325d33545c3d355d3c3d365b17585e1ffffffffff4040404040404098989898404040404040404040404040404040404040407c1e7e8e1e1e1e9eae1ebe1e1e1e1e1e1e1e1e72821ebe1e72821ebe1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 009:ffffffffffb264e395a5b594e39564c5d5946494e5b1b1f5b2ffffffffff4040404040404040404040404040404040404040404040404040404040405d1e05151ebe1ecedeeefe1e1e1e1e1e1e1e1ee395eefe1ee395eefe1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 010:ffffffffffe1c306162636d34656c3e3d5d3c3d376b1b1b1e1ffffffffff404040404040409898989840b9c9c9d9e9f90a0a0a0a4040400a0a0a0a407c1ee395eefe1e0f1f2f3f1e1e1e1e1e1e1e1ee3952f3f1ee3952f3f1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 011:ffffffffffb264946494649464948696a694649410201020b2ffffffffff4040404040404040404040401a2a3a4a5a6a7a40404040404040404040405d1ee3952f3f1e4f5f1ebe1e1e1e1e1e1e1e1ee3951ebe1ee3951ebe1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 012:ffffffffffe1c37282d3c3d3c3d3b6c6d6d3c3d300e6f607e1ffffffffff4040404040404098989898408a9aaabaca9ada40404040404040404040407c1e4f5f1ebe1e0515eefe1e1e1e1e1e1e1e1ee395eefe1ee395eefe1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 013:ffffffffffb264e395946494649464946494649465172737b2ffffffffffeaeaeaeaeaeaeafafafafaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea5d1e0515eefe1e6f7f2f3f1e1e1e1e1e1e1e1e6f7f2f3f1e6f7f2f3f1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 014:ffffffffffe1c34454d3c3d3c3d3c3d3c3d3c3d3e5b14757e1fffffffffff3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f37c1e6f7f2f3f1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 015:ffffffffffb2649464946494649464946494649476b1b1b1b2fffffffffff3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f35d1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 016:ffffffffff0010201020766777001020102010201020102000fffffffffff3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f30b1b2b1b2b7667776777761b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- </MAP>
-- <SFX>
-- 016:00000000000000400040004000700070007000400040004000700070007000c000c000c000c000c000c000c000c000c000c000c000c000c000c000c0470000000000

View File

@@ -54,12 +54,27 @@ local ASC_45_TEXT = [[
]]
local ASC_78_TEXT = [[
The road has run out
of road.
Norman walked back
and forth
until the street
forgot which way it went.
And then - finally -
he stopped walking.
]]
local ascension_texts = {
[1] = ASC_01_TEXT,
[2] = ASC_12_TEXT,
[3] = ASC_23_TEXT,
[4] = ASC_34_TEXT,
[5] = ASC_45_TEXT,
[8] = ASC_78_TEXT,
}
function MysteriousManScreen.get_text_for_level(level)
@@ -132,7 +147,6 @@ 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()

View File

@@ -5,9 +5,9 @@ Screen.register({
"do_work",
"go_to_walking_to_home",
"have_a_coffee",
"go_to_truth",
},
init = function()
Audio.music_play_room_work()
Context.have_been_to_office = true
local possible_sprites = {
@@ -37,14 +37,34 @@ Screen.register({
{x = -4 + 5 * 8, y = 9 * 8}
}
if CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7 then
Audio.music_play_mystery()
Context.office_sprites = { "norman_echo" }
else
Audio.music_play_room_work(CommuteGlitch.music_speed())
Context.office_sprites = Sprite.list_randomize(possible_sprites, possible_positions)
if CommuteGlitch.is_active() then
Context.office_sprites = CommuteGlitch.corrupt_sprite_list(Context.office_sprites)
end
end
end,
background = function()
return CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7 and "" or "office"
end,
background = "office",
draw = function()
if Window.get_current_id() == "game" then
Sprite.draw_at("norman", 13 * 8, 9 * 8)
Sprite.draw_list(Context.office_sprites)
if CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7 then
Sprite.draw_at("norman_echo", 13 * 8, 9 * 8)
CommuteGlitch.draw_background_flicker()
else
CommuteGlitch.draw_sprite_list(Context.office_sprites)
end
if CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 6 then
Glitch.draw()
end
end
end
})

View File

@@ -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 ism_bm_combo_pct = math.floor((combo_mult - 1) * 100)
local wpm_combo_pct = math.floor((1 / combo_mult - 1) * 100 + 0.5)
local combo_pct = math.floor((combo_mult - 1) * 100)
local mult_text = string.format("+%d%%", combo_pct)
local meter_start_y = text_y + 10
local meter_list = {
@@ -73,12 +73,6 @@ 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)

View File

@@ -4,16 +4,60 @@ Screen.register({
decisions = {
"go_to_home",
"go_to_office",
"sumphore_discussion",
"go_to_truth",
},
init = function()
Audio.music_play_room_work()
local possible_sprites = {
"matrix_trinity",
"matrix_neo",
{id="matrix_oraculum", y_correct=1 * 8},
"matrix_architect"
}
local possible_positions = {
{x = 5 * 8, y = 11 * 8},
{x = 7 * 8, y = 11 * 8},
{x = 9 * 8, y = 11 * 8},
{x = 11 * 8, y = 11 * 8},
{x = 13 * 8, y = 11 * 8},
{x = 15 * 8, y = 11 * 8},
{x = 18 * 8, y = 11 * 8},
{x = 21 * 8, y = 11 * 8},
{x = 24 * 8, y = 11 * 8},
{x = 27 * 8, y = 11 * 8},
}
if CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7 then
Audio.music_play_mystery()
Context.walking_to_home_sprites = {}
else
Audio.music_play_room_work(CommuteGlitch.music_speed())
Context.walking_to_home_sprites = Sprite.list_randomize(possible_sprites, possible_positions)
if CommuteGlitch.is_active() then
Context.walking_to_home_sprites = CommuteGlitch.corrupt_sprite_list(Context.walking_to_home_sprites)
end
end
end,
background = function()
return CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7 and "" or "street"
end,
background = "street",
draw = function()
if Window.get_current_id() == "game" then
Sprite.draw_at("norman", 7 * 8, 3 * 8)
Sprite.draw_at("sumphore", 9 * 8, 2 * 8)
if not (CommuteGlitch.is_active() and CommuteGlitch.get_level() >= 7) then
Sprite.draw_at("pizza_vendor", 19 * 8, 1 * 8)
Sprite.draw_at("dev_guard", 22 * 8, 2 * 8)
end
CommuteGlitch.draw_sprite_list(Context.walking_to_home_sprites)
if CommuteGlitch.is_active() then
if CommuteGlitch.get_level() >= 7 then CommuteGlitch.draw_background_flicker() end
if CommuteGlitch.get_level() >= 6 then Glitch.draw() end
end
end
end
})

View File

@@ -4,11 +4,9 @@ Screen.register({
decisions = {
"go_to_home",
"go_to_office",
"sumphore_discussion",
"sumphore_discussion"
},
init = function()
Audio.music_play_room_work()
local possible_sprites = {
"matrix_trinity",
"matrix_neo",
@@ -29,6 +27,7 @@ Screen.register({
{x = 27 * 8, y = 11 * 8},
}
Audio.music_play_room_work()
Context.walking_to_office_sprites = Sprite.list_randomize(possible_sprites, possible_positions)
end,
background = "street",

View File

@@ -80,7 +80,7 @@ function Sprite.draw_list(sprite_list)
for _, sprite_info in ipairs(sprite_list) do
local sprite_data = _sprites[sprite_info.id]
if not sprite_data then
trace("Error: Attempted to draw non-registered sprite with id: " .. sprite_info.id)
trace("Error: Attempted to draw non-registered sprite with id: " .. tostring(sprite_info.id))
else
draw_sprite_instance(sprite_data, sprite_info)
end

View File

@@ -0,0 +1,14 @@
-- Norman echo: same tile indices as norman.
-- Color remap is applied by CommuteGlitch.draw_sprite_list via pal().
-- Implementer: set ECHO_SRC/ECHO_DST in logic.commute_glitch.lua after inspecting the palette.
Sprite.register({
id = "norman_echo",
sprites = {
{ s = 272, x_offset = -4, y_offset = -4 },
{ s = 273, x_offset = 4, y_offset = -4 },
{ s = 288, x_offset = -4, y_offset = 4 },
{ s = 289, x_offset = 4, y_offset = 4 },
{ s = 304, x_offset = -4, y_offset = 12 },
{ s = 305, x_offset = 4, y_offset = 12 },
}
})

View File

@@ -30,9 +30,3 @@ function Input.back() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_BACKSPACE) end
--- Checks if Enter is pressed.
--- @within Input
function Input.enter() return keyp(INPUT_KEY_ENTER) end
--- Checks if Up is pressed or held (with repeat).
--- @within Input
function Input.up_repeat() return btnp(INPUT_KEY_UP, 20, 4) end
--- Checks if Down is pressed or held (with repeat).
--- @within Input
function Input.down_repeat() return btnp(INPUT_KEY_DOWN, 20, 4) end

View File

@@ -1,81 +0,0 @@
--- @section TextInput
TextInput = {}
local LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
local _pos = {}
local _cursor = 1
local _max_len = 3
--- Initialises a new text input session.
--- @within TextInput
--- @param max_len number Maximum character count (default 3).
function TextInput.init(max_len)
_max_len = max_len or 3
_pos = {}
for i = 1, _max_len do _pos[i] = 1 end
_cursor = 1
end
--- Advances to the next letter at the cursor position (wraps Z→A).
--- @within TextInput
function TextInput.next_letter()
_pos[_cursor] = (_pos[_cursor] % #LETTERS) + 1
end
--- Goes back to the previous letter at the cursor position (wraps A→Z).
--- @within TextInput
function TextInput.prev_letter()
_pos[_cursor] = ((_pos[_cursor] - 2) % #LETTERS) + 1
end
--- Confirms the current letter and advances the cursor to the next position.
--- When called on the last position the cursor moves into the done state.
--- @within TextInput
function TextInput.select_letter()
if _cursor <= _max_len then _cursor = _cursor + 1 end
end
--- Moves the cursor one position to the right (stops at last position).
--- @within TextInput
function TextInput.next_position()
if _cursor < _max_len then _cursor = _cursor + 1 end
end
--- Moves the cursor one position to the left (stops at first position).
--- Also steps back out of the done state.
--- @within TextInput
function TextInput.prev_position()
if _cursor > 1 then _cursor = _cursor - 1 end
end
--- Returns the assembled name string.
--- @within TextInput
--- @return string
function TextInput.get_name()
local s = ""
for i = 1, _max_len do s = s .. LETTERS:sub(_pos[i], _pos[i]) end
return s
end
--- Returns the current 1-based cursor position.
--- @within TextInput
--- @return number
function TextInput.get_position()
return _cursor
end
--- Returns the letter at the given 1-based position.
--- @within TextInput
--- @param i number
--- @return string
function TextInput.get_letter(i)
return LETTERS:sub(_pos[i], _pos[i])
end
--- Returns true when all positions have been confirmed.
--- @within TextInput
--- @return boolean
function TextInput.is_done()
return _cursor > _max_len
end

View File

@@ -0,0 +1,41 @@
--- @section AscendDebugWindow
local _level = 0
--- Initialises the ASCEND debug start window.
--- @within AscendDebugWindow
function AscendDebugWindow.init()
_level = 0
end
--- Draws the ASCEND debug start window.
--- @within AscendDebugWindow
function AscendDebugWindow.draw()
UI.draw_top_bar("ASCEND Debug Start")
local cx = Config.screen.width / 2
local cy = Config.screen.height / 2
local left_arrow = _level > 0 and "<- " or " "
local right_arrow = _level < Ascension.get_max_level() and " ->" or " "
local label = left_arrow .. "Start at: " .. _level .. right_arrow
Print.text_center(label, cx, cy - 4, Config.colors.white, false, 1)
Print.text_center("Z/select: start X/back: menu", cx, Config.screen.height - 10, Config.colors.dark_grey, false, 1)
end
--- Updates the ASCEND debug start window logic.
--- @within AscendDebugWindow
function AscendDebugWindow.update()
if Input.left() then
_level = math.max(0, _level - 1)
elseif Input.right() then
_level = math.min(Ascension.get_max_level(), _level + 1)
elseif Input.select() then
Audio.sfx_select()
Context.new_game_debug(_level)
GameWindow.set_state("game")
elseif Input.back() then
Audio.sfx_deselect()
GameWindow.set_state("menu")
end
end

View File

@@ -1,197 +0,0 @@
--- @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

@@ -30,15 +30,9 @@ function EndWindow.draw()
Print.text(yes_text, centerX - 40, y, yes_color)
Print.text(no_text, centerX + 10, y, no_color)
elseif Context._end.state == "ending" then
local cx = Config.screen.width / 2
local name = Context.player_name or "AAA"
local code = CodeGenerator.encrypt(name)
Print.text_center("Game over -- good ending.", cx, 40, Config.colors.light_blue)
Print.text_center("Congrats " .. name .. "!", cx, 54, Config.colors.white)
Print.text_center("Your personal code:", cx, 72, Config.colors.light_grey)
Print.text_center(code, cx, 84, Config.colors.white, false, 3)
Print.text_center("Write it down!", cx, 112, Config.colors.item)
Print.text_center("Press Z to return to menu", cx, 126, Config.colors.dark_grey)
Print.text_center("Game over -- good ending.", Config.screen.width / 2, 50, Config.colors.light_blue)
Print.text_center("Congratulations!", Config.screen.width / 2, 70, Config.colors.white)
Print.text_center("Press Z to return to menu", Config.screen.width / 2, 110, Config.colors.light_grey)
end
end

View File

@@ -6,7 +6,8 @@ local function draw_game_scene(underlay_draw)
local screen = Screen.get_by_id(Context.game.current_screen)
if not screen then return end
if screen.background then
Map.draw(screen.background)
local actual_background = (type(screen.background) == "function" and screen.background()) or screen.background
Map.draw(actual_background)
elseif screen.background_color then
rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color)
end

View File

@@ -1,59 +0,0 @@
--- @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

@@ -102,13 +102,17 @@ function MenuWindow.update()
end
end
--- Opens player name entry then starts a new game.
--- Starts a new game from the menu.
--- @within MenuWindow
function MenuWindow.new_game()
PlayerNameWindow.init(function()
Context.new_game()
end)
Window.set_current("player_name")
end
--- Loads a game from the menu.
--- @within MenuWindow
function MenuWindow.load_game()
Context.load_game()
GameWindow.set_state("game")
end
--- Saves the current game from the menu.
@@ -135,20 +139,6 @@ function MenuWindow.controls()
Window.set_current("controls")
end
--- Opens the player name entry screen (test mode shortcut).
--- @within MenuWindow
function MenuWindow.player_name()
PlayerNameWindow.init()
Window.set_current("player_name")
end
--- Opens the credits screen.
--- @within MenuWindow
function MenuWindow.credits()
CreditsWindow.init()
Window.set_current("credits")
end
--- Opens the audio test menu.
--- @within MenuWindow
function MenuWindow.audio_test()
@@ -163,14 +153,6 @@ function MenuWindow.continued()
GameWindow.set_state("continued")
end
--- Opens the end screen for testing.
--- @within MenuWindow
function MenuWindow.end_screen()
Context._end.state = "ending"
Context._end.selection = 1
GameWindow.set_state("end")
end
--- Opens the DDR minigame test.
--- @within MenuWindow
function MenuWindow.ddr_test()
@@ -179,6 +161,13 @@ function MenuWindow.ddr_test()
MinigameDDRWindow.start("menu", "generated", { special_mode = "only_nothing" })
end
--- Opens the ASCEND debug start window.
--- @within MenuWindow
function MenuWindow.ascend_debug()
AscendDebugWindow.init()
GameWindow.set_state("ascend_debug")
end
--- Refreshes the list of menu items based on current game state.
--- @within MenuWindow
function MenuWindow.refresh_menu_items()
@@ -189,15 +178,14 @@ function MenuWindow.refresh_menu_items()
end
table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game})
table.insert(_menu_items, {label = "Load Game", decision = MenuWindow.load_game})
table.insert(_menu_items, {label = "Controls", decision = MenuWindow.controls})
table.insert(_menu_items, {label = "Credits", decision = MenuWindow.credits})
if Context.test_mode then
table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test})
table.insert(_menu_items, {label = "To Be Continued...", decision = MenuWindow.continued})
table.insert(_menu_items, {label = "DDR Test", decision = MenuWindow.ddr_test})
table.insert(_menu_items, {label = "End Screen", decision = MenuWindow.end_screen})
table.insert(_menu_items, {label = "Player Name", decision = MenuWindow.player_name})
table.insert(_menu_items, {label = "Start at ASCEND N", decision = MenuWindow.ascend_debug})
end
table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit})

View File

@@ -130,7 +130,6 @@ 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
@@ -142,12 +141,10 @@ 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
@@ -176,9 +173,6 @@ 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.
@@ -342,8 +336,7 @@ function MinigameDDRWindow.update()
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Audio.music_stop()
Meter.apply_ddr_reward(mg.total_misses)
if not Context.game_in_progress then return end
Meter.on_minigame_complete()
if mg.on_win then
mg.on_win(mg)
else

View File

@@ -1,35 +1,6 @@
--- @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 MinigameButtonMashState
--- @return result table The default button mash minigame configuration.
function MinigameButtonMashWindow.init_context()
return {
bar_fill = 0,
@@ -53,11 +24,7 @@ function MinigameButtonMashWindow.init_context()
focus_center_y = nil,
focus_initial_radius = 0,
win_timer = 0,
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,
on_win = nil
}
end
@@ -84,10 +51,8 @@ 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
@@ -99,18 +64,12 @@ 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
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
Meter.on_minigame_complete()
if mg.focus_center_x then Focus.stop() end
Context.home_norman_visible = true
Context.have_done_work_today = false
@@ -138,7 +97,6 @@ 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
@@ -158,7 +116,6 @@ 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,8 +73,7 @@ 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(false)
if not Context.game_in_progress then return end
Meter.on_minigame_complete()
if mg.focus_center_x then Focus.stop() end
if mg.on_win then
mg.on_win()

View File

@@ -1,115 +0,0 @@
--- @section PlayerNameWindow
local _frame = 0
local _on_confirm = nil
local MAX_LEN = 3
local BOX_W = 24
local BOX_H = 24
local BOX_GAP = 12
local BOX_Y = 50
local WARN_Y = 104
local function box_start_x()
return math.floor((Config.screen.width - (MAX_LEN * BOX_W + (MAX_LEN - 1) * BOX_GAP)) / 2)
end
local function box_x(i)
return box_start_x() + (i - 1) * (BOX_W + BOX_GAP)
end
--- Initialises the player name window.
--- @within PlayerNameWindow
--- @param on_confirm function Called with the entered name when the player saves.
function PlayerNameWindow.init(on_confirm)
_frame = 0
_on_confirm = on_confirm
TextInput.init(MAX_LEN)
end
local function draw_boxes()
local cursor = TextInput.get_position()
local blink = math.floor(_frame / 18) % 2 == 0
for i = 1, MAX_LEN do
local x = box_x(i)
local is_cur = (i == cursor)
local done = TextInput.is_done()
local fill = (is_cur and not done) and Config.colors.blue or Config.colors.black
local border = (is_cur and not done) and Config.colors.white
or done and Config.colors.light_blue
or Config.colors.dark_grey
rect (x, BOX_Y, BOX_W, BOX_H, fill)
rectb(x, BOX_Y, BOX_W, BOX_H, border)
local show = not (is_cur and blink and not done)
if show then
local ch = TextInput.get_letter(i)
local cw = print(ch, 0, -100, 0, false, 2)
local cx = x + math.floor((BOX_W - cw) / 2)
local cy = BOX_Y + math.floor((BOX_H - 11) / 2)
local col = (is_cur and not done) and Config.colors.white or Config.colors.light_grey
print(ch, cx, cy, col, false, 2)
end
end
-- caret arrow below active box
if not TextInput.is_done() then
local cx = box_x(cursor) + math.floor(BOX_W / 2)
local ay = BOX_Y + BOX_H + 4
line(cx - 4, ay, cx, ay + 4, Config.colors.white)
line(cx + 4, ay, cx, ay + 4, Config.colors.white)
end
end
--- Draws the player name window.
--- @within PlayerNameWindow
function PlayerNameWindow.draw()
cls(Config.colors.black)
Print.text_center("Player Name", Config.screen.width / 2, 14, Config.colors.white, false, 2)
draw_boxes()
if TextInput.is_done() then
Print.text_center("Z: save name B: edit", Config.screen.width / 2, BOX_Y + BOX_H + 12, Config.colors.light_blue)
else
Print.text_center("Up/Dn: letter Lft/Rgt: move Z: ok", Config.screen.width / 2, BOX_Y + BOX_H + 12, Config.colors.dark_grey)
end
-- Warning section
rect(0, WARN_Y, Config.screen.width, Config.screen.height - WARN_Y, Config.colors.blue)
rectb(0, WARN_Y, Config.screen.width, Config.screen.height - WARN_Y, Config.colors.light_blue)
Print.text_center("Remember your name!", Config.screen.width / 2, WARN_Y + 8, Config.colors.white)
Print.text_center("You will need it to load the game.", Config.screen.width / 2, WARN_Y + 20, Config.colors.light_grey)
end
--- Updates player name window logic.
--- @within PlayerNameWindow
function PlayerNameWindow.update()
_frame = _frame + 1
if TextInput.is_done() then
if Input.select() then
Context.player_name = TextInput.get_name()
if _on_confirm then _on_confirm() else Window.set_current("menu") end
elseif Input.back() then
TextInput.prev_position()
end
return
end
if Input.up_repeat() then TextInput.next_letter() end
if Input.down_repeat() then TextInput.prev_letter() end
if Input.right() then TextInput.next_position() end
if Input.select() then TextInput.select_letter() end
if Input.left() or Input.back() then
if TextInput.get_position() > 1 then
TextInput.prev_position()
else
Window.set_current("menu")
end
end
end

View File

@@ -22,6 +22,9 @@ Window.register("controls", ControlsWindow)
AudioTestWindow = {}
Window.register("audiotest", AudioTestWindow)
AscendDebugWindow = {}
Window.register("ascend_debug", AscendDebugWindow)
MinigameButtonMashWindow = {}
Window.register("minigame_button_mash", MinigameButtonMashWindow)
@@ -31,9 +34,6 @@ Window.register("minigame_rhythm", MinigameRhythmWindow)
MinigameDDRWindow = {}
Window.register("minigame_ddr", MinigameDDRWindow)
GameOverWindow = {}
Window.register("game_over", GameOverWindow)
EndWindow = {}
Window.register("end", EndWindow)
@@ -42,9 +42,3 @@ Window.register("discussion", DiscussionWindow)
ContinuedWindow = {}
Window.register("continued", ContinuedWindow)
CreditsWindow = {}
Window.register("credits", CreditsWindow)
PlayerNameWindow = {}
Window.register("player_name", PlayerNameWindow)