4
0

Compare commits

...

34 Commits

Author SHA1 Message Date
85ce2e0238 branch into version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 20:16:38 +01:00
9460a2eb73 add name to header
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-28 00:08:49 +01:00
ebf7799674 pipeline update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-27 15:16:06 +01:00
ac0bcf4aa4 reset version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-25 19:34:28 +01:00
4907c9cadf tic80 image change
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-13 22:52:22 +01:00
45f2e746d6 new version 2025-12-13 20:13:08 +01:00
8e8d181c34 shadowed text
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-13 20:11:50 +01:00
5379e30cf3 keyboard button fixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-12 15:30:47 +01:00
d3fb12703c v0.13
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-11 20:40:02 +01:00
f9c539a854 resume button 2025-12-11 20:39:26 +01:00
a777241d7a context init fix 2025-12-11 20:36:11 +01:00
34340d9664 save/load functionality (WIP)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-11 20:00:43 +01:00
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
3ead2b0ce0 more dummy dialogs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 23:35:45 +01:00
fc398d0f65 rename partial files
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 23:16:39 +01:00
cb95285c2a add vscode config
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 22:43:30 +01:00
0230578452 makefile fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 22:40:01 +01:00
9b244d2e4e replace custom build command to make
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-10 22:38:18 +01:00
9dfbdcd052 split source
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 22:33:36 +01:00
94931d3651 pipeline local registry
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-10 00:42:21 +01:00
7f805a0a9d Revert "pipeline local registry"
This reverts commit d877467da6.
2025-12-10 00:15:14 +01:00
d877467da6 pipeline local registry
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2025-12-09 23:51:55 +01:00
2c79da5244 dialog text fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 22:03:26 +01:00
7c5997cd00 remove whitespaces 2025-12-09 21:46:18 +01:00
a5ed57777a v0.7
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-12-09 18:46:24 +01:00
d52d7a0cf1 state to window 2025-12-09 18:45:51 +01:00
90192cb6a7 rename entities 2025-12-09 18:32:16 +01:00
243447a1d1 bg color fix 2025-12-09 18:16:31 +01:00
cebd487c12 talk to 2025-12-09 17:51:26 +01:00
0614390b59 Docs: Add agent directive regarding git operations
Added a new section to GEMINI.md to explicitly state that the agent should not perform git add or git commit operations in the future, as this responsibility will be handled by the user.
2025-12-09 17:17:38 +01:00
3086603892 Refactor: Group dialog-related state into a common table
Consolidate dialog-related properties into a single 'dialog' table within the main 'State' object. This improves data organization and clarifies ownership of the dialog state.

The following properties were moved into :
-  ->
-  ->
-  ->
-  ->
-  ->  (previously implicit global)

All references to these properties have been updated throughout the codebase to reflect the new structure.
2025-12-09 17:15:08 +01:00
27 changed files with 1547 additions and 834 deletions

2
.gitignore vendored
View File

@@ -1 +1 @@
.vscode
mranderson.lua

19
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
"Lua.diagnostics.globals": [
"rect",
"exit",
"spr"
],
"Lua.workspace.library": [
"./inc"
],
"Lua.runtime.path": [
"?.lua",
"?/init.lua",
"inc/?.lua"
],
"Lua.diagnostics.disable": [
"undefined-global"
]
}

View File

@@ -1,20 +1,20 @@
environment: &environment
GAME_NAME: mranderson
GAME_LANG: lua
steps:
- name: version
image: alpine
commands:
- 'apk add --no-cache make'
- 'make ci-version'
- name: build
image: rastasi/tic80pro:latest
image: git.teletype.hu/internal/tic80pro:latest
environment:
<<: *environment
XDG_RUNTIME_DIR: /tmp
commands:
- tic80 --cli --skip --fs=. --cmd="load $GAME_NAME.$GAME_LANG & save $GAME_NAME & export html $GAME_NAME.html & exit"
- 'make ci-export'
- name: artifact
image: alpine
environment:
<<: *environment
DROPAREA_HOST: vps.teletype.hu
DROPAREA_PORT: 2223
DROPAREA_TARGET_PATH: /home/drop
@@ -22,17 +22,15 @@ steps:
DROPAREA_SSH_PASSWORD:
from_secret: droparea_ssh_password
commands:
- apk add --no-cache openssh-client sshpass
- mkdir -p /root/.ssh
- sshpass -p $DROPAREA_SSH_PASSWORD scp -o StrictHostKeyChecking=no -P $DROPAREA_PORT $GAME_NAME.$GAME_LANG $GAME_NAME.tic $GAME_NAME.html.zip $DROPAREA_USER@$DROPAREA_HOST:$DROPAREA_TARGET_PATH
- 'apk add --no-cache make openssh-client sshpass'
- 'make ci-upload'
- name: update
image: alpine
environment:
<<: *environment
UPDATE_SERVER: https://games.vps.teletype.hu
UPDATE_SECRET:
from_secret: update_secret_key
commands:
- apk add --no-cache curl
- curl "$UPDATE_SERVER/update?secret=$UPDATE_SECRET&name=$GAME_NAME&platform=tic80"
- 'apk add --no-cache make curl'
- 'make ci-update'

View File

@@ -39,10 +39,16 @@ Based on the analysis of `mranderson.lua`, the following regularities and conven
11. **`spr()` for Sprites:** Individual sprites should be rendered using the `spr()` function.
12. **`map()` for Maps:** Tilemaps should be drawn using the `map()` function.
13. **`print()` for Text:** Text display should utilize the `print()` function.
13. **`Print.text()` for Text:** Text display should utilize the `Print.text()` function.
## Code Style
14. **Indentation:** Use consistent indentation, likely 2 spaces, for code blocks to enhance readability.
15. **Comments:** Employ comments to explain complex logic, delineate code sections, or clarify non-obvious design choices.
16. **Code Sections:** Use comments (e.g., `--- INIT ---`, `--- UPDATE ---`, `--- DRAW ---`, `--- HELPERS ---`) to clearly delineate logical sections of the codebase.
---
## Agent Directives
- **Git Operations:** In the future, do not perform `git add` or `git commit` operations. This responsibility will be handled by the user.

115
Makefile Normal file
View File

@@ -0,0 +1,115 @@
# -----------------------------------------
# Makefile TIC-80 project builder
# -----------------------------------------
PROJECT = mranderson
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))
ASSETS_LUA = inc/meta/meta.assets.lua
# CI/CD variables
VERSION_FILE = .version
GAME_LANG ?= lua
DROPAREA_HOST ?= vps.teletype.hu
DROPAREA_PORT ?= 2223
DROPAREA_TARGET_PATH ?= /home/drop
DROPAREA_USER ?= drop
UPDATE_SERVER ?= https://games.vps.teletype.hu
all: build
build: $(OUTPUT)
$(OUTPUT): $(SRC) $(ORDER)
@rm -f $(OUTPUT)
@while read f; do \
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
echo "" >> $(OUTPUT); \
done < $(ORDER)
export: build
@if [ -z "$(VERSION)" ]; then \
echo "ERROR: VERSION not set!"; \
exit 1; \
fi
@echo "==> Exporting HTML for version $(VERSION)"
@tic80 --cli --skip --fs=. \
--cmd="load $(OUTPUT) & save $(PROJECT)-$(VERSION) & export html $(PROJECT)-$(VERSION).html & exit"
@echo "==> Creating versioned files"
@if [ -f "$(PROJECT)-$(VERSION).tic" ]; then \
cp $(PROJECT)-$(VERSION).tic $(PROJECT).tic; \
fi
@if [ -f "$(PROJECT)-$(VERSION).html.zip" ]; then \
cp $(PROJECT)-$(VERSION).html.zip $(PROJECT).html.zip; \
fi
@echo "==> Generated files:"
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
watch:
make build
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
import_assets:
@for t in $(ASSET_TYPES); do \
for f in $(ASSETS_DIR)/$$t/*.png; do \
[ -e "$$f" ] || continue; \
echo "==> Importing $$f as $$t..."; \
tic80 --cli --skip --fs=. --cmd="import $$t $$f & exit"; \
done; \
done
export_assets: build
@echo "==> Exporting TIC-80 asset sections"
@mkdir -p inc/meta
@sed -n '/^-- <PALETTE>/,/^-- <\/PALETTE>/p;\
/^-- <TILES>/,/^-- <\/TILES>/p;\
/^-- <SPRITES>/,/^-- <\/SPRITES>/p;\
/^-- <MAP>/,/^-- <\/MAP>/p;\
/^-- <SFX>/,/^-- <\/SFX>/p;\
/^-- <MUSIC>/,/^-- <\/MUSIC>/p' \
$(OUTPUT) > $(ASSETS_LUA)
clean:
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(OUTPUT)
@echo "==> Cleaned build artifacts"
# CI/CD Targets
ci-version:
@VERSION=$$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]"); \
BRANCH=$${CI_COMMIT_BRANCH:-$${WOODPECKER_BRANCH}}; \
if [ "$$BRANCH" != "main" ] && [ "$$BRANCH" != "master" ] && [ -n "$$BRANCH" ]; then \
VERSION=dev-$$VERSION-$$BRANCH; \
fi; \
echo "VERSION is: $$VERSION"; \
echo $$VERSION > $(VERSION_FILE)
ci-export:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Building and exporting version $$VERSION"; \
$(MAKE) export VERSION=$$VERSION
ci-upload:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Uploading artifacts for version $$VERSION"; \
ls -lh $(PROJECT)-$$VERSION.* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true; \
cp $(PROJECT).lua $(PROJECT)-$$VERSION.lua; \
FILE_LUA=$(PROJECT)-$$VERSION.lua; \
FILE_TIC=$(PROJECT)-$$VERSION.tic; \
FILE_HTML_ZIP=$(PROJECT)-$$VERSION.html.zip; \
SCP_TARGET="$(DROPAREA_USER)@$(DROPAREA_HOST):$(DROPAREA_TARGET_PATH)/"; \
sshpass -p "$(DROPAREA_SSH_PASSWORD)" scp -o StrictHostKeyChecking=no -P $(DROPAREA_PORT) $$FILE_LUA $$FILE_TIC $$FILE_HTML_ZIP $$SCP_TARGET
ci-update:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Triggering update for version $$VERSION"; \
curl "$(UPDATE_SERVER)/update?secret=$(UPDATE_SECRET)&name=$(PROJECT)&platform=tic80&version=$$VERSION"
.PHONY: all build export watch import_assets export_assets clean ci-version ci-export ci-upload ci-update

View File

@@ -0,0 +1,49 @@
function Item.use()
Print.text("Used item: " .. Context.dialog.active_entity.name)
GameWindow.set_state(WINDOW_INVENTORY)
end
function Item.look_at()
PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
end
function Item.put_away()
-- Add item to inventory
table.insert(Context.inventory, Context.dialog.active_entity)
-- Remove item from screen
local currentScreenData = Context.screens[Context.current_screen]
for i, item in ipairs(currentScreenData.items) do
if item == Context.dialog.active_entity then
table.remove(currentScreenData.items, i)
break
end
end
-- Go back to game
GameWindow.set_state(WINDOW_GAME)
end
function Item.go_back_from_item_dialog()
GameWindow.set_state(WINDOW_GAME)
end
function Item.go_back_from_inventory_action()
GameWindow.set_state(WINDOW_GAME)
end
function Item.drop()
-- Remove item from inventory
for i, item in ipairs(Context.inventory) do
if item == Context.dialog.active_entity then
table.remove(Context.inventory, i)
break
end
end
-- Add item to screen
local currentScreenData = Context.screens[Context.current_screen]
Context.dialog.active_entity.x = Context.player.x
Context.dialog.active_entity.y = Context.player.y
table.insert(currentScreenData.items, Context.dialog.active_entity)
-- Go back to inventory
GameWindow.set_state(WINDOW_INVENTORY)
end

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

@@ -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 = NPC.talk_to},
{label = "Fight", action = NPC.fight},
{label = "Go back", action = NPC.go_back}
}, WINDOW_POPUP)
interaction_found = true
break
end
end
if not interaction_found then
-- Item interaction
for _, item in ipairs(currentScreenData.items) do
if math.abs(Context.player.x - item.x) < Config.physics.interaction_radius_item and math.abs(Context.player.y - item.y) < Config.physics.interaction_radius_item then
PopupWindow.show_menu_dialog(item, {
{label = "Use", action = Item.use},
{label = "Look at", action = Item.look_at},
{label = "Put away", action = Item.put_away},
{label = "Go back", action = Item.go_back_from_item_dialog}
}, WINDOW_POPUP)
interaction_found = true
break
end
end
end
-- If no interaction happened, open inventory
if not interaction_found then
GameWindow.set_state(WINDOW_INVENTORY)
end
end
end

73
inc/init/init.config.lua Normal file
View File

@@ -0,0 +1,73 @@
local DEFAULT_CONFIG = {
screen = {
width = 240,
height = 136
},
colors = {
black = 0,
light_grey = 13,
dark_grey = 14,
green = 6,
npc = 8,
item = 12 -- yellow
},
player = {
w = 8,
h = 8,
start_x = 120,
start_y = 128,
sprite_id = 1
},
physics = {
gravity = 0.5,
jump_power = -5,
move_speed = 1.5,
max_jumps = 2,
interaction_radius_npc = 12,
interaction_radius_item = 8
},
timing = {
splash_duration = 120
}
}
local Config = {
-- Copy default values initially
screen = DEFAULT_CONFIG.screen,
colors = DEFAULT_CONFIG.colors,
player = DEFAULT_CONFIG.player,
physics = DEFAULT_CONFIG.physics,
timing = DEFAULT_CONFIG.timing,
}
local CONFIG_SAVE_BANK = 7
local CONFIG_SAVE_ADDRESS_MOVE_SPEED = 0
local CONFIG_SAVE_ADDRESS_MAX_JUMPS = 1
local CONFIG_MAGIC_VALUE_ADDRESS = 2
local CONFIG_MAGIC_VALUE = 0xDE -- A magic number to check if config is saved
function Config.save()
-- Save physics settings
mset(Config.physics.move_speed * 10, CONFIG_SAVE_ADDRESS_MOVE_SPEED, CONFIG_SAVE_BANK)
mset(Config.physics.max_jumps, CONFIG_SAVE_ADDRESS_MAX_JUMPS, CONFIG_SAVE_BANK)
mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) -- Mark as saved
end
function Config.load()
-- Check if config has been saved before using a magic value
if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then
Config.physics.move_speed = mget(CONFIG_SAVE_ADDRESS_MOVE_SPEED, CONFIG_SAVE_BANK) / 10
Config.physics.max_jumps = mget(CONFIG_SAVE_ADDRESS_MAX_JUMPS, CONFIG_SAVE_BANK)
else
Config.restore_defaults()
end
end
function Config.restore_defaults()
Config.physics.move_speed = DEFAULT_CONFIG.physics.move_speed
Config.physics.max_jumps = DEFAULT_CONFIG.physics.max_jumps
-- Any other configurable items should be reset here
end
-- Load configuration on startup
Config.load()

521
inc/init/init.context.lua Normal file
View File

@@ -0,0 +1,521 @@
local SAVE_GAME_BANK = 6
local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
local SAVE_GAME_MAGIC_VALUE = 0xCA
local SAVE_GAME_PLAYER_X_ADDRESS = 1
local SAVE_GAME_PLAYER_Y_ADDRESS = 2
local SAVE_GAME_PLAYER_VX_ADDRESS = 3
local SAVE_GAME_PLAYER_VY_ADDRESS = 4
local SAVE_GAME_PLAYER_JUMPS_ADDRESS = 5
local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6
local VX_VY_OFFSET = 128 -- Offset for negative velocities
-- Helper for deep copying tables
local function clone_table(t)
local copy = {}
for k, v in pairs(t) do
if type(v) == "table" then
copy[k] = clone_table(v)
else
copy[k] = v
end
end
return copy
end
-- This function returns a table containing only the initial *data* for Context
local function get_initial_data()
return {
active_window = WINDOW_SPLASH,
inventory = {},
intro = {
y = Config.screen.height,
speed = 0.5,
text = "Mr. Anderson is an average\nprogrammer. His daily life\nrevolves around debugging,\npull requests, and end-of-sprint\nmeetings, all while secretly\ndreaming of being destined\nfor something more."
},
current_screen = 1,
splash_timer = Config.timing.splash_duration,
dialog = {
text = "",
menu_items = {},
selected_menu_item = 1,
active_entity = nil,
showing_description = false,
current_node_key = nil
},
player = {
x = Config.player.start_x,
y = Config.player.start_y,
w = Config.player.w,
h = Config.player.h,
vx = 0,
vy = 0,
jumps = 0,
sprite_id = Config.player.sprite_id
},
ground = {
x = 0,
y = Config.screen.height,
w = Config.screen.width,
h = 8
},
menu_items = {},
selected_menu_item = 1,
selected_inventory_item = 1,
game_in_progress = false, -- New flag
screens = clone_table({
{
-- Screen 1
name = "Screen 1",
platforms = {
{
x = 80,
y = 110,
w = 40,
h = 8
},
{
x = 160,
y = 90,
w = 40,
h = 8
}
},
npcs = {
{
x = 180,
y = 82,
name = "Trinity",
sprite_id = 2,
dialog = {
start = {
text = "Hello, Neo.",
options = {
{label = "Who are you?", next_node = "who_are_you"},
{label = "My name is not Neo.", next_node = "not_neo"},
{label = "...", next_node = "silent"}
}
},
who_are_you = {
text = "I am Trinity. I've been looking for you.",
options = {
{label = "The famous hacker?", next_node = "famous_hacker"},
{label = "Why me?", next_node = "why_me"}
}
},
not_neo = {
text = "I know. But you will be.",
options = {
{label = "What are you talking about?", next_node = "who_are_you"}
}
},
silent = {
text = "You're not much of a talker, are you?",
options = {
{label = "I guess not.", next_node = "dialog_end"}
}
},
famous_hacker = {
text = "The one and only.",
options = {
{label = "Wow.", next_node = "dialog_end"}
}
},
why_me = {
text = "Morpheus believes you are The One.",
options = {
{label = "The One?", next_node = "the_one"}
}
},
the_one = {
text = "The one who will save us all.",
options = {
{label = "I'm just a programmer.", next_node = "dialog_end"}
}
},
dialog_end = {
text = "We'll talk later.",
options = {} -- No options, ends conversation
}
}
},
{
x = 90,
y = 102,
name = "Oracle",
sprite_id = 3,
dialog = {
start = {
text = "I know what you're thinking. 'Am I in the right place?'",
options = {
{label = "Who are you?", next_node = "who_are_you"},
{label = "I guess I am.", next_node = "you_are"}
}
},
who_are_are = {
text = "I'm the Oracle. And you're right on time. Want a cookie?",
options = {
{label = "Sure.", next_node = "cookie"},
{label = "No, thank you.", next_node = "no_cookie"}
}
},
you_are = {
text = "Of course you are. Sooner or later, everyone comes to see me. Want a cookie?",
options = {
{label = "Yes, please.", next_node = "cookie"},
{label = "I'm good.", next_node = "no_cookie"}
}
},
cookie = {
text = "Here you go. Now, what's really on your mind?",
options = {
{label = "Am I The One?", next_node = "the_one"},
{label = "What is the Matrix?", next_node = "the_matrix"}
}
},
no_cookie = {
text = "Suit yourself. Now, what's troubling you?",
options = {
{label = "Am I The One?", next_node = "the_one"},
{label = "What is the Matrix?", next_node = "the_matrix"}
}
},
the_one = {
text = "Being The One is just like being in love. No one can tell you you're in love, you just know it. Through and through. Balls to bones.",
options = {
{label = "So I'm not?", next_node = "dialog_end"}
}
},
the_matrix = {
text = "The Matrix is a system, Neo. That system is our enemy. But when you're inside, you look around, what do you see? The very minds of the people we are trying to save.",
options = {
{label = "I see.", next_node = "dialog_end"}
}
},
dialog_end = {
text = "You have to understand, most of these people are not ready to be unplugged.",
options = {}
}
}
}
},
items = {
{
x = 100,
y = 128,
w = 8,
h = 8,
name = "Key",
sprite_id = 4,
desc = "A rusty old key. It might open something."
}
}
},
{
-- Screen 2
name = "Screen 2",
platforms = {
{
x = 30,
y = 100,
w = 50,
h = 8
},
{
x = 100,
y = 80,
w = 50,
h = 8
},
{
x = 170,
y = 60,
w = 50,
h = 8
}
},
npcs = {
{
x = 120,
y = 72,
name = "Morpheus",
sprite_id = 5,
dialog = {
start = {
text = "At last. Welcome, Neo. As you no doubt have guessed, I am Morpheus.",
options = {
{label = "It's an honor to meet you.", next_node = "honor"},
{label = "You've been looking for me.", next_node = "looking_for_me"}
}
},
honor = {
text = "No, the honor is mine.",
options = {
{label = "What is this place?", next_node = "what_is_this_place"}
}
},
looking_for_me = {
text = "I have. For some time.",
options = {
{label = "What is this place?", next_node = "what_is_this_place"}
}
},
what_is_this_place = {
text = "This is the construct. It's our loading program. We can load anything from clothing, to equipment, weapons, training simulations. Anything we need.",
options = {
{label = "Right.", next_node = "dialog_end"}
}
},
dialog_end = {
text = "I've been waiting for you, Neo. We have much to discuss.",
options = {} -- Ends conversation
}
}
},
{
x = 40,
y = 92,
name = "Tank",
sprite_id = 6,
dialog = {
start = {
text = "Hey, Neo! Welcome to the construct. I'm Tank.",
options = {
{label = "Good to meet you.", next_node = "good_to_meet_you"},
{label = "This place is incredible.", next_node = "incredible"}
}
},
good_to_meet_you = {
text = "You too! We've been waiting for you. Need anything? Training? Weapons?",
options = {
{label = "Training?", next_node = "training"},
{label = "I'm good for now.", next_node = "dialog_end"}
}
},
incredible = {
text = "Isn't it? The boss's design. We can load anything we need. What do you want to learn?",
options = {
{label = "Show me.", next_node = "training"}
}
},
training = {
text = "Jujitsu? Kung Fu? How about... all of them?",
options = {
{label = "All of them.", next_node = "all_of_them"}
}
},
all_of_them = {
text = "Operator, load the combat training program.",
options = {
{label = "...", next_node = "dialog_end"}
}
},
dialog_end = {
text = "Just holler if you need anything. Anything at all.",
options = {}
}
}
}
},
items = {
{
x = 180,
y = 52,
w = 8,
h = 8,
name = "Potion",
sprite_id = 7,
desc = "A glowing red potion. It looks potent."
}
}
},
{
-- Screen 3
name = "Screen 3",
platforms = {
{
x = 50,
y = 110,
w = 30,
h = 8
},
{
x = 100,
y = 90,
w = 30,
h = 8
},
{
x = 150,
y = 70,
w = 30,
h = 8
},
{
x = 200,
y = 50,
w = 30,
h = 8
}
},
npcs = {
{
x = 210,
y = 42,
name = "Agent Smith",
sprite_id = 8,
dialog = {
start = {
text = "Mr. Anderson. We've been expecting you.",
options = {
{label = "My name is Neo.", next_node = "name_is_neo"},
{label = "...", next_node = "silent"}
}
},
name_is_neo = {
text = "Whatever you say. You're here for a reason.",
options = {
{label = "What reason?", next_node = "what_reason"}
}
},
silent = {
text = "The silent type. It doesn't matter. You are an anomaly.",
options = {
{label = "What do you want?", next_node = "what_reason"}
}
},
what_reason = {
text = "To be deleted. The system has no place for your kind.",
options = {
{label = "I won't let you.", next_node = "wont_let_you"}
}
},
wont_let_you = {
text = "You hear that, Mr. Anderson? That is the sound of inevitability.",
options = {
{label = "...", next_node = "dialog_end"}
}
},
dialog_end = {
text = "It is purpose that created us. Purpose that connects us. Purpose that pulls us. That guides us. That drives us. It is purpose that defines. Purpose that binds us.",
options = {}
}
}
},
{
x = 160,
y = 62,
name = "Cypher",
sprite_id = 9,
dialog = {
start = {
text = "Well, well. The new messiah. Welcome to the real world.",
options = {
{label = "You don't seem happy.", next_node = "not_happy"},
{label = "...", next_node = "silent"}
}
},
not_happy = {
text = "Happy? Ignorance is bliss, Neo. We've been fighting this war for years. For what?",
options = {
{label = "For freedom.", next_node = "freedom"}
}
},
silent = {
text = "Not a talker, huh? Smart. Less to regret later. Want a drink?",
options = {
{label = "Sure.", next_node = "drink"},
{label = "No thanks.", next_node = "no_drink"}
}
},
drink = {
text = "Good stuff. The little things you miss, you know? Like a good steak.",
options = {
{label = "I guess.", next_node = "dialog_end"}
}
},
no_drink = {
text = "Your loss. More for me.",
options = {
{label = "...", next_node = "dialog_end"}
}
},
freedom = {
text = "Freedom... right. If Morpheus told you you could fly, would you believe him?",
options = {
{label = "He's our leader.", next_node = "dialog_end"}
}
},
dialog_end = {
text = "Just be careful who you trust.",
options = {}
}
}
}
},
items = {}
}
})
}
end
Context = {}
local function reset_context_to_initial_state()
local initial_data = get_initial_data()
-- Clear existing data properties from Context (but not methods)
for k in pairs(Context) do
if type(Context[k]) ~= "function" then -- Only clear data, leave functions
Context[k] = nil
end
end
-- Copy all initial data properties into Context
for k, v in pairs(initial_data) do
Context[k] = v
end
end
-- Initially populate Context with data
reset_context_to_initial_state()
-- Now define the methods for Context
function Context.new_game()
reset_context_to_initial_state()
Context.game_in_progress = true
MenuWindow.refresh_menu_items()
end
function Context.save_game()
if not Context.game_in_progress then return end
mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK)
mset(Context.player.x * 10, SAVE_GAME_PLAYER_X_ADDRESS, SAVE_GAME_BANK)
mset(Context.player.y * 10, SAVE_GAME_PLAYER_Y_ADDRESS, SAVE_GAME_BANK)
mset( (Context.player.vx * 100) + VX_VY_OFFSET, SAVE_GAME_PLAYER_VX_ADDRESS, SAVE_GAME_BANK)
mset( (Context.player.vy * 100) + VX_VY_OFFSET, SAVE_GAME_PLAYER_VY_ADDRESS, SAVE_GAME_BANK)
mset(Context.player.jumps, SAVE_GAME_PLAYER_JUMPS_ADDRESS, SAVE_GAME_BANK)
mset(Context.current_screen, SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK)
end
function Context.load_game()
if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then
-- No saved game found, start a new one
Context.new_game()
return
end
reset_context_to_initial_state() -- Reset data, preserve methods
Context.player.x = mget(SAVE_GAME_PLAYER_X_ADDRESS, SAVE_GAME_BANK) / 10
Context.player.y = mget(SAVE_GAME_PLAYER_Y_ADDRESS, SAVE_GAME_BANK) / 10
Context.player.vx = (mget(SAVE_GAME_PLAYER_VX_ADDRESS, SAVE_GAME_BANK) - VX_VY_OFFSET) / 100
Context.player.vy = (mget(SAVE_GAME_PLAYER_VY_ADDRESS, SAVE_GAME_BANK) - VX_VY_OFFSET) / 100
Context.player.jumps = mget(SAVE_GAME_PLAYER_JUMPS_ADDRESS, SAVE_GAME_BANK)
Context.current_screen = mget(SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK)
Context.game_in_progress = true
MenuWindow.refresh_menu_items()
end

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

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

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

29
inc/meta/meta.assets.lua Normal file
View File

@@ -0,0 +1,29 @@
-- <TILES>
-- 000:4444444444444444444444444444444444444444444444444444444444444444
-- 001:1111111111111111111111111111111111111111111111111111111111111111
-- 002:5555555555555555555555555555555555555555555555555555555555555555
-- 003:6666666666666666666666666666666666666666666666666666666666666666
-- 004:7777777777777777777777777777777777777777777777777777777777777777
-- 005:8888888888888888888888888888888888888888888888888888888888888888
-- 006:9999999999999999999999999999999999999999999999999999999999999999
-- 007:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-- 008:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-- </TILES>
-- <WAVES>
-- 000:00000000ffffffff00000000ffffffff
-- 001:0123456789abcdeffedcba9876543210
-- 02:0123456789abcdef0123456789abcdef
-- </WAVES>
-- <SFX>
-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000
-- </SFX>
-- <TRACKS>
-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- </TRACKS>
-- <PALETTE>
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
-- </PALETTE>

8
inc/meta/meta.header.lua Normal file
View File

@@ -0,0 +1,8 @@
-- title: Mr Anderson's Adventure
-- name: mranderson
-- author: Zsolt Tasnadi
-- desc: Life of a programmer in the Vector
-- site: https://games.teletype.hu
-- license: MIT License
-- version: 0.1
-- script: lua

View File

@@ -0,0 +1,25 @@
-- Gamepad buttons
local INPUT_KEY_UP = 0
local INPUT_KEY_DOWN = 1
local INPUT_KEY_LEFT = 2
local INPUT_KEY_RIGHT = 3
local INPUT_KEY_A = 4 -- Z key
local INPUT_KEY_B = 5 -- X key
local INPUT_KEY_X = 6 -- A key
local INPUT_KEY_Y = 7 -- S key
-- Keyboard keys
-- TODO: Find correct key codes for SPACE and LCTRL
local INPUT_KEY_SPACE = 48
local INPUT_KEY_BACKSPACE = 51
local INPUT_KEY_ENTER = 50
function Input.up() return btnp(INPUT_KEY_UP) end
function Input.down() return btnp(INPUT_KEY_DOWN) end
function Input.left() return btn(INPUT_KEY_LEFT) end
function Input.right() return btn(INPUT_KEY_RIGHT) end
function Input.player_jump() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_SPACE) end
function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end
function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end -- B button
function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end
function Input.toggle_popup() return keyp(INPUT_KEY_ENTER) end

View File

@@ -0,0 +1,55 @@
local STATE_HANDLERS = {
[WINDOW_SPLASH] = function()
SplashWindow.update()
SplashWindow.draw()
end,
[WINDOW_INTRO] = function()
IntroWindow.update()
IntroWindow.draw()
end,
[WINDOW_MENU] = function()
MenuWindow.update()
MenuWindow.draw()
end,
[WINDOW_GAME] = function()
GameWindow.update()
GameWindow.draw()
end,
[WINDOW_POPUP] = function()
GameWindow.draw()
PopupWindow.update()
PopupWindow.draw()
end,
[WINDOW_INVENTORY] = function()
InventoryWindow.update()
InventoryWindow.draw()
end,
[WINDOW_INVENTORY_ACTION] = function()
InventoryWindow.draw()
PopupWindow.draw()
PopupWindow.update()
end,
[WINDOW_CONFIGURATION] = function()
ConfigurationWindow.update()
ConfigurationWindow.draw()
end,
}
local initialized_game = false
function init_game()
if initialized_game then return end
MenuWindow.refresh_menu_items()
initialized_game = true
end
function TIC()
init_game()
cls(Config.colors.black)
local handler = STATE_HANDLERS[Context.active_window]
if handler then
handler()
end
end

View File

@@ -0,0 +1,8 @@
function Print.text(text, x, y, color, fixed, scale)
local shadow_color = Config.colors.black
if color == shadow_color then shadow_color = Config.colors.light_grey end
scale = scale or 1
print(text, x + 1, y + 1, shadow_color, fixed, scale)
print(text, x, y, color, fixed, scale)
end

87
inc/system/system.ui.lua Normal file
View File

@@ -0,0 +1,87 @@
function UI.draw_top_bar(title)
rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
Print.text(title, 3, 2, Config.colors.green)
end
function UI.draw_dialog()
PopupWindow.draw()
end
function UI.draw_menu(items, selected_item, x, y)
for i, item in ipairs(items) do
local current_y = y + (i-1)*10
if i == selected_item then
Print.text(">", x - 8, current_y, Config.colors.green)
end
Print.text(item.label, x, current_y, Config.colors.green)
end
end
function UI.update_menu(items, selected_item)
if Input.up() then
selected_item = selected_item - 1
if selected_item < 1 then
selected_item = #items
end
elseif Input.down() then
selected_item = selected_item + 1
if selected_item > #items then
selected_item = 1
end
end
return selected_item
end
function UI.word_wrap(text, max_chars_per_line)
if text == nil then return {""} end
local lines = {}
for input_line in (text .. "\n"):gmatch("(.-)\n") do
local current_line = ""
local words_in_line = 0
for word in input_line:gmatch("%S+") do
words_in_line = words_in_line + 1
if #current_line == 0 then
current_line = word
elseif #current_line + #word + 1 <= max_chars_per_line then
current_line = current_line .. " " .. word
else
table.insert(lines, current_line)
current_line = word
end
end
if words_in_line > 0 then
table.insert(lines, current_line)
else
table.insert(lines, "")
end
end
if #lines == 0 then
return {""}
end
return lines
end
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
return {
label = label,
get = value_getter,
set = value_setter,
min = min,
max = max,
step = step,
format = format or "%.1f",
type = "numeric_stepper"
}
end
function UI.create_action_item(label, action)
return {
label = label,
action = action,
type = "action_item"
}
end

View File

@@ -0,0 +1,112 @@
ConfigurationWindow = {
controls = {},
selected_control = 1,
}
function ConfigurationWindow.init()
ConfigurationWindow.controls = {
UI.create_numeric_stepper(
"Move Speed",
function() return Config.physics.move_speed end,
function(v) Config.physics.move_speed = v end,
0.5, 3, 0.1, "%.1f"
),
UI.create_numeric_stepper(
"Max Jumps",
function() return Config.physics.max_jumps end,
function(v) Config.physics.max_jumps = v end,
1, 5, 1, "%d"
),
UI.create_action_item(
"Save",
function() Config.save() end
),
UI.create_action_item(
"Restore Defaults",
function() Config.restore_defaults() end
),
}
end
function ConfigurationWindow.draw()
UI.draw_top_bar("Configuration")
local x_start = 10 -- Left margin for labels
local y_start = 40
local x_value_right_align = Config.screen.width - 10 -- Right margin for values
local char_width = 4 -- Approximate character width for default font
for i, control in ipairs(ConfigurationWindow.controls) do
local current_y = y_start + (i - 1) * 12
local color = Config.colors.green
if control.type == "numeric_stepper" then
local value = control.get()
local label_text = control.label
local value_text = string.format(control.format, value)
-- Calculate x position for right-aligned value
local value_x = x_value_right_align - (#value_text * char_width)
if i == ConfigurationWindow.selected_control then
color = Config.colors.item
Print.text("<", x_start -8, current_y, color)
Print.text(label_text, x_start, current_y, color) -- Shift label due to '<'
Print.text(value_text, value_x, current_y, color)
Print.text(">", x_value_right_align + 4, current_y, color) -- Print '>' after value
else
Print.text(label_text, x_start, current_y, color)
Print.text(value_text, value_x, current_y, color)
end
elseif control.type == "action_item" then
local label_text = control.label
if i == ConfigurationWindow.selected_control then
color = Config.colors.item
Print.text("<", x_start -8, current_y, color)
Print.text(label_text, x_start, current_y, color)
Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color)
else
Print.text(label_text, x_start, current_y, color)
end
end
end
Print.text("Press B to go back", x_start, 120, Config.colors.light_grey)
end
function ConfigurationWindow.update()
if Input.menu_back() then
GameWindow.set_state(WINDOW_MENU)
return
end
if Input.up() then
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1
if ConfigurationWindow.selected_control < 1 then
ConfigurationWindow.selected_control = #ConfigurationWindow.controls
end
elseif Input.down() then
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control + 1
if ConfigurationWindow.selected_control > #ConfigurationWindow.controls then
ConfigurationWindow.selected_control = 1
end
end
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
if control then
if control.type == "numeric_stepper" then
local current_value = control.get()
if btnp(2) then -- Left
local new_value = math.max(control.min, current_value - control.step)
control.set(new_value)
elseif btnp(3) then -- Right
local new_value = math.min(control.max, current_value + control.step)
control.set(new_value)
end
elseif control.type == "action_item" then
if Input.menu_confirm() then
control.action()
end
end
end
end

View File

@@ -0,0 +1,40 @@
function GameWindow.draw()
local currentScreenData = Context.screens[Context.current_screen]
UI.draw_top_bar(currentScreenData.name)
-- Draw platforms
for _, p in ipairs(currentScreenData.platforms) do
rect(p.x, p.y, p.w, p.h, Config.colors.green)
end
-- Draw items
for _, item in ipairs(currentScreenData.items) do
spr(item.sprite_id, item.x, item.y, 0)
end
-- Draw NPCs
for _, npc in ipairs(currentScreenData.npcs) do
spr(npc.sprite_id, npc.x, npc.y, 0)
end
-- Draw ground
rect(Context.ground.x, Context.ground.y, Context.ground.w, Context.ground.h, Config.colors.dark_grey)
-- Draw player
Player.draw()
end
function GameWindow.update()
if Input.menu_back() then
Context.active_window = WINDOW_MENU
MenuWindow.refresh_menu_items()
return
end
Player.update() -- Call the encapsulated player update logic
end
function GameWindow.set_state(new_state)
Context.active_window = new_state
-- Add any state-specific initialization/cleanup here later if needed
end

View File

@@ -0,0 +1,25 @@
function IntroWindow.draw()
local x = (Config.screen.width - 132) / 2 -- Centered text
Print.text(Context.intro.text, x, Context.intro.y, Config.colors.green)
end
function IntroWindow.update()
Context.intro.y = Context.intro.y - Context.intro.speed
-- Count lines in intro text to determine when scrolling is done
local lines = 1
for _ in string.gmatch(Context.intro.text, "\n") do
lines = lines + 1
end
-- When text is off-screen, go to menu
if Context.intro.y < -lines * 8 then
GameWindow.set_state(WINDOW_MENU)
end
-- Skip intro by pressing A
if Input.menu_confirm() then
GameWindow.set_state(WINDOW_MENU)
end
end

View File

@@ -0,0 +1,34 @@
function InventoryWindow.draw()
UI.draw_top_bar("Inventory")
if #Context.inventory == 0 then
Print.text("Inventory is empty.", 70, 70, Config.colors.light_grey)
else
for i, item in ipairs(Context.inventory) do
local color = Config.colors.light_grey
if i == Context.selected_inventory_item then
color = Config.colors.green
Print.text(">", 60, 20 + i * 10, color)
end
Print.text(item.name, 70, 20 + i * 10, color)
end
end
end
function InventoryWindow.update()
Context.selected_inventory_item = UI.update_menu(Context.inventory, Context.selected_inventory_item)
if Input.menu_confirm() and #Context.inventory > 0 then
local selected_item = Context.inventory[Context.selected_inventory_item]
PopupWindow.show_menu_dialog(selected_item, {
{label = "Use", action = Item.use},
{label = "Drop", action = Item.drop},
{label = "Look at", action = Item.look_at},
{label = "Go back", action = Item.go_back_from_inventory_action}
}, WINDOW_INVENTORY_ACTION)
end
if Input.menu_back() then
GameWindow.set_state(WINDOW_GAME)
end
end

View File

@@ -0,0 +1,60 @@
function MenuWindow.draw()
UI.draw_top_bar("Main Menu")
UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70)
end
function MenuWindow.update()
Context.selected_menu_item = UI.update_menu(Context.menu_items, Context.selected_menu_item)
if Input.menu_confirm() then
local selected_item = Context.menu_items[Context.selected_menu_item]
if selected_item and selected_item.action then
selected_item.action()
end
end
end
function MenuWindow.new_game()
Context.new_game() -- This function will be created in Context
GameWindow.set_state(WINDOW_GAME)
end
function MenuWindow.load_game()
Context.load_game() -- This function will be created in Context
GameWindow.set_state(WINDOW_GAME)
end
function MenuWindow.save_game()
Context.save_game() -- This function will be created in Context
end
function MenuWindow.resume_game()
GameWindow.set_state(WINDOW_GAME)
end
function MenuWindow.exit()
exit()
end
function MenuWindow.configuration()
ConfigurationWindow.init()
GameWindow.set_state(WINDOW_CONFIGURATION)
end
function MenuWindow.refresh_menu_items()
Context.menu_items = {} -- Start with an empty table
if Context.game_in_progress then
table.insert(Context.menu_items, {label = "Resume Game", action = MenuWindow.resume_game})
table.insert(Context.menu_items, {label = "Save Game", action = MenuWindow.save_game})
end
table.insert(Context.menu_items, {label = "New Game", action = MenuWindow.new_game})
table.insert(Context.menu_items, {label = "Load Game", action = MenuWindow.load_game})
table.insert(Context.menu_items, {label = "Configuration", action = MenuWindow.configuration})
table.insert(Context.menu_items, {label = "Exit", action = MenuWindow.exit})
Context.selected_menu_item = 1 -- Reset selection after refreshing
end

102
inc/window/window.popup.lua Normal file
View File

@@ -0,0 +1,102 @@
function PopupWindow.set_dialog_node(node_key)
local npc = Context.dialog.active_entity
local node = npc.dialog[node_key]
if not node then
GameWindow.set_state(WINDOW_GAME)
return
end
Context.dialog.current_node_key = node_key
Context.dialog.text = node.text
local menu_items = {}
if node.options then
for _, option in ipairs(node.options) do
table.insert(menu_items, {
label = option.label,
action = function()
PopupWindow.set_dialog_node(option.next_node)
end
})
end
end
-- if no options, it's the end of this branch.
if #menu_items == 0 then
table.insert(menu_items, {
label = "Go back",
action = function() GameWindow.set_state(WINDOW_GAME) end
})
end
Context.dialog.menu_items = menu_items
Context.dialog.selected_menu_item = 1
Context.dialog.showing_description = false
GameWindow.set_state(WINDOW_POPUP)
end
function PopupWindow.update()
if Context.dialog.showing_description then
if Input.menu_confirm() or Input.menu_back() then
Context.dialog.showing_description = false
Context.dialog.text = "" -- Clear the description text
-- No need to change active_window, as it remains in WINDOW_POPUP or WINDOW_INVENTORY_ACTION
end
else
Context.dialog.selected_menu_item = UI.update_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item)
if Input.menu_confirm() then
local selected_item = Context.dialog.menu_items[Context.dialog.selected_menu_item]
if selected_item and selected_item.action then
selected_item.action()
end
end
if Input.menu_back() then
GameWindow.set_state(WINDOW_GAME)
end
end
end
function PopupWindow.show_menu_dialog(entity, menu_items, dialog_active_window)
Context.dialog.active_entity = entity
Context.dialog.text = "" -- Initial dialog text is empty, name is title
GameWindow.set_state(dialog_active_window or WINDOW_POPUP)
Context.dialog.showing_description = false
Context.dialog.menu_items = menu_items
Context.dialog.selected_menu_item = 1
end
function PopupWindow.show_description_dialog(entity, description_text)
Context.dialog.active_entity = entity
Context.dialog.text = description_text
GameWindow.set_state(WINDOW_POPUP)
Context.dialog.showing_description = true
-- No menu items needed for description dialog
end
function PopupWindow.draw()
rect(40, 40, 160, 80, Config.colors.black)
rectb(40, 40, 160, 80, Config.colors.green)
-- Display the entity's name as the dialog title
if Context.dialog.active_entity and Context.dialog.active_entity.name then
Print.text(Context.dialog.active_entity.name, 120 - #Context.dialog.active_entity.name * 2, 45, Config.colors.green)
end
-- Display the dialog content (description for "look at", or initial name/dialog for others)
local wrapped_lines = UI.word_wrap(Context.dialog.text, 25) -- Max 25 chars per line
local current_y = 55 -- Starting Y position for the first line of content
for _, line in ipairs(wrapped_lines) do
Print.text(line, 50, current_y, Config.colors.light_grey)
current_y = current_y + 8 -- Move to the next line (8 pixels for default font height + padding)
end
-- Adjust menu position based on the number of wrapped lines
if not Context.dialog.showing_description then
UI.draw_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item, 50, current_y + 2)
else
Print.text("[A] Go Back", 50, current_y + 10, Config.colors.green)
end
end

View File

@@ -0,0 +1,11 @@
function SplashWindow.draw()
Print.text("Mr. Anderson's", 78, 60, Config.colors.green)
Print.text("Addventure", 90, 70, Config.colors.green)
end
function SplashWindow.update()
Context.splash_timer = Context.splash_timer - 1
if Context.splash_timer <= 0 or Input.menu_confirm() then
GameWindow.set_state(WINDOW_INTRO)
end
end

20
mranderson.inc Normal file
View File

@@ -0,0 +1,20 @@
meta/meta.header.lua
init/init.modules.lua
init/init.config.lua
init/init.windows.lua
init/init.context.lua
system/system.print.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

View File

@@ -1,817 +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.7
-- 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 States
--------------------------------------------------------------------------------
local GAME_STATE_SPLASH = 0
local GAME_STATE_INTRO = 1
local GAME_STATE_MENU = 2
local GAME_STATE_GAME = 3
local GAME_STATE_DIALOG = 4
local GAME_STATE_INVENTORY = 5
local GAME_STATE_INVENTORY_ACTION = 6
--------------------------------------------------------------------------------
-- Modules
--------------------------------------------------------------------------------
-- 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 NpcActions = {}
local ItemActions = {}
local MenuActions = {}
local Player = {}
--------------------------------------------------------------------------------
-- Game State
--------------------------------------------------------------------------------
local State = {
game_state = GAME_STATE_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,
dialog_text = "",
splash_timer = Config.timing.splash_duration,
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,
dialog_menu_items = {},
selected_dialog_menu_item = 1,
active_entity = nil,
-- 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
},
{
x = 90,
y = 102,
name = "Oracle",
sprite_id = 3
}
},
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
},
{
x = 40,
y = 92,
name = "Tank",
sprite_id = 6
}
},
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
},
{
x = 160,
y = 62,
name = "Cypher",
sprite_id = 9
}
},
items = {}
}
}
}
--------------------------------------------------------------------------------
-- Inventory Module
--------------------------------------------------------------------------------
function InventoryState.draw()
cls(Config.colors.dark_grey)
UI.draw_top_bar("Inventory")
if #State.inventory == 0 then
print("Inventory is empty.", 70, 70, Config.colors.light_grey)
else
for i, item in ipairs(State.inventory) do
local color = Config.colors.light_grey
if i == State.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 InventoryState.update()
State.selected_inventory_item = UI.update_menu(State.inventory, State.selected_inventory_item)
if Input.menu_confirm() and #State.inventory > 0 then
local selected_item = State.inventory[State.selected_inventory_item]
DialogState.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}
}, GAME_STATE_INVENTORY_ACTION)
end
if Input.menu_back() then
GameState.set_state(GAME_STATE_GAME)
end
end
--------------------------------------------------------------------------------
-- Menu Actions
--------------------------------------------------------------------------------
function MenuActions.play()
-- Reset player state and screen for a new game
State.player.x = Config.player.start_x
State.player.y = Config.player.start_y
State.player.vx = 0
State.player.vy = 0
State.player.jumps = 0
State.current_screen = 1
GameState.set_state(GAME_STATE_GAME)
end
function MenuActions.exit()
exit()
end
-- Initialize menu items after actions are defined
State.menu_items = {
{label = "Play", action = MenuActions.play},
{label = "Exit", action = MenuActions.exit}
}
--------------------------------------------------------------------------------
-- NPC Actions
--------------------------------------------------------------------------------
function NpcActions.talk_to() end
function NpcActions.fight() end
function NpcActions.go_back()
GameState.set_state(GAME_STATE_GAME)
end
--------------------------------------------------------------------------------
-- Item Actions
--------------------------------------------------------------------------------
function ItemActions.use()
print("Used item: " .. State.active_entity.name)
GameState.set_state(GAME_STATE_INVENTORY)
end
function ItemActions.look_at()
DialogState.show_description_dialog(State.active_entity, State.active_entity.desc)
end
function ItemActions.put_away()
-- Add item to inventory
table.insert(State.inventory, State.active_entity)
-- Remove item from screen
local currentScreenData = State.screens[State.current_screen]
for i, item in ipairs(currentScreenData.items) do
if item == State.active_entity then
table.remove(currentScreenData.items, i)
break
end
end
-- Go back to game
GameState.set_state(GAME_STATE_GAME)
end
function ItemActions.go_back_from_item_dialog()
GameState.set_state(GAME_STATE_GAME)
end
function ItemActions.go_back_from_inventory_action()
GameState.set_state(GAME_STATE_GAME)
end
function ItemActions.drop()
-- Remove item from inventory
for i, item in ipairs(State.inventory) do
if item == State.active_entity then
table.remove(State.inventory, i)
break
end
end
-- Add item to screen
local currentScreenData = State.screens[State.current_screen]
State.active_entity.x = State.player.x
State.active_entity.y = State.player.y
table.insert(currentScreenData.items, State.active_entity)
-- Go back to inventory
GameState.set_state(GAME_STATE_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()
DialogState.draw()
end
function DialogState.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 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)
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)
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 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 SplashState.update()
State.splash_timer = State.splash_timer - 1
if State.splash_timer <= 0 or Input.menu_confirm() then
GameState.set_state(GAME_STATE_INTRO)
end
end
--------------------------------------------------------------------------------
-- Intro Module
--------------------------------------------------------------------------------
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 IntroState.update()
State.intro.y = State.intro.y - State.intro.speed
-- Count lines in intro text to determine when scrolling is done
local lines = 1
for _ in string.gmatch(State.intro.text, "\n") do
lines = lines + 1
end
-- When text is off-screen, go to menu
if State.intro.y < -lines * 8 then
GameState.set_state(GAME_STATE_MENU)
end
-- Skip intro by pressing A
if Input.menu_confirm() then
GameState.set_state(GAME_STATE_MENU)
end
end
--------------------------------------------------------------------------------
-- Menu Module
--------------------------------------------------------------------------------
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 MenuState.update()
State.selected_menu_item = UI.update_menu(State.menu_items, State.selected_menu_item)
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()
end
end
end
--------------------------------------------------------------------------------
-- Game Module
--------------------------------------------------------------------------------
function GameState.draw()
local currentScreenData = State.screens[State.current_screen]
cls(Config.colors.dark_grey)
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(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 Player.update()
-- Handle input
if Input.left() then
State.player.vx = -Config.physics.move_speed
elseif Input.right() then
State.player.vx = Config.physics.move_speed
else
State.player.vx = 0
end
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
-- Update player position
State.player.x = State.player.x + State.player.vx
State.player.y = State.player.y + State.player.vy
-- Screen transition
if State.player.x > Config.screen.width - State.player.w then
if State.current_screen < #State.screens then
State.current_screen = State.current_screen + 1
State.player.x = 0
else
State.player.x = Config.screen.width - State.player.w
end
elseif State.player.x < 0 then
if State.current_screen > 1 then
State.current_screen = State.current_screen - 1
State.player.x = Config.screen.width - State.player.w
else
State.player.x = 0
end
end
-- Apply gravity
State.player.vy = State.player.vy + Config.physics.gravity
local currentScreenData = State.screens[State.current_screen]
-- Collision detection with platforms
for _, p in ipairs(currentScreenData.platforms) do
if State.player.vy > 0 and State.player.y + State.player.h >= p.y and State.player.y + State.player.h <= p.y + p.h and State.player.x + State.player.w > p.x and State.player.x < p.x + p.w then
State.player.y = p.y - State.player.h
State.player.vy = 0
State.player.jumps = 0
end
end
-- Collision detection with ground
if State.player.y + State.player.h > State.ground.y then
State.player.y = State.ground.y - State.player.h
State.player.vy = 0
State.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(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 = "Go back", action = NpcActions.go_back}
}, GAME_STATE_DIALOG)
interaction_found = true
break
end
end
if not interaction_found then
-- Item interaction
for _, item in ipairs(currentScreenData.items) do
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 = "Go back", action = ItemActions.go_back_from_item_dialog}
}, GAME_STATE_DIALOG)
interaction_found = true
break
end
end
end
-- If no interaction happened, open inventory
if not interaction_found then
GameState.set_state(GAME_STATE_INVENTORY)
end
end
end
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.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.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()
SplashState.update()
SplashState.draw()
end,
[GAME_STATE_INTRO] = function()
IntroState.update()
IntroState.draw()
end,
[GAME_STATE_MENU] = function()
MenuState.update()
MenuState.draw()
end,
[GAME_STATE_GAME] = function()
GameState.update()
GameState.draw()
end,
[GAME_STATE_DIALOG] = function()
GameState.draw() -- Draw game behind dialog
DialogState.draw()
DialogState.update()
end,
[GAME_STATE_INVENTORY] = function()
InventoryState.update()
InventoryState.draw()
end,
[GAME_STATE_INVENTORY_ACTION] = function()
InventoryState.draw() -- Draw inventory behind dialog
DialogState.draw()
DialogState.update()
end,
}
function TIC()
local handler = STATE_HANDLERS[State.game_state]
if handler then
handler()
end
end
-- <TILES>
-- 000:4444444444444444444444444444444444444444444444444444444444444444
-- 001:1111111111111111111111111111111111111111111111111111111111111111
-- 002:5555555555555555555555555555555555555555555555555555555555555555
-- 003:6666666666666666666666666666666666666666666666666666666666666666
-- 004:7777777777777777777777777777777777777777777777777777777777777777
-- 005:8888888888888888888888888888888888888888888888888888888888888888
-- 006:9999999999999999999999999999999999999999999999999999999999999999
-- 007:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-- 008:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-- </TILES>
-- <WAVES>
-- 000:00000000ffffffff00000000ffffffff
-- 001:0123456789abcdeffedcba9876543210
-- 002:0123456789abcdef0123456789abcdef
-- </WAVES>
-- <SFX>
-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000
-- </SFX>
-- <TRACKS>
-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- </TRACKS>
-- <PALETTE>
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
-- </PALETTE>