Compare commits
22 Commits
1913d7d7d4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc28849c4 | |||
| 8a6214e893 | |||
| d943b6deaa | |||
| b3b2159d75 | |||
| ae56cf3555 | |||
| 2fc241fee7 | |||
| 4e0145982f | |||
| 1c987fa08b | |||
| b6d0823875 | |||
| ffa82e8f92 | |||
| cfc07afe59 | |||
| 3fbce5aced | |||
| b3158bdc37 | |||
| 4907c9cadf | |||
| 45f2e746d6 | |||
| 8e8d181c34 | |||
| 5379e30cf3 | |||
| d3fb12703c | |||
| f9c539a854 | |||
| a777241d7a | |||
| 34340d9664 | |||
| 6a39128962 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
mranderson.lua
|
.local
|
||||||
|
impostor.lua
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
environment: &environment
|
|
||||||
GAME_NAME: mranderson
|
|
||||||
GAME_LANG: lua
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: version
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- 'apk add --no-cache make'
|
||||||
|
- 'make ci-version'
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: gitea.vps.teletype.hu/games/tic80pro:latest
|
image: git.teletype.hu/internal/tic80pro:latest
|
||||||
environment:
|
environment:
|
||||||
<<: *environment
|
|
||||||
XDG_RUNTIME_DIR: /tmp
|
XDG_RUNTIME_DIR: /tmp
|
||||||
commands:
|
commands:
|
||||||
- make build PROJECT=$GAME_NAME
|
- 'make ci-export'
|
||||||
- make export PROJECT=$GAME_NAME
|
|
||||||
|
|
||||||
- name: artifact
|
- name: artifact
|
||||||
image: alpine
|
image: alpine
|
||||||
environment:
|
environment:
|
||||||
<<: *environment
|
|
||||||
DROPAREA_HOST: vps.teletype.hu
|
DROPAREA_HOST: vps.teletype.hu
|
||||||
DROPAREA_PORT: 2223
|
DROPAREA_PORT: 2223
|
||||||
DROPAREA_TARGET_PATH: /home/drop
|
DROPAREA_TARGET_PATH: /home/drop
|
||||||
@@ -23,17 +22,15 @@ steps:
|
|||||||
DROPAREA_SSH_PASSWORD:
|
DROPAREA_SSH_PASSWORD:
|
||||||
from_secret: droparea_ssh_password
|
from_secret: droparea_ssh_password
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache openssh-client sshpass
|
- 'apk add --no-cache make openssh-client sshpass'
|
||||||
- mkdir -p /root/.ssh
|
- 'make ci-upload'
|
||||||
- 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
|
|
||||||
|
|
||||||
- name: update
|
- name: update
|
||||||
image: alpine
|
image: alpine
|
||||||
environment:
|
environment:
|
||||||
<<: *environment
|
|
||||||
UPDATE_SERVER: https://games.vps.teletype.hu
|
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||||
UPDATE_SECRET:
|
UPDATE_SECRET:
|
||||||
from_secret: update_secret_key
|
from_secret: update_secret_key
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache curl
|
- 'apk add --no-cache make curl'
|
||||||
- curl "$UPDATE_SERVER/update?secret=$UPDATE_SECRET&name=$GAME_NAME&platform=tic80"
|
- 'make ci-update'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# TIC-80 Lua Code Regularities
|
# 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
|
## 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.
|
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.
|
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
|
## Code Style
|
||||||
|
|
||||||
|
|||||||
133
Makefile
133
Makefile
@@ -1,15 +1,8 @@
|
|||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# Makefile – TIC-80 project builder
|
# Makefile – TIC-80 project builder
|
||||||
# Usage:
|
|
||||||
# make PROJECT=mranderson
|
|
||||||
# make build PROJECT=mranderson
|
|
||||||
# make watch PROJECT=mranderson
|
|
||||||
# make export PROJECT=mranderson
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
|
|
||||||
ifndef PROJECT
|
PROJECT = impostor
|
||||||
$(error Specify the project name: make PROJECT=name)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ORDER = $(PROJECT).inc
|
ORDER = $(PROJECT).inc
|
||||||
OUTPUT = $(PROJECT).lua
|
OUTPUT = $(PROJECT).lua
|
||||||
@@ -17,30 +10,126 @@ OUTPUT_ZIP = $(PROJECT).html.zip
|
|||||||
OUTPUT_TIC = $(PROJECT).tic
|
OUTPUT_TIC = $(PROJECT).tic
|
||||||
|
|
||||||
SRC_DIR = inc
|
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
|
all: build
|
||||||
|
|
||||||
build: $(OUTPUT)
|
build: $(OUTPUT)
|
||||||
@echo "==> Build complete: $(OUTPUT)"
|
|
||||||
|
|
||||||
$(OUTPUT): $(SRC) $(ORDER)
|
$(OUTPUT): $(SRC) $(ORDER)
|
||||||
@echo "==> Building $(OUTPUT)..."
|
|
||||||
@rm -f $(OUTPUT)
|
@rm -f $(OUTPUT)
|
||||||
@while read f; do \
|
@while read f; do \
|
||||||
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
|
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
|
||||||
echo "\n" >> $(OUTPUT); \
|
echo "" >> $(OUTPUT); \
|
||||||
done < $(ORDER)
|
done < $(ORDER)
|
||||||
@echo "==> Done."
|
|
||||||
|
|
||||||
export: $(OUTPUT)
|
export: build
|
||||||
@echo "==> TIC-80 export..."
|
@if [ -z "$(VERSION)" ]; then \
|
||||||
tic80 --cli --skip --fs=. \
|
echo "ERROR: VERSION not set!"; \
|
||||||
--cmd="load $(OUTPUT) & save $(PROJECT) & export html $(PROJECT).html & exit"
|
exit 1; \
|
||||||
@echo "==> HTML ZIP: $(OUTPUT_ZIP)"
|
fi
|
||||||
@echo "==> TIC: $(OUTPUT_TIC)"
|
@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:
|
watch:
|
||||||
@echo "==> Watching project: $(PROJECT)"
|
make build
|
||||||
make build PROJECT=$(PROJECT)
|
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
|
||||||
fswatch -o $(SRC_DIR) $(ORDER) | while read; do make build PROJECT=$(PROJECT); 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>
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Mr. Anderson's Matrix Escape
|
# Definitely not an Impostor
|
||||||
|
|
||||||
## Installation
|
## 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. **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.
|
2. **Launch TIC-80:** Start the TIC-80 application.
|
||||||
3. **Load the Game:**
|
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.
|
* Type `load game.lua` and press Enter.
|
||||||
* Once loaded, type `run` and press Enter to start the game.
|
* 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
0
assets/music/.keep
Normal file
0
assets/sfx/.keep
Normal file
0
assets/sfx/.keep
Normal file
0
assets/sprites/.keep
Normal file
0
assets/sprites/.keep
Normal file
0
assets/tiles/.keep
Normal file
0
assets/tiles/.keep
Normal file
BIN
assets_src/26.01.14 Norman szobája v1.6.ase
Normal file
BIN
assets_src/26.01.14 Norman szobája v1.6.ase
Normal file
Binary file not shown.
BIN
assets_src/26.01.20 Iroda v1.0.ase
Normal file
BIN
assets_src/26.01.20 Iroda v1.0.ase
Normal file
Binary file not shown.
BIN
assets_src/26.01.20 Iroda v1.1.ase
Normal file
BIN
assets_src/26.01.20 Iroda v1.1.ase
Normal file
Binary file not shown.
BIN
assets_src/26.01.20 Iroda v1.2.ase
Normal file
BIN
assets_src/26.01.20 Iroda v1.2.ase
Normal file
Binary file not shown.
BIN
assets_src/26.01.20 Norman szobája v2.0.ase
Normal file
BIN
assets_src/26.01.20 Norman szobája v2.0.ase
Normal file
Binary file not shown.
@@ -1,19 +1,19 @@
|
|||||||
meta/meta.header.lua
|
meta/meta.header.lua
|
||||||
|
init/init.modules.lua
|
||||||
init/init.config.lua
|
init/init.config.lua
|
||||||
init/init.windows.lua
|
init/init.windows.lua
|
||||||
init/init.modules.lua
|
|
||||||
init/init.context.lua
|
init/init.context.lua
|
||||||
|
system/system.print.lua
|
||||||
entity/entity.npc.lua
|
entity/entity.npc.lua
|
||||||
entity/entity.item.lua
|
entity/entity.item.lua
|
||||||
entity/entity.player.lua
|
|
||||||
system/system.input.lua
|
system/system.input.lua
|
||||||
system/system.ui.lua
|
system/system.ui.lua
|
||||||
|
map/map.bedroom.lua
|
||||||
window/window.splash.lua
|
window/window.splash.lua
|
||||||
window/window.intro.lua
|
window/window.intro.lua
|
||||||
window/window.menu.lua
|
window/window.menu.lua
|
||||||
window/window.configuration.lua
|
window/window.configuration.lua
|
||||||
window/window.popup.lua
|
window/window.popup.lua
|
||||||
window/window.inventory.lua
|
|
||||||
window/window.game.lua
|
window/window.game.lua
|
||||||
system/system.main.lua
|
system/system.main.lua
|
||||||
meta/meta.assets.lua
|
meta/meta.assets.lua
|
||||||
@@ -1,49 +1,7 @@
|
|||||||
function Item.use()
|
function Item.use()
|
||||||
print("Used item: " .. Context.dialog.active_entity.name)
|
Print.text("Used item: " .. Context.dialog.active_entity.name)
|
||||||
GameWindow.set_state(WINDOW_INVENTORY)
|
|
||||||
end
|
end
|
||||||
function Item.look_at()
|
function Item.look_at()
|
||||||
PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
|
PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
|
||||||
end
|
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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
local Config = {
|
local DEFAULT_CONFIG = {
|
||||||
screen = {
|
screen = {
|
||||||
width = 240,
|
width = 240,
|
||||||
height = 136
|
height = 136
|
||||||
@@ -12,21 +12,38 @@ local Config = {
|
|||||||
item = 12 -- yellow
|
item = 12 -- yellow
|
||||||
},
|
},
|
||||||
player = {
|
player = {
|
||||||
w = 8,
|
|
||||||
h = 8,
|
|
||||||
start_x = 120,
|
|
||||||
start_y = 128,
|
|
||||||
sprite_id = 1
|
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 = {
|
timing = {
|
||||||
splash_duration = 120
|
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()
|
||||||
|
|||||||
@@ -1,432 +1,422 @@
|
|||||||
local Context = {
|
local SAVE_GAME_BANK = 6
|
||||||
active_window = WINDOW_SPLASH,
|
local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
|
||||||
inventory = {},
|
local SAVE_GAME_MAGIC_VALUE = 0xCA
|
||||||
intro = {
|
|
||||||
y = Config.screen.height,
|
local SAVE_GAME_PLAYER_X_ADDRESS = 1
|
||||||
speed = 0.5,
|
local SAVE_GAME_PLAYER_Y_ADDRESS = 2
|
||||||
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."
|
local SAVE_GAME_PLAYER_VX_ADDRESS = 3
|
||||||
},
|
local SAVE_GAME_PLAYER_VY_ADDRESS = 4
|
||||||
current_screen = 1,
|
local SAVE_GAME_selectS_ADDRESS = 5
|
||||||
splash_timer = Config.timing.splash_duration,
|
local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6
|
||||||
dialog = {
|
|
||||||
text = "",
|
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 = {},
|
menu_items = {},
|
||||||
selected_menu_item = 1,
|
selected_menu_item = 1,
|
||||||
active_entity = nil,
|
game_in_progress = false, -- New flag
|
||||||
showing_description = false,
|
screens = clone_table({
|
||||||
current_node_key = nil
|
{
|
||||||
},
|
-- Screen 1
|
||||||
player = {
|
name = "Screen 1",
|
||||||
x = Config.player.start_x,
|
npcs = {
|
||||||
y = Config.player.start_y,
|
{
|
||||||
w = Config.player.w,
|
name = "Trinity",
|
||||||
h = Config.player.h,
|
sprite_id = 2,
|
||||||
vx = 0,
|
dialog = {
|
||||||
vy = 0,
|
start = {
|
||||||
jumps = 0,
|
text = "Hello, Neo.",
|
||||||
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.",
|
|
||||||
options = {
|
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 = {
|
who_are_you = {
|
||||||
text = "Morpheus believes you are The One.",
|
text = "I am Trinity. I've been looking for you.",
|
||||||
options = {
|
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 = {
|
not_neo = {
|
||||||
text = "The one who will save us all.",
|
text = "I know. But you will be.",
|
||||||
options = {
|
options = {
|
||||||
{label = "I'm just a programmer.", next_node = "dialog_end"}
|
{label = "What are you talking about?", next_node = "who_are_you"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dialog_end = {
|
silent = {
|
||||||
text = "We'll talk later.",
|
text = "You're not much of a talker, are you?",
|
||||||
options = {} -- No options, ends conversation
|
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 = {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
items = {
|
||||||
x = 90,
|
{
|
||||||
y = 102,
|
name = "Key",
|
||||||
name = "Oracle",
|
sprite_id = 4,
|
||||||
sprite_id = 3,
|
desc = "A rusty old key. It might open something."
|
||||||
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 = {
|
{
|
||||||
{
|
-- Screen 2
|
||||||
x = 100,
|
name = "Screen 2",
|
||||||
y = 128,
|
npcs = {
|
||||||
w = 8,
|
{
|
||||||
h = 8,
|
name = "Morpheus",
|
||||||
name = "Key",
|
sprite_id = 5,
|
||||||
sprite_id = 4,
|
dialog = {
|
||||||
desc = "A rusty old key. It might open something."
|
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
|
||||||
@@ -3,10 +3,10 @@ local IntroWindow = {}
|
|||||||
local MenuWindow = {}
|
local MenuWindow = {}
|
||||||
local GameWindow = {}
|
local GameWindow = {}
|
||||||
local PopupWindow = {}
|
local PopupWindow = {}
|
||||||
local InventoryWindow = {}
|
|
||||||
local ConfigurationWindow = {}
|
local ConfigurationWindow = {}
|
||||||
|
|
||||||
local UI = {}
|
local UI = {}
|
||||||
|
local Print = {}
|
||||||
local Input = {}
|
local Input = {}
|
||||||
local NPC = {}
|
local NPC = {}
|
||||||
local Item = {}
|
local Item = {}
|
||||||
|
|||||||
@@ -3,6 +3,4 @@ local WINDOW_INTRO = 1
|
|||||||
local WINDOW_MENU = 2
|
local WINDOW_MENU = 2
|
||||||
local WINDOW_GAME = 3
|
local WINDOW_GAME = 3
|
||||||
local WINDOW_POPUP = 4
|
local WINDOW_POPUP = 4
|
||||||
local WINDOW_INVENTORY = 5
|
|
||||||
local WINDOW_INVENTORY_ACTION = 6
|
|
||||||
local WINDOW_CONFIGURATION = 7
|
local WINDOW_CONFIGURATION = 7
|
||||||
|
|||||||
19
inc/map/map.bedroom.lua
Normal file
19
inc/map/map.bedroom.lua
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
MapBedroom = {
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"10141410101010101010101010101010",
|
||||||
|
"10141410101010101010101010101010",
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"10101010101010101010101010101010",
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
"11111516111213111111111111111111",
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
"11111111111111111111111111111111"
|
||||||
|
}
|
||||||
@@ -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>
|
-- <PALETTE>
|
||||||
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
|
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
|
||||||
-- </PALETTE>
|
-- </PALETTE>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
-- title: Mr Anderson's Adventure
|
-- title: Definitely not an Impostor
|
||||||
-- author: Zsolt Tasnadi
|
-- name: impostor
|
||||||
|
-- author: Teletype Games
|
||||||
-- desc: Life of a programmer in the Vector
|
-- desc: Life of a programmer in the Vector
|
||||||
-- site: https://github.com/rastasi/mranderson
|
-- site: https://git.teletype.hu/games/impostor
|
||||||
-- license: MIT License
|
-- license: MIT License
|
||||||
-- version: 0.10
|
-- version: 0.1
|
||||||
-- script: lua
|
-- script: lua
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
function Input.up() return btnp(0) end
|
-- Gamepad buttons
|
||||||
function Input.down() return btnp(1) end
|
local INPUT_KEY_UP = 0
|
||||||
function Input.left() return btnp(2) end
|
local INPUT_KEY_DOWN = 1
|
||||||
function Input.right() return btnp(3) end
|
local INPUT_KEY_LEFT = 2
|
||||||
function Input.player_jump() return btnp(4) end
|
local INPUT_KEY_RIGHT = 3
|
||||||
function Input.menu_confirm() return btnp(4) end
|
local INPUT_KEY_A = 4 -- Z key
|
||||||
function Input.player_interact() return btnp(5) end -- B button
|
local INPUT_KEY_B = 5 -- X key
|
||||||
function Input.menu_back() return btnp(5) end
|
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
|
||||||
|
|||||||
@@ -20,22 +20,24 @@ local STATE_HANDLERS = {
|
|||||||
PopupWindow.update()
|
PopupWindow.update()
|
||||||
PopupWindow.draw()
|
PopupWindow.draw()
|
||||||
end,
|
end,
|
||||||
[WINDOW_INVENTORY] = function()
|
|
||||||
InventoryWindow.update()
|
|
||||||
InventoryWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_INVENTORY_ACTION] = function()
|
|
||||||
InventoryWindow.draw()
|
|
||||||
PopupWindow.draw()
|
|
||||||
PopupWindow.update()
|
|
||||||
end,
|
|
||||||
[WINDOW_CONFIGURATION] = function()
|
[WINDOW_CONFIGURATION] = function()
|
||||||
ConfigurationWindow.update()
|
ConfigurationWindow.update()
|
||||||
ConfigurationWindow.draw()
|
ConfigurationWindow.draw()
|
||||||
end,
|
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()
|
function TIC()
|
||||||
|
init_game()
|
||||||
|
|
||||||
cls(Config.colors.black)
|
cls(Config.colors.black)
|
||||||
local handler = STATE_HANDLERS[Context.active_window]
|
local handler = STATE_HANDLERS[Context.active_window]
|
||||||
if handler then
|
if handler then
|
||||||
|
|||||||
8
inc/system/system.print.lua
Normal file
8
inc/system/system.print.lua
Normal 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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
function UI.draw_top_bar(title)
|
function UI.draw_top_bar(title)
|
||||||
rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
|
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
|
end
|
||||||
|
|
||||||
function UI.draw_dialog()
|
function UI.draw_dialog()
|
||||||
@@ -11,9 +11,9 @@ function UI.draw_menu(items, selected_item, x, y)
|
|||||||
for i, item in ipairs(items) do
|
for i, item in ipairs(items) do
|
||||||
local current_y = y + (i-1)*10
|
local current_y = y + (i-1)*10
|
||||||
if i == selected_item then
|
if i == selected_item then
|
||||||
print(">", x - 8, current_y, Config.colors.green)
|
Print.text(">", x - 8, current_y, Config.colors.green)
|
||||||
end
|
end
|
||||||
print(item.label, x, current_y, Config.colors.green)
|
Print.text(item.label, x, current_y, Config.colors.green)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -77,3 +77,11 @@ function UI.create_numeric_stepper(label, value_getter, value_setter, min, max,
|
|||||||
type = "numeric_stepper"
|
type = "numeric_stepper"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function UI.create_action_item(label, action)
|
||||||
|
return {
|
||||||
|
label = label,
|
||||||
|
action = action,
|
||||||
|
type = "action_item"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|||||||
@@ -5,17 +5,13 @@ ConfigurationWindow = {
|
|||||||
|
|
||||||
function ConfigurationWindow.init()
|
function ConfigurationWindow.init()
|
||||||
ConfigurationWindow.controls = {
|
ConfigurationWindow.controls = {
|
||||||
UI.create_numeric_stepper(
|
UI.create_action_item(
|
||||||
"Move Speed",
|
"Save",
|
||||||
function() return Config.physics.move_speed end,
|
function() Config.save() end
|
||||||
function(v) Config.physics.move_speed = v end,
|
|
||||||
0.5, 3, 0.1, "%.1f"
|
|
||||||
),
|
),
|
||||||
UI.create_numeric_stepper(
|
UI.create_action_item(
|
||||||
"Max Jumps",
|
"Restore Defaults",
|
||||||
function() return Config.physics.max_jumps end,
|
function() Config.restore_defaults() end
|
||||||
function(v) Config.physics.max_jumps = v end,
|
|
||||||
1, 5, 1, "%d"
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -31,38 +27,47 @@ function ConfigurationWindow.draw()
|
|||||||
for i, control in ipairs(ConfigurationWindow.controls) do
|
for i, control in ipairs(ConfigurationWindow.controls) do
|
||||||
local current_y = y_start + (i - 1) * 12
|
local current_y = y_start + (i - 1) * 12
|
||||||
local color = Config.colors.green
|
local color = Config.colors.green
|
||||||
|
|
||||||
local value = control.get()
|
if control.type == "numeric_stepper" then
|
||||||
local label_text = control.label
|
local value = control.get()
|
||||||
local value_text = string.format(control.format, value)
|
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
|
if i == ConfigurationWindow.selected_control then
|
||||||
print("<", x_start -8, current_y, color)
|
color = Config.colors.item
|
||||||
print(label_text, x_start, current_y, color) -- Shift label due to '<'
|
Print.text("<", x_start -8, current_y, color)
|
||||||
print(value_text, value_x, current_y, color)
|
Print.text(label_text, x_start, current_y, color) -- Shift label due to '<'
|
||||||
print(">", x_value_right_align + 4, current_y, color) -- Print '>' after value
|
Print.text(value_text, value_x, current_y, color)
|
||||||
else
|
Print.text(">", x_value_right_align + 4, current_y, color) -- Print '>' after value
|
||||||
print(label_text, x_start, current_y, color)
|
else
|
||||||
print(value_text, value_x, current_y, color)
|
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
|
||||||
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
|
end
|
||||||
|
|
||||||
function ConfigurationWindow.update()
|
function ConfigurationWindow.update()
|
||||||
if Input.menu_back() then
|
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)
|
GameWindow.set_state(WINDOW_MENU)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Navigate between controls
|
|
||||||
if Input.up() then
|
if Input.up() then
|
||||||
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1
|
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1
|
||||||
if ConfigurationWindow.selected_control < 1 then
|
if ConfigurationWindow.selected_control < 1 then
|
||||||
@@ -75,16 +80,21 @@ function ConfigurationWindow.update()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Modify control value
|
|
||||||
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
|
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
|
||||||
if control then
|
if control then
|
||||||
local current_value = control.get()
|
if control.type == "numeric_stepper" then
|
||||||
if Input.left() then
|
local current_value = control.get()
|
||||||
local new_value = math.max(control.min, current_value - control.step)
|
if btnp(2) then -- Left
|
||||||
control.set(new_value)
|
local new_value = math.max(control.min, current_value - control.step)
|
||||||
elseif Input.right() then
|
control.set(new_value)
|
||||||
local new_value = math.min(control.max, current_value + control.step)
|
elseif btnp(3) then -- Right
|
||||||
control.set(new_value)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,31 +2,29 @@ function GameWindow.draw()
|
|||||||
local currentScreenData = Context.screens[Context.current_screen]
|
local currentScreenData = Context.screens[Context.current_screen]
|
||||||
|
|
||||||
UI.draw_top_bar(currentScreenData.name)
|
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
|
end
|
||||||
|
|
||||||
function GameWindow.update()
|
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
|
end
|
||||||
|
|
||||||
function GameWindow.set_state(new_state)
|
function GameWindow.set_state(new_state)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
function IntroWindow.draw()
|
function IntroWindow.draw()
|
||||||
local x = (Config.screen.width - 132) / 2 -- Centered text
|
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
|
end
|
||||||
|
|
||||||
function IntroWindow.update()
|
function IntroWindow.update()
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -14,14 +14,21 @@ function MenuWindow.update()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function MenuWindow.play()
|
function MenuWindow.new_game()
|
||||||
-- Reset player state and screen for a new game
|
Context.new_game() -- This function will be created in Context
|
||||||
Context.player.x = Config.player.start_x
|
GameWindow.set_state(WINDOW_GAME)
|
||||||
Context.player.y = Config.player.start_y
|
end
|
||||||
Context.player.vx = 0
|
|
||||||
Context.player.vy = 0
|
function MenuWindow.load_game()
|
||||||
Context.player.jumps = 0
|
Context.load_game() -- This function will be created in Context
|
||||||
Context.current_screen = 1
|
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)
|
GameWindow.set_state(WINDOW_GAME)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -34,9 +41,20 @@ function MenuWindow.configuration()
|
|||||||
GameWindow.set_state(WINDOW_CONFIGURATION)
|
GameWindow.set_state(WINDOW_CONFIGURATION)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Initialize menu items after actions are defined
|
function MenuWindow.refresh_menu_items()
|
||||||
Context.menu_items = {
|
Context.menu_items = {} -- Start with an empty table
|
||||||
{label = "Play", action = MenuWindow.play},
|
|
||||||
{label = "Configuration", action = MenuWindow.configuration},
|
if Context.game_in_progress then
|
||||||
{label = "Exit", action = MenuWindow.exit}
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function PopupWindow.update()
|
|||||||
if Input.menu_confirm() or Input.menu_back() then
|
if Input.menu_confirm() or Input.menu_back() then
|
||||||
Context.dialog.showing_description = false
|
Context.dialog.showing_description = false
|
||||||
Context.dialog.text = "" -- Clear the description text
|
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
|
end
|
||||||
else
|
else
|
||||||
Context.dialog.selected_menu_item = UI.update_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item)
|
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
|
-- Display the entity's name as the dialog title
|
||||||
if Context.dialog.active_entity and Context.dialog.active_entity.name then
|
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
|
end
|
||||||
|
|
||||||
-- Display the dialog content (description for "look at", or initial name/dialog for others)
|
-- 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 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
|
local current_y = 55 -- Starting Y position for the first line of content
|
||||||
for _, line in ipairs(wrapped_lines) do
|
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)
|
current_y = current_y + 8 -- Move to the next line (8 pixels for default font height + padding)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,6 +97,6 @@ function PopupWindow.draw()
|
|||||||
if not Context.dialog.showing_description then
|
if not Context.dialog.showing_description then
|
||||||
UI.draw_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item, 50, current_y + 2)
|
UI.draw_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item, 50, current_y + 2)
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
function SplashWindow.draw()
|
function SplashWindow.draw()
|
||||||
print("Mr. Anderson's", 78, 60, Config.colors.green)
|
local txt = "Definitely not an Impostor"
|
||||||
print("Addventure", 90, 70, Config.colors.green)
|
local w = #txt * 6
|
||||||
|
local x = (240 - w) / 2
|
||||||
|
local y = (136 - 6) / 2
|
||||||
|
print(txt, x, y, 12)
|
||||||
end
|
end
|
||||||
|
|
||||||
function SplashWindow.update()
|
function SplashWindow.update()
|
||||||
|
|||||||
289
infra.md
Normal file
289
infra.md
Normal 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-80’s 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.
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user