Compare commits

...

10 Commits

Author SHA1 Message Date
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
30 changed files with 422 additions and 440 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,21 +1,20 @@
environment: &environment
GAME_NAME: impostor
GAME_LANG: lua
steps:
- name: version
image: alpine
commands:
- 'apk add --no-cache make'
- 'make ci-version'
- name: build
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'

113
Makefile
View File

@@ -1,15 +1,8 @@
# -----------------------------------------
# Makefile TIC-80 project builder
# Usage:
# make PROJECT=impostor
# make build PROJECT=impostor
# make watch PROJECT=impostor
# make export PROJECT=impostor
# -----------------------------------------
ifndef PROJECT
$(error Specify the project name: make PROJECT=name)
endif
PROJECT = impostor
ORDER = $(PROJECT).inc
OUTPUT = $(PROJECT).lua
@@ -17,30 +10,106 @@ 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
# 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:
@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

@@ -1,4 +1,4 @@
# Mr. Anderson's Matrix Escape
# Definitely not an Impostor
## Installation
@@ -11,11 +11,3 @@ This game is designed for the TIC-80 fantasy computer. To play, follow these ste
* 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

@@ -6,7 +6,6 @@ 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
@@ -15,7 +14,6 @@ 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.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

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

@@ -12,20 +12,8 @@ local DEFAULT_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
}
@@ -36,36 +24,24 @@ local Config = {
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()
Config.restore_defaults()
-- 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

View File

@@ -6,7 +6,7 @@ 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_selectS_ADDRESS = 5
local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6
local VX_VY_OFFSET = 128 -- Offset for negative velocities
@@ -28,11 +28,10 @@ end
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."
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,
@@ -45,13 +44,6 @@ local function get_initial_data()
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 = {
@@ -62,30 +54,13 @@ local function get_initial_data()
},
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 = {
@@ -141,8 +116,6 @@ local function get_initial_data()
}
},
{
x = 90,
y = 102,
name = "Oracle",
sprite_id = 3,
dialog = {
@@ -202,10 +175,6 @@ local function get_initial_data()
},
items = {
{
x = 100,
y = 128,
w = 8,
h = 8,
name = "Key",
sprite_id = 4,
desc = "A rusty old key. It might open something."
@@ -215,30 +184,8 @@ local function get_initial_data()
{
-- 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 = {
@@ -274,8 +221,6 @@ local function get_initial_data()
}
},
{
x = 40,
y = 92,
name = "Tank",
sprite_id = 6,
dialog = {
@@ -320,10 +265,6 @@ local function get_initial_data()
},
items = {
{
x = 180,
y = 52,
w = 8,
h = 8,
name = "Potion",
sprite_id = 7,
desc = "A glowing red potion. It looks potent."
@@ -333,36 +274,8 @@ local function get_initial_data()
{
-- 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 = {
@@ -404,8 +317,6 @@ local function get_initial_data()
}
},
{
x = 160,
y = 62,
name = "Cypher",
sprite_id = 9,
dialog = {
@@ -492,11 +403,6 @@ 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
@@ -509,11 +415,6 @@ function Context.load_game()
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

View File

@@ -3,7 +3,6 @@ local IntroWindow = {}
local MenuWindow = {}
local GameWindow = {}
local PopupWindow = {}
local InventoryWindow = {}
local ConfigurationWindow = {}
local UI = {}

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

View File

@@ -1,36 +1,3 @@
-- <TILES>
-- 000:4444444444444444444444444444444444444444444444444444444444444444
-- 001:1111111111111111111111111111111111111111111111111111111111111111
-- 002:5555555555555555555555555555555555555555555555555555555555555555
-- 003:6666666666666666666666666666666666666666666666666666666666666666
-- 004:7777777777777777777777777777777777777777777777777777777777777777
-- 005:8888888888888888888888888888888888888888888888888888888888888888
-- 006:9999999999999999999999999999999999999999999999999999999999999999
-- 007:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-- 008:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-- 016:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
-- 017:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-- 018:cc222222cc222222cc222222cc222222222222222222222222222222eeeeeeee
-- 019:222222cc222222cc222222cc222222cc222222222222222222222222eeeeeeee
-- 020:daaaabdddaaaabdddaaaabdddaaaabdddaaaabdddaaaabdddddddddddddddddd
-- 021:f888888ff888888ff888888ff888888f8888888f8888888ffeeeeffeeeeffee
-- 022:e000000ee000000ee000000ee000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
-- </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: Definitely not an Impostor
-- name: impostor
-- author: Teletype Games
-- desc: Life of a programmer in the Vector
-- site: https://git.teletype.hu/games/impostor
-- license: MIT License
-- version: 0.15
-- version: 0.1
-- script: lua

View File

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

View File

@@ -20,15 +20,6 @@ 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()
@@ -37,7 +28,7 @@ local STATE_HANDLERS = {
local initialized_game = false
function init_game()
local function init_game()
if initialized_game then return end
MenuWindow.refresh_menu_items()
@@ -45,7 +36,7 @@ function init_game()
end
function TIC()
init_game()
init_game()
cls(Config.colors.black)
local handler = STATE_HANDLERS[Context.active_window]

View File

@@ -5,18 +5,6 @@ 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_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

View File

@@ -2,27 +2,6 @@ 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()
@@ -31,7 +10,21 @@ function GameWindow.update()
MenuWindow.refresh_menu_items()
return
end
Player.update() -- Call the encapsulated player update logic
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,34 +0,0 @@
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

@@ -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)

View File

@@ -1,13 +1,9 @@
function SplashWindow.draw()
for y, row in ipairs(MapBedroom) do
for x = 1, #row, 2 do
local tile_id_hex = string.sub(row, x, x + 1)
local tile_id = tonumber(tile_id_hex, 16)
local screen_x = (x - 1) / 2 * 8
local screen_y = (y - 1) * 8
spr(tile_id, screen_x, screen_y, -1, 1, 0, 0, 1, 1)
end
end
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.