4
0

Compare commits

..

5 Commits

Author SHA1 Message Date
36abe3a25d reorder object declarations
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 08:03:10 +01:00
1a68ef69cb refakt 2025-12-09 07:59:39 +01:00
3ca5adf414 State postfix 2025-12-09 07:45:48 +01:00
6dae70c7da refact 2025-12-09 07:39:43 +01:00
db99ea3b9a look at 2025-12-09 07:32:34 +01:00

View File

@@ -3,7 +3,7 @@
-- desc: Life of a programmer in the Vector
-- site: http://teletype.hu
-- license: MIT License
-- version: 0.6
-- version: 0.7
-- script: lua
--------------------------------------------------------------------------------
@@ -34,6 +34,8 @@ local Config = {
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
@@ -54,16 +56,21 @@ local GAME_STATE_INVENTORY_ACTION = 6
--------------------------------------------------------------------------------
-- Modules
--------------------------------------------------------------------------------
local Splash = {}
local Intro = {}
local Menu = {}
local Game = {}
-- State Modules (in GAME_STATE order)
local SplashState = {}
local IntroState = {}
local MenuState = {}
local GameState = {}
local DialogState = {} -- Used for GAME_STATE_DIALOG and GAME_STATE_INVENTORY_ACTION
local InventoryState = {} -- Used for GAME_STATE_INVENTORY
-- Other Modules
local UI = {}
local Input = {}
local Inventory = {}
local NpcActions = {}
local ItemActions = {}
local MenuActions = {}
local Player = {}
--------------------------------------------------------------------------------
-- Game State
@@ -114,7 +121,7 @@ local State = {
{x = 90, y = 102, name = "Oracle", sprite_id = 3}
},
items = {
{x = 100, y = 128, w=8, h=8, name = "Key", sprite_id = 4}
{x = 100, y = 128, w=8, h=8, name = "Key", sprite_id = 4, desc = "A rusty old key. It might open something."}
}
},
{ -- Screen 2
@@ -129,7 +136,7 @@ local State = {
{x = 40, y = 92, name = "Tank", sprite_id = 6}
},
items = {
{x = 180, y = 52, w=8, h=8, name = "Potion", sprite_id = 7}
{x = 180, y = 52, w=8, h=8, name = "Potion", sprite_id = 7, desc = "A glowing red potion. It looks potent."}
}
},
{ -- Screen 3
@@ -152,7 +159,7 @@ local State = {
--------------------------------------------------------------------------------
-- Inventory Module
--------------------------------------------------------------------------------
function Inventory.draw()
function InventoryState.draw()
cls(Config.colors.dark_grey)
UI.draw_top_bar("Inventory")
@@ -170,25 +177,21 @@ function Inventory.draw()
end
end
function Inventory.update()
function InventoryState.update()
State.selected_inventory_item = UI.update_menu(State.inventory, State.selected_inventory_item)
if Input.action() and #State.inventory > 0 then
if Input.menu_confirm() and #State.inventory > 0 then
local selected_item = State.inventory[State.selected_inventory_item]
State.active_entity = selected_item
State.dialog_text = selected_item.name
State.game_state = GAME_STATE_INVENTORY_ACTION
State.dialog_menu_items = {
DialogState.show_menu_dialog(selected_item, {
{label = "Use", action = ItemActions.use},
{label = "Drop", action = ItemActions.drop},
{label = "Look at", action = ItemActions.look_at},
{label = "Goodbye", action = ItemActions.inventory_goodbye}
}
State.selected_dialog_menu_item = 1
{label = "Go back", action = ItemActions.go_back_from_inventory_action}
}, GAME_STATE_INVENTORY_ACTION)
end
if Input.back() then
State.game_state = GAME_STATE_GAME
if Input.menu_back() then
GameState.set_state(GAME_STATE_GAME)
end
end
@@ -203,7 +206,7 @@ function MenuActions.play()
State.player.vy = 0
State.player.jumps = 0
State.current_screen = 1
State.game_state = GAME_STATE_GAME
GameState.set_state(GAME_STATE_GAME)
end
function MenuActions.exit()
@@ -221,8 +224,8 @@ State.menu_items = {
--------------------------------------------------------------------------------
function NpcActions.talk_to() end
function NpcActions.fight() end
function NpcActions.goodbye()
State.game_state = GAME_STATE_GAME
function NpcActions.go_back()
GameState.set_state(GAME_STATE_GAME)
end
--------------------------------------------------------------------------------
@@ -230,11 +233,10 @@ end
--------------------------------------------------------------------------------
function ItemActions.use()
print("Used item: " .. State.active_entity.name)
State.game_state = GAME_STATE_INVENTORY
GameState.set_state(GAME_STATE_INVENTORY)
end
function ItemActions.look_at()
print("Looked at item: " .. State.active_entity.name)
State.game_state = GAME_STATE_INVENTORY
DialogState.show_description_dialog(State.active_entity, State.active_entity.desc)
end
function ItemActions.put_away()
-- Add item to inventory
@@ -250,14 +252,14 @@ function ItemActions.put_away()
end
-- Go back to game
State.game_state = GAME_STATE_GAME
GameState.set_state(GAME_STATE_GAME)
end
function ItemActions.goodbye()
State.game_state = GAME_STATE_GAME
function ItemActions.go_back_from_item_dialog()
GameState.set_state(GAME_STATE_GAME)
end
function ItemActions.inventory_goodbye()
State.game_state = GAME_STATE_INVENTORY
function ItemActions.go_back_from_inventory_action()
GameState.set_state(GAME_STATE_GAME)
end
function ItemActions.drop()
@@ -276,7 +278,7 @@ function ItemActions.drop()
table.insert(currentScreenData.items, State.active_entity)
-- Go back to inventory
State.game_state = GAME_STATE_INVENTORY
GameState.set_state(GAME_STATE_INVENTORY)
end
@@ -287,10 +289,10 @@ 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.jump() return btnp(4) end
function Input.action() return btnp(4) end
function Input.interact() return btnp(5) end -- B button
function Input.back() return btnp(5) 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
@@ -301,10 +303,36 @@ function UI.draw_top_bar(title)
end
function UI.draw_dialog()
DialogState.draw()
end
function DialogState.draw()
rect(40, 40, 160, 80, Config.colors.black)
rectb(40, 40, 160, 80, Config.colors.green)
print(State.dialog_text, 120 - #State.dialog_text * 2, 45, Config.colors.light_grey)
UI.draw_menu(State.dialog_menu_items, State.selected_dialog_menu_item, 50, 60)
-- Display the entity's name as the dialog title
if State.active_entity and State.active_entity.name then
print(State.active_entity.name, 120 - #State.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(State.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 State.showing_description then
UI.draw_menu(State.dialog_menu_items, State.selected_dialog_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)
@@ -332,32 +360,74 @@ function UI.update_menu(items, selected_item)
return selected_item
end
function UI.word_wrap(text, max_chars_per_line)
local result_lines = {}
local segments = {}
-- Split the input text by explicit newline characters first
for segment in text:gmatch("([^\n]*)\n?") do
table.insert(segments, segment)
end
-- Process each segment for word wrapping
for _, segment_text in ipairs(segments) do
local current_line = ""
local words = {}
-- Split segment into words
for word in segment_text:gmatch("%S+") do
table.insert(words, word)
end
local i = 1
while i <= #words do
local word = words[i]
if #current_line == 0 then
current_line = word
elseif #current_line + 1 + #word <= max_chars_per_line then
current_line = current_line .. " " .. word
else
table.insert(result_lines, current_line)
current_line = word
end
i = i + 1
end
-- Add the last line of the segment if not empty
if #current_line > 0 then
table.insert(result_lines, current_line)
end
end
return result_lines
end
--------------------------------------------------------------------------------
-- Splash Module
--------------------------------------------------------------------------------
function Splash.draw()
function SplashState.draw()
cls(Config.colors.dark_grey)
print("Mr. Anderson's", 78, 60, Config.colors.green)
print("Addventure", 90, 70, Config.colors.green)
end
function Splash.update()
function SplashState.update()
State.splash_timer = State.splash_timer - 1
if State.splash_timer <= 0 or Input.action() then
State.game_state = GAME_STATE_INTRO
if State.splash_timer <= 0 or Input.menu_confirm() then
GameState.set_state(GAME_STATE_INTRO)
end
end
--------------------------------------------------------------------------------
-- Intro Module
--------------------------------------------------------------------------------
function Intro.draw()
function IntroState.draw()
cls(Config.colors.dark_grey)
local x = (Config.screen.width - 132) / 2 -- Centered text
print(State.intro.text, x, State.intro.y, Config.colors.green)
end
function Intro.update()
function IntroState.update()
State.intro.y = State.intro.y - State.intro.speed
-- Count lines in intro text to determine when scrolling is done
@@ -368,28 +438,28 @@ function Intro.update()
-- When text is off-screen, go to menu
if State.intro.y < -lines * 8 then
State.game_state = GAME_STATE_MENU
GameState.set_state(GAME_STATE_MENU)
end
-- Skip intro by pressing A
if Input.action() then
State.game_state = GAME_STATE_MENU
if Input.menu_confirm() then
GameState.set_state(GAME_STATE_MENU)
end
end
--------------------------------------------------------------------------------
-- Menu Module
--------------------------------------------------------------------------------
function Menu.draw()
function MenuState.draw()
cls(Config.colors.dark_grey)
UI.draw_top_bar("Main Menu")
UI.draw_menu(State.menu_items, State.selected_menu_item, 108, 70)
end
function Menu.update()
function MenuState.update()
State.selected_menu_item = UI.update_menu(State.menu_items, State.selected_menu_item)
if Input.action() then
if Input.menu_confirm() then
local selected_item = State.menu_items[State.selected_menu_item]
if selected_item and selected_item.action then
selected_item.action()
@@ -400,7 +470,7 @@ end
--------------------------------------------------------------------------------
-- Game Module
--------------------------------------------------------------------------------
function Game.draw()
function GameState.draw()
local currentScreenData = State.screens[State.current_screen]
cls(Config.colors.dark_grey)
@@ -425,10 +495,14 @@ function Game.draw()
rect(State.ground.x, State.ground.y, State.ground.w, State.ground.h, Config.colors.dark_grey)
-- Draw player
Player.draw()
end
function Player.draw()
spr(State.player.sprite_id, State.player.x, State.player.y, 0)
end
function Game.update()
function Player.update()
-- Handle input
if Input.left() then
State.player.vx = -Config.physics.move_speed
@@ -438,7 +512,7 @@ function Game.update()
State.player.vx = 0
end
if Input.jump() and State.player.jumps < Config.physics.max_jumps then
if Input.player_jump() and State.player.jumps < Config.physics.max_jumps then
State.player.vy = Config.physics.jump_power
State.player.jumps = State.player.jumps + 1
end
@@ -485,20 +559,16 @@ function Game.update()
end
-- Entity interaction
if Input.interact() then
if Input.player_interact() then
local interaction_found = false
-- NPC interaction
for _, npc in ipairs(currentScreenData.npcs) do
if math.abs(State.player.x - npc.x) < 12 and math.abs(State.player.y - npc.y) < 12 then
State.active_entity = npc
State.dialog_text = npc.name
State.game_state = GAME_STATE_DIALOG
State.dialog_menu_items = {
if math.abs(State.player.x - npc.x) < Config.physics.interaction_radius_npc and math.abs(State.player.y - npc.y) < Config.physics.interaction_radius_npc then
DialogState.show_menu_dialog(npc, {
{label = "Talk to", action = NpcActions.talk_to},
{label = "Fight", action = NpcActions.fight},
{label = "Goodbye", action = NpcActions.goodbye}
}
State.selected_dialog_menu_item = 1
{label = "Go back", action = NpcActions.go_back}
}, GAME_STATE_DIALOG)
interaction_found = true
break
end
@@ -507,17 +577,13 @@ function Game.update()
if not interaction_found then
-- Item interaction
for _, item in ipairs(currentScreenData.items) do
if math.abs(State.player.x - item.x) < 8 and math.abs(State.player.y - item.y) < 8 then
State.active_entity = item
State.dialog_text = item.name
State.game_state = GAME_STATE_DIALOG
State.dialog_menu_items = {
if math.abs(State.player.x - item.x) < Config.physics.interaction_radius_item and math.abs(State.player.y - item.y) < Config.physics.interaction_radius_item then
DialogState.show_menu_dialog(item, {
{label = "Use", action = ItemActions.use},
{label = "Look at", action = ItemActions.look_at},
{label = "Put away", action = ItemActions.put_away},
{label = "Goodbye", action = ItemActions.goodbye}
}
State.selected_dialog_menu_item = 1
{label = "Go back", action = ItemActions.go_back_from_item_dialog}
}, GAME_STATE_DIALOG)
interaction_found = true
break
end
@@ -526,59 +592,97 @@ function Game.update()
-- If no interaction happened, open inventory
if not interaction_found then
State.game_state = GAME_STATE_INVENTORY
GameState.set_state(GAME_STATE_INVENTORY)
end
end
end
function Game.update_dialog()
function GameState.update()
Player.update() -- Call the encapsulated player update logic
end
function GameState.set_state(new_state)
State.game_state = new_state
-- Add any state-specific initialization/cleanup here later if needed
end
function DialogState.update()
if State.showing_description then
if Input.menu_confirm() or Input.menu_back() then
State.showing_description = false
State.dialog_text = "" -- Clear the description text
-- No need to change game_state, as it remains in GAME_STATE_DIALOG or GAME_STATE_INVENTORY_ACTION
end
else
State.selected_dialog_menu_item = UI.update_menu(State.dialog_menu_items, State.selected_dialog_menu_item)
if Input.action() then
if Input.menu_confirm() then
local selected_item = State.dialog_menu_items[State.selected_dialog_menu_item]
if selected_item and selected_item.action then
selected_item.action()
end
end
if Input.back() then
State.game_state = GAME_STATE_GAME
if Input.menu_back() then
GameState.set_state(GAME_STATE_GAME)
end
end
end
function DialogState.show_menu_dialog(entity, menu_items, dialog_game_state)
State.active_entity = entity
State.dialog_text = "" -- Initial dialog text is empty, name is title
GameState.set_state(dialog_game_state or GAME_STATE_DIALOG)
State.showing_description = false
State.dialog_menu_items = menu_items
State.selected_dialog_menu_item = 1
end
function DialogState.show_description_dialog(entity, description_text)
State.active_entity = entity
State.dialog_text = description_text
GameState.set_state(GAME_STATE_DIALOG)
State.showing_description = true
-- No menu items needed for description dialog
end
--------------------------------------------------------------------------------
-- Main Game Loop
--------------------------------------------------------------------------------
local STATE_HANDLERS = {
[GAME_STATE_SPLASH] = function()
Splash.update()
Splash.draw()
SplashState.update()
SplashState.draw()
end,
[GAME_STATE_INTRO] = function()
Intro.update()
Intro.draw()
IntroState.update()
IntroState.draw()
end,
[GAME_STATE_MENU] = function()
Menu.update()
Menu.draw()
MenuState.update()
MenuState.draw()
end,
[GAME_STATE_GAME] = function()
Game.update()
Game.draw()
GameState.update()
GameState.draw()
end,
[GAME_STATE_DIALOG] = function()
Game.draw() -- Draw game behind dialog
UI.draw_dialog()
Game.update_dialog()
GameState.draw() -- Draw game behind dialog
DialogState.draw()
DialogState.update()
end,
[GAME_STATE_INVENTORY] = function()
Inventory.update()
Inventory.draw()
InventoryState.update()
InventoryState.draw()
end,
[GAME_STATE_INVENTORY_ACTION] = function()
Inventory.draw() -- Draw inventory behind dialog
UI.draw_dialog()
Game.update_dialog()
InventoryState.draw() -- Draw inventory behind dialog
DialogState.draw()
DialogState.update()
end,
}