4
0

Compare commits

..

4 Commits

Author SHA1 Message Date
6a39128962 v0.11
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-11 18:57:30 +01:00
1913d7d7d4 configuration window 2025-12-11 18:56:59 +01:00
755b648280 order inc files to folders 2025-12-11 18:07:36 +01:00
4d3349720c remove comments 2025-12-11 18:03:09 +01:00
27 changed files with 286 additions and 322 deletions

View File

@@ -28,7 +28,6 @@ $(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)

View File

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

View File

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

View File

@@ -1,14 +1,11 @@
--------------------------------------------------------------------------------
-- Item Actions
--------------------------------------------------------------------------------
function ItemActions.use()
function Item.use()
print("Used item: " .. Context.dialog.active_entity.name)
GameWindow.set_state(WINDOW_INVENTORY)
end
function ItemActions.look_at()
function Item.look_at()
PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
end
function ItemActions.put_away()
function Item.put_away()
-- Add item to inventory
table.insert(Context.inventory, Context.dialog.active_entity)
@@ -24,15 +21,15 @@ function ItemActions.put_away()
-- Go back to game
GameWindow.set_state(WINDOW_GAME)
end
function ItemActions.go_back_from_item_dialog()
function Item.go_back_from_item_dialog()
GameWindow.set_state(WINDOW_GAME)
end
function ItemActions.go_back_from_inventory_action()
function Item.go_back_from_inventory_action()
GameWindow.set_state(WINDOW_GAME)
end
function ItemActions.drop()
function Item.drop()
-- Remove item from inventory
for i, item in ipairs(Context.inventory) do
if item == Context.dialog.active_entity then

13
inc/entity/entity.npc.lua Normal file
View File

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

View File

@@ -65,9 +65,9 @@ function Player.update()
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}
{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
@@ -79,10 +79,10 @@ function Player.update()
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}
{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

View File

@@ -1,18 +0,0 @@
--------------------------------------------------------------------------------
-- 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 = {}

View File

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

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- Game Configuration
--------------------------------------------------------------------------------
local Config = {
screen = {
width = 240,
@@ -26,10 +23,10 @@ local Config = {
jump_power = -5,
move_speed = 1.5,
max_jumps = 2,
interaction_radius_npc = 12, -- New constant
interaction_radius_item = 8 -- New constant
interaction_radius_npc = 12,
interaction_radius_item = 8
},
timing = {
splash_duration = 120 -- 2 seconds at 60fps
splash_duration = 120
}
}

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- Game Window
--------------------------------------------------------------------------------
local Context = {
active_window = WINDOW_SPLASH,
inventory = {},

13
inc/init/init.modules.lua Normal file
View File

@@ -0,0 +1,13 @@
local SplashWindow = {}
local IntroWindow = {}
local MenuWindow = {}
local GameWindow = {}
local PopupWindow = {}
local InventoryWindow = {}
local ConfigurationWindow = {}
local UI = {}
local Input = {}
local NPC = {}
local Item = {}
local Player = {}

View File

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

View File

@@ -3,5 +3,5 @@
-- desc: Life of a programmer in the Vector
-- site: https://github.com/rastasi/mranderson
-- license: MIT License
-- version: 0.10
-- version: 0.11
-- script: lua

View File

@@ -1,10 +1,7 @@
--------------------------------------------------------------------------------
-- 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.left() return btnp(2) end
function Input.right() return btnp(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

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- Main Game Loop
--------------------------------------------------------------------------------
local STATE_HANDLERS = {
[WINDOW_SPLASH] = function()
SplashWindow.update()
@@ -19,7 +16,7 @@ local STATE_HANDLERS = {
GameWindow.draw()
end,
[WINDOW_POPUP] = function()
GameWindow.draw() -- Draw game behind dialog
GameWindow.draw()
PopupWindow.update()
PopupWindow.draw()
end,
@@ -28,10 +25,14 @@ local STATE_HANDLERS = {
InventoryWindow.draw()
end,
[WINDOW_INVENTORY_ACTION] = function()
InventoryWindow.draw() -- Draw inventory behind dialog
InventoryWindow.draw()
PopupWindow.draw()
PopupWindow.update()
end,
[WINDOW_CONFIGURATION] = function()
ConfigurationWindow.update()
ConfigurationWindow.draw()
end,
}
function TIC()

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- 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)
@@ -10,35 +7,6 @@ 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
@@ -97,3 +65,15 @@ function UI.word_wrap(text, max_chars_per_line)
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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
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"
),
}
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
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("<", x_start -8, current_y, color)
print(label_text, x_start, current_y, color) -- Shift label due to '<'
print(value_text, value_x, current_y, color)
print(">", x_value_right_align + 4, current_y, color) -- Print '>' after value
else
print(label_text, x_start, current_y, color)
print(value_text, value_x, current_y, color)
end
end
print("Press B to go back", x_start, 120, Config.colors.light_grey)
end
function ConfigurationWindow.update()
if Input.menu_back() then
-- I need to find out how to switch back to the menu
-- For now, I'll assume a function GameWindow.set_state exists
GameWindow.set_state(WINDOW_MENU)
return
end
-- Navigate between controls
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
-- Modify control value
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
if control then
local current_value = control.get()
if Input.left() then
local new_value = math.max(control.min, current_value - control.step)
control.set(new_value)
elseif Input.right() then
local new_value = math.min(control.max, current_value + control.step)
control.set(new_value)
end
end
end

View File

@@ -0,0 +1,35 @@
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()
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

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- 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)

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- Inventory Module
--------------------------------------------------------------------------------
function InventoryWindow.draw()
UI.draw_top_bar("Inventory")
@@ -24,10 +21,10 @@ function InventoryWindow.update()
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}
{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

View File

@@ -0,0 +1,42 @@
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.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 MenuWindow.exit()
exit()
end
function MenuWindow.configuration()
ConfigurationWindow.init()
GameWindow.set_state(WINDOW_CONFIGURATION)
end
-- Initialize menu items after actions are defined
Context.menu_items = {
{label = "Play", action = MenuWindow.play},
{label = "Configuration", action = MenuWindow.configuration},
{label = "Exit", action = MenuWindow.exit}
}

View File

@@ -75,3 +75,28 @@ function PopupWindow.show_description_dialog(entity, description_text)
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(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
print("[A] Go Back", 50, current_y + 10, Config.colors.green)
end
end

View File

@@ -1,6 +1,3 @@
--------------------------------------------------------------------------------
-- Splash Module
--------------------------------------------------------------------------------
function SplashWindow.draw()
print("Mr. Anderson's", 78, 60, Config.colors.green)
print("Addventure", 90, 70, Config.colors.green)

View File

@@ -1,18 +1,19 @@
meta.header.lua
init.config.lua
init.windows.lua
init.modules.lua
init.context.lua
window.inventory.lua
actions.menu.lua
actions.npc.lua
actions.inventory.lua
system.input.lua
system.ui.lua
window.splash.lua
window.intro.lua
window.menu.lua
window.game.lua
window.popup.lua
system.main.lua
meta.assets.lua
meta/meta.header.lua
init/init.config.lua
init/init.windows.lua
init/init.modules.lua
init/init.context.lua
entity/entity.npc.lua
entity/entity.item.lua
entity/entity.player.lua
system/system.input.lua
system/system.ui.lua
window/window.splash.lua
window/window.intro.lua
window/window.menu.lua
window/window.configuration.lua
window/window.popup.lua
window/window.inventory.lua
window/window.game.lua
system/system.main.lua
meta/meta.assets.lua