diff --git a/.gitignore b/.gitignore
index 722d5e7..9ad8137 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.vscode
+mranderson.lua
diff --git a/.woodpecker.yml b/.woodpecker.yml
index ae41584..08d9203 100644
--- a/.woodpecker.yml
+++ b/.woodpecker.yml
@@ -9,6 +9,7 @@ steps:
<<: *environment
XDG_RUNTIME_DIR: /tmp
commands:
+ - make build PROJECT=$GAME_NAME
- tic80 --cli --skip --fs=. --cmd="load $GAME_NAME.$GAME_LANG & save $GAME_NAME & export html $GAME_NAME.html & exit"
- name: artifact
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..564759c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+# -----------------------------------------
+# Makefile – TIC-80 project builder
+# Usage:
+# make PROJECT=mranderson
+# make build PROJECT=mranderson
+# make watch PROJECT=mranderson
+# make export PROJECT=mranderson
+# -----------------------------------------
+
+ifndef PROJECT
+$(error Specify the project name: make PROJECT=name)
+endif
+
+ORDER = $(PROJECT).inc
+OUTPUT = $(PROJECT).lua
+OUTPUT_ZIP = $(PROJECT).html.zip
+OUTPUT_TIC = $(PROJECT).tic
+
+SRC_DIR = inc
+SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
+
+all: build
+
+build: $(OUTPUT)
+ @echo "==> Build complete: $(OUTPUT)"
+
+$(OUTPUT): $(SRC) $(ORDER)
+ @echo "==> Building $(OUTPUT)..."
+ @rm -f $(OUTPUT)
+ @while read f; do \
+ echo "-- FILE: $$f" >> $(OUTPUT); \
+ cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
+ echo "\n" >> $(OUTPUT); \
+ done < $(ORDER)
+ @echo "==> Done."
+
+export: $(OUTPUT)
+ @echo "==> TIC-80 export..."
+ tic80 --cli --skip --fs=. \
+ --cmd="load $(OUTPUT) & save $(PROJECT) & export html $(PROJECT).html & exit"
+ @zip -q $(OUTPUT_ZIP) $(PROJECT).html
+ @echo "==> HTML ZIP: $(OUTPUT_ZIP)"
+ @echo "==> TIC: $(OUTPUT_TIC)"
+
+watch:
+ @echo "==> Watching project: $(PROJECT)"
+ fswatch -o $(SRC_DIR) $(ORDER) | while read; do make build PROJECT=$(PROJECT); done
diff --git a/inc/assets.lua b/inc/assets.lua
new file mode 100644
index 0000000..f2b56df
--- /dev/null
+++ b/inc/assets.lua
@@ -0,0 +1,29 @@
+--
+-- 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
+--
diff --git a/inc/config.lua b/inc/config.lua
new file mode 100644
index 0000000..af74633
--- /dev/null
+++ b/inc/config.lua
@@ -0,0 +1,35 @@
+--------------------------------------------------------------------------------
+-- Game Configuration
+--------------------------------------------------------------------------------
+local 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, -- New constant
+ interaction_radius_item = 8 -- New constant
+ },
+ timing = {
+ splash_duration = 120 -- 2 seconds at 60fps
+ }
+}
diff --git a/inc/context.lua b/inc/context.lua
new file mode 100644
index 0000000..72bde21
--- /dev/null
+++ b/inc/context.lua
@@ -0,0 +1,267 @@
+--------------------------------------------------------------------------------
+-- Game Window
+--------------------------------------------------------------------------------
+local Context = {
+ 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,
+ -- Screen data
+ screens = {
+ {
+ -- 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 = {}
+ }
+ },
+ 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 = {}
+ }
+ },
+ 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 = {}
+ },
+ {
+ x = 160,
+ y = 62,
+ name = "Cypher",
+ sprite_id = 9,
+ dialog = {}
+ }
+ },
+ items = {}
+ }
+ }
+}
diff --git a/inc/game.lua b/inc/game.lua
new file mode 100644
index 0000000..8a06897
--- /dev/null
+++ b/inc/game.lua
@@ -0,0 +1,137 @@
+--------------------------------------------------------------------------------
+-- Game Module
+--------------------------------------------------------------------------------
+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 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 = NpcActions.talk_to},
+ {label = "Fight", action = NpcActions.fight},
+ {label = "Go back", action = NpcActions.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 = ItemActions.use},
+ {label = "Look at", action = ItemActions.look_at},
+ {label = "Put away", action = ItemActions.put_away},
+ {label = "Go back", action = ItemActions.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
+
+function GameWindow.update()
+ 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
\ No newline at end of file
diff --git a/inc/header.lua b/inc/header.lua
new file mode 100644
index 0000000..a29be5d
--- /dev/null
+++ b/inc/header.lua
@@ -0,0 +1,7 @@
+-- title: Mr Anderson's Adventure
+-- author: Zsolt Tasnadi
+-- desc: Life of a programmer in the Vector
+-- site: https://github.com/rastasi/mranderson
+-- license: MIT License
+-- version: 0.9
+-- script: lua
diff --git a/inc/input.lua b/inc/input.lua
new file mode 100644
index 0000000..d567081
--- /dev/null
+++ b/inc/input.lua
@@ -0,0 +1,11 @@
+--------------------------------------------------------------------------------
+-- Input Module
+--------------------------------------------------------------------------------
+function Input.up() return btnp(0) end
+function Input.down() return btnp(1) end
+function Input.left() return btn(2) end
+function Input.right() return btn(3) end
+function Input.player_jump() return btnp(4) end
+function Input.menu_confirm() return btnp(4) end
+function Input.player_interact() return btnp(5) end -- B button
+function Input.menu_back() return btnp(5) end
diff --git a/inc/intro.lua b/inc/intro.lua
new file mode 100644
index 0000000..38d30ab
--- /dev/null
+++ b/inc/intro.lua
@@ -0,0 +1,28 @@
+--------------------------------------------------------------------------------
+-- Intro Module
+--------------------------------------------------------------------------------
+function IntroWindow.draw()
+ local x = (Config.screen.width - 132) / 2 -- Centered text
+ print(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
+
diff --git a/inc/inventory.lua b/inc/inventory.lua
new file mode 100644
index 0000000..843be1f
--- /dev/null
+++ b/inc/inventory.lua
@@ -0,0 +1,37 @@
+--------------------------------------------------------------------------------
+-- Inventory Module
+--------------------------------------------------------------------------------
+function InventoryWindow.draw()
+ UI.draw_top_bar("Inventory")
+
+ if #Context.inventory == 0 then
+ print("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(">", 60, 20 + i * 10, color)
+ end
+ print(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 = ItemActions.use},
+ {label = "Drop", action = ItemActions.drop},
+ {label = "Look at", action = ItemActions.look_at},
+ {label = "Go back", action = ItemActions.go_back_from_inventory_action}
+ }, WINDOW_INVENTORY_ACTION)
+ end
+
+ if Input.menu_back() then
+ GameWindow.set_state(WINDOW_GAME)
+ end
+end
diff --git a/inc/item_actions.lua b/inc/item_actions.lua
new file mode 100644
index 0000000..d9b8075
--- /dev/null
+++ b/inc/item_actions.lua
@@ -0,0 +1,52 @@
+--------------------------------------------------------------------------------
+-- Item Actions
+--------------------------------------------------------------------------------
+function ItemActions.use()
+ print("Used item: " .. Context.dialog.active_entity.name)
+ GameWindow.set_state(WINDOW_INVENTORY)
+end
+function ItemActions.look_at()
+ PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
+end
+function ItemActions.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 ItemActions.go_back_from_item_dialog()
+ GameWindow.set_state(WINDOW_GAME)
+end
+
+function ItemActions.go_back_from_inventory_action()
+ GameWindow.set_state(WINDOW_GAME)
+end
+
+function ItemActions.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
diff --git a/inc/main.lua b/inc/main.lua
new file mode 100644
index 0000000..15b19f7
--- /dev/null
+++ b/inc/main.lua
@@ -0,0 +1,43 @@
+--------------------------------------------------------------------------------
+-- Main Game Loop
+--------------------------------------------------------------------------------
+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() -- Draw game behind dialog
+ PopupWindow.update()
+ PopupWindow.draw()
+ end,
+ [WINDOW_INVENTORY] = function()
+ InventoryWindow.update()
+ InventoryWindow.draw()
+ end,
+ [WINDOW_INVENTORY_ACTION] = function()
+ InventoryWindow.draw() -- Draw inventory behind dialog
+ PopupWindow.draw()
+ PopupWindow.update()
+ end,
+}
+
+function TIC()
+ cls(Config.colors.black)
+ local handler = STATE_HANDLERS[Context.active_window]
+ if handler then
+ handler()
+ end
+end
diff --git a/inc/menu.lua b/inc/menu.lua
new file mode 100644
index 0000000..74ae850
--- /dev/null
+++ b/inc/menu.lua
@@ -0,0 +1,18 @@
+--------------------------------------------------------------------------------
+-- Menu Module
+--------------------------------------------------------------------------------
+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
diff --git a/inc/menu_actions.lua b/inc/menu_actions.lua
new file mode 100644
index 0000000..fcf2142
--- /dev/null
+++ b/inc/menu_actions.lua
@@ -0,0 +1,23 @@
+--------------------------------------------------------------------------------
+-- Menu Actions
+--------------------------------------------------------------------------------
+function MenuActions.play()
+ -- Reset player state and screen for a new game
+ Context.player.x = Config.player.start_x
+ Context.player.y = Config.player.start_y
+ Context.player.vx = 0
+ Context.player.vy = 0
+ Context.player.jumps = 0
+ Context.current_screen = 1
+ GameWindow.set_state(WINDOW_GAME)
+end
+
+function MenuActions.exit()
+ exit()
+end
+
+-- Initialize menu items after actions are defined
+Context.menu_items = {
+ {label = "Play", action = MenuActions.play},
+ {label = "Exit", action = MenuActions.exit}
+}
diff --git a/inc/modules.lua b/inc/modules.lua
new file mode 100644
index 0000000..3ebc5ee
--- /dev/null
+++ b/inc/modules.lua
@@ -0,0 +1,18 @@
+--------------------------------------------------------------------------------
+-- Modules
+--------------------------------------------------------------------------------
+-- Window Modules (in WINDOW order)
+local SplashWindow = {}
+local IntroWindow = {}
+local MenuWindow = {}
+local GameWindow = {}
+local PopupWindow = {} -- Manages popups for WINDOW_POPUP and WINDOW_INVENTORY_ACTION
+local InventoryWindow = {} -- Used for WINDOW_INVENTORY
+
+-- Other Modules
+local UI = {}
+local Input = {}
+local NpcActions = {}
+local ItemActions = {}
+local MenuActions = {}
+local Player = {}
diff --git a/inc/npc_actions.lua b/inc/npc_actions.lua
new file mode 100644
index 0000000..4d7b829
--- /dev/null
+++ b/inc/npc_actions.lua
@@ -0,0 +1,16 @@
+--------------------------------------------------------------------------------
+-- NPC Actions
+--------------------------------------------------------------------------------
+function NpcActions.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 NpcActions.fight() end
+function NpcActions.go_back()
+ GameWindow.set_state(WINDOW_GAME)
+end
diff --git a/inc/player.lua b/inc/player.lua
new file mode 100644
index 0000000..bd93c59
--- /dev/null
+++ b/inc/player.lua
@@ -0,0 +1,98 @@
+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 = NpcActions.talk_to},
+ {label = "Fight", action = NpcActions.fight},
+ {label = "Go back", action = NpcActions.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 = ItemActions.use},
+ {label = "Look at", action = ItemActions.look_at},
+ {label = "Put away", action = ItemActions.put_away},
+ {label = "Go back", action = ItemActions.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
diff --git a/inc/popup.lua b/inc/popup.lua
new file mode 100644
index 0000000..1f7cdcc
--- /dev/null
+++ b/inc/popup.lua
@@ -0,0 +1,77 @@
+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
diff --git a/inc/splash.lua b/inc/splash.lua
new file mode 100644
index 0000000..4ea927a
--- /dev/null
+++ b/inc/splash.lua
@@ -0,0 +1,14 @@
+--------------------------------------------------------------------------------
+-- Splash Module
+--------------------------------------------------------------------------------
+function SplashWindow.draw()
+ print("Mr. Anderson's", 78, 60, Config.colors.green)
+ print("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
diff --git a/inc/ui.lua b/inc/ui.lua
new file mode 100644
index 0000000..102eb06
--- /dev/null
+++ b/inc/ui.lua
@@ -0,0 +1,99 @@
+--------------------------------------------------------------------------------
+-- UI Module
+--------------------------------------------------------------------------------
+function UI.draw_top_bar(title)
+ rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
+ print(title, 3, 2, Config.colors.green)
+end
+
+function UI.draw_dialog()
+ PopupWindow.draw()
+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(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(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
+ -- If description is showing, provide a "Go back" option automatically, or close dialog on action
+ -- For now, let's just make it implicitly wait for Input.menu_confirm() or Input.menu_back() to close
+ -- Or we can add a specific "Back" option here.
+ -- Let's add a "Back" option for explicit return from description.
+ print("[A] Go Back", 50, current_y + 10, Config.colors.green)
+ end
+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(">", x - 8, current_y, Config.colors.green)
+ end
+ print(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
+
diff --git a/inc/windows.lua b/inc/windows.lua
new file mode 100644
index 0000000..137df0e
--- /dev/null
+++ b/inc/windows.lua
@@ -0,0 +1,10 @@
+--------------------------------------------------------------------------------
+-- Game Windows
+--------------------------------------------------------------------------------
+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
diff --git a/mranderson.inc b/mranderson.inc
new file mode 100644
index 0000000..d1498e4
--- /dev/null
+++ b/mranderson.inc
@@ -0,0 +1,18 @@
+header.lua
+config.lua
+windows.lua
+modules.lua
+context.lua
+inventory.lua
+menu_actions.lua
+npc_actions.lua
+item_actions.lua
+input.lua
+ui.lua
+splash.lua
+intro.lua
+menu.lua
+game.lua
+popup.lua
+main.lua
+assets.lua
diff --git a/mranderson.lua b/mranderson.lua
deleted file mode 100644
index faa0cb7..0000000
--- a/mranderson.lua
+++ /dev/null
@@ -1,935 +0,0 @@
--- title: Mr Anderson's Adventure
--- author: Zsolt Tasnadi
--- desc: Life of a programmer in the Vector
--- site: https://github.com/rastasi/mranderson
--- license: MIT License
--- version: 0.9
--- script: lua
-
---------------------------------------------------------------------------------
--- Game Configuration
---------------------------------------------------------------------------------
-local 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, -- New constant
- interaction_radius_item = 8 -- New constant
- },
- timing = {
- splash_duration = 120 -- 2 seconds at 60fps
- }
-}
-
---------------------------------------------------------------------------------
--- Game Windows
---------------------------------------------------------------------------------
-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
-
---------------------------------------------------------------------------------
--- Modules
---------------------------------------------------------------------------------
--- Window Modules (in WINDOW order)
-local SplashWindow = {}
-local IntroWindow = {}
-local MenuWindow = {}
-local GameWindow = {}
-local PopupWindow = {} -- Manages popups for WINDOW_POPUP and WINDOW_INVENTORY_ACTION
-local InventoryWindow = {} -- Used for WINDOW_INVENTORY
-
--- Other Modules
-local UI = {}
-local Input = {}
-local NpcActions = {}
-local ItemActions = {}
-local MenuActions = {}
-local Player = {}
-
---------------------------------------------------------------------------------
--- Game Window
---------------------------------------------------------------------------------
-local Context = {
- 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,
- -- Screen data
- screens = {
- { -- 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 = {}
- }
- },
- 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 = {}
- }
- }
- },
- {
- x = 40,
- y = 92,
- name = "Tank",
- sprite_id = 6,
- dialog = {}
- }
- },
- 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 = {}
- },
- {
- x = 160,
- y = 62,
- name = "Cypher",
- sprite_id = 9,
- dialog = {}
- }
- },
- items = {}
- }
- }
-}
-
---------------------------------------------------------------------------------
--- Inventory Module
---------------------------------------------------------------------------------
-function InventoryWindow.draw()
- UI.draw_top_bar("Inventory")
-
- if #Context.inventory == 0 then
- print("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(">", 60, 20 + i * 10, color)
- end
- print(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 = ItemActions.use},
- {label = "Drop", action = ItemActions.drop},
- {label = "Look at", action = ItemActions.look_at},
- {label = "Go back", action = ItemActions.go_back_from_inventory_action}
- }, WINDOW_INVENTORY_ACTION)
- end
-
- if Input.menu_back() then
- GameWindow.set_state(WINDOW_GAME)
- end
-end
-
---------------------------------------------------------------------------------
--- Menu Actions
---------------------------------------------------------------------------------
-function MenuActions.play()
- -- Reset player state and screen for a new game
- Context.player.x = Config.player.start_x
- Context.player.y = Config.player.start_y
- Context.player.vx = 0
- Context.player.vy = 0
- Context.player.jumps = 0
- Context.current_screen = 1
- GameWindow.set_state(WINDOW_GAME)
-end
-
-function MenuActions.exit()
- exit()
-end
-
--- Initialize menu items after actions are defined
-Context.menu_items = {
- {label = "Play", action = MenuActions.play},
- {label = "Exit", action = MenuActions.exit}
-}
-
---------------------------------------------------------------------------------
--- NPC Actions
---------------------------------------------------------------------------------
-function NpcActions.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 NpcActions.fight() end
-function NpcActions.go_back()
- GameWindow.set_state(WINDOW_GAME)
-end
-
---------------------------------------------------------------------------------
--- Item Actions
---------------------------------------------------------------------------------
-function ItemActions.use()
- print("Used item: " .. Context.dialog.active_entity.name)
- GameWindow.set_state(WINDOW_INVENTORY)
-end
-function ItemActions.look_at()
- PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
-end
-function ItemActions.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 ItemActions.go_back_from_item_dialog()
- GameWindow.set_state(WINDOW_GAME)
-end
-
-function ItemActions.go_back_from_inventory_action()
- GameWindow.set_state(WINDOW_GAME)
-end
-
-function ItemActions.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
-
-
---------------------------------------------------------------------------------
--- Input Module
---------------------------------------------------------------------------------
-function Input.up() return btnp(0) end
-function Input.down() return btnp(1) end
-function Input.left() return btn(2) end
-function Input.right() return btn(3) end
-function Input.player_jump() return btnp(4) end
-function Input.menu_confirm() return btnp(4) end
-function Input.player_interact() return btnp(5) end -- B button
-function Input.menu_back() return btnp(5) end
-
---------------------------------------------------------------------------------
--- UI Module
---------------------------------------------------------------------------------
-function UI.draw_top_bar(title)
- rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
- print(title, 3, 2, Config.colors.green)
-end
-
-function UI.draw_dialog()
- PopupWindow.draw()
-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(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(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
- -- If description is showing, provide a "Go back" option automatically, or close dialog on action
- -- For now, let's just make it implicitly wait for Input.menu_confirm() or Input.menu_back() to close
- -- Or we can add a specific "Back" option here.
- -- Let's add a "Back" option for explicit return from description.
- print("[A] Go Back", 50, current_y + 10, Config.colors.green)
- end
-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(">", x - 8, current_y, Config.colors.green)
- end
- print(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
-
---------------------------------------------------------------------------------
--- Splash Module
---------------------------------------------------------------------------------
-function SplashWindow.draw()
- print("Mr. Anderson's", 78, 60, Config.colors.green)
- print("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
-
---------------------------------------------------------------------------------
--- Intro Module
---------------------------------------------------------------------------------
-function IntroWindow.draw()
- local x = (Config.screen.width - 132) / 2 -- Centered text
- print(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
-
---------------------------------------------------------------------------------
--- Menu Module
---------------------------------------------------------------------------------
-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
-
---------------------------------------------------------------------------------
--- Game Module
---------------------------------------------------------------------------------
-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 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 = NpcActions.talk_to},
- {label = "Fight", action = NpcActions.fight},
- {label = "Go back", action = NpcActions.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 = ItemActions.use},
- {label = "Look at", action = ItemActions.look_at},
- {label = "Put away", action = ItemActions.put_away},
- {label = "Go back", action = ItemActions.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
-
-function GameWindow.update()
- 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
-
-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
-
---------------------------------------------------------------------------------
--- Main Game Loop
---------------------------------------------------------------------------------
-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() -- Draw game behind dialog
- PopupWindow.update()
- PopupWindow.draw()
- end,
- [WINDOW_INVENTORY] = function()
- InventoryWindow.update()
- InventoryWindow.draw()
- end,
- [WINDOW_INVENTORY_ACTION] = function()
- InventoryWindow.draw() -- Draw inventory behind dialog
- PopupWindow.draw()
- PopupWindow.update()
- end,
-}
-
-function TIC()
- 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
--- 002:0123456789abcdef0123456789abcdef
---
-
---
--- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000
---
-
---
--- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
---
-
---
--- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
---