Compare commits

...

22 Commits

Author SHA1 Message Date
3dc28849c4 #24 Assets Import-export fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 23:21:00 +01:00
8a6214e893 restore asset constants
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 21:49:18 +01:00
d943b6deaa branch into version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 20:08:14 +01:00
b3b2159d75 add name to header
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-27 22:12:11 +01:00
ae56cf3555 pipeline update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-26 22:16:02 +01:00
2fc241fee7 Add some versions of office.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-20 22:47:23 +01:00
4e0145982f Add the firest version of the office.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-20 22:34:35 +01:00
1c987fa08b Add the player's home.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-20 21:24:24 +01:00
b6d0823875 restore import_assets
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 22:26:34 +01:00
ffa82e8f92 export_assets make target
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 21:25:00 +01:00
cfc07afe59 asset importer
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 18:14:47 +01:00
3fbce5aced purge
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 00:10:55 +01:00
b3158bdc37 rename project to impostor 2026-01-12 21:19:45 +01:00
4907c9cadf tic80 image change 2025-12-13 22:52:22 +01:00
45f2e746d6 new version 2025-12-13 20:13:08 +01:00
8e8d181c34 shadowed text 2025-12-13 20:11:50 +01:00
5379e30cf3 keyboard button fixes 2025-12-12 15:30:47 +01:00
d3fb12703c v0.13 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) 2025-12-11 20:00:43 +01:00
6a39128962 v0.11 2025-12-11 18:57:30 +01:00
36 changed files with 1048 additions and 791 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
mranderson.lua
.local
impostor.lua

View File

@@ -1,21 +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: gitea.vps.teletype.hu/games/tic80pro:latest
image: git.teletype.hu/internal/tic80pro:latest
environment:
<<: *environment
XDG_RUNTIME_DIR: /tmp
commands:
- make build PROJECT=$GAME_NAME
- make export PROJECT=$GAME_NAME
- 'make ci-export'
- name: artifact
image: alpine
environment:
<<: *environment
DROPAREA_HOST: vps.teletype.hu
DROPAREA_PORT: 2223
DROPAREA_TARGET_PATH: /home/drop
@@ -23,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

@@ -1,6 +1,6 @@
# TIC-80 Lua Code Regularities
Based on the analysis of `mranderson.lua`, the following regularities and conventions should be followed for future modifications and development within this project:
Based on the analysis of `impostor.lua`, the following regularities and conventions should be followed for future modifications and development within this project:
## General Structure & Lifecycle
@@ -39,7 +39,7 @@ 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

133
Makefile
View File

@@ -1,15 +1,8 @@
# -----------------------------------------
# Makefile TIC-80 project builder
# Usage:
# make PROJECT=mranderson
# make build PROJECT=mranderson
# make watch PROJECT=mranderson
# make export PROJECT=mranderson
# -----------------------------------------
ifndef PROJECT
$(error Specify the project name: make PROJECT=name)
endif
PROJECT = impostor
ORDER = $(PROJECT).inc
OUTPUT = $(PROJECT).lua
@@ -17,30 +10,126 @@ OUTPUT_ZIP = $(PROJECT).html.zip
OUTPUT_TIC = $(PROJECT).tic
SRC_DIR = inc
SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
ASSETS_LUA = inc/meta/meta.assets.lua
ASSETS_DIR = assets
ASSET_TYPES = tiles sprites sfx music
# 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)
@echo "==> Build complete: $(OUTPUT)"
$(OUTPUT): $(SRC) $(ORDER)
@echo "==> Building $(OUTPUT)..."
@rm -f $(OUTPUT)
@while read f; do \
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
echo "\n" >> $(OUTPUT); \
echo "" >> $(OUTPUT); \
done < $(ORDER)
@echo "==> Done."
export: $(OUTPUT)
@echo "==> TIC-80 export..."
tic80 --cli --skip --fs=. \
--cmd="load $(OUTPUT) & save $(PROJECT) & export html $(PROJECT).html & exit"
@echo "==> HTML ZIP: $(OUTPUT_ZIP)"
@echo "==> TIC: $(OUTPUT_TIC)"
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:
@echo "==> Watching project: $(PROJECT)"
make build PROJECT=$(PROJECT)
fswatch -o $(SRC_DIR) $(ORDER) | while read; do make build PROJECT=$(PROJECT); done
make build
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
import_assets: $(OUTPUT)
@TIC_CMD="load $(OUTPUT) &"; \
for t in $(ASSET_TYPES); do \
for f in $(ASSETS_DIR)/$$t/*.png; do \
[ -e "$$f" ] || continue; \
echo "==> Importing $$f as $$t..."; \
TIC_CMD="$${TIC_CMD} & import $$t $$f"; \
done; \
done; \
TIC_CMD="$$TIC_CMD save & exit"; \
echo $$TIC_CMD; \
tic80 --cli --skip --fs=. --cmd="$$TIC_CMD"
# export helper function
define f_export_asset_awk
cat $(2) | awk '/-- <$(1)>/,/<\/$(1)>/' >> $(3)
endef
export_assets:
# $(OUTPUT) would be a circular dependency
@test -e $(OUTPUT)
@echo "==> Exporting TIC-80 asset sections"
@mkdir -p inc/meta
@echo -n '' > $(ASSETS_LUA)
@$(call f_export_asset_awk,PALETTE,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,TILES,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,SPRITES,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,MAP,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,SFX,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,WAVES,$(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
#-- <WAVES>
#-- 000:224578acdeeeeddcba95434567653100
#-- </WAVES>
#
#-- <SFX>
#-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000
#-- </SFX>

View File

@@ -1,4 +1,4 @@
# Mr. Anderson's Matrix Escape
# Definitely not an Impostor
## Installation
@@ -8,14 +8,6 @@ This game is designed for the TIC-80 fantasy computer. To play, follow these ste
2. **Clone this repository** Clone this repository to tic80's cartridge folder (MacOS: ~Library/Application Support/com.nesbox.tic/TIC-80)
2. **Launch TIC-80:** Start the TIC-80 application.
3. **Load the Game:**
* Navigate to the directory where `game.lua` is located using the TIC-80 command line (`cd mranderson`).
* Navigate to the directory where `game.lua` is located using the TIC-80 command line (`cd impostor`).
* Type `load game.lua` and press Enter.
* Once loaded, type `run` and press Enter to start the game.
## Story: The Coder's Lament
Before he was "The One," before he dodged bullets and shattered the illusion, Thomas Anderson was just a software developer named Neo. Trapped in a cubicle farm of endless bugs and looming deadlines, Neo's days were a monotonous cycle of debugging legacy code, attending pointless meetings, and battling unresponsive APIs. Each line of code felt like a chain, each project a heavier burden, pulling him deeper into a digital malaise.
He yearned for something more, a glitch in the system, a whisper of a different reality. His fingers, calloused from countless hours on the keyboard, danced across cryptic forums late at night, searching for answers, for meaning beyond the mundane syntax of his corporate prison. The coffee flowed freely, the pizza boxes piled high, and the lines of code blurred into an indistinguishable stream of ones and zeros.
This game chronicles Mr. Anderson's final, desperate struggles within the software development matrix. Navigate the labyrinthine codebase, escape the relentless pursuit of project managers (Agents), and uncover the hidden truths that will lead him to question everything he knows. Will he find the "red pill" in a sea of green code, or will he forever be just another drone in the system?

0
assets/music/.keep Normal file
View File

0
assets/sfx/.keep Normal file
View File

0
assets/sprites/.keep Normal file
View File

0
assets/tiles/.keep Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,19 +1,19 @@
meta/meta.header.lua
init/init.modules.lua
init/init.config.lua
init/init.windows.lua
init/init.modules.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
map/map.bedroom.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,49 +1,7 @@
function Item.use()
print("Used item: " .. Context.dialog.active_entity.name)
GameWindow.set_state(WINDOW_INVENTORY)
Print.text("Used item: " .. Context.dialog.active_entity.name)
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

View File

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

View File

@@ -1,4 +1,4 @@
local Config = {
local DEFAULT_CONFIG = {
screen = {
width = 240,
height = 136
@@ -12,21 +12,38 @@ local Config = {
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,
timing = DEFAULT_CONFIG.timing,
}
local CONFIG_SAVE_BANK = 7
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_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) -- Mark as saved
end
function Config.load()
Config.restore_defaults()
-- Check if config has been saved before using a magic value
end
function Config.restore_defaults()
-- Any other configurable items should be reset here
end
-- Load configuration on startup
Config.load()

View File

@@ -1,432 +1,422 @@
local Context = {
active_window = WINDOW_SPLASH,
inventory = {},
intro = {
y = Config.screen.height,
speed = 0.5,
text = "Mr. Anderson is an average\nprogrammer. His daily life\nrevolves around debugging,\npull requests, and end-of-sprint\nmeetings, all while secretly\ndreaming of being destined\nfor something more."
},
current_screen = 1,
splash_timer = Config.timing.splash_duration,
dialog = {
text = "",
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_selectS_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,
intro = {
y = Config.screen.height,
speed = 0.5,
text = "Norman Reds everyday life\nseems ordinary: work,\nmeetings, coffee, and\nendless notifications.\nBut beneath the surface\n— within him, or around\nhim — something is\nconstantly building, and\nit soon becomes clear\nthat there is more going\non than meets the eye."
},
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 = {
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,
active_entity = nil,
showing_description = false,
current_node_key = nil
},
player = {
x = Config.player.start_x,
y = Config.player.start_y,
w = Config.player.w,
h = Config.player.h,
vx = 0,
vy = 0,
jumps = 0,
sprite_id = Config.player.sprite_id
},
ground = {
x = 0,
y = Config.screen.height,
w = Config.screen.width,
h = 8
},
menu_items = {},
selected_menu_item = 1,
selected_inventory_item = 1,
-- Screen data
screens = {
{
-- Screen 1
name = "Screen 1",
platforms = {
{
x = 80,
y = 110,
w = 40,
h = 8
},
{
x = 160,
y = 90,
w = 40,
h = 8
}
},
npcs = {
{
x = 180,
y = 82,
name = "Trinity",
sprite_id = 2,
dialog = {
start = {
text = "Hello, Neo.",
options = {
{label = "Who are you?", next_node = "who_are_you"},
{label = "My name is not Neo.", next_node = "not_neo"},
{label = "...", next_node = "silent"}
}
},
who_are_you = {
text = "I am Trinity. I've been looking for you.",
options = {
{label = "The famous hacker?", next_node = "famous_hacker"},
{label = "Why me?", next_node = "why_me"}
}
},
not_neo = {
text = "I know. But you will be.",
options = {
{label = "What are you talking about?", next_node = "who_are_you"}
}
},
silent = {
text = "You're not much of a talker, are you?",
options = {
{label = "I guess not.", next_node = "dialog_end"}
}
},
famous_hacker = {
text = "The one and only.",
game_in_progress = false, -- New flag
screens = clone_table({
{
-- Screen 1
name = "Screen 1",
npcs = {
{
name = "Trinity",
sprite_id = 2,
dialog = {
start = {
text = "Hello, Neo.",
options = {
{label = "Wow.", next_node = "dialog_end"}
{label = "Who are you?", next_node = "who_are_you"},
{label = "My name is not Neo.", next_node = "not_neo"},
{label = "...", next_node = "silent"}
}
},
why_me = {
text = "Morpheus believes you are The One.",
},
who_are_you = {
text = "I am Trinity. I've been looking for you.",
options = {
{label = "The One?", next_node = "the_one"}
{label = "The famous hacker?", next_node = "famous_hacker"},
{label = "Why me?", next_node = "why_me"}
}
},
the_one = {
text = "The one who will save us all.",
},
not_neo = {
text = "I know. But you will be.",
options = {
{label = "I'm just a programmer.", next_node = "dialog_end"}
{label = "What are you talking about?", next_node = "who_are_you"}
}
},
dialog_end = {
text = "We'll talk later.",
options = {} -- No options, ends conversation
},
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
}
}
},
{
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 = {}
}
}
}
},
{
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_you = {
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 = {
{
name = "Key",
sprite_id = 4,
desc = "A rusty old key. It might open something."
}
}
},
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",
npcs = {
{
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
}
}
},
{
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 = {
{
name = "Potion",
sprite_id = 7,
desc = "A glowing red potion. It looks potent."
}
}
},
{
-- Screen 3
name = "Screen 3",
npcs = {
{
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 = {}
}
}
},
{
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 = {}
}
},
{
-- 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.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.current_screen = mget(SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK)
Context.game_in_progress = true
MenuWindow.refresh_menu_items()
end

View File

@@ -3,10 +3,10 @@ local IntroWindow = {}
local MenuWindow = {}
local GameWindow = {}
local PopupWindow = {}
local InventoryWindow = {}
local ConfigurationWindow = {}
local UI = {}
local Print = {}
local Input = {}
local NPC = {}
local Item = {}

View File

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

19
inc/map/map.bedroom.lua Normal file
View File

@@ -0,0 +1,19 @@
MapBedroom = {
"10101010101010101010101010101010",
"10141410101010101010101010101010",
"10141410101010101010101010101010",
"10101010101010101010101010101010",
"10101010101010101010101010101010",
"10101010101010101010101010101010",
"10101010101010101010101010101010",
"10101010101010101010101010101010",
"10101010101010101010101010101010",
"11111111111111111111111111111111",
"11111111111111111111111111111111",
"11111111111111111111111111111111",
"11111516111213111111111111111111",
"11111111111111111111111111111111",
"11111111111111111111111111111111",
"11111111111111111111111111111111",
"11111111111111111111111111111111"
}

View File

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

View File

@@ -1,7 +1,8 @@
-- title: Mr Anderson's Adventure
-- author: Zsolt Tasnadi
-- title: Definitely not an Impostor
-- name: impostor
-- author: Teletype Games
-- desc: Life of a programmer in the Vector
-- site: https://github.com/rastasi/mranderson
-- site: https://git.teletype.hu/games/impostor
-- license: MIT License
-- version: 0.10
-- version: 0.1
-- script: lua

View File

@@ -1,8 +1,25 @@
function Input.up() return btnp(0) end
function Input.down() return btnp(1) 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
function Input.menu_back() return btnp(5) end
-- Gamepad buttons
local INPUT_KEY_UP = 0
local INPUT_KEY_DOWN = 1
local INPUT_KEY_LEFT = 2
local INPUT_KEY_RIGHT = 3
local INPUT_KEY_A = 4 -- Z key
local INPUT_KEY_B = 5 -- X key
local INPUT_KEY_X = 6 -- A key
local INPUT_KEY_Y = 7 -- S key
-- Keyboard keys
-- TODO: Find correct key codes for SPACE and LCTRL
local INPUT_KEY_SPACE = 48
local INPUT_KEY_BACKSPACE = 51
local INPUT_KEY_ENTER = 50
function Input.up() return btnp(INPUT_KEY_UP) end
function Input.down() return btnp(INPUT_KEY_DOWN) end
function Input.left() return btn(INPUT_KEY_LEFT) end
function Input.right() return btn(INPUT_KEY_RIGHT) end
function Input.select() 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

@@ -20,22 +20,24 @@ local STATE_HANDLERS = {
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
local 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

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

View File

@@ -1,6 +1,6 @@
function UI.draw_top_bar(title)
rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
print(title, 3, 2, Config.colors.green)
Print.text(title, 3, 2, Config.colors.green)
end
function UI.draw_dialog()
@@ -11,9 +11,9 @@ 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)
Print.text(">", x - 8, current_y, Config.colors.green)
end
print(item.label, x, current_y, Config.colors.green)
Print.text(item.label, x, current_y, Config.colors.green)
end
end
@@ -77,3 +77,11 @@ function UI.create_numeric_stepper(label, value_getter, value_setter, min, max,
type = "numeric_stepper"
}
end
function UI.create_action_item(label, action)
return {
label = label,
action = action,
type = "action_item"
}
end

View File

@@ -5,17 +5,13 @@ ConfigurationWindow = {
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_action_item(
"Save",
function() Config.save() end
),
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(
"Restore Defaults",
function() Config.restore_defaults() end
),
}
end
@@ -32,37 +28,46 @@ function ConfigurationWindow.draw()
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)
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)
-- 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)
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("Press B to go back", x_start, 120, Config.colors.light_grey)
Print.text("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
@@ -75,16 +80,21 @@ function ConfigurationWindow.update()
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)
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

@@ -2,31 +2,29 @@ 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
if Input.menu_back() then
Context.active_window = WINDOW_MENU
MenuWindow.refresh_menu_items()
return
end
if Input.select() then
if Context.current_screen == #Context.screens then
Context.current_screen = 1
else
Context.current_screen = Context.current_screen + 1
end
end
if Input.player_interact() 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)
end
end
function GameWindow.set_state(new_state)

View File

@@ -1,6 +1,6 @@
function IntroWindow.draw()
local x = (Config.screen.width - 132) / 2 -- Centered text
print(Context.intro.text, x, Context.intro.y, Config.colors.green)
Print.text(Context.intro.text, x, Context.intro.y, Config.colors.green)
end
function IntroWindow.update()

View File

@@ -1,34 +0,0 @@
function InventoryWindow.draw()
UI.draw_top_bar("Inventory")
if #Context.inventory == 0 then
print("Inventory is empty.", 70, 70, Config.colors.light_grey)
else
for i, item in ipairs(Context.inventory) do
local color = Config.colors.light_grey
if i == Context.selected_inventory_item then
color = Config.colors.green
print(">", 60, 20 + i * 10, color)
end
print(item.name, 70, 20 + i * 10, color)
end
end
end
function InventoryWindow.update()
Context.selected_inventory_item = UI.update_menu(Context.inventory, Context.selected_inventory_item)
if Input.menu_confirm() and #Context.inventory > 0 then
local selected_item = Context.inventory[Context.selected_inventory_item]
PopupWindow.show_menu_dialog(selected_item, {
{label = "Use", action = 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

@@ -14,14 +14,21 @@ function MenuWindow.update()
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
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
@@ -34,9 +41,20 @@ function MenuWindow.configuration()
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}
}
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

View File

@@ -41,7 +41,7 @@ function PopupWindow.update()
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
-- No need to change active_window, as it remains in WINDOW_POPUP
end
else
Context.dialog.selected_menu_item = UI.update_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item)
@@ -82,14 +82,14 @@ function PopupWindow.draw()
-- 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)
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(line, 50, current_y, Config.colors.light_grey)
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
@@ -97,6 +97,6 @@ function PopupWindow.draw()
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)
Print.text("[A] Go Back", 50, current_y + 10, Config.colors.green)
end
end

View File

@@ -1,6 +1,9 @@
function SplashWindow.draw()
print("Mr. Anderson's", 78, 60, Config.colors.green)
print("Addventure", 90, 70, Config.colors.green)
local txt = "Definitely not an Impostor"
local w = #txt * 6
local x = (240 - w) / 2
local y = (136 - 6) / 2
print(txt, x, y, 12)
end
function SplashWindow.update()

289
infra.md Normal file
View File

@@ -0,0 +1,289 @@
# Server
```mermaid
graph TD
Internet --> Nginx
Nginx --> Traefik
Traefik --> Gitea
Gitea --> GiteaDB[(Gitea data / SQLite)]
Traefik --> WoodpeckerServer
WoodpeckerServer --> WoodpeckerDB[(Woodpecker data / SQLite)]
WoodpeckerServer --> WoodpeckerAgent
WoodpeckerAgent --> DockerSocket[(Docker)]
Traefik --> WebApp
WebApp --> MySQL[(MySQL)]
WebApp --> Softwares[(Volume)]
Droparea --> Softwares
Nginx --> Discourse
Discourse --> ForumDB[(Postgres)]
Discourse --> Redis[(Redis)]
Nginx --> Wiki
Wiki --> WikiDB[(Postgres)]
```
# TIC-80 Pipeline
This document describes the Woodpecker CI pipeline used to build, export, upload, and publish a TIC-80 game project.
---
## Overview
The pipeline performs the following steps:
1. **Build** the TIC-80 project using a custom Docker image
2. **Export** the game to `.tic` and HTML formats
3. **Upload artifacts** to a remote server via SCP
4. **Notify an update server** to publish the new version
The pipeline is driven by environment variables so it can be reused across projects.
---
## Global Environment
```yaml
environment: &environment
GAME_NAME: mranderson
GAME_LANG: lua
```
- **GAME_NAME**: Project name (used for all outputs)
- **GAME_LANG**: Source language used by TIC-80 (Lua)
The anchor (`&environment`) allows reuse across steps.
---
## Step 1: Build & Export
```yaml
- name: build
image: git.teletype.hu/internal/tic80pro:latest
environment:
<<: *environment
XDG_RUNTIME_DIR: /tmp
commands:
- make build
- make export
```
**What it does:**
- Uses a custom TIC-80 Pro Docker image hosted in Gitea
- Runs the Makefile `build` target to assemble source files
- Runs the `export` target to generate:
- `.tic` cartridge
- `.html.zip` web build
---
## Step 2: Artifact Upload
```yaml
- name: artifact
image: alpine
environment:
<<: *environment
DROPAREA_HOST: vps.teletype.hu
DROPAREA_PORT: 2223
DROPAREA_TARGET_PATH: /home/drop
DROPAREA_USER: drop
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
```
**What it does:**
- Installs SCP tooling in a minimal Alpine container
- Uploads:
- Source file
- TIC-80 cartridge
- HTML export ZIP
- Uses secrets for SSH authentication
---
## Step 3: Update Notification
```yaml
- 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"
```
**What it does:**
- Sends an HTTP request to the update server
- Notifies that a new TIC-80 build is available
- Uses a secret key to authorize the update
---
## Result
After a successful run:
- The game is built and exported
- Artifacts are uploaded to the server
- The public game index is updated automatically
This pipeline enables **fully automated TIC-80 releases** using open tools and infrastructure.
# TIC-80 Makefile Project Builder
This Makefile provides a simple, reproducible workflow for building **TIC-80 Lua projects** from multiple source files. It is designed for small indie or experimental projects where the source code is split into logical parts and then merged into a single `.lua` cartridge.
---
## Overview
The workflow is based on four core ideas:
- Source code is split into multiple Lua files inside an `inc/` directory
- A project-specific `.inc` file defines the **build order**
- All source files are concatenated into one final `.lua` file
- TIC-80 is used in CLI mode to export runnable artifacts
This approach keeps the codebase modular while remaining compatible with TIC-80s single-file cartridge model.
---
## Project Structure
```text
project-root/
├── inc/
│ ├── core.lua
│ ├── player.lua
│ └── world.lua
├── mranderson.inc
├── Makefile
└── README.md
```
- `inc/` contains all Lua source fragments
- `<project>.inc` defines the order in which files are merged
- `<project>.lua` is generated automatically
---
## The `.inc` File
The `.inc` file is a **plain text file** listing Lua source files in build order:
```text
core.lua
player.lua
world.lua
```
The order matters. Files listed earlier are concatenated first and must define any globals used later.
---
## Usage
### Build (default)
```sh
make build
```
- Reads `mranderson.inc`
- Concatenates files from `inc/`
- Produces `mranderson.lua`
### Export (TIC-80)
```sh
make export
```
- Loads the generated Lua file into TIC-80 (CLI mode)
- Saves a `.tic` cartridge
- Exports an HTML build
### Export Assets
```sh
make export_assets
```
- **Purpose**: Extracts asset sections (PALETTE, TILES, SPRITES, MAP, SFX, MUSIC) from the compiled `<project>.lua` file.
- **Mechanism**: Uses `sed` to directly parse the generated `<project>.lua` and saves the extracted data into `inc/meta/meta.assets.lua`. This file can then be used to embed the asset data directly into other parts of the project or for version control of visual assets.
### Import Assets
The `import_assets` target was considered during development but is currently not part of the build workflow. Asset handling for TIC-80 projects within this Makefile relies solely on direct extraction (`export_assets`) from the built Lua cartridge, rather than importing external asset definitions. This target may be implemented in the future if a need for pre-build asset injection arises.
### Watch Mode
```sh
make watch
```
- Performs an initial build
- Watches the `inc/` directory and `.inc` file
- Rebuilds automatically on any change
Requires `fswatch` to be installed.
---
## Generated Artifacts
| File | Description |
|-----|-------------|
| `<project>.lua` | Merged Lua source (input for TIC-80) |
| `<project>.tic` | TIC-80 cartridge |
| `<project>.html` | Web export |
| `<project>.html.zip` | Packaged HTML build |
---
## Design Goals
- Keep TIC-80 projects modular
- Avoid manual copy-paste between files
- Enable fast iteration and experimentation
- Remain fully compatible with open-source tooling
This Makefile is intentionally minimal and transparent, favoring simplicity over abstraction.
---
## Requirements
- `make`
- `tic80` available in PATH
- `fswatch` (only for watch mode)
---
## License
MIT License — free to use, modify, and redistribute.