-- title: Mr Anderson's Adventure -- name: mranderson -- author: Zsolt Tasnadi -- desc: Life of a programmer in the Vector -- site: https://games.teletype.hu -- license: MIT License -- version: 0.1 -- script: lua local SplashWindow = {} local IntroWindow = {} local MenuWindow = {} local GameWindow = {} local PopupWindow = {} local InventoryWindow = {} local ConfigurationWindow = {} local UI = {} local Print = {} local Input = {} local NPC = {} local Item = {} local Player = {} local DEFAULT_CONFIG = { screen = { width = 240, height = 136 }, colors = { black = 0, light_grey = 13, dark_grey = 14, green = 6, npc = 8, item = 12 -- yellow }, player = { w = 8, h = 8, start_x = 120, start_y = 128, sprite_id = 1 }, physics = { gravity = 0.5, jump_power = -5, move_speed = 1.5, max_jumps = 2, interaction_radius_npc = 12, interaction_radius_item = 8 }, timing = { splash_duration = 120 } } local Config = { -- Copy default values initially screen = DEFAULT_CONFIG.screen, colors = DEFAULT_CONFIG.colors, player = DEFAULT_CONFIG.player, physics = DEFAULT_CONFIG.physics, timing = DEFAULT_CONFIG.timing, } local CONFIG_SAVE_BANK = 7 local CONFIG_SAVE_ADDRESS_MOVE_SPEED = 0 local CONFIG_SAVE_ADDRESS_MAX_JUMPS = 1 local CONFIG_MAGIC_VALUE_ADDRESS = 2 local CONFIG_MAGIC_VALUE = 0xDE -- A magic number to check if config is saved function Config.save() -- Save physics settings mset(Config.physics.move_speed * 10, CONFIG_SAVE_ADDRESS_MOVE_SPEED, CONFIG_SAVE_BANK) mset(Config.physics.max_jumps, CONFIG_SAVE_ADDRESS_MAX_JUMPS, CONFIG_SAVE_BANK) mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) -- Mark as saved end function Config.load() -- Check if config has been saved before using a magic value if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then Config.physics.move_speed = mget(CONFIG_SAVE_ADDRESS_MOVE_SPEED, CONFIG_SAVE_BANK) / 10 Config.physics.max_jumps = mget(CONFIG_SAVE_ADDRESS_MAX_JUMPS, CONFIG_SAVE_BANK) else Config.restore_defaults() end end function Config.restore_defaults() Config.physics.move_speed = DEFAULT_CONFIG.physics.move_speed Config.physics.max_jumps = DEFAULT_CONFIG.physics.max_jumps -- Any other configurable items should be reset here end -- Load configuration on startup Config.load() local WINDOW_SPLASH = 0 local WINDOW_INTRO = 1 local WINDOW_MENU = 2 local WINDOW_GAME = 3 local WINDOW_POPUP = 4 local WINDOW_INVENTORY = 5 local WINDOW_INVENTORY_ACTION = 6 local WINDOW_CONFIGURATION = 7 local SAVE_GAME_BANK = 6 local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0 local SAVE_GAME_MAGIC_VALUE = 0xCA local SAVE_GAME_PLAYER_X_ADDRESS = 1 local SAVE_GAME_PLAYER_Y_ADDRESS = 2 local SAVE_GAME_PLAYER_VX_ADDRESS = 3 local SAVE_GAME_PLAYER_VY_ADDRESS = 4 local SAVE_GAME_PLAYER_JUMPS_ADDRESS = 5 local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6 local VX_VY_OFFSET = 128 -- Offset for negative velocities -- Helper for deep copying tables local function clone_table(t) local copy = {} for k, v in pairs(t) do if type(v) == "table" then copy[k] = clone_table(v) else copy[k] = v end end return copy end -- This function returns a table containing only the initial *data* for Context local function get_initial_data() return { active_window = WINDOW_SPLASH, inventory = {}, intro = { y = Config.screen.height, speed = 0.5, text = "Mr. Anderson is an average\nprogrammer. His daily life\nrevolves around debugging,\npull requests, and end-of-sprint\nmeetings, all while secretly\ndreaming of being destined\nfor something more." }, current_screen = 1, splash_timer = Config.timing.splash_duration, dialog = { text = "", menu_items = {}, selected_menu_item = 1, active_entity = nil, showing_description = false, current_node_key = nil }, player = { x = Config.player.start_x, y = Config.player.start_y, w = Config.player.w, h = Config.player.h, vx = 0, vy = 0, jumps = 0, sprite_id = Config.player.sprite_id }, ground = { x = 0, y = Config.screen.height, w = Config.screen.width, h = 8 }, menu_items = {}, selected_menu_item = 1, selected_inventory_item = 1, game_in_progress = false, -- New flag screens = clone_table({ { -- Screen 1 name = "Screen 1", platforms = { { x = 80, y = 110, w = 40, h = 8 }, { x = 160, y = 90, w = 40, h = 8 } }, npcs = { { x = 180, y = 82, name = "Trinity", sprite_id = 2, dialog = { start = { text = "Hello, Neo.", options = { {label = "Who are you?", next_node = "who_are_you"}, {label = "My name is not Neo.", next_node = "not_neo"}, {label = "...", next_node = "silent"} } }, who_are_you = { text = "I am Trinity. I've been looking for you.", options = { {label = "The famous hacker?", next_node = "famous_hacker"}, {label = "Why me?", next_node = "why_me"} } }, not_neo = { text = "I know. But you will be.", options = { {label = "What are you talking about?", next_node = "who_are_you"} } }, silent = { text = "You're not much of a talker, are you?", options = { {label = "I guess not.", next_node = "dialog_end"} } }, famous_hacker = { text = "The one and only.", options = { {label = "Wow.", next_node = "dialog_end"} } }, why_me = { text = "Morpheus believes you are The One.", options = { {label = "The One?", next_node = "the_one"} } }, the_one = { text = "The one who will save us all.", options = { {label = "I'm just a programmer.", next_node = "dialog_end"} } }, dialog_end = { text = "We'll talk later.", options = {} -- No options, ends conversation } } }, { x = 90, y = 102, name = "Oracle", sprite_id = 3, dialog = { start = { text = "I know what you're thinking. 'Am I in the right place?'", options = { {label = "Who are you?", next_node = "who_are_you"}, {label = "I guess I am.", next_node = "you_are"} } }, who_are_are = { text = "I'm the Oracle. And you're right on time. Want a cookie?", options = { {label = "Sure.", next_node = "cookie"}, {label = "No, thank you.", next_node = "no_cookie"} } }, you_are = { text = "Of course you are. Sooner or later, everyone comes to see me. Want a cookie?", options = { {label = "Yes, please.", next_node = "cookie"}, {label = "I'm good.", next_node = "no_cookie"} } }, cookie = { text = "Here you go. Now, what's really on your mind?", options = { {label = "Am I The One?", next_node = "the_one"}, {label = "What is the Matrix?", next_node = "the_matrix"} } }, no_cookie = { text = "Suit yourself. Now, what's troubling you?", options = { {label = "Am I The One?", next_node = "the_one"}, {label = "What is the Matrix?", next_node = "the_matrix"} } }, the_one = { text = "Being The One is just like being in love. No one can tell you you're in love, you just know it. Through and through. Balls to bones.", options = { {label = "So I'm not?", next_node = "dialog_end"} } }, the_matrix = { text = "The Matrix is a system, Neo. That system is our enemy. But when you're inside, you look around, what do you see? The very minds of the people we are trying to save.", options = { {label = "I see.", next_node = "dialog_end"} } }, dialog_end = { text = "You have to understand, most of these people are not ready to be unplugged.", options = {} } } } }, items = { { x = 100, y = 128, w = 8, h = 8, name = "Key", sprite_id = 4, desc = "A rusty old key. It might open something." } } }, { -- Screen 2 name = "Screen 2", platforms = { { x = 30, y = 100, w = 50, h = 8 }, { x = 100, y = 80, w = 50, h = 8 }, { x = 170, y = 60, w = 50, h = 8 } }, npcs = { { x = 120, y = 72, name = "Morpheus", sprite_id = 5, dialog = { start = { text = "At last. Welcome, Neo. As you no doubt have guessed, I am Morpheus.", options = { {label = "It's an honor to meet you.", next_node = "honor"}, {label = "You've been looking for me.", next_node = "looking_for_me"} } }, honor = { text = "No, the honor is mine.", options = { {label = "What is this place?", next_node = "what_is_this_place"} } }, looking_for_me = { text = "I have. For some time.", options = { {label = "What is this place?", next_node = "what_is_this_place"} } }, what_is_this_place = { text = "This is the construct. It's our loading program. We can load anything from clothing, to equipment, weapons, training simulations. Anything we need.", options = { {label = "Right.", next_node = "dialog_end"} } }, dialog_end = { text = "I've been waiting for you, Neo. We have much to discuss.", options = {} -- Ends conversation } } }, { x = 40, y = 92, name = "Tank", sprite_id = 6, dialog = { start = { text = "Hey, Neo! Welcome to the construct. I'm Tank.", options = { {label = "Good to meet you.", next_node = "good_to_meet_you"}, {label = "This place is incredible.", next_node = "incredible"} } }, good_to_meet_you = { text = "You too! We've been waiting for you. Need anything? Training? Weapons?", options = { {label = "Training?", next_node = "training"}, {label = "I'm good for now.", next_node = "dialog_end"} } }, incredible = { text = "Isn't it? The boss's design. We can load anything we need. What do you want to learn?", options = { {label = "Show me.", next_node = "training"} } }, training = { text = "Jujitsu? Kung Fu? How about... all of them?", options = { {label = "All of them.", next_node = "all_of_them"} } }, all_of_them = { text = "Operator, load the combat training program.", options = { {label = "...", next_node = "dialog_end"} } }, dialog_end = { text = "Just holler if you need anything. Anything at all.", options = {} } } } }, items = { { x = 180, y = 52, w = 8, h = 8, name = "Potion", sprite_id = 7, desc = "A glowing red potion. It looks potent." } } }, { -- Screen 3 name = "Screen 3", platforms = { { x = 50, y = 110, w = 30, h = 8 }, { x = 100, y = 90, w = 30, h = 8 }, { x = 150, y = 70, w = 30, h = 8 }, { x = 200, y = 50, w = 30, h = 8 } }, npcs = { { x = 210, y = 42, name = "Agent Smith", sprite_id = 8, dialog = { start = { text = "Mr. Anderson. We've been expecting you.", options = { {label = "My name is Neo.", next_node = "name_is_neo"}, {label = "...", next_node = "silent"} } }, name_is_neo = { text = "Whatever you say. You're here for a reason.", options = { {label = "What reason?", next_node = "what_reason"} } }, silent = { text = "The silent type. It doesn't matter. You are an anomaly.", options = { {label = "What do you want?", next_node = "what_reason"} } }, what_reason = { text = "To be deleted. The system has no place for your kind.", options = { {label = "I won't let you.", next_node = "wont_let_you"} } }, wont_let_you = { text = "You hear that, Mr. Anderson? That is the sound of inevitability.", options = { {label = "...", next_node = "dialog_end"} } }, dialog_end = { text = "It is purpose that created us. Purpose that connects us. Purpose that pulls us. That guides us. That drives us. It is purpose that defines. Purpose that binds us.", options = {} } } }, { x = 160, y = 62, name = "Cypher", sprite_id = 9, dialog = { start = { text = "Well, well. The new messiah. Welcome to the real world.", options = { {label = "You don't seem happy.", next_node = "not_happy"}, {label = "...", next_node = "silent"} } }, not_happy = { text = "Happy? Ignorance is bliss, Neo. We've been fighting this war for years. For what?", options = { {label = "For freedom.", next_node = "freedom"} } }, silent = { text = "Not a talker, huh? Smart. Less to regret later. Want a drink?", options = { {label = "Sure.", next_node = "drink"}, {label = "No thanks.", next_node = "no_drink"} } }, drink = { text = "Good stuff. The little things you miss, you know? Like a good steak.", options = { {label = "I guess.", next_node = "dialog_end"} } }, no_drink = { text = "Your loss. More for me.", options = { {label = "...", next_node = "dialog_end"} } }, freedom = { text = "Freedom... right. If Morpheus told you you could fly, would you believe him?", options = { {label = "He's our leader.", next_node = "dialog_end"} } }, dialog_end = { text = "Just be careful who you trust.", options = {} } } } }, items = {} } }) } end Context = {} local function reset_context_to_initial_state() local initial_data = get_initial_data() -- Clear existing data properties from Context (but not methods) for k in pairs(Context) do if type(Context[k]) ~= "function" then -- Only clear data, leave functions Context[k] = nil end end -- Copy all initial data properties into Context for k, v in pairs(initial_data) do Context[k] = v end end -- Initially populate Context with data reset_context_to_initial_state() -- Now define the methods for Context function Context.new_game() reset_context_to_initial_state() Context.game_in_progress = true MenuWindow.refresh_menu_items() end function Context.save_game() if not Context.game_in_progress then return end mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) mset(Context.player.x * 10, SAVE_GAME_PLAYER_X_ADDRESS, SAVE_GAME_BANK) mset(Context.player.y * 10, SAVE_GAME_PLAYER_Y_ADDRESS, SAVE_GAME_BANK) mset( (Context.player.vx * 100) + VX_VY_OFFSET, SAVE_GAME_PLAYER_VX_ADDRESS, SAVE_GAME_BANK) mset( (Context.player.vy * 100) + VX_VY_OFFSET, SAVE_GAME_PLAYER_VY_ADDRESS, SAVE_GAME_BANK) mset(Context.player.jumps, SAVE_GAME_PLAYER_JUMPS_ADDRESS, SAVE_GAME_BANK) mset(Context.current_screen, SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK) end function Context.load_game() if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then -- No saved game found, start a new one Context.new_game() return end reset_context_to_initial_state() -- Reset data, preserve methods Context.player.x = mget(SAVE_GAME_PLAYER_X_ADDRESS, SAVE_GAME_BANK) / 10 Context.player.y = mget(SAVE_GAME_PLAYER_Y_ADDRESS, SAVE_GAME_BANK) / 10 Context.player.vx = (mget(SAVE_GAME_PLAYER_VX_ADDRESS, SAVE_GAME_BANK) - VX_VY_OFFSET) / 100 Context.player.vy = (mget(SAVE_GAME_PLAYER_VY_ADDRESS, SAVE_GAME_BANK) - VX_VY_OFFSET) / 100 Context.player.jumps = mget(SAVE_GAME_PLAYER_JUMPS_ADDRESS, SAVE_GAME_BANK) Context.current_screen = mget(SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK) Context.game_in_progress = true MenuWindow.refresh_menu_items() end function Print.text(text, x, y, color, fixed, scale) local shadow_color = Config.colors.black if color == shadow_color then shadow_color = Config.colors.light_grey end scale = scale or 1 print(text, x + 1, y + 1, shadow_color, fixed, scale) print(text, x, y, color, fixed, scale) end function NPC.talk_to() local npc = Context.dialog.active_entity if npc.dialog and npc.dialog.start then PopupWindow.set_dialog_node("start") else -- if no dialog, go back GameWindow.set_state(WINDOW_GAME) end end function NPC.fight() end function NPC.go_back() GameWindow.set_state(WINDOW_GAME) end function Item.use() Print.text("Used item: " .. Context.dialog.active_entity.name) GameWindow.set_state(WINDOW_INVENTORY) end function Item.look_at() PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc) end function Item.put_away() -- Add item to inventory table.insert(Context.inventory, Context.dialog.active_entity) -- Remove item from screen local currentScreenData = Context.screens[Context.current_screen] for i, item in ipairs(currentScreenData.items) do if item == Context.dialog.active_entity then table.remove(currentScreenData.items, i) break end end -- Go back to game GameWindow.set_state(WINDOW_GAME) end function Item.go_back_from_item_dialog() GameWindow.set_state(WINDOW_GAME) end function Item.go_back_from_inventory_action() GameWindow.set_state(WINDOW_GAME) end function Item.drop() -- Remove item from inventory for i, item in ipairs(Context.inventory) do if item == Context.dialog.active_entity then table.remove(Context.inventory, i) break end end -- Add item to screen local currentScreenData = Context.screens[Context.current_screen] Context.dialog.active_entity.x = Context.player.x Context.dialog.active_entity.y = Context.player.y table.insert(currentScreenData.items, Context.dialog.active_entity) -- Go back to inventory GameWindow.set_state(WINDOW_INVENTORY) end function Player.draw() spr(Context.player.sprite_id, Context.player.x, Context.player.y, 0) end function Player.update() -- Handle input if Input.left() then Context.player.vx = -Config.physics.move_speed elseif Input.right() then Context.player.vx = Config.physics.move_speed else Context.player.vx = 0 end if Input.player_jump() and Context.player.jumps < Config.physics.max_jumps then Context.player.vy = Config.physics.jump_power Context.player.jumps = Context.player.jumps + 1 end -- Update player position Context.player.x = Context.player.x + Context.player.vx Context.player.y = Context.player.y + Context.player.vy -- Screen transition if Context.player.x > Config.screen.width - Context.player.w then if Context.current_screen < #Context.screens then Context.current_screen = Context.current_screen + 1 Context.player.x = 0 else Context.player.x = Config.screen.width - Context.player.w end elseif Context.player.x < 0 then if Context.current_screen > 1 then Context.current_screen = Context.current_screen - 1 Context.player.x = Config.screen.width - Context.player.w else Context.player.x = 0 end end -- Apply gravity Context.player.vy = Context.player.vy + Config.physics.gravity local currentScreenData = Context.screens[Context.current_screen] -- Collision detection with platforms for _, p in ipairs(currentScreenData.platforms) do if Context.player.vy > 0 and Context.player.y + Context.player.h >= p.y and Context.player.y + Context.player.h <= p.y + p.h and Context.player.x + Context.player.w > p.x and Context.player.x < p.x + p.w then Context.player.y = p.y - Context.player.h Context.player.vy = 0 Context.player.jumps = 0 end end -- Collision detection with ground if Context.player.y + Context.player.h > Context.ground.y then Context.player.y = Context.ground.y - Context.player.h Context.player.vy = 0 Context.player.jumps = 0 end -- Entity interaction if Input.player_interact() then local interaction_found = false -- NPC interaction for _, npc in ipairs(currentScreenData.npcs) do if math.abs(Context.player.x - npc.x) < Config.physics.interaction_radius_npc and math.abs(Context.player.y - npc.y) < Config.physics.interaction_radius_npc then PopupWindow.show_menu_dialog(npc, { {label = "Talk to", action = NPC.talk_to}, {label = "Fight", action = NPC.fight}, {label = "Go back", action = NPC.go_back} }, WINDOW_POPUP) interaction_found = true break end end if not interaction_found then -- Item interaction for _, item in ipairs(currentScreenData.items) do if math.abs(Context.player.x - item.x) < Config.physics.interaction_radius_item and math.abs(Context.player.y - item.y) < Config.physics.interaction_radius_item then PopupWindow.show_menu_dialog(item, { {label = "Use", action = Item.use}, {label = "Look at", action = Item.look_at}, {label = "Put away", action = Item.put_away}, {label = "Go back", action = Item.go_back_from_item_dialog} }, WINDOW_POPUP) interaction_found = true break end end end -- If no interaction happened, open inventory if not interaction_found then GameWindow.set_state(WINDOW_INVENTORY) end end end -- Gamepad buttons local INPUT_KEY_UP = 0 local INPUT_KEY_DOWN = 1 local INPUT_KEY_LEFT = 2 local INPUT_KEY_RIGHT = 3 local INPUT_KEY_A = 4 -- Z key local INPUT_KEY_B = 5 -- X key local INPUT_KEY_X = 6 -- A key local INPUT_KEY_Y = 7 -- S key -- Keyboard keys -- TODO: Find correct key codes for SPACE and LCTRL local INPUT_KEY_SPACE = 48 local INPUT_KEY_BACKSPACE = 51 local INPUT_KEY_ENTER = 50 function Input.up() return btnp(INPUT_KEY_UP) end function Input.down() return btnp(INPUT_KEY_DOWN) end function Input.left() return btn(INPUT_KEY_LEFT) end function Input.right() return btn(INPUT_KEY_RIGHT) end function Input.player_jump() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_SPACE) end function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end -- B button function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end function Input.toggle_popup() return keyp(INPUT_KEY_ENTER) end function UI.draw_top_bar(title) rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey) Print.text(title, 3, 2, Config.colors.green) end function UI.draw_dialog() PopupWindow.draw() end function UI.draw_menu(items, selected_item, x, y) for i, item in ipairs(items) do local current_y = y + (i-1)*10 if i == selected_item then Print.text(">", x - 8, current_y, Config.colors.green) end Print.text(item.label, x, current_y, Config.colors.green) end end function UI.update_menu(items, selected_item) if Input.up() then selected_item = selected_item - 1 if selected_item < 1 then selected_item = #items end elseif Input.down() then selected_item = selected_item + 1 if selected_item > #items then selected_item = 1 end end return selected_item end function UI.word_wrap(text, max_chars_per_line) if text == nil then return {""} end local lines = {} for input_line in (text .. "\n"):gmatch("(.-)\n") do local current_line = "" local words_in_line = 0 for word in input_line:gmatch("%S+") do words_in_line = words_in_line + 1 if #current_line == 0 then current_line = word elseif #current_line + #word + 1 <= max_chars_per_line then current_line = current_line .. " " .. word else table.insert(lines, current_line) current_line = word end end if words_in_line > 0 then table.insert(lines, current_line) else table.insert(lines, "") end end if #lines == 0 then return {""} end return lines end function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format) return { label = label, get = value_getter, set = value_setter, min = min, max = max, step = step, format = format or "%.1f", type = "numeric_stepper" } end function UI.create_action_item(label, action) return { label = label, action = action, type = "action_item" } end function SplashWindow.draw() Print.text("Mr. Anderson's", 78, 60, Config.colors.green) Print.text("Addventure", 90, 70, Config.colors.green) end function SplashWindow.update() Context.splash_timer = Context.splash_timer - 1 if Context.splash_timer <= 0 or Input.menu_confirm() then GameWindow.set_state(WINDOW_INTRO) end end function IntroWindow.draw() local x = (Config.screen.width - 132) / 2 -- Centered text Print.text(Context.intro.text, x, Context.intro.y, Config.colors.green) end function IntroWindow.update() Context.intro.y = Context.intro.y - Context.intro.speed -- Count lines in intro text to determine when scrolling is done local lines = 1 for _ in string.gmatch(Context.intro.text, "\n") do lines = lines + 1 end -- When text is off-screen, go to menu if Context.intro.y < -lines * 8 then GameWindow.set_state(WINDOW_MENU) end -- Skip intro by pressing A if Input.menu_confirm() then GameWindow.set_state(WINDOW_MENU) end end function MenuWindow.draw() UI.draw_top_bar("Main Menu") UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70) end function MenuWindow.update() Context.selected_menu_item = UI.update_menu(Context.menu_items, Context.selected_menu_item) if Input.menu_confirm() then local selected_item = Context.menu_items[Context.selected_menu_item] if selected_item and selected_item.action then selected_item.action() end end end function MenuWindow.new_game() Context.new_game() -- This function will be created in Context GameWindow.set_state(WINDOW_GAME) end function MenuWindow.load_game() Context.load_game() -- This function will be created in Context GameWindow.set_state(WINDOW_GAME) end function MenuWindow.save_game() Context.save_game() -- This function will be created in Context end function MenuWindow.resume_game() GameWindow.set_state(WINDOW_GAME) end function MenuWindow.exit() exit() end function MenuWindow.configuration() ConfigurationWindow.init() GameWindow.set_state(WINDOW_CONFIGURATION) end function MenuWindow.refresh_menu_items() Context.menu_items = {} -- Start with an empty table if Context.game_in_progress then table.insert(Context.menu_items, {label = "Resume Game", action = MenuWindow.resume_game}) table.insert(Context.menu_items, {label = "Save Game", action = MenuWindow.save_game}) end table.insert(Context.menu_items, {label = "New Game", action = MenuWindow.new_game}) table.insert(Context.menu_items, {label = "Load Game", action = MenuWindow.load_game}) table.insert(Context.menu_items, {label = "Configuration", action = MenuWindow.configuration}) table.insert(Context.menu_items, {label = "Exit", action = MenuWindow.exit}) Context.selected_menu_item = 1 -- Reset selection after refreshing end ConfigurationWindow = { controls = {}, selected_control = 1, } function ConfigurationWindow.init() ConfigurationWindow.controls = { UI.create_numeric_stepper( "Move Speed", function() return Config.physics.move_speed end, function(v) Config.physics.move_speed = v end, 0.5, 3, 0.1, "%.1f" ), UI.create_numeric_stepper( "Max Jumps", function() return Config.physics.max_jumps end, function(v) Config.physics.max_jumps = v end, 1, 5, 1, "%d" ), UI.create_action_item( "Save", function() Config.save() end ), UI.create_action_item( "Restore Defaults", function() Config.restore_defaults() end ), } end function ConfigurationWindow.draw() UI.draw_top_bar("Configuration") local x_start = 10 -- Left margin for labels local y_start = 40 local x_value_right_align = Config.screen.width - 10 -- Right margin for values local char_width = 4 -- Approximate character width for default font for i, control in ipairs(ConfigurationWindow.controls) do local current_y = y_start + (i - 1) * 12 local color = Config.colors.green if control.type == "numeric_stepper" then local value = control.get() local label_text = control.label local value_text = string.format(control.format, value) -- Calculate x position for right-aligned value local value_x = x_value_right_align - (#value_text * char_width) if i == ConfigurationWindow.selected_control then color = Config.colors.item Print.text("<", x_start -8, current_y, color) Print.text(label_text, x_start, current_y, color) -- Shift label due to '<' Print.text(value_text, value_x, current_y, color) Print.text(">", x_value_right_align + 4, current_y, color) -- Print '>' after value else Print.text(label_text, x_start, current_y, color) Print.text(value_text, value_x, current_y, color) end elseif control.type == "action_item" then local label_text = control.label if i == ConfigurationWindow.selected_control then color = Config.colors.item Print.text("<", x_start -8, current_y, color) Print.text(label_text, x_start, current_y, color) Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color) else Print.text(label_text, x_start, current_y, color) end end end Print.text("Press B to go back", x_start, 120, Config.colors.light_grey) end function ConfigurationWindow.update() if Input.menu_back() then GameWindow.set_state(WINDOW_MENU) return end if Input.up() then ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1 if ConfigurationWindow.selected_control < 1 then ConfigurationWindow.selected_control = #ConfigurationWindow.controls end elseif Input.down() then ConfigurationWindow.selected_control = ConfigurationWindow.selected_control + 1 if ConfigurationWindow.selected_control > #ConfigurationWindow.controls then ConfigurationWindow.selected_control = 1 end end local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control] if control then if control.type == "numeric_stepper" then local current_value = control.get() if btnp(2) then -- Left local new_value = math.max(control.min, current_value - control.step) control.set(new_value) elseif btnp(3) then -- Right local new_value = math.min(control.max, current_value + control.step) control.set(new_value) end elseif control.type == "action_item" then if Input.menu_confirm() then control.action() end end end end function PopupWindow.set_dialog_node(node_key) local npc = Context.dialog.active_entity local node = npc.dialog[node_key] if not node then GameWindow.set_state(WINDOW_GAME) return end Context.dialog.current_node_key = node_key Context.dialog.text = node.text local menu_items = {} if node.options then for _, option in ipairs(node.options) do table.insert(menu_items, { label = option.label, action = function() PopupWindow.set_dialog_node(option.next_node) end }) end end -- if no options, it's the end of this branch. if #menu_items == 0 then table.insert(menu_items, { label = "Go back", action = function() GameWindow.set_state(WINDOW_GAME) end }) end Context.dialog.menu_items = menu_items Context.dialog.selected_menu_item = 1 Context.dialog.showing_description = false GameWindow.set_state(WINDOW_POPUP) end function PopupWindow.update() if Context.dialog.showing_description then if Input.menu_confirm() or Input.menu_back() then Context.dialog.showing_description = false Context.dialog.text = "" -- Clear the description text -- No need to change active_window, as it remains in WINDOW_POPUP or WINDOW_INVENTORY_ACTION end else Context.dialog.selected_menu_item = UI.update_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item) if Input.menu_confirm() then local selected_item = Context.dialog.menu_items[Context.dialog.selected_menu_item] if selected_item and selected_item.action then selected_item.action() end end if Input.menu_back() then GameWindow.set_state(WINDOW_GAME) end end end function PopupWindow.show_menu_dialog(entity, menu_items, dialog_active_window) Context.dialog.active_entity = entity Context.dialog.text = "" -- Initial dialog text is empty, name is title GameWindow.set_state(dialog_active_window or WINDOW_POPUP) Context.dialog.showing_description = false Context.dialog.menu_items = menu_items Context.dialog.selected_menu_item = 1 end function PopupWindow.show_description_dialog(entity, description_text) Context.dialog.active_entity = entity Context.dialog.text = description_text GameWindow.set_state(WINDOW_POPUP) Context.dialog.showing_description = true -- No menu items needed for description dialog end function PopupWindow.draw() rect(40, 40, 160, 80, Config.colors.black) rectb(40, 40, 160, 80, Config.colors.green) -- Display the entity's name as the dialog title if Context.dialog.active_entity and Context.dialog.active_entity.name then Print.text(Context.dialog.active_entity.name, 120 - #Context.dialog.active_entity.name * 2, 45, Config.colors.green) end -- Display the dialog content (description for "look at", or initial name/dialog for others) local wrapped_lines = UI.word_wrap(Context.dialog.text, 25) -- Max 25 chars per line local current_y = 55 -- Starting Y position for the first line of content for _, line in ipairs(wrapped_lines) do Print.text(line, 50, current_y, Config.colors.light_grey) current_y = current_y + 8 -- Move to the next line (8 pixels for default font height + padding) end -- Adjust menu position based on the number of wrapped lines if not Context.dialog.showing_description then UI.draw_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item, 50, current_y + 2) else Print.text("[A] Go Back", 50, current_y + 10, Config.colors.green) end end function InventoryWindow.draw() UI.draw_top_bar("Inventory") if #Context.inventory == 0 then Print.text("Inventory is empty.", 70, 70, Config.colors.light_grey) else for i, item in ipairs(Context.inventory) do local color = Config.colors.light_grey if i == Context.selected_inventory_item then color = Config.colors.green Print.text(">", 60, 20 + i * 10, color) end Print.text(item.name, 70, 20 + i * 10, color) end end end function InventoryWindow.update() Context.selected_inventory_item = UI.update_menu(Context.inventory, Context.selected_inventory_item) if Input.menu_confirm() and #Context.inventory > 0 then local selected_item = Context.inventory[Context.selected_inventory_item] PopupWindow.show_menu_dialog(selected_item, { {label = "Use", action = Item.use}, {label = "Drop", action = Item.drop}, {label = "Look at", action = Item.look_at}, {label = "Go back", action = Item.go_back_from_inventory_action} }, WINDOW_INVENTORY_ACTION) end if Input.menu_back() then GameWindow.set_state(WINDOW_GAME) end end function GameWindow.draw() local currentScreenData = Context.screens[Context.current_screen] UI.draw_top_bar(currentScreenData.name) -- Draw platforms for _, p in ipairs(currentScreenData.platforms) do rect(p.x, p.y, p.w, p.h, Config.colors.green) end -- Draw items for _, item in ipairs(currentScreenData.items) do spr(item.sprite_id, item.x, item.y, 0) end -- Draw NPCs for _, npc in ipairs(currentScreenData.npcs) do spr(npc.sprite_id, npc.x, npc.y, 0) end -- Draw ground rect(Context.ground.x, Context.ground.y, Context.ground.w, Context.ground.h, Config.colors.dark_grey) -- Draw player Player.draw() end function GameWindow.update() if Input.menu_back() then Context.active_window = WINDOW_MENU MenuWindow.refresh_menu_items() return end Player.update() -- Call the encapsulated player update logic end function GameWindow.set_state(new_state) Context.active_window = new_state -- Add any state-specific initialization/cleanup here later if needed end local STATE_HANDLERS = { [WINDOW_SPLASH] = function() SplashWindow.update() SplashWindow.draw() end, [WINDOW_INTRO] = function() IntroWindow.update() IntroWindow.draw() end, [WINDOW_MENU] = function() MenuWindow.update() MenuWindow.draw() end, [WINDOW_GAME] = function() GameWindow.update() GameWindow.draw() end, [WINDOW_POPUP] = function() GameWindow.draw() PopupWindow.update() PopupWindow.draw() end, [WINDOW_INVENTORY] = function() InventoryWindow.update() InventoryWindow.draw() end, [WINDOW_INVENTORY_ACTION] = function() InventoryWindow.draw() PopupWindow.draw() PopupWindow.update() end, [WINDOW_CONFIGURATION] = function() ConfigurationWindow.update() ConfigurationWindow.draw() end, } local initialized_game = false function init_game() if initialized_game then return end MenuWindow.refresh_menu_items() initialized_game = true end function TIC() init_game() cls(Config.colors.black) local handler = STATE_HANDLERS[Context.active_window] if handler then handler() end end -- -- 000:4444444444444444444444444444444444444444444444444444444444444444 -- 001:1111111111111111111111111111111111111111111111111111111111111111 -- 002:5555555555555555555555555555555555555555555555555555555555555555 -- 003:6666666666666666666666666666666666666666666666666666666666666666 -- 004:7777777777777777777777777777777777777777777777777777777777777777 -- 005:8888888888888888888888888888888888888888888888888888888888888888 -- 006:9999999999999999999999999999999999999999999999999999999999999999 -- 007:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -- 008:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -- -- -- 000:00000000ffffffff00000000ffffffff -- 001:0123456789abcdeffedcba9876543210 -- 02:0123456789abcdef0123456789abcdef -- -- -- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000 -- -- -- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -- -- -- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57 --