Compare commits

..

1 Commits

Author SHA1 Message Date
787b6656b0 docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-21 23:53:36 +01:00
95 changed files with 1504 additions and 3454 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,4 @@
.local .local
impostor.lua impostor.lua
impostor.original.lua
prompts prompts
docs docs
minify.lua

View File

@@ -2,12 +2,6 @@
-- Configuration for luacheck -- Configuration for luacheck
globals = { globals = {
"Focus",
"Day",
"Timer",
"Glitch",
"Trigger",
"Discussion",
"Util", "Util",
"Decision", "Decision",
"Situation", "Situation",
@@ -17,16 +11,11 @@ globals = {
"Print", "Print",
"Input", "Input",
"Audio", "Audio",
"AsciiArt",
"Config",
"Context", "Context",
"Meter", "Meters",
"Minigame", "Minigames",
"Window", "SplashWindow",
"ContinuedWindow", "IntroWindow",
"TTGIntroWindow",
"BriefIntroWindow",
"TitleIntroWindow",
"MenuWindow", "MenuWindow",
"GameWindow", "GameWindow",
"PopupWindow", "PopupWindow",
@@ -35,9 +24,6 @@ globals = {
"MinigameButtonMashWindow", "MinigameButtonMashWindow",
"MinigameRhythmWindow", "MinigameRhythmWindow",
"MinigameDDRWindow", "MinigameDDRWindow",
"MysteriousManWindow",
"DiscussionWindow",
"EndWindow",
"mset", "mset",
"mget", "mget",
"btnp", "btnp",
@@ -51,8 +37,6 @@ globals = {
"circb", "circb",
"cls", "cls",
"tri", "tri",
"pix",
"line",
"Songs", "Songs",
"frame_from_beat", "frame_from_beat",
"beats_to_pattern", "beats_to_pattern",

View File

@@ -14,10 +14,7 @@
"inc/?.lua" "inc/?.lua"
], ],
"Lua.diagnostics.disable": [ "Lua.diagnostics.disable": [
"undefined-global", "undefined-global"
"undefined-doc-param",
"undefined-doc-name",
"luadoc-miss-param-name"
], ],
"python.autoComplete.extraPaths": [ "python.autoComplete.extraPaths": [
"${workspaceFolder}/sources/poky/bitbake/lib", "${workspaceFolder}/sources/poky/bitbake/lib",

8
.vscode/tasks.json vendored
View File

@@ -11,19 +11,17 @@
{ {
"label": "Build & Run TIC80", "label": "Build & Run TIC80",
"type": "shell", "type": "shell",
"command": "make build && tic80 --fs=. impostor.lua", "command": "make build && tic80 --fs=. impostor.lua"
"problemMatcher": []
}, },
{ {
"label": "Export assets", "label": "Export assets",
"type": "shell", "type": "shell",
"command": "make export_assets", "command": "make export_assets"
"problemMatcher": []
}, },
{ {
"label": "Make build", "label": "Make build",
"type": "shell", "type": "shell",
"command": "make build" "command": "make build"
} },
] ]
} }

View File

@@ -5,32 +5,7 @@ steps:
- 'apk add --no-cache make' - 'apk add --no-cache make'
- 'make ci-version' - 'make ci-version'
- name: lint - name: build
image: alpine
commands:
- 'apk add --no-cache make lua5.4 lua5.4-dev luarocks gcc musl-dev'
- 'ln -sf /usr/bin/lua5.4 /usr/bin/lua'
- 'ln -sf /usr/bin/luarocks-5.4 /usr/bin/luarocks'
- 'luarocks install luacheck'
- 'make ci-lint'
- name: minify
image: alpine
commands:
- 'apk add --no-cache make lua5.4 curl'
- 'ln -sf /usr/bin/lua5.4 /usr/bin/lua'
- 'make ci-minify'
- name: docs
image: alpine
commands:
- 'apk add --no-cache make lua5.4 lua5.4-dev luarocks gcc musl-dev zip'
- 'ln -sf /usr/bin/lua5.4 /usr/bin/lua'
- 'ln -sf /usr/bin/luarocks-5.4 /usr/bin/luarocks'
- 'luarocks install ldoc'
- 'make ci-docs'
- name: export
image: git.teletype.hu/internal/tic80pro:latest image: git.teletype.hu/internal/tic80pro:latest
environment: environment:
XDG_RUNTIME_DIR: /tmp XDG_RUNTIME_DIR: /tmp
@@ -48,14 +23,14 @@ steps:
from_secret: droparea_ssh_password from_secret: droparea_ssh_password
commands: commands:
- 'apk add --no-cache make openssh-client sshpass' - 'apk add --no-cache make openssh-client sshpass'
- 'make ci-artifact' - 'make ci-upload'
- name: update - name: update
image: alpine image: alpine
environment: environment:
UPDATE_SERVER: https://games.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 make curl' - 'apk add --no-cache make curl'
- 'make ci-update' - 'make ci-update'

View File

@@ -55,7 +55,7 @@ Based on the analysis of `impostor.lua`, the following regularities and conventi
--- ---
# Impostor Minigame Documentation # Impostor Minigames Documentation
This document provides comprehensive documentation for all three minigames implemented in the Impostor game: Button Mash, Rhythm, and DDR (Dance Dance Revolution). This document provides comprehensive documentation for all three minigames implemented in the Impostor game: Button Mash, Rhythm, and DDR (Dance Dance Revolution).
@@ -74,7 +74,7 @@ This document provides comprehensive documentation for all three minigames imple
The Impostor game includes three interactive minigames that can be triggered during gameplay. Each minigame presents a unique challenge that the player must complete to progress. All minigames feature: The Impostor game includes three interactive minigames that can be triggered during gameplay. Each minigame presents a unique challenge that the player must complete to progress. All minigames feature:
- **Overlay rendering** - Minigame render over the current game window - **Overlay rendering** - Minigames render over the current game window
- **Progress tracking** - Visual indicators show completion status - **Progress tracking** - Visual indicators show completion status
- **Return mechanism** - Automatic return to the calling window upon completion - **Return mechanism** - Automatic return to the calling window upon completion
- **Visual feedback** - Button presses and hits are visually indicated - **Visual feedback** - Button presses and hits are visually indicated

128
Makefile
View File

@@ -6,13 +6,9 @@ PROJECT = impostor
ORDER = $(PROJECT).inc ORDER = $(PROJECT).inc
OUTPUT = $(PROJECT).lua OUTPUT = $(PROJECT).lua
OUTPUT_ORIGINAL = $(PROJECT).original.lua
OUTPUT_ZIP = $(PROJECT).html.zip OUTPUT_ZIP = $(PROJECT).html.zip
OUTPUT_TIC = $(PROJECT).tic OUTPUT_TIC = $(PROJECT).tic
MINIFY = minify.lua
MINIFY_URL = https://raw.githubusercontent.com/ztimar31/lua-minify-tic80/refs/heads/master/minify.lua
SRC_DIR = inc SRC_DIR = inc
SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER)) SRC = $(shell sed 's|^|$(SRC_DIR)/|' $(ORDER))
@@ -30,26 +26,20 @@ DROPAREA_HOST ?= vps.teletype.hu
DROPAREA_PORT ?= 2223 DROPAREA_PORT ?= 2223
DROPAREA_TARGET_PATH ?= /home/drop DROPAREA_TARGET_PATH ?= /home/drop
DROPAREA_USER ?= drop DROPAREA_USER ?= drop
UPDATE_SERVER ?= https://games.teletype.hu UPDATE_SERVER ?= https://games.vps.teletype.hu
all: build all: build
build: build: $(OUTPUT)
$(OUTPUT): $(SRC) $(ORDER)
@rm -f $(OUTPUT) @rm -f $(OUTPUT)
@sed 's/\r$$//' $(ORDER) | while read f; do \ @sed 's/\r$$//' $(ORDER) | while read f; do \
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \ cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
echo "" >> $(OUTPUT); \ echo "" >> $(OUTPUT); \
done done
download-minify: export: build
@test -f $(MINIFY) || { echo "==> Downloading $(MINIFY)"; curl -fsSL $(MINIFY_URL) -o $(MINIFY); }
minify: build download-minify
@echo "==> Minifying $(OUTPUT)"
@cp $(OUTPUT) $(OUTPUT_ORIGINAL)
@lua $(MINIFY) minify $(OUTPUT_ORIGINAL) > $(OUTPUT)
export: minify
@if [ -z "$(VERSION)" ]; then \ @if [ -z "$(VERSION)" ]; then \
echo "ERROR: VERSION not set!"; \ echo "ERROR: VERSION not set!"; \
exit 1; \ exit 1; \
@@ -68,10 +58,10 @@ export: minify
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true @ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
watch: watch:
$(MAKE) build make build
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do $(MAKE) build; done fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do make build; done
import_assets: build import_assets: $(OUTPUT)
@TIC_CMD="load $(OUTPUT) &"; \ @TIC_CMD="load $(OUTPUT) &"; \
for t in $(ASSET_TYPES); do \ for t in $(ASSET_TYPES); do \
for f in $(ASSETS_DIR)/$$t/*.png; do \ for f in $(ASSETS_DIR)/$$t/*.png; do \
@@ -164,13 +154,43 @@ export_assets:
@$(call f_export_asset_awk,MAP,$(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,SFX,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,WAVES,$(OUTPUT),$(ASSETS_LUA)) @$(call f_export_asset_awk,WAVES,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,PATTERNS,$(OUTPUT),$(ASSETS_LUA))
@$(call f_export_asset_awk,TRACKS,$(OUTPUT),$(ASSETS_LUA))
clean: clean:
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(PROJECT)-*-docs.zip $(PROJECT)-docs.zip $(OUTPUT) $(OUTPUT_ORIGINAL) @rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(OUTPUT)
@echo "==> Cleaned build artifacts" @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}}; \
BRANCH=$$(echo "$$BRANCH" | tr '/' '-'); \
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"
install_precommit_hook: install_precommit_hook:
@echo "Installing Git pre-commit hook (lint check)..." @echo "Installing Git pre-commit hook (lint check)..."
@mkdir -p .git/hooks @mkdir -p .git/hooks
@@ -199,62 +219,12 @@ docs: build
@ldoc ${OUTPUT} -d docs @ldoc ${OUTPUT} -d docs
@echo "==> Documentation generated." @echo "==> Documentation generated."
# ----------------------------------------- .PHONY: all build export watch import_assets export_assets clean lint ci-version ci-export ci-upload ci-update install_precommit_hook docs
# CI/CD Pipeline targets
# -----------------------------------------
ci-version: #-- <WAVES>
@VERSION=$$(sed -n "s/^-- version: //p" inc/meta/meta.header.lua | head -n 1 | tr -d "[:space:]"); \ #-- 000:224578acdeeeeddcba95434567653100
BRANCH=$${CI_COMMIT_BRANCH:-$${WOODPECKER_BRANCH}}; \ #-- </WAVES>
BRANCH=$$(echo "$$BRANCH" | tr '/' '-'); \ #
if [ "$$BRANCH" != "main" ] && [ "$$BRANCH" != "master" ] && [ -n "$$BRANCH" ]; then \ #-- <SFX>
VERSION=dev-$$VERSION-$$BRANCH; \ #-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000
fi; \ #-- </SFX>
echo "VERSION is: $$VERSION"; \
echo $$VERSION > $(VERSION_FILE)
ci-lint: lint
ci-minify: minify
ci-docs:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Generating docs from $(OUTPUT_ORIGINAL)"; \
ldoc $(OUTPUT_ORIGINAL) -d docs; \
echo "==> Zipping docs for version $$VERSION"; \
(cd docs && zip -r ../$(PROJECT)-$$VERSION-docs.zip .); \
cp $(PROJECT)-$$VERSION-docs.zip $(PROJECT)-docs.zip; \
echo "==> Docs zip created"
ci-export:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Exporting HTML for version $$VERSION"; \
tic80 --cli --skip --fs=. \
--cmd="load $(OUTPUT) & save $(PROJECT)-$$VERSION & export html $(PROJECT)-$$VERSION.html & exit"; \
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
ci-artifact:
@VERSION=$$(cat $(VERSION_FILE)); \
echo "==> Uploading artifacts for version $$VERSION"; \
cp $(PROJECT).lua $(PROJECT)-$$VERSION.lua; \
SCP_TARGET="$(DROPAREA_USER)@$(DROPAREA_HOST):$(DROPAREA_TARGET_PATH)/"; \
sshpass -p "$(DROPAREA_SSH_PASSWORD)" scp -o StrictHostKeyChecking=no -P $(DROPAREA_PORT) \
$(PROJECT)-$$VERSION.lua \
$(PROJECT)-$$VERSION.tic \
$(PROJECT)-$$VERSION.html.zip \
$(PROJECT)-$$VERSION-docs.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 download-minify minify export watch import_assets export_assets clean lint install_precommit_hook docs ci-version ci-lint ci-minify ci-docs ci-export ci-artifact ci-update

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

View File

@@ -1 +0,0 @@
0404005f574fc2c3c71d2b53fff1e8ab52367e2553ffa30000875129adff83769c00e436fa77a8ff004dc3c3c7ffccaa

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -1,22 +1,10 @@
meta/meta.header.lua meta/meta.header.lua
init/init.module.lua init/init.modules.lua
init/init.config.lua init/init.config.lua
init/init.context.lua init/init.minigames.lua
init/init.meters.lua
system/system.util.lua system/system.util.lua
system/system.print.lua init/init.windows.lua
system/system.input.lua
system/system.asciiart.lua
logic/logic.meter.lua
logic/logic.focus.lua
logic/logic.day.lua
logic/logic.timer.lua
logic/logic.trigger.lua
logic/logic.minigame.lua
logic/logic.glitch.lua
logic/logic.discussion.lua
system/system.ui.lua
audio/audio.manager.lua
audio/audio.songs.lua
sprite/sprite.manager.lua sprite/sprite.manager.lua
sprite/sprite.norman.lua sprite/sprite.norman.lua
situation/situation.manager.lua situation/situation.manager.lua
@@ -27,29 +15,26 @@ decision/decision.go_to_home.lua
decision/decision.go_to_toilet.lua decision/decision.go_to_toilet.lua
decision/decision.go_to_walking_to_office.lua decision/decision.go_to_walking_to_office.lua
decision/decision.go_to_office.lua decision/decision.go_to_office.lua
decision/decision.go_to_end.lua
decision/decision.go_to_walking_to_home.lua decision/decision.go_to_walking_to_home.lua
decision/decision.go_to_sleep.lua decision/decision.play_button_mash.lua
decision/decision.do_work.lua decision/decision.play_rhythm.lua
decision/decision.start_discussion.lua decision/decision.play_ddr.lua
discussion/discussion.sumphore.lua
map/map.manager.lua map/map.manager.lua
map/map.bedroom.lua map/map.bedroom.lua
map/map.street.lua
map/map.office.lua
screen/screen.manager.lua screen/screen.manager.lua
screen/screen.home.lua screen/screen.home.lua
screen/screen.toilet.lua screen/screen.toilet.lua
screen/screen.walking_to_office.lua screen/screen.walking_to_office.lua
screen/screen.office.lua screen/screen.office.lua
screen/screen.walking_to_home.lua screen/screen.walking_to_home.lua
screen/screen.work.lua init/init.context.lua
window/window.manager.lua data/data.songs.lua
window/window.register.lua system/system.print.lua
window/window.end.lua system/system.input.lua
window/window.intro.title.lua system/system.audio.lua
window/window.intro.ttg.lua system/system.ui.lua
window/window.intro.brief.lua window/window.splash.lua
window/window.intro.lua
window/window.menu.lua window/window.menu.lua
window/window.configuration.lua window/window.configuration.lua
window/window.audiotest.lua window/window.audiotest.lua
@@ -57,9 +42,6 @@ window/window.popup.lua
window/window.minigame.mash.lua window/window.minigame.mash.lua
window/window.minigame.rhythm.lua window/window.minigame.rhythm.lua
window/window.minigame.ddr.lua window/window.minigame.ddr.lua
window/window.mysterious_man.lua
window/window.discussion.lua
window/window.continued.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

View File

@@ -1,48 +0,0 @@
--- @section Audio
--- Stops current music.
--- @within Audio
function Audio.music_stop() music() end
--- Plays main menu music.
--- @within Audio
function Audio.music_play_mainmenu() end
--- Plays waking up music.
--- @within Audio
function Audio.music_play_wakingup() end
--- Plays room morning music.
--- @within Audio
function Audio.music_play_room_morning() end
--- Plays room street 1 music.
--- @within Audio
function Audio.music_play_room_street_1() end
--- Plays room street 2 music.
--- @within Audio
function Audio.music_play_room_street_2() end
--- Plays room music.
--- @within Audio
function Audio.music_play_room_() end
--- Plays room work music.
--- @within Audio
function Audio.music_play_room_work() music(0) end
--- Plays activity work music.
--- @within Audio
function Audio.music_play_activity_work() music(1) end
--- Plays select sound effect.
--- @within Audio
function Audio.sfx_select() sfx(17, 'C-7', 30) end
--- Plays deselect sound effect.
--- @within Audio
function Audio.sfx_deselect() sfx(18, 'C-7', 30) end
--- Plays beep sound effect.
--- @within Audio
function Audio.sfx_beep() sfx(19, 'C-6', 30) end
--- Plays success sound effect.
--- @within Audio
function Audio.sfx_success() sfx(16, 'C-7', 60) end
--- Plays bloop sound effect.
--- @within Audio
function Audio.sfx_bloop() sfx(21, 'C-3', 60) end
--- Plays alarm sound effect.
--- @within Audio
function Audio.sfx_alarm() sfx(61) end

View File

@@ -1,7 +1,6 @@
--- @section Songs
-- DDR Arrow Spawn Patterns -- DDR Arrow Spawn Patterns
-- Each song defines when arrows should spawn, synced to music beats -- Each song defines when arrows should spawn, synced to music beats
Songs = { Songs = {
-- Example song pattern -- Example song pattern
test_song = { test_song = {
@@ -108,12 +107,8 @@ Songs = {
} }
} }
--- Converts beats to frames. -- Helper function to calculate frame from beat
--- @within Songs -- Usage: frame_from_beat(beat_number, bpm, fps)
--- @param beat number The beat number.
--- @param bpm number Beats per minute.
--- @param[opt] fps number Frames per second (default: 60).
--- @return number The corresponding frame number.
function frame_from_beat(beat, bpm, fps) function frame_from_beat(beat, bpm, fps)
fps = fps or 60 fps = fps or 60
local seconds_per_beat = 60 / bpm local seconds_per_beat = 60 / bpm
@@ -121,17 +116,8 @@ function frame_from_beat(beat, bpm, fps)
return math.floor(beat * frames_per_beat) return math.floor(beat * frames_per_beat)
end end
--- Converts beat notation to frame pattern. -- Helper function to convert simple beat notation to frame pattern
--- @within Songs -- Usage: beats_to_pattern({{1, "left"}, {2, "down"}}, 120)
--- @param beats table A table of beat data, e.g., {{1, "left"}, {2, "down"}}.
--- @param beats.1 number The beat number.
--- @param beats.2 string Arrow direction ("left", "down", "up", or "right").
--- @param bpm number Beats per minute.
--- @param[opt] fps number Frames per second (default: 60).
--- @return result table The generated pattern or nil. </br>
--- Fields: </br>
--- * frame (number) The frame number when the arrow should spawn.<br/>
--- * dir (string) Arrow direction ("left", "down", "up", or "right").<br/>
function beats_to_pattern(beats, bpm, fps) function beats_to_pattern(beats, bpm, fps)
fps = fps or 60 fps = fps or 60
local pattern = {} local pattern = {}

View File

@@ -1,15 +0,0 @@
Decision.register({
id = "do_work",
label = "Do Work",
handle = function()
Meter.hide()
Util.go_to_screen_by_id("work")
MinigameDDRWindow.start("game", nil, {
on_win = function()
Meter.show()
Util.go_to_screen_by_id("office")
Window.set_current("game")
end,
})
end,
})

View File

@@ -1,7 +0,0 @@
Decision.register({
id = "go_to_end",
label = "Break the cycle",
handle = function()
Window.set_current("end")
end,
})

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("home") Util.go_to_screen_by_id("home")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("office") Util.go_to_screen_by_id("office")
end, end,
condition = function() return true end
}) })

View File

@@ -1,16 +0,0 @@
Decision.register({
id = "go_to_sleep",
label = "Go to Sleep",
handle = function()
Meter.hide()
Day.increase()
MinigameRhythmWindow.start("game", {
focus_center_x = (Config.screen.width / 2) - 22,
focus_center_y = (Config.screen.height / 2) - 18,
focus_initial_radius = 0,
on_win = function()
MysteriousManWindow.start()
end,
})
end,
})

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("toilet") Util.go_to_screen_by_id("toilet")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("walking_to_home") Util.go_to_screen_by_id("walking_to_home")
end, end,
condition = function() return true end
}) })

View File

@@ -4,4 +4,5 @@ Decision.register({
handle = function() handle = function()
Util.go_to_screen_by_id("walking_to_office") Util.go_to_screen_by_id("walking_to_office")
end, end,
condition = function() return true end
}) })

View File

@@ -2,7 +2,7 @@ Decision.register({
id = "have_a_coffee", id = "have_a_coffee",
label = "Have a Coffee", label = "Have a Coffee",
handle = function() handle = function()
local new_situation_id = Situation.apply("drink_coffee", Context.game.current_screen) Situation.apply("drink_coffee")
Context.game.current_situation = new_situation_id
end, end,
condition = function() return true end
}) })

View File

@@ -1,20 +1,12 @@
--- @section Decision
local _decisions = {} local _decisions = {}
--- Registers a decision definition.
--- @within Decision
--- @param decision table The decision data table.
--- @param decision.id string Unique decision identifier.
--- @param decision.label string|function Display text for the decision, or a function returning it.
--- @param[opt] decision.condition function Returns true if decision is available. Defaults to always true.
--- @param[opt] decision.handle function Called when the decision is selected. Defaults to noop.
function Decision.register(decision) function Decision.register(decision)
if not decision or not decision.id then if not decision or not decision.id then
trace("Error: Invalid decision object registered (missing id)!") PopupWindow.show({"Error: Invalid decision object registered (missing id)!"})
return return
end end
if not decision.label then if not decision.label then
trace("Error: Invalid decision object registered (missing label)!") PopupWindow.show({"Error: Invalid decision object registered (missing label)!"})
return return
end end
@@ -30,117 +22,10 @@ function Decision.register(decision)
_decisions[decision.id] = decision _decisions[decision.id] = decision
end end
--- Gets the display label for a decision. function Decision.get(id)
--- @within Decision
--- @param decision table The decision data table.
--- @return string result The resolved decision label.
function Decision.get_label(decision)
if not decision then return "" end
if type(decision.label) == "function" then
return decision.label() or ""
end
return decision.label or ""
end
--- Gets a decision by ID.
--- @within Decision
--- @param id string The ID of the decision.
--- @return table|nil result The decision table or nil. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.
function Decision.get_by_id(id)
return _decisions[id] return _decisions[id]
end end
--- Gets all registered decisions.
--- @within Decision
--- @return result table A table of all registered decisions, indexed by their IDs. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.
function Decision.get_all() function Decision.get_all()
return _decisions return _decisions
end end
--- Gets decision objects based on a screen's data.
--- @within Decision
--- @param screen_data table The data for the screen.
--- @param screen_data.decisions table Array of decision ID strings.
--- @return result table An array of decision objects relevant to the screen or nil. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.<br/>
function Decision.get_for_screen(screen_data)
if not screen_data or not screen_data.decisions then
return {}
end
local screen_decisions = {}
for _, decision_id in ipairs(screen_data.decisions) do
local decision = Decision.get_by_id(decision_id)
if decision then
table.insert(screen_decisions, decision)
end
end
return screen_decisions
end
--- Filters a list of decision objects based on their condition function.
--- @within Decision
--- @param decisions_list table A table of decision objects.
--- @return result table An array of decisions for which condition() is true or nil. </br>
--- Fields: </br>
--- * id (string) Unique decision identifier.<br/>
--- * label (string) Display text for the decision.<br/>
--- * condition (function) Returns true if decision is available.<br/>
--- * handle (function) Called when the decision is selected.<br/>
function Decision.filter_available(decisions_list)
local available = {}
for _, decision in ipairs(decisions_list) do
if decision and (not decision.condition or decision.condition()) then
table.insert(available, decision)
end
end
return available
end
--- Draws decision selector.
--- @within Decision
--- @param decisions table A table of decision items.<br/>
--- @param selected_decision_index number The index of the selected decision.<br/>
function Decision.draw(decisions, selected_decision_index)
local bar_height = 16
local bar_y = Config.screen.height - bar_height
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
if #decisions > 0 then
local selected_decision = decisions[selected_decision_index]
local decision_label = Decision.get_label(selected_decision)
local text_y = bar_y + 4
Print.text("<", 2, text_y, Config.colors.light_blue)
Print.text_center(decision_label, Config.screen.width / 2, text_y, Config.colors.item)
Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
end
end
--- Updates decision selector.
--- @within Decision
--- @param decisions table A table of decision items.<br/>
--- @param selected_decision_index number The current index of the selected decision.<br/>
--- @return number selected_decision_index The updated index of the selected decision.
function Decision.update(decisions, selected_decision_index)
if Input.left() then
Audio.sfx_beep()
selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1)
elseif Input.right() then
Audio.sfx_beep()
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
end
return selected_decision_index
end

View File

@@ -1,12 +1,6 @@
Decision.register({ Decision.register({
id = "play_button_mash", id = "play_button_mash",
label = "Play Button Mash", label = "Play Button Mash",
handle = function() handle = function() Meters.hide() MinigameButtonMashWindow.start(WINDOW_GAME) end,
Meter.hide() condition = function() return true end
MinigameButtonMashWindow.start("game", {
focus_center_x = (Config.screen.width / 2) - 22,
focus_center_y = (Config.screen.height / 2) - 18,
focus_initial_radius = 0,
})
end,
}) })

View File

@@ -1,5 +1,6 @@
Decision.register({ Decision.register({
id = "play_ddr", id = "play_ddr",
label = "Play DDR (Random)", label = "Play DDR (Random)",
handle = function() Meter.hide() MinigameDDRWindow.start("game", nil) end, handle = function() Meters.hide() MinigameDDRWindow.start(WINDOW_GAME, nil) end,
condition = function() return true end
}) })

View File

@@ -1,12 +1,6 @@
Decision.register({ Decision.register({
id = "play_rhythm", id = "play_rhythm",
label = "Play Rhythm Game", label = "Play Rhythm Game",
handle = function() handle = function() Meters.hide() MinigameRhythmWindow.start(WINDOW_GAME) end,
Meter.hide() condition = function() return true end
MinigameRhythmWindow.start("game", {
focus_center_x = (Config.screen.width / 2) - 22,
focus_center_y = (Config.screen.height / 2) - 18,
focus_initial_radius = 0,
})
end,
}) })

View File

@@ -1,18 +0,0 @@
Decision.register({
id = "start_discussion",
label = function()
if Context.day_count >= 3 then
return "Talk to Sumphore"
end
return "Talk to the homeless guy"
end,
handle = function()
if Context.day_count < 3 then
Discussion.start("homeless_guy", "game")
end
if Context.day_count >= 3 then
Discussion.start("sumphore_day_3", "game")
return
end
end,
})

View File

@@ -1,59 +0,0 @@
Discussion.register({
id = "sumphore_day_3",
steps = {
{
question = "Are you still seeking the ox?",
answers = {
{ label = "Huh? What ox?", next_step = 2 },
{ label = "Are you drunk, old man?", next_step = nil },
},
},
{
question = "Did you never think there would be more to this?",
answers = {
{ label = "I'm not sure what you mean.", next_step = nil },
},
},
},
})
Discussion.register({
id = "homeless_guy",
steps = {
{
question = "Sup bro, how are you?",
answers = {
{ label = "I'm doing great, thanks!", next_step = 2 },
{ label = "Not as good as you", next_step = nil },
},
},
{
question = "What's your name?",
answers = {
{ label = "Norman Reds, nice to meet you.", next_step = 3 },
{ label = "Mom told me not to talk to strangers.", next_step = nil },
},
},
{
question = "That name ... could it be? I know a guy with that name...",
answers = {
{ label = "Never met you before.", next_step = 4 },
{ label = "I'm not sure what you mean.", next_step = nil },
},
},
{
question = "My name is Sumphore, nice to meet you.",
answers = {
{ label = "Nice to meet you, Sumphore.", next_step = 5 },
},
},
{
question = "You're a good guy, I can tell. You abide by the rules. Life would be so much easier if more people were like you ...",
answers = {
{ label = "Thanks, I try my best.", next_step = nil },
},
},
},
})

View File

@@ -1,57 +1,53 @@
Config = {} local DEFAULT_CONFIG = {
screen = {
--- Return initial data for Config width = 240,
--- @within Config height = 136
function Config.initial_data() },
return { colors = {
screen = { black = 0,
width = 240, light_grey = 13,
height = 136 dark_grey = 14,
}, red = 2,
colors = { green = 6,
black = 0, blue = 9,
light_grey = 2, white = 12,
dark_grey = 1, item = 12,
red = 13, meter_bg = 12
light_blue = 9, },
blue = 3, player = {
white = 4, sprite_id = 1
item = 7, },
meter_bg = 1 timing = {
}, splash_duration = 120
timing = {
minigame_win_duration = 180
}
} }
end }
--- Restores default configuration settings. local Config = {
--- @within Config screen = DEFAULT_CONFIG.screen,
function Config.reset() colors = DEFAULT_CONFIG.colors,
local initial = Config.initial_data() player = DEFAULT_CONFIG.player,
Config.screen = initial.screen timing = DEFAULT_CONFIG.timing,
Config.colors = initial.colors }
Config.timing = initial.timing
end
local CONFIG_SAVE_BANK = 7 local CONFIG_SAVE_BANK = 7
local CONFIG_MAGIC_VALUE_ADDRESS = 2 local CONFIG_MAGIC_VALUE_ADDRESS = 2
local CONFIG_SPLASH_DURATION_ADDRESS = 3
local CONFIG_MAGIC_VALUE = 0xDE local CONFIG_MAGIC_VALUE = 0xDE
--- Saves the current configuration.
--- @within Config
function Config.save() function Config.save()
mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK)
end end
--- Loads saved configuration.
--- @within Config
function Config.load() function Config.load()
if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then
return Config.timing.splash_duration = mget(CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
else else
Config.reset() Config.restore_defaults()
end end
end end
function Config.restore_defaults()
Config.timing.splash_duration = DEFAULT_CONFIG.timing.splash_duration
end
Config.load() Config.load()

View File

@@ -2,135 +2,111 @@ local SAVE_GAME_BANK = 6
local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0 local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
local SAVE_GAME_MAGIC_VALUE = 0xCA local SAVE_GAME_MAGIC_VALUE = 0xCA
--- Global game context. local SAVE_GAME_CURRENT_SCREEN_ADDRESS = 6
--- @section Context
Context = {}
--- Gets initial data for Context. local function get_initial_data()
--- @within Context
--- @return result table Initial context data or nil. </br>
--- Fields: </br>
--- * current_menu_item (number) Index of the currently selected menu item.<br/>
--- * popup (table) Popup window state. Contains: `show` (boolean) whether popup is visible, `content` (table) array of strings to display.<br/>
--- * game_in_progress (boolean) Whether a game is currently active.<br/>
--- * minigame_ddr (table) DDR minigame state (see Minigame.get_default_ddr).<br/>
--- * minigame_button_mash (table) Button mash minigame state (see Minigame.get_default_button_mash).<br/>
--- * minigame_rhythm (table) Rhythm minigame state (see Minigame.get_default_rhythm).<br/>
--- * meters (table) Meter values (see Meter.get_initial).<br/>
--- * triggers (table) Active trigger runtime state, keyed by trigger ID.<br/>
--- * stat_screen_active (boolean) Whether the stat screen overlay is currently shown.<br/>
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.<br/>
function Context.initial_data()
return { return {
current_menu_item = 1, active_window = WINDOW_SPLASH,
intro = {
y = Config.screen.height,
speed = 0.5,
text = [[Norman Reds everyday life
seems ordinary: work,
meetings, coffee, and
endless notifications.
But beneath the surface
— within him, or around
him — something is
constantly building, and
it soon becomes clear
that there is more going
on than meets the eye.]]
},
current_screen = 1,
splash_timer = Config.timing.splash_duration,
popup = { popup = {
show = false, show = false,
content = {} content = {}
}, },
player = {
sprite_id = Config.player.sprite_id
},
ground = {
x = 0,
y = Config.screen.height,
w = Config.screen.width,
h = 8
},
menu_items = {},
selected_menu_item = 1,
selected_decision_index = 1,
game_in_progress = false, game_in_progress = false,
stat_screen_active = false, screens = {},
minigame_ddr = {}, minigame_ddr = Minigames.get_default_ddr(),
minigame_button_mash = {}, minigame_button_mash = Minigames.get_default_button_mash(),
minigame_rhythm = {}, minigame_rhythm = Minigames.get_default_rhythm(),
meters = Meter.get_initial(), meters = Meters.get_initial(),
timer = Timer.get_initial(), --- Table storing currently active sprites to be drawn.
triggers = {}, -- Each entry is a table with `id`, `x`, `y`, and other drawing parameters.
home_norman_visible = false, sprites = {},
game = { --- The ID of the currently active situation.
current_screen = "home", -- Set by `Situation.apply()` and `nil` if no situation is active.
current_situation = nil, current_situation = nil,
},
day_count = 1,
glitch = {
enabled = false,
state = "active",
timer = 0,
},
_end = {
state = "choice",
selection = 1,
},
discussion = {
active = false,
id = nil,
step = 1,
selected_answer = 1,
scroll_y = 0,
scroll_timer = 0,
auto_scroll = true,
return_window = nil,
},
} }
end end
--- Resets game context to initial state. Context = {}
--- @within Context
function Context.reset() local function reset_context_to_initial_state()
local initial_data = Context.initial_data() local initial_data = get_initial_data()
for k in pairs(Context) do for k in pairs(Context) do
if type(Context[k]) ~= "function" then if type(Context[k]) ~= "function" then Context[k] = nil
Context[k] = nil
end end
end end
for k, v in pairs(initial_data) do for k, v in pairs(initial_data) do
Context[k] = v Context[k] = v
end end
Context.screens = {}
Context.screen_indices_by_id = {}
local screen_order = {"home", "toilet", "walking_to_office", "office", "walking_to_home"}
for i, screen_id in ipairs(screen_order) do
local screen_data = Screen.get_by_id(screen_id)
if screen_data then
table.insert(Context.screens, screen_data)
Context.screen_indices_by_id[screen_id] = i
else
PopupWindow.show({"Error: Screen '" .. screen_id .. "' not registered!"})
end
end
end end
--- Starts a new game. reset_context_to_initial_state()
--- @within Context
function Context.new_game() function Context.new_game()
Context.reset() reset_context_to_initial_state()
Context.game_in_progress = true Context.game_in_progress = true
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
Screen.get_by_id(Context.game.current_screen).init()
MysteriousManWindow.start({
text = [[
Norman was never a bad
simulation engineer, but
we need to be careful in
letting him improve. We
need to distract him.
]],
on_text_complete = function()
Audio.sfx_alarm()
Context.home_norman_visible = false
Util.go_to_screen_by_id("home")
MinigameButtonMashWindow.start("game", {
focus_center_x = (Config.screen.width / 2) - 22,
focus_center_y = (Config.screen.height / 2) - 18,
focus_initial_radius = 0,
target_points = 100,
instruction_text = "Wake up Norman!",
show_progress_text = false,
on_win = function()
Audio.music_play_wakingup()
Context.home_norman_visible = true
Meter.show()
Window.set_current("game")
end,
})
end,
})
end end
--- Saves the current game state.
--- @within Context
function Context.save_game() function Context.save_game()
if not Context.game_in_progress then return end if not Context.game_in_progress then return end
mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) 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 end
--- Loads a saved game state.
--- @within Context
function Context.load_game() function Context.load_game()
if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then
Context.new_game() Context.new_game()
return return
end end
Context.reset()
reset_context_to_initial_state()
Context.current_screen = mget(SAVE_GAME_CURRENT_SCREEN_ADDRESS, SAVE_GAME_BANK)
Context.game_in_progress = true Context.game_in_progress = true
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
Screen.get_by_id(Context.game.current_screen).init()
end end

75
inc/init/init.meters.lua Normal file
View File

@@ -0,0 +1,75 @@
local METER_MAX = 1000
local METER_DEFAULT = 500
local METER_DECAY_PER_FRAME = 0.02
local METER_GAIN_PER_CHORE = 100
local COMBO_BASE_BONUS = 0.02
local COMBO_MAX_BONUS = 0.16
local COMBO_TIMEOUT_FRAMES = 600
Meters.COLOR_ISM = Config.colors.red
Meters.COLOR_WPM = Config.colors.blue
Meters.COLOR_BM = Config.colors.black
Meters.COLOR_BG = Config.colors.meter_bg
function Meters.get_initial()
return {
ism = METER_DEFAULT,
wpm = METER_DEFAULT,
bm = METER_DEFAULT,
combo = 0,
combo_timer = 0,
hidden = false,
}
end
function Meters.hide()
if Context and Context.meters then Context.meters.hidden = true end
end
function Meters.show()
if Context and Context.meters then Context.meters.hidden = false end
end
function Meters.get_max()
return METER_MAX
end
function Meters.get_combo_multiplier()
if not Context or not Context.meters then return 1 end
local combo = Context.meters.combo
if combo == 0 then return 1 end
return 1 + math.min(COMBO_MAX_BONUS, COMBO_BASE_BONUS * (2 ^ (combo - 1)))
end
function Meters.update()
if not Context or not Context.game_in_progress or not Context.meters then return end
local m = Context.meters
m.ism = math.max(0, m.ism - METER_DECAY_PER_FRAME)
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_FRAME)
m.bm = math.max(0, m.bm - METER_DECAY_PER_FRAME)
if m.combo > 0 then
m.combo_timer = m.combo_timer + 1
if m.combo_timer >= COMBO_TIMEOUT_FRAMES then
m.combo = 0
m.combo_timer = 0
end
end
end
function Meters.add(key, amount)
if not Context or not Context.meters then return end
local m = Context.meters
if m[key] ~= nil then
m[key] = math.min(METER_MAX, m[key] + amount)
end
end
function Meters.on_minigame_complete()
local m = Context.meters
local gain = math.floor(METER_GAIN_PER_CHORE * Meters.get_combo_multiplier())
Meters.add("wpm", gain)
Meters.add("ism", gain)
Meters.add("bm", gain)
m.combo = m.combo + 1
m.combo_timer = 0
end

107
inc/init/init.minigames.lua Normal file
View File

@@ -0,0 +1,107 @@
Minigames = {}
local function apply_params(defaults, params)
if not params then return defaults end
for k, v in pairs(params) do
defaults[k] = v
end
return defaults
end
function Minigames.get_default_ddr()
local arrow_size = 12
local arrow_spacing = 30
local total_width = (4 * arrow_size) + (3 * arrow_spacing)
local start_x = (Config.screen.width - total_width) / 2
return {
bar_fill = 0,
max_fill = 100,
fill_per_hit = 10,
miss_penalty = 5,
bar_x = 20,
bar_y = 10,
bar_width = 200,
bar_height = 12,
arrow_size = arrow_size,
arrow_spawn_timer = 0,
arrow_spawn_interval = 45,
arrow_fall_speed = 1.5,
arrows = {},
target_y = 115,
target_arrows = {
{ dir = "left", x = start_x },
{ dir = "down", x = start_x + arrow_size + arrow_spacing },
{ dir = "up", x = start_x + (arrow_size + arrow_spacing) * 2 },
{ dir = "right", x = start_x + (arrow_size + arrow_spacing) * 3 }
},
hit_threshold = 8,
button_pressed_timers = {},
button_press_duration = 8,
input_cooldowns = { left = 0, down = 0, up = 0, right = 0 },
input_cooldown_duration = 10,
frame_counter = 0,
current_song = nil,
pattern_index = 1,
use_pattern = false,
return_window = nil
}
end
function Minigames.get_default_button_mash()
return {
bar_fill = 0,
max_fill = 100,
fill_per_press = 8,
base_degradation = 0.15,
degradation_multiplier = 0.006,
button_pressed_timer = 0,
button_press_duration = 8,
return_window = nil,
bar_x = 20,
bar_y = 10,
bar_width = 200,
bar_height = 12,
button_x = 20,
button_y = 110,
button_size = 12
}
end
function Minigames.get_default_rhythm()
return {
line_position = 0,
line_speed = 0.015,
line_direction = 1,
target_center = 0.5,
target_width = 0.3,
initial_target_width = 0.3,
min_target_width = 0.08,
target_shrink_rate = 0.9,
score = 0,
max_score = 10,
button_pressed_timer = 0,
button_press_duration = 10,
return_window = nil,
bar_x = 20,
bar_y = 10,
bar_width = 200,
bar_height = 12,
button_x = 210,
button_y = 110,
button_size = 10,
press_cooldown = 0,
press_cooldown_duration = 15
}
end
function Minigames.configure_ddr(params)
return apply_params(Minigames.get_default_ddr(), params)
end
function Minigames.configure_button_mash(params)
return apply_params(Minigames.get_default_button_mash(), params)
end
function Minigames.configure_rhythm(params)
return apply_params(Minigames.get_default_rhythm(), params)
end

View File

@@ -1,19 +0,0 @@
Window = {}
Util = {}
Meter = {}
Minigame = {}
Decision = {}
Situation = {}
Screen = {}
Map = {}
UI = {}
Print = {}
Input = {}
Sprite = {}
Audio = {}
Focus = {}
Day = {}
Timer = {}
Trigger = {}
Discussion = {}
AsciiArt = {}

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

@@ -0,0 +1,22 @@
SplashWindow = {}
IntroWindow = {}
MenuWindow = {}
GameWindow = {}
PopupWindow = {}
ConfigurationWindow = {}
AudioTestWindow = {}
MinigameButtonMashWindow = {}
MinigameRhythmWindow = {}
MinigameDDRWindow = {}
Util = {}
Meters = {}
Minigames = {}
Decision = {}
Situation = {}
Screen = {}
Map = {}
UI = {}
Print = {}
Input = {}
Sprite = {}
Audio = {}

10
inc/init/init.windows.lua Normal file
View File

@@ -0,0 +1,10 @@
local WINDOW_SPLASH = 0
local WINDOW_INTRO = 1
local WINDOW_MENU = 2
local WINDOW_GAME = 3
local WINDOW_POPUP = 4
local WINDOW_CONFIGURATION = 7
local WINDOW_MINIGAME_BUTTON_MASH = 8
local WINDOW_MINIGAME_RHYTHM = 9
local WINDOW_MINIGAME_DDR = 10
local WINDOW_AUDIOTEST = 9001

View File

@@ -1,25 +0,0 @@
--- @section Day
local _day_increase_handlers = {}
--- Increases the day count and triggers registered handlers.
--- @within Day
function Day.increase()
Context.day_count = Context.day_count + 1
for _, handler in ipairs(_day_increase_handlers) do
handler()
end
end
--- Registers a handler to be called when the day increases.
--- @within Day
--- @param handler function The function to call when the day increases.
function Day.register_handler(handler)
table.insert(_day_increase_handlers, handler)
end
Day.register_handler(function()
local m = Context.meters
m.ism = math.max(0, m.ism - METER_DECAY_PER_DAY)
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_DAY)
m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY)
end)

View File

@@ -1,103 +0,0 @@
--- @section Discussion
local _discussions = {}
--- Registers a discussion definition.
--- @within Discussion
--- @param discussion table The discussion data table.
--- @param discussion.id string Unique discussion identifier.
--- @param discussion.steps table Array of step tables, each with `question` (string) and `answers` (array of {label, next_step} tables).
--- @param[opt] discussion.on_end function Called when the discussion ends. Defaults to noop.
function Discussion.register(discussion)
if not discussion or not discussion.id then
trace("Error: Invalid discussion registered (missing id)!")
return
end
if not discussion.steps or #discussion.steps == 0 then
trace("Error: Discussion '" .. discussion.id .. "' has no steps!")
return
end
if not discussion.on_end then
discussion.on_end = function() end
end
if _discussions[discussion.id] then
trace("Warning: Overwriting discussion with id: " .. discussion.id)
end
_discussions[discussion.id] = discussion
end
--- Gets a discussion by ID.
--- @within Discussion
--- @param id string The discussion ID.
--- @return table|nil result The discussion table or nil.
function Discussion.get_by_id(id)
return _discussions[id]
end
--- Starts a discussion, switching to the discussion window.
--- @within Discussion
--- @param id string The discussion ID to start.
--- @param return_window string The window ID to return to after the discussion.
function Discussion.start(id, return_window)
local discussion = _discussions[id]
if not discussion then
trace("Error: Discussion not found: " .. tostring(id))
return
end
Context.discussion.active = true
Context.discussion.id = id
Context.discussion.step = 1
Context.discussion.selected_answer = 1
Context.discussion.scroll_y = 0
Context.discussion.scroll_timer = 0
Context.discussion.auto_scroll = true
Context.discussion.return_window = return_window or "game"
Meter.hide()
Window.set_current("discussion")
end
--- Gets the current step data for the active discussion.
--- @within Discussion
--- @return table|nil result The current step table or nil.
function Discussion.get_current_step()
if not Context.discussion.active or not Context.discussion.id then return nil end
local discussion = _discussions[Context.discussion.id]
if not discussion then return nil end
return discussion.steps[Context.discussion.step]
end
--- Advances to a specific step or ends the discussion.
--- @within Discussion
--- @param next_step number|nil The step index to go to, or nil to end.
function Discussion.go_to_step(next_step)
if not next_step then
Discussion.finish()
return
end
local discussion = _discussions[Context.discussion.id]
if not discussion or not discussion.steps[next_step] then
Discussion.finish()
return
end
Context.discussion.step = next_step
Context.discussion.selected_answer = 1
Context.discussion.scroll_y = 0
Context.discussion.scroll_timer = 0
Context.discussion.auto_scroll = true
end
--- Ends the active discussion and returns to the previous window.
--- @within Discussion
function Discussion.finish()
local discussion = _discussions[Context.discussion.id]
local return_window = Context.discussion.return_window or "game"
Context.discussion.active = false
Context.discussion.id = nil
Context.discussion.scroll_y = 0
Context.discussion.scroll_timer = 0
Context.discussion.auto_scroll = true
Meter.show()
if discussion and discussion.on_end then
discussion.on_end()
end
Window.set_current(return_window)
end

View File

@@ -1,166 +0,0 @@
--- @section Focus
local FOCUS_DEFAULT_SPEED = 5
local active = false
local closing = false
local driven = false
local center_x = 0
local center_y = 0
local radius = 0
local speed = FOCUS_DEFAULT_SPEED
local on_complete = nil
local driven_initial_r = 0
local driven_max_r = 0
local function max_radius(cx, cy)
local dx = math.max(cx, Config.screen.width - cx)
local dy = math.max(cy, Config.screen.height - cy)
return math.sqrt(dx * dx + dy * dy)
end
--- Starts a focus overlay that reveals content through an expanding circle.
--- @within Focus
--- @param cx number The x-coordinate of the circle center.
--- @param cy number The y-coordinate of the circle center.
--- @param[opt] params table Optional parameters: `speed` (number) expansion rate in pixels/frame, `initial_radius` (number) starting radius in pixels (default 0), `on_complete` (function) callback when overlay disperses.
function Focus.start(cx, cy, params)
params = params or {}
active = true
closing = false
driven = false
center_x = cx
center_y = cy
radius = params.initial_radius or 0
speed = params.speed or FOCUS_DEFAULT_SPEED
on_complete = params.on_complete
end
--- Starts a closing focus overlay that hides content by shrinking the visible circle.
--- @within Focus
--- @param cx number The x-coordinate of the circle center.
--- @param cy number The y-coordinate of the circle center.
--- @param[opt] params table Optional parameters: `speed` (number) shrink rate in pixels/frame, `on_complete` (function) callback when screen is fully covered.
function Focus.close(cx, cy, params)
params = params or {}
active = true
closing = true
driven = false
center_x = cx
center_y = cy
radius = max_radius(cx, cy)
speed = params.speed or FOCUS_DEFAULT_SPEED
on_complete = params.on_complete
end
--- Starts a driven focus overlay whose radius is controlled externally via Focus.set_percentage().
--- The radius maps linearly from initial_radius (at 0%) to the screen corner distance (at 100%).
--- @within Focus
--- @param cx number The x-coordinate of the circle center.
--- @param cy number The y-coordinate of the circle center.
--- @param[opt] params table Optional parameters: `initial_radius` (number) radius at 0% (default 0).
function Focus.start_driven(cx, cy, params)
params = params or {}
active = true
closing = false
driven = true
center_x = cx
center_y = cy
driven_initial_r = params.initial_radius or 0
driven_max_r = max_radius(cx, cy)
radius = driven_initial_r
on_complete = nil
end
--- Sets the visible radius as a percentage of the full screen extent.
--- Only has effect when the overlay is in driven mode (started via Focus.start_driven).
--- @within Focus
--- @param pct number A value from 0 to 1 (0 = initial_radius, 1 = full screen).
function Focus.set_percentage(pct)
if not driven then return end
radius = driven_initial_r + pct * (driven_max_r - driven_initial_r)
end
--- Checks whether the focus overlay is currently active.
--- @within Focus
--- @return boolean Whether the focus overlay is active.
function Focus.is_active()
return active
end
--- Stops the focus overlay immediately.
--- @within Focus
function Focus.stop()
active = false
closing = false
driven = false
radius = 0
on_complete = nil
end
--- Updates the focus overlay animation. No-op in driven mode.
--- @within Focus
function Focus.update()
if not active then return end
if driven then return end
if closing then
radius = radius - speed
if radius <= 0 then
local cb = on_complete
Focus.stop()
if cb then cb() end
end
else
radius = radius + speed
if radius >= max_radius(center_x, center_y) then
local cb = on_complete
Focus.stop()
if cb then cb() end
end
end
end
--- Draws the focus overlay (black screen with circular cutout).
--- Must be called after all other drawing to appear on top of every visual layer.
--- @within Focus
function Focus.draw()
if not active then return end
local cx = center_x
local cy = center_y
local r = radius
local w = Config.screen.width
local h = Config.screen.height
local color = Config.colors.black
if closing and r <= 0 then
rect(0, 0, w, h, color)
return
end
local top = math.max(0, math.floor(cy - r))
local bottom = math.min(h - 1, math.ceil(cy + r))
if top > 0 then
rect(0, 0, w, top, color)
end
if bottom < h - 1 then
rect(0, bottom + 1, w, h - bottom - 1, color)
end
for y = top, bottom do
local dy = y - cy
local half_w = math.sqrt(math.max(0, r * r - dy * dy))
local left = math.floor(cx - half_w)
local right = math.ceil(cx + half_w)
if left > 0 then
rect(0, y, left, 1, color)
end
if right < w then
rect(right, y, w - right, 1, color)
end
end
end

View File

@@ -1,58 +0,0 @@
--- @section Glitch
Glitch = {}
--- Shows the glitch effect.
--- @within Glitch
function Glitch.show()
if Context and Context.glitch then
Context.glitch.enabled = true
end
end
--- Hides the glitch effect.
--- @within Glitch
function Glitch.hide()
if Context and Context.glitch then
Context.glitch.enabled = false
end
end
--- Draws the glitch effect if active.
--- @within Glitch
function Glitch.draw()
if not Context or not Context.glitch or not Context.glitch.enabled then return end
-- Update state timer
Context.glitch.timer = Context.glitch.timer - 1
if Context.glitch.timer <= 0 then
if Context.glitch.state == "active" then
Context.glitch.state = "waiting"
Context.glitch.timer = math.random(20, 60) -- Time to stay fixed
else
Context.glitch.state = "active"
Context.glitch.timer = math.random(40, 100) -- Time to stay glitchy
end
end
-- Draw stripes only when active
if Context.glitch.state == "active" then
for _ = 1, 15 do
local rx = math.random(0, Config.screen.width - 1)
local ry = math.random(0, Config.screen.height - 1)
-- Sample color at the random point
local color = pix(rx, ry)
-- Determine random length for the stripe (2-40)
local length = math.random(2, 40)
-- Draw the vertical stripe
for sy = 0, length - 1 do
local dy = ry + sy
if dy < Config.screen.height then
pix(rx, dy, color)
end
end
end
end
end

View File

@@ -1,155 +0,0 @@
--- @section Meter
local METER_MAX = 1000
local METER_DEFAULT = 500
local METER_GAIN_PER_CHORE = 100
local METER_DECAY_PER_DAY = 20
local COMBO_BASE_BONUS = 0.02
local COMBO_MAX_BONUS = 0.16
local COMBO_TIMEOUT_FRAMES = 600
-- Internal meters for tracking game progress and player stats.
Meter.COLOR_ISM = Config.colors.red
Meter.COLOR_WPM = Config.colors.blue
Meter.COLOR_BM = Config.colors.black
Meter.COLOR_BG = Config.colors.meter_bg
--- Gets initial meter values.
--- @within Meter
--- @return result table Initial meter values. </br>
--- Fields: </br>
--- * ism (number) Initial ISM meter value.<br/>
--- * wpm (number) Initial WPM meter value.<br/>
--- * bm (number) Initial BM meter value.<br/>
--- * combo (number) Current combo count.<br/>
--- * combo_timer (number) Frames since last combo action.<br/>
--- * hidden (boolean) Whether meters are hidden.
function Meter.get_initial()
return {
ism = METER_DEFAULT,
wpm = METER_DEFAULT,
bm = METER_DEFAULT,
combo = 0,
combo_timer = 0,
hidden = false,
}
end
--- Hides meters.
--- @within Meter
function Meter.hide()
if Context and Context.meters then Context.meters.hidden = true end
end
--- Shows meters.
--- @within Meter
function Meter.show()
if Context and Context.meters then Context.meters.hidden = false end
end
--- Gets max meter value.
--- @within Meter
--- @return number The maximum meter value.
function Meter.get_max()
return METER_MAX
end
--- Sets the decay amount applied to all meters per day.
--- @within Meter
--- @param amount number Amount to subtract from each meter.
function Meter.set_decay(amount)
METER_DECAY_PER_DAY = amount
end
--- Gets the meter decay as a percentage of the max meter value.
--- @within Meter
--- @return number The decay percentage per day.
function Meter.get_decay_percentage()
return math.floor(METER_DECAY_PER_DAY / METER_MAX * 100)
end
--- Gets combo multiplier.
--- @within Meter
--- @return number The current combo multiplier.
function Meter.get_combo_multiplier()
if not Context or not Context.meters then return 1 end
local combo = Context.meters.combo
if combo == 0 then return 1 end
return 1 + math.min(COMBO_MAX_BONUS, COMBO_BASE_BONUS * (2 ^ (combo - 1)))
end
--- Updates all meters.
--- @within Meter
function Meter.update()
if not Context or not Context.game_in_progress or not Context.meters then return end
local m = Context.meters
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
if not in_minigame then
if m.combo > 0 then
m.combo_timer = m.combo_timer + 1
if m.combo_timer >= COMBO_TIMEOUT_FRAMES then
m.combo = 0
m.combo_timer = 0
end
end
end
end
--- Adds amount to a meter.
--- @within Meter
--- @param key string The meter key (e.g., "wpm", "ism", "bm").
--- @param amount number The amount to add.
function Meter.add(key, amount)
if not Context or not Context.meters then return end
local m = Context.meters
if m[key] ~= nil then
m[key] = math.min(METER_MAX, m[key] + amount)
end
end
--- Called on minigame completion.
--- @within Meter
function Meter.on_minigame_complete()
local m = Context.meters
local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier())
Meter.add("wpm", gain)
Meter.add("ism", gain)
Meter.add("bm", gain)
m.combo = m.combo + 1
m.combo_timer = 0
end
--- Draws meters.
--- @within Meter
function Meter.draw()
if not Context or not Context.game_in_progress or not Context.meters then return end
if Context.meters.hidden then return end
local m = Context.meters
local max = Meter.get_max()
local bar_w = 44
local bar_h = 2
local bar_x = 182
local label_x = 228
local line_h = 5
local start_y = 1
local bar_offset = math.floor((line_h - bar_h) / 2)
local meter_list = {
{ key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 },
{ key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 },
{ key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 },
}
for _, meter in ipairs(meter_list) do
local label_y = start_y + meter.row * line_h
local bar_y = label_y + bar_offset
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
if fill_w > 0 then
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
end
print(meter.label, label_x, label_y, meter.color, false, 1, true)
end
end

View File

@@ -1,19 +0,0 @@
-- Manages minigame configurations and initial states.
--- @section Minigame
--- Draws a unified win message overlay.
--- @within Minigame
function Minigame.draw_win_overlay()
local text = "SUCCESS"
local tw = #text * 4
local th = 6
local padding = 4
local box_w = tw + padding * 2
local box_h = th + padding * 2
local box_x = (Config.screen.width - box_w) / 2
local box_y = (Config.screen.height - box_h) / 2
rect(box_x, box_y, box_w, box_h, Config.colors.dark_grey)
rectb(box_x, box_y, box_w, box_h, Config.colors.white)
Print.text_center(text, Config.screen.width / 2, box_y + padding, Config.colors.white)
end

View File

@@ -1,88 +0,0 @@
--- @section Timer
local timer_duration = 1800
--- Gets initial timer values.
--- @within Timer
--- @return result table Initial timer values. </br>
--- Fields: </br>
--- * progress (number) Clock timer revolution progress (0 to 1).
function Timer.get_initial()
return {
progress = 0,
}
end
--- Sets the number of frames for one full timer revolution.
--- @within Timer
--- @param frames number Frames per revolution.
function Timer.set_duration(frames)
timer_duration = frames
end
--- Updates the timer and handles revolution events.
--- @within Timer
function Timer.update()
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
local t = Context.timer
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
if not in_minigame then
t.progress = t.progress + (1 / timer_duration)
if t.progress >= 1 then
Day.increase()
t.progress = t.progress - 1
end
end
end
--- Draws the clock timer indicator as a circular progress bar.
--- @within Timer
function Timer.draw()
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
if Context.meters.hidden and not Context.stat_screen_active then return end
local cx = 10
local cy = 8
local r_outer = 5
local r_inner = 3
local progress = Context.timer.progress
local fg_color
if progress <= 0.25 then
fg_color = Config.colors.dark_grey
elseif progress <= 0.5 then
fg_color = Config.colors.light_blue
elseif progress <= 0.75 then
fg_color = Config.colors.blue
elseif progress <= 1 then
fg_color = Config.colors.red
end
local bg_color = Config.colors.white
local start_angle = -math.pi * 0.5
local progress_angle = progress * 2 * math.pi
local r_outer_sq = r_outer * r_outer
local r_inner_sq = r_inner * r_inner
for dy = -r_outer, r_outer do
for dx = -r_outer, r_outer do
local dist_sq = dx * dx + dy * dy
if dist_sq <= r_outer_sq and dist_sq > r_inner_sq then
local angle = math.atan(dy, dx)
local relative = angle - start_angle
if relative < 0 then relative = relative + 2 * math.pi end
if relative <= progress_angle then
pix(cx + dx, cy + dy, fg_color)
else
pix(cx + dx, cy + dy, bg_color)
end
end
end
end
local hand_angle = start_angle + progress_angle
local hand_x = math.floor(cx + math.cos(hand_angle) * (r_inner - 1) + 0.5)
local hand_y = math.floor(cy + math.sin(hand_angle) * (r_inner - 1) + 0.5)
line(cx, cy, hand_x, hand_y, Config.colors.white)
end

View File

@@ -1,135 +0,0 @@
--- @section Trigger
local triggers = {}
--- @within Trigger
--- @param trigger table The trigger data table.
--- @param trigger.id string Unique trigger identifier.
--- @param trigger.duration number Duration in frames before the trigger fires.
--- @param[opt] trigger.on_start function Called when the trigger starts. Defaults to noop.
--- @param[opt] trigger.on_stop function Called when the trigger fires or is manually stopped. Defaults to noop.
--- @param[opt] trigger.repeating boolean If true, trigger restarts after firing. Defaults to false.
function Trigger.register(trigger)
if not trigger or not trigger.id then
trace("Error: Invalid trigger registered (missing id)!")
return
end
if not trigger.duration or trigger.duration <= 0 then
trace("Error: Invalid trigger registered (missing or invalid duration)!")
return
end
if not trigger.on_start then
trigger.on_start = function() end
end
if not trigger.on_stop then
trigger.on_stop = function() end
end
if trigger.repeating == nil then
trigger.repeating = false
end
if triggers[trigger.id] then
trace("Warning: Overwriting trigger with id: " .. trigger.id)
end
triggers[trigger.id] = trigger
end
--- @within Trigger
--- @param id string The trigger ID.
--- @return table|nil result The trigger definition or nil.
function Trigger.get_by_id(id)
return triggers[id]
end
--- @within Trigger
--- @return table result All trigger definitions keyed by ID.
function Trigger.get_all()
return triggers
end
--- @within Trigger
--- @param id string The trigger ID.
--- @return boolean active True if the trigger is running.
function Trigger.is_active(id)
if not Context or not Context.triggers then return false end
return Context.triggers[id] ~= nil
end
--- If already active, restarts from 0.
--- @within Trigger
--- @param id string The trigger ID.
function Trigger.start(id)
if not Context or not Context.triggers then return end
local trigger = triggers[id]
if not trigger then
trace("Error: Cannot start unknown trigger: " .. tostring(id))
return
end
Context.triggers[id] = { elapsed = 0 }
trigger.on_start()
end
--- @within Trigger
--- @param id string The trigger ID.
function Trigger.stop(id)
if not Context or not Context.triggers then return end
local trigger = triggers[id]
if not trigger then
trace("Error: Cannot stop unknown trigger: " .. tostring(id))
return
end
if not Context.triggers[id] then return end
Context.triggers[id] = nil
trigger.on_stop()
end
--- Resets elapsed time to 0 without calling handlers. No-op if inactive.
--- @within Trigger
--- @param id string The trigger ID.
function Trigger.reset(id)
if not Context or not Context.triggers then return end
if not triggers[id] then
trace("Error: Cannot reset unknown trigger: " .. tostring(id))
return
end
if not Context.triggers[id] then return end
Context.triggers[id].elapsed = 0
end
--- Pauses during minigames.
--- @within Trigger
function Trigger.update()
if not Context or not Context.game_in_progress or not Context.triggers then return end
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
if in_minigame then return end
local fired = {}
for id, state in pairs(Context.triggers) do
local trigger = triggers[id]
if trigger then
state.elapsed = state.elapsed + 1
if state.elapsed >= trigger.duration then
table.insert(fired, id)
end
else
table.insert(fired, id)
end
end
for _, id in ipairs(fired) do
local trigger = triggers[id]
if trigger then
trigger.on_stop()
if trigger.repeating then
Context.triggers[id] = { elapsed = 0 }
else
Context.triggers[id] = nil
end
else
Context.triggers[id] = nil
end
end
end

View File

@@ -1,20 +1,5 @@
-- Manages game maps.
--- @section Map
local _maps = {} local _maps = {}
--- Gets all registered maps as an array.
--- @within Map
--- @return result table An array of registered map data. </br>
--- Fields: </br>
--- * id (string) Unique map identifier.<br/>
--- * from_x (number) Source tile X coordinate in the map sheet.<br/>
--- * from_y (number) Source tile Y coordinate in the map sheet.<br/>
--- * width (number) Width in tiles.<br/>
--- * height (number) Height in tiles.<br/>
--- * to_x (number) Destination X coordinate on screen.<br/>
--- * to_y (number) Destination Y coordinate on screen.<br/>
function Map.get_maps_array() function Map.get_maps_array()
local maps_array = {} local maps_array = {}
for _, map_data in pairs(_maps) do for _, map_data in pairs(_maps) do
@@ -23,16 +8,6 @@ function Map.get_maps_array()
return maps_array return maps_array
end end
--- Registers a map definition.
--- @within Map
--- @param map_data table The map data table.
--- @param map_data.id string Unique map identifier.<br/>
--- @param map_data.from_x number Source tile X coordinate in the map sheet.<br/>
--- @param map_data.from_y number Source tile Y coordinate in the map sheet.<br/>
--- @param map_data.width number Width in tiles.<br/>
--- @param map_data.height number Height in tiles.<br/>
--- @param map_data.to_x number Destination X coordinate on screen.<br/>
--- @param map_data.to_y number Destination Y coordinate on screen.<br/>
function Map.register(map_data) function Map.register(map_data)
if _maps[map_data.id] then if _maps[map_data.id] then
trace("Warning: Overwriting map with id: " .. map_data.id) trace("Warning: Overwriting map with id: " .. map_data.id)
@@ -40,25 +15,10 @@ function Map.register(map_data)
_maps[map_data.id] = map_data _maps[map_data.id] = map_data
end end
--- Gets a map by ID.
--- @within Map
--- @param map_id string The ID of the map.
--- @return result table The map data table or nil. </br>
--- Fields: </br>
--- * id (string) Unique map identifier.<br/>
--- * from_x (number) Source tile X coordinate in the map sheet.<br/>
--- * from_y (number) Source tile Y coordinate in the map sheet.<br/>
--- * width (number) Width in tiles.<br/>
--- * height (number) Height in tiles.<br/>
--- * to_x (number) Destination X coordinate on screen.<br/>
--- * to_y (number) Destination Y coordinate on screen.<br/>
function Map.get_by_id(map_id) function Map.get_by_id(map_id)
return _maps[map_id] return _maps[map_id]
end end
--- Draws a map.
--- @within Map
--- @param map_id string The ID of the map to draw.
function Map.draw(map_id) function Map.draw(map_id)
local map_data = Map.get_by_id(map_id) local map_data = Map.get_by_id(map_id)
if not map_data then if not map_data then
@@ -72,4 +32,4 @@ function Map.draw(map_id)
map_data.to_x, map_data.to_x,
map_data.to_y map_data.to_y
) )
end end

View File

@@ -1,9 +0,0 @@
Map.register({
id = "office",
from_x = 60,
from_y = 0,
width = 30,
height = 17,
to_x = 0,
to_y = 0,
})

View File

@@ -1,9 +0,0 @@
Map.register({
id = "street",
from_x = 30,
from_y = 0,
width = 30,
height = 17,
to_x = 0,
to_y = 0,
})

View File

@@ -1,298 +1,7 @@
--luacheck: ignore max_line_length
-- <PALETTE> -- <PALETTE>
-- 000:0404005f574fc2c3c71d2b53fff1e8ab52367e2553ffa30000875129adff83769c00e436fa77a8ff004dc3c3c7ffccaa -- 000:ab53375f574f0101017f2553c3c3c71d2b53fff1e929adff83779d3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
-- </PALETTE> -- </PALETTE>
-- <TILES>
-- 000:00000000c666666006606060c636366006606060c606366006666660c0000000
-- 001:0000000006666666060600600666666606000600066666660633633300000000
-- 002:0000000066666666006000066666666660060006666666666333363300000000
-- 004:1111111111111111111111111111111111111111111111111111111111111111
-- 005:1111111111111111111111111111111111111111111111111111111100000000
-- 006:0222222200010010033055010130150603305501013015060330550100000000
-- 007:2222222001022220601022206001022060201020602201006022201000000000
-- 008:0000000003330333033303330333000003330111033301110333011103330111
-- 009:0000000033333333333333330000000011100111111001111110011111100111
-- 010:0000000033303330333033300000333011103330111033301110333011103330
-- 011:111111111111111111111100111110a511110a51111015a51101515111000000
-- 012:111111111111111101111111101111115a01111115a011115151011100000111
-- 013:1000000004444444044444440444444404444444044444440444444404444444
-- 014:0000000144444240444424204444424044442420444442404444242044444240
-- 015:0000000004244444024444440424400002444444042424240242424200000000
-- 016:000000004444442044444240c004442044444240e424242042424240c0000000
-- 017:0000000002424240042424200244444004244440024404400424044002440440
-- 018:0000000002424240042424200444424004444420044042400440442004404240
-- 019:0222222200010012055016020150660205501602015066020550160200000000
-- 020:2222222022221000222203302222013022220330222201302222033000000000
-- 021:0333011103330111033301110333011103330111033300000333011103330111
-- 022:1110011111100111111001111110011111100111000000001110011111100111
-- 023:1110333011103330111033301110333011103330000033301110333011103330
-- 024:1111111011111110111111101111111011111110111111101111111011111110
-- 025:0444444404444444044444440444444404444444044044440440444404404444
-- 026:4444242044444240444424204444424044442420444442404444242044444240
-- 027:3333111133331111333311113333111111113333111133331111333311113333
-- 028:0424044002440440042404400244444004244440024242400424242000000000
-- 029:0440442004404240044044200444424004444420024242400424242000000000
-- 030:0000000006666660036060600360666006606060036660600360666003606060
-- 031:0222222200010010033033030130130103303303013013010330330300000000
-- 032:22222220c10012203033022030130220303302203013022030330220c0000000
-- 033:0333011103330111033301110333011103330111033301110333011100000000
-- 034:1110011111100111111001111110011111100111111001111110011100000000
-- 035:1110333011103330111033301110333011103330111033301110333000000000
-- 036:0440444404404444044044440444444404444444044444440444444404444444
-- 037:0000000002222222022000220201110202011102020111020220002202222222
-- 038:0000000022222220220002202011102020111020201110202200022022222220
-- 039:0000000002222222022222220222222202222222022222220222222202222222
-- 040:0000000022222220222222202222222022222220222222202222222022222220
-- 041:0000000002222222022200000220111102201121020112000201120402012200
-- 042:0000000022222220000002201111102021211020000210203430102000430020
-- 043:0666606003606060036066600366606003606060066060600360606003666660
-- 044:0222222202222222022222220222222202222222000000000001111100011111
-- 045:2222222022222220222222202222222022222220000000001111100011111000
-- 046:1111111111111111111111111000000002222222022222220222222202222222
-- 047:1111111111111111111111110000000022222222222222222222222222222222
-- 048:111111111111111111111111c000100022220444e222044422220444e2220444
-- 049:1111111111111111111111111100001100222201022222200222222002222220
-- 050:1111111011111110111111101111111011111110111111101111111011110000
-- 051:1111111111111111111111111111111111111111111111111111111100011111
-- 052:0444444404444444044444440444444404444444044444440444444410000000
-- 053:4444242044444240444424204444424044442420444442404444242000000001
-- 054:0222222202222222022000220201110202011102020111020220002202222222
-- 055:2222222022222220220002202011102020111020201110202200022022222220
-- 056:0222222202222222022222220222222202222222022222220222222200000000
-- 057:2222222022222220222222202222222022222220222222202222222000000000
-- 058:0201122202012222020112220201222202201222022022220222000002222222
-- 059:2204302022204020202200202000002020222020222220200000022022222220
-- 060:5555555555555555555555556666111155555555555555555555555566616666
-- 061:5555555555555555555555556666666655551555555515555555655566666611
-- 062:0222222202222222022222220222222202222222022222220222222202222222
-- 063:2222222222222222222222222222222222222222222222222222222222222222
-- 064:22220444e222044422220444e222044422220444e222044422220444e2220444
-- 065:0222222002222220020000206022220002222220022222200200002000111100
-- 066:5000000004444444044044440440444404404444044044440444444460000000
-- 067:0000000544444240444424204444424044442420444442404444242000000001
-- 068:0000000001111111011111110111111101111111011111110111111100000000
-- 069:0000000011111110111111101111111011111110111111101111111000000000
-- 070:5555555555555555555555551111666655555555555555555555555511666111
-- 071:000000000222220002222090022209900220999002099d900099199009919790
-- 072:0000000022222220222222202222222022220000222011002212000021222220
-- 073:5555555555555555555555556611166655555551555555515555555166666111
-- 074:0000000001111111000000001111666655555555555555555555555511666111
-- 075:0000000011111111000000006611166655555551555555515555555166666111
-- 076:0000000011111111000000001111666655555555555555555555555511666111
-- 077:0000000011111110000000006611166655555551555555515555555166666111
-- 078:1111111111111111111100001110919111101010110191011019191000919190
-- 079:1111111111111111000000009191919000000010111110901111101000000090
-- 080:09191990c991990209199020c999020209902020c902020200202020c0000000
-- 081:0000000002020200202020200200022020002220000222202022222002222220
-- 082:5555555555555055555501056666010155550105555501055555010566610106
-- 083:5555555555555555000000000222222202222222022222220222222202222222
-- 084:5555555555555555000000002222222022222220222222202222222022222220
-- 085:5555000055500220550202206022022002220220022202200222022002220220
-- 086:1000000110333301103333011033330110333301103333011033330110333301
-- 087:3010101033331111333311113333111111113333111133331111333311113333
-- 088:3333101033331090333310103333109011113010111010901110201011101090
-- 089:2222222022222220222222202222222022222220222222202222222022222220
-- 090:5555550055555022555502226611022255550222555502225555501266666100
-- 091:0005010522200105222201052222010622220105222201052210505500066111
-- 092:5000000002222222022222220222222202222222022222220222222202222222
-- 093:0222022002220220022202200222022002220220022202200222022002220220
-- 094:1033330110333301103333011033330110333301103333011033330110000001
-- 095:3333101033331090333310103333109011113010111130901111301011113000
-- 096:00000000c111111100000000c106666601051555c105155501056555c0066611
-- 097:0000000011111110000000006666101055555010555550105555501066616000
-- 098:5555555555555550555555056666116655550055555500555555655566666611
-- 099:0555555500555555050555550661111105500555115005550055555500616666
-- 100:0222222202222222022222220000000000000000501055555010555560006666
-- 101:2222222022222220222222200000000000000000555501055555010566660001
-- 102:0222022002220220022202200222022002220220022202200222022012220220
-- 103:1000000103333330033333300333333003333330033333300333333010000001
-- 104:5500000050222222022222220222222202222222022222220222222202222222
-- 105:0222222222222222222222222222222222222222222222222222222222222222
-- 106:1222000012201110120111101011111001111110011111100111111001111110
-- 107:0000000001111111011111110111111101111111011111115000000066000066
-- 108:0000000011111111111111111111111111111111111111110000000066666611
-- 109:1111111011111110111111101111111011111110111111100000000566000066
-- 110:1111111111111111111111111111010011101044111010441110104400000010
-- 111:1111111111111111111111110011111144011111440111114401111110000000
-- 112:11111111111111111111111111111111111111111111111111110111c0004000
-- 113:3333104433331044333310003333111111113333111133331111333311113333
-- 114:4403111144031111000311113333111111110000111044441104444411014444
-- 115:3304440133044401330444013304440100104033440103334440333344103333
-- 116:3330111133020000330124243330111111110111111130111111301111113300
-- 117:1103111100401111242011111240111111203333124033331120333300013333
-- 118:1111111100000000033333330333333303333333033333330000000011111111
-- 119:1111111100000000333333303333333033333330333333300000000011111111
-- 120:1a12222222a222222212222222a222222212222222a222222212222222222222
-- 121:1a8b888822888b8b228b888822b8bb8b22888b88228b88882288b88b228b8b88
-- 122:8b8b8888b8b88b8bb88b888888b8bb8b8b888b8888bb88888b88b88bb88b8b88
-- 123:8b8b888800000000055555550555555500000000055555550555555500000000
-- 124:8b8b888800000000555555555555555500000000555555555555555500000000
-- 125:8b8b888800000000555555505555555000000000555555505555555000000000
-- 126:0000000007777777070000000703333307033333070333330703333307033333
-- 127:0000000077777777000000003333333033333330333333303333333033333330
-- 128:000000007777777700000000e222222222222222e222222222222222e2222222
-- 129:0000000077777770000000702222207022222070222220702222207022222070
-- 130:3000000300ffff000ddddff00dddddf00dddddf00dddddf000dddd0000000000
-- 131:0555555505555555055555550555555500000000050b88880508b88b000b8b88
-- 132:555555555555555555555555555555550000000088bb88888b88b88bb88b8b88
-- 133:555555505555555055555550555555500000000088bb80508b88b050b88b8000
-- 134:0703333307033333070333330703333307033333070333330703333307033333
-- 135:3333333033333330333333303333333033333330333333303333333033333330
-- 136:2222207022222070222220702222207022222070222220702222207022222070
-- 137:4444444444444444444444444444444444444444444444444444444444444444
-- 138:4442222244422222444222224442222244422222444222224442222244422222
-- 139:0000000000bbbb0008888bb0088888b0088888b0088888b00088880030000003
-- 140:0000000007777777076666770767776707677776076777760767777607766667
-- 141:0000000077777770777777707777777077777770777777777677777777777777
-- 142:0000000066666666666666666666666600000000777777777777777777777777
-- 143:0000000066666070666660706666607000000070777777707777777077777770
-- 144:1a80088be2800b8822800888e280088b22800b88e220022221a001a2ea100a12
-- 145:88888888b88b8b888b8888b8888b888888888b8b12222222a1a1a1a21a1a1a12
-- 146:0776777707767777077677770776777707677777076777770777777700000000
-- 147:7677666676777776767777677677767767776777677666667777777700000000
-- 148:7766667777777677777767767776777677677776766666767777777700000000
-- 149:7667777067767770777767706666677077776770777767707777777000000000
-- 150:2220022222200222222002222220022222200222222002222220022222200222
-- 151:1a12222222a222222212222222a222222212222222222222a1a1a1a21a1a1a12
-- 152:2222222222222222222222222222222222222222444444444444444444444444
-- 153:222002222220022222200222222002222220022212200222a000000200000000
-- 154:222222222222222222222222222222222222222212222222a1a1a1a21a1a1a12
-- 155:11111000111100331110a033110a903310a9a0330a9a903309a9a0330a9a9033
-- 156:0000000033333333333333333333333333333333333333333333333333333333
-- 157:000000000a9a9a9a09a9a9a90a9a9a9a09a9a9a90a9a9a9a09a9a9a90a9a9a9a
-- 158:001111119a001111a9a904449a9a9004a9a9a9a09a9a9a9aa9a9a9a99a9a9a9a
-- 159:111111111111111111111111111111111111111100111111090111110a900111
-- 160:1111111111111111444444444444444444444444444444441111111111111111
-- 161:09a9a0330a9a903309a9a0330000000003333333033333330333333303333333
-- 162:3333333333333333333333330000000033333333333333333000000301111110
-- 163:3333333333333333333333330000000033333333333333333333333333333333
-- 164:09a9a9a90a9a9a9a09a9a9a90000000003333333033333330333333303333333
-- 165:a9a9a9a99a9a9a9aa9a9a9a90000000033333333333333333333333333333333
-- 166:09a9a0110a9a9a0109a9a9a00000000033333333333333333000000301111110
-- 167:1111111111111111111111110011111133001111333301113333300133333330
-- 168:0333333003333301033333010333330103333301000000011111111111111111
-- 169:1100001110555501055005500506605005066050055005501055550111000011
-- 170:0333333310333333103333331033333310333333100000001111111111111111
-- 171:0333333303333333033333330333333303333333000000001111111111111111
-- 172:3333333033333301333333013333330133333301000000011111111111111111
-- 173:0333333010333330103333301033333010333330100000001111111111111111
-- 174:111111111111111111111111111111111111111112222222a1a1a1a21a1a1a12
-- 175:1111111111111111111111111111111111111111444444444444444444444444
-- 176:00000000c111111001101010c131311001101010c101311001111110c0000000
-- 177:0000000001111111010100100111111101000100011111110133133300000000
-- 178:0000000011111111001000011111111110010001111111111333313300000000
-- 179:11111111111111111111111111111111111100001110b161110b100010810101
-- 180:1111111111111111111111111111111101111111b01111110001111101011111
-- 181:111111111000111000880110080880100b08801008b080100b80801010b80010
-- 182:081b01010081b00011001b1b11110000111111111111110111111080111108b0
-- 183:01011111000111111b0111110011111111111111111111111111111111111111
-- 184:1111111111111100111110081111108011111010111110811111101811111101
-- 185:110b80100110801080110000b80111108b80111008b801100b1b011080b18010
-- 186:11108b801108b801008b80111100011111111111111111101111100111110818
-- 187:1111111111111111111111111111111111111111000111118001111108011111
-- 188:0000000004444444040010040440404404404004044040440440400404444444
-- 189:0000000044444444044001000440440404400404044044040040040444444444
-- 190:0000000044444444404040044040404044004004444040444000404444444444
-- 191:0000000044444444400410444044044440040004404444044004001444444444
-- 192:0000000044444440444444404444444044444440444444404444444044444440
-- 193:0000000002222222020000000200000002000000020000000200000002000000
-- 194:0000000022222222000000000000000000000000000000000000000000000000
-- 195:0000000022222220000000100000001000000010000000100000001000000010
-- 196:3333999933339999333399993000000002222222000000000444444404111111
-- 197:3333999933339999333399990000000922222203000000004440d6d011201110
-- 198:3333999933339999333399993333999999993333999933339999333399993333
-- 199:0000000001111110031010100310111001101010031110100310111003101010
-- 200:1111110811111101111111101111111011111111111111111111111111111111
-- 201:180b8010810180101810b0108180101008180010108100101108101011108010
-- 202:111081801110180811018081110810b8101801801080b8011008101100100111
-- 203:8b011111b8011111801111110111111111111111111111111111111111111111
-- 204:0440000404404444044040040440440404440014044444440444444404444444
-- 205:4001404404404004000040400440404404404044444444444444444444400000
-- 206:4040041000404404404004004040444440400400444444444404444400044444
-- 207:4444000044440222044402220444022214440222444402224444000044444444
-- 208:00000040e222204022222040e222204022222040e22220400000004044444440
-- 209:0200000002000000020000000200000002000000020000000200000002000000
-- 210:0000001000000010000000100000001000000010000000100000001000000010
-- 211:0444444404111111044444440411111104444444000000000111111100000000
-- 212:4440222011201110444024201120444044402420000000001111111000000000
-- 213:0111101003101010031011100311101003101010011010100310101003111110
-- 214:1000000000606000060606060000000010565656106565651106565611100000
-- 215:0000000100606060060606000000000055555501555555015555501100000111
-- 216:0444444404444044044404040440440004404404044044440444444400000000
-- 217:4404444440444444044444404444440444444440444444444444444400000000
-- 218:4404444440444004044404404444044400004440444444444444444400000000
-- 219:4444440044444402404444000044441044044444444444104444444400000000
-- 220:0000444022204440000044401101444000444440440144404444444000000000
-- 221:0200000002000000020000000200000002000000020000000211111100000000
-- 222:0000000000000000000000000000000000000000000000001111111100000000
-- 223:000000000000000000000000000000000000000000000000111dd11100000000
-- 224:00000010c000001000000010c000001000000010c000001011111110c0000000
-- 225:3333333333333333333333333333333313131313313131311313131331313131
-- 226:0203333302033333020333330203333302031313020131310203131300013131
-- 227:3333302033333020333330203333302013131020313130201313102031313000
-- 228:0000000011111111111111111111111111111111111111111111111100000000
-- 229:3000000004444444044044440440444404404444044044440444444430000000
-- 230:0000000344444240444424204444424044442420444442404444242000000001
-- 231:333333303333330033333090333309901310999031099d901099199009919790
-- 232:3333333333333333333333333333333313130000313011001313000331313131
-- 233:0000000002222222022222220222200202220440022204440220444402204414
-- 234:0000000022222220100001202100002002222220400222204440022044444020
-- 235:3333333333333033333301033333010313130103313101011313010331310101
-- 236:0204414402200441022220040222222002222222022222220222200002220111
-- 237:1444402044140220444402204144022000402220220222200222222010222220
-- 238:3333330033333022333302223333022213130222313102221313101231313100
-- 239:0003010322200103222201032222010322220103222201012210101300013131
-- 240:02220111c222001102220000c222033302220333c222200002222222c2222222
-- 241:1002222001202220002022203002222031222220022222202222222022222220
-- 242:3333333333333330333333033333113313130013313100311313131331313131
-- 243:0333333300333333030333330331133303100313113001310013131300313131
-- 244:022222200222220002222090022209900220999002099d900099199009919790
-- 245:2222222022222220222222202222222022220000222011002212000021222220
-- 246:0000000001111111000000000103333301031313010131310103131300013131
-- 247:0000000011111110000000003333301013131010313130101313101031313000
-- </TILES>
-- <SPRITES>
-- 002:00000000000000000000444400044444000444440044ffff004fffff004f3333
-- 003:0000000000000000441600004242600044241000ff426000fff4100033f26000
-- 004:00000333000035550003655500365555003555ff00356fff00365f3f00355fff
-- 005:33000000553000005563000055563000ff553000fff53000f3f63000fff53000
-- 016:0000000000000000000000000000003000000353000035350003535100353535
-- 017:0000000000000000000000003000000053300000151300005151300015151300
-- 018:004f99ff000fffff0000ff3300000fff00003666000355550035652503163555
-- 019:99f41000fff26000ff600000f600000063300000555330005555530055535530
-- 020:00356f6f003655f60365511f3653122f3531222f363221220532232203322322
-- 021:f6f530006f563000f1156300f2215300f2226300221233002232130022121300
-- 032:0033535100353533003351ff00351f3f0003ff3f0003ffff00003ff3000323ff
-- 033:5555530033555300ff155300f3f15300f3ff3000ffff30003ff30000ff323000
-- 034:036135250316355503613525031633110333331103f333330333333300033333
-- 035:55565530555355305556553013335530133333303333ff303333333033330000
-- 036:00322322003223330033331a003ff31a003ff3a1003333a100003a1a00003a1a
-- 037:223213003332130011133300111f300011133000111300001113000011130000
-- 048:00323123003231430032314300313339000f3333000033300000333000033330
-- 049:324303003443030034430300933313003333f000033300000333000003333000
-- 050:0003333000033330000333300003333000053530003311300031113000333330
-- 051:3333000033330000333300003333000035350000311330003111300033333000
-- 052:000031a1000031a100003a1a00003a1a00003333000003f3000003f300000330
-- 053:111300001113000011130000111300003333000003f3000003f3000003300000
-- </SPRITES>
-- <MAP>
-- 000:ffffffffff0010201020102010201020102010201020102000ffffffffff40404040404087f3f3f3f397a7b7c7d7a7e7f70818a7b7c7d7a7b7c7d7a70b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 001:ffffffffff0040404040404040404040404040404040404000ffffffffff40404040404087f3f3f3f328a7384858a76878f388a7384858a7384858a70b40403b4b4040404040404040404040404040404040404040404040400b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 002:ffffffffff00406070408090a040b0c0d0e0f001f001112100ffffffffff984098409840a8f3f3f3f3b8a7a7a7a7a7c8d8e8f8a7a7a7a7a7a7a7a7a70b405b6b7b4040404040404040404040404040d0e0f001f001f00111210b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 003:ffffffffff004031414051617140814091a1b1b1b1b1c1d100ffffffffff984098409840a8f3f3f3f3091919191919293949591919191919191919190b8b9babbb4040cbdbebfb0c401c2c2c2c3c4091a14c5c6c6c6c6cc1d10b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 004:ffffffffffe140f1024012223240814042a15262728292a2e1ffffffffff984098409840a8f3f3f3f369f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f37c8c9cacbc7282ccdcecfc0d401d3030302d4042a13d4d7282728292a27c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 005:ffffffffffb240c2d240e2f203132333435363738393a3b3b2ffffffffff984098409840a8f3f3f3f369f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f35d406d7d40e3958d9dadbdcd40ddedfded0e404353839383938393a3b35d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 006:ffffffffffe1c3d3c3d3e3f30414c3d32434445410201020e1ffffffffff404040404040798989898999a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a97c1e1e1e1e44542e1e1e1e3e1e444e4e4e541e243444544454445444547c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 007:ffffffffffb264748494a4b4c4d46494649464940040e4f4b2ffffffffff4040404040404040404040404040404040404040404040404040404040405d1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 008:ffffffffffe1c30515d325d33545c3d355d3c3d365b17585e1ffffffffff4040404040404098989898404040404040404040404040404040404040407c1e7e8e1e1e1e9eae1ebe1e1e1e1e1e1e1e1e72821ebe1e72821ebe1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 009:ffffffffffb264e395a5b594e39564c5d5946494e5b1b1f5b2ffffffffff4040404040404040404040404040404040404040404040404040404040405d1e05151ebe1ecedeeefe1e1e1e1e1e1e1e1ee395eefe1ee395eefe1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 010:ffffffffffe1c306162636d34656c395d5d3c3d376b1b1b1e1ffffffffff404040404040409898989840b9c9c9d9e9f90a0a0a0a4040400a0a0a0a407c1ee395eefe1e0f1f2f3f1e1e1e1e1e1e1e1ee3952f3f1ee3952f3f1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 011:ffffffffffb264946494649464948696a694649410201020b2ffffffffff4040404040404040404040401a2a3a4a5a6a7a40404040404040404040405d1ee3952f3f1e4f5f1ebe1e1e1e1e1e1e1e1ee3951ebe1ee3951ebe1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 012:ffffffffffe1c37282d3c3d3c3d3b6c6d6d3c3d300e6f607e1ffffffffff4040404040404098989898408a9aaabaca9ada40404040404040404040407c1e4f5f1ebe1e0515eefe1e1e1e1e1e1e1e1ee395eefe1ee395eefe1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 013:ffffffffffb264e395946494649464946494649465172737b2ffffffffffeaeaeaeaeaeaeafafafafaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea5d1e0515eefe1e6f7f2f3f1e1e1e1e1e1e1e1e6f7f2f3f1e6f7f2f3f1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 014:ffffffffffe1c34454d3c3d3c3d3c3d3c3d3c3d3e5b14757e1fffffffffff3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f37c1e6f7f2f3f1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e7c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 015:ffffffffffb2649464946494649464946494649476b1b1b1b2fffffffffff3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f35d1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 016:ffffffffff0010201020766777001020102010201020102000fffffffffff3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f30b1b2b1b2b7667776777761b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b1b2b0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- </MAP>
-- <SFX> -- <SFX>
-- 000:060006400600064006000640060006400600060006000600060006000600060006000600060006000600060006000600060006000600060006000600300000000900 -- 000:060006400600064006000640060006400600060006000600060006000600060006000600060006000600060006000600060006000600060006000600300000000900
-- 016:05000500050005400540054005700570057005400540054005700570057005c005c005c005c005c005c005c005c005c005c005c005c005c005c005c0470000000000 -- 016:05000500050005400540054005700570057005400540054005700570057005c005c005c005c005c005c005c005c005c005c005c005c005c005c005c0470000000000
@@ -305,15 +14,13 @@
-- 033:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004 -- 033:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
-- 044:0600f6000620f6000600f6000610f600f600f6000600f600f600f600f6000600060006000600060006000600060006000600060006000600060006004600000f0f00 -- 044:0600f6000620f6000600f6000610f600f600f6000600f600f600f600f6000600060006000600060006000600060006000600060006000600060006004600000f0f00
-- 045:0000f0000020f0000000f0000010f000f000f0000000f000f000f000f0000000000000000000000000000000000000000000000000000000000000004600000f0f00 -- 045:0000f0000020f0000000f0000010f000f000f0000000f000f000f000f0000000000000000000000000000000000000000000000000000000000000004600000f0f00
-- 048:090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900400000000000
-- 056:4100510061406140717081709100b100c100d100e100e100e100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f10058a000000600
-- 057:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004 -- 057:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
-- 058:41004110410041104100411041004110c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100500000080800 -- 058:41004110410041104100411041004110c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100100000080800
-- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000 -- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000
-- 060:220022002200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200100000000000 -- 060:220022002200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200100000000000
-- 061:9f009f00bf00df00df00ef00ef00ef00ef00ef00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00400000000000 -- 061:9f009f00bf00df00df00ef00ef00ef00ef00ef00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00400000000000
-- 062:00000100010001000100510081008100910091009100a100a100a100a100a100b100b100b100b100c100c100c100d100d100d100e100e100e100f100484000000000 -- 062:00000100010001000100510081008100910091009100a100a100a100a100a100b100b100b100b100c100c100c100d100d100d100e100e100e100f100484000000000
-- 063:00b000100000000000000000100060009000b000c000d000d000e000e000e000f000f000f000f000f000f000f000f000f000f000f000f000f000f000200000000000 -- 063:00b000100000000000000000100060009000b000c000d000d000e000e000e000f000f000f000f000f000f000f000f000f000f000f000f000f000f000405000000000
-- </SFX> -- </SFX>
-- <WAVES> -- <WAVES>
-- 000:bcceefceedddddc84333121268abaa99 -- 000:bcceefceedddddc84333121268abaa99
@@ -327,14 +34,280 @@
-- 008:0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f -- 008:0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
-- 009:fff000fff000fff000fff000fff000ff -- 009:fff000fff000fff000fff000fff000ff
-- </WAVES> -- </WAVES>
-- <PATTERNS> -- <TILES>
-- 000:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000008c10000000008c10000000000000000000000000000000000000000000000000000000000000000004008b50000000000000000001008c10000004008b50000001008c10000000008c1000000e008b30000004008b50000001008c10000000008c10000000008c10000000008c10000000008c10000000008c1000000000000000000000000000000 -- 000:2222222223333332233232322353533223323232232353322333333222222222
-- 001:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007008b50000007008b50000001008c10000007008b50000001008c10000000008c10000007008b50000009008b50000001008c10000009008b50000001008c10000009008b50000009008b50000001008c10000009008b50000001008c1000000 -- 001:2222222223333333232322322333333323222322233333332355355522222222
-- 003:4008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d9000000000000000000000000000000000000000000 -- 002:2222222233333333223222233333333332232223333333333555535522222222
-- 004:40088d000000e0088b000000b0088b000881e0088b00000040088d000000e0088b000881b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e00889000000 -- 003:2222222223333333232322322333333323222322233333332355355522222222
-- 005:400881000000000881000000000881000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000 -- 004:2222222233333333223222233333333332232223333333333555535522222222
-- </PATTERNS> -- 005:2222222223333333232322322333333323222322233333332355355522222222
-- <TRACKS> -- 006:2222222233333333223222233333333332232223333333333555535522222222
-- 000:100001200001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -- 007:2222222223333333232322322333333323222322233333332355355522222222
-- 001:581000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -- 008:2222222233333333223222233333333332232223333333333555535522222222
-- </TRACKS> -- 009:2222222223333333232322322333333323222322233333332355355522222222
-- 010:2222222233333333223222233333333332232223333333333555535522222222
-- 011:2222222223333333232322322333333323222322233333332355355522222222
-- 012:2222222233333333223222233333333332232223333333333555535522222222
-- 013:2222222223333333232322322333333323222322233333332355355522222222
-- 014:2222222233333333223222233333333332232223333333333555535522222222
-- 015:2222222223333333232322322333333323222322233333332355355522222222
-- 016:2222222223333332233232322353533223323232232353322333333222222222
-- 017:1111111111111111111111111111111111111111111111111111111111111111
-- 018:1111111111111111111111111111111111111111111111111111111122222222
-- 019:1111111111111111111111111111111111111111111111111111111122222222
-- 020:1111111111111111111111111111111111111111111111111111111111111111
-- 021:1111111111111111111111111111111111111111111111111111111111111111
-- 022:1111111111111111111111111111111111111111111111111111111111111111
-- 023:1111111111111111111111111111111111111111111111111111111111111111
-- 024:1111111111111111111111111111111111111111111111111111111111111111
-- 025:1111111111111111111111111111111111111111111111111111111111111111
-- 026:1111111111111111111111111111111111111111111111111111111111111111
-- 027:1111111111111111111111111111111111111111111111111111111111111111
-- 028:1111111111111111111111111111111111111111111111111111111111111111
-- 029:1111111111111111111111111111111111111111111111111111111111111111
-- 030:1111111111111111111111111111111111111111111111111111111111111111
-- 031:1111111111111111111111111111111111111111111111111111111111111111
-- 032:2222222223333332233232322353533223323232232353322333333222222222
-- 033:1111111111111111111111111111111111111111111111111111111111111111
-- 034:2444444422212212255200212152102325520021215210232552002122222222
-- 035:4444444221244442321244423221244232421242324421223244421222222222
-- 036:1111111111111111111111111111111111111111111111111111111111111111
-- 037:2222222225552555255525552555222225552111255521112555211125552111
-- 038:2222222255555555555555552222222211122111111221111112211111122111
-- 039:2222222255525552555255522222555211125552111255521112555211125552
-- 040:1111111111111111111111111111111111111111111111111111111111111111
-- 041:1111111111111111111111221111128011112801111210801121010111222222
-- 042:1111111111111111211111111211111108211111108211110101211122222111
-- 043:1222222226666666266666662666666626666666266666662666666626666666
-- 044:2222222166666462666646426666646266664642666664626666464266666462
-- 045:2222222226466666246666662646622224666666264646462464646422222222
-- 046:2222222266666642666664622226664266666462464646426464646222222222
-- 047:2222222226466666246666662646622224666666264646462464646422222222
-- 048:2222222223333332233232322353533223323232232353322333333222222222
-- 049:1111111111111111111111111111111111111111111111111111111111111111
-- 050:2444444422212214200213242102332420021324210233242002132422222222
-- 051:4444444244441222444425524444215244442552444421524444255222222222
-- 052:1111111111111111111111111111111111111111111111111111111111111111
-- 053:2555211125552111255521112555211125552111255522222555211125552111
-- 054:1112211111122111111221111112211111122111222222221112211111122111
-- 055:1112555211125552111255521112555211125552222255521112555211125552
-- 056:1111111111111111111111111111111111111111111111111111111111111111
-- 057:1111111211111112111111121111111211111112111111121111111211111112
-- 058:1111111111111111111111111111111111111111111111111111111111111111
-- 059:2666666626666666266666662666666626666666266266662662666626626666
-- 060:6666464266666462666646426666646266664642666664626666464266666462
-- 061:5555111155551111555511115555111111115555111155551111555511115555
-- 062:5555111155551111555511115555111111115555111155551111555511115555
-- 063:5555111155551111555511115555111111115555111155551111555511115555
-- 064:2222222223333332253232322532333223323232253332322532333225323232
-- 065:1111111111111111111111111111111111111111111111111111111111111111
-- 066:2444444422212212255255252152152125525525215215212552552522222222
-- 067:4444444221221442525524425215244252552442521524425255244222222222
-- 068:1111111111111111111111111111111111111111111111111111111111111111
-- 069:2555211125552111255521112555211125552111255521112555211122222222
-- 070:1112211111122111111221111112211111122111111221111112211122222222
-- 071:1112555211125552111255521112555211125552111255521112555222222222
-- 072:1111111111111111111111111111111111111111111111111111111111111111
-- 073:1111111211111112111111121111111211111112111111121111111211111112
-- 074:1111111111111111111111111111111111111111111111111111111111111111
-- 075:2662666626626666266266662666666626666666266666662666666626666666
-- 076:6666464266666462666646426666646266664642666664626666464266666462
-- 077:2222222224444444244222442421112424211124242111242442224424444444
-- 078:2222222244444442442224424211124242111242421112424422244244444442
-- 079:2222222224444444244444442444444424444444244444442444444424444444
-- 080:2333323225323232253233322533323225323232233232322532323225333332
-- 081:1111111111111111111111111111111111111111111111111111111111111111
-- 082:2444444424444444244444442444444424444444222222222221111122211111
-- 083:4444444244444442444444424444444244444442222222221111122211111222
-- 084:1111111111111111111111111111111111111111111111111111111111111111
-- 085:1111111111111111111111111222222224444444244444442444444424444444
-- 086:1111111111111111111111112222222244444444444444444444444444444444
-- 087:1111111111111111111111112222122244442666444426664444266644442666
-- 088:1111111111111111111111111122221122444421244444422444444224444442
-- 089:1111111211111112111111121111111211111112111111121111111211112222
-- 090:1111111111111111111111111111111111111111111111111111111122211111
-- 091:2666666626666666266666662666666626666666266666662666666612222222
-- 092:6666464266666462666646426666646266664642666664626666464222222221
-- 093:2444444424444444244222442421112424211124242111242442224424444444
-- 094:4444444244444442442224424211124242111242421112424422244244444442
-- 095:2444444424444444244444442444444424444444244444442444444422222222
-- 096:2222222223333332253232322532333223323232253332322532333225323232
-- 097:0000000000000000000000003333111100000000000000000000000033313333
-- 098:0000000000000000000000003333333300001000000010000000300033333311
-- 099:0000000000000000000000003333111100000000000000000000000033313333
-- 100:0000000000000000000000003333333300001000000010000000300033333311
-- 101:2444444424444444244444442444444424444444244444442444444424444444
-- 102:4444444444444444444444444444444444444444444444444444444444444444
-- 103:4444266644442666444426664444266644442666444426664444266644442666
-- 104:2444444224444442242222423244442224444442244444422422224222111122
-- 105:0000000000000000000000003333111100000000000000000000000033313333
-- 106:0000000000000000000000003333333300001000000010000000300033333311
-- 107:0222222226666666266266662662666626626666266266662666666632222222
-- 108:2222222066666462666646426666646266664642666664626666464222222221
-- 109:2222222221111111211111112111111121111111211111112111111122222222
-- 110:2222222211111112111111121111111211111112111111121111111222222222
-- 111:2222222223333333232322322333333323222322233333332355355522222222
-- 112:2333323225323232253233322533323225323232233232322532323225333332
-- 113:0000000000000000000000001111333300000000000000000000000011333111
-- 114:2222222224444422244442722444277224427772242770722277177227717072
-- 115:2222222244444442444444424444444244442222444211224414222241444442
-- 116:0000000000000000000000003311133300000001000000010000000133333111
-- 117:2222222221111111222222221111333300000000000000000000000011333111
-- 118:2222222211111111222222223311133300000001000000010000000133333111
-- 119:2222222211111111222222221111333300000000000000000000000011333111
-- 120:2222222211111112222222223311133300000001000000010000000133333111
-- 121:0000000000000000000000001111333300000000000000000000000011333111
-- 122:0000000000000000000000003311133300000001000000010000000133333111
-- 123:0000000000000000000000001111333300000000000000000000000011333111
-- 124:0000000000000000000000003311133300000001000000010000000133333111
-- 125:0000000000000000000000001111333300000000000000000000000011333111
-- 126:0000000000000000000000003311133300000001000000010000000133333111
-- 127:2222222223333332233232322353533223323232232353322333333222222222
-- 128:2222222223333332253232322532333223323232253332322532333225323232
-- 129:0000000000000000000000003333111100000000000000000000000033313333
-- 130:2717177227717724271772422777242427724242272424242242424222222222
-- 131:2222222224242422424242422422244242224442222444424244444224444442
-- 132:0000000000000000000000003333333300001000000010000000300033333311
-- 133:0000000000000200000021203333212100002120000021200000212033312123
-- 134:0000000000000000000000003333333300001000000010000000300033333311
-- 135:0000000000000000222222222444444424444444244444442444444424444444
-- 136:0000000000000000222222224444444244444442444444424444444244444442
-- 137:0000000000000000000000003333111100000000000000000000000033313333
-- 138:0000000000000000000000003333333300001000000010000000300033333311
-- 139:0000222200022442002424423244244224442442244424422444244224442442
-- 140:0000000000000000000000003333333300001000000010000000300033333311
-- 141:0000000000000000000000003333111100000000000000000000000033313333
-- 142:0000000000000000000000003333333300001000000010000000300033333311
-- 143:1222222112555521125555211255552112555521125555211255552112555521
-- 144:2333323225323232253233322533323225323232233232322532323225333332
-- 145:0000000000000000000000001111333300000000000000000000000011333111
-- 146:2444444424444444244444442444444424444444244444442444444424444444
-- 147:4444444244444442444444424444444244444442444444424444444244444442
-- 148:0000002200000244000024443311244400002444000024440000021433333122
-- 149:2220212044422120444421204444212344442120444421204412020022233111
-- 150:0000000000000000000000003311133300000001000000010000000133333111
-- 151:2444444424444444244444442444444424444444244444442444444424444444
-- 152:4444444244444442444444424444444244444442444444424444444244444442
-- 153:0000000000000000000000001111333300000000000000000000000011333111
-- 154:0222222224444444244444442444444424444444244444442444444424444444
-- 155:2444244224442442244424422444244224442442244424422444244224442442
-- 156:0000000000000000000000003311133300000001000000010000000133333111
-- 157:0000000000000000000000001111333300000000000000000000000011333111
-- 158:0000000000000000000000003311133300000001000000010000000133333111
-- 159:1255552112555521125555211255552112555521125555211255552112222221
-- 160:2222222223333332253232322532333223323232253332322532333225323232
-- 161:0000000000000000000000003333111100000000000000000000000033313333
-- 162:2222222221111111222222222123333321201000212010002120300022233311
-- 163:2222222211111112222222223333121200000212000002120000021233313222
-- 164:0000000000000002000000203333113300002200000022000000300033333311
-- 165:2000000022000000202000002331111120022000110220002200000022313333
-- 166:0000000000000000000000003333333300001000000010000000300033333311
-- 167:2444444424444444244444442222222222222222021200000212000032223333
-- 168:4444444244444442444444422222222222222222000021200000212033332221
-- 169:0000000000000000000000003333111100000000000000000000000033313333
-- 170:2444444424444444244444442444444424444444244444442444444424444444
-- 171:2444244224442442244424422444244224442442244424422444244214442442
-- 172:0000000000000000000000003333333300001000000010000000300033333311
-- 173:0000000000000000000000003333111100000000000000000000000033313333
-- 174:0000000000000000000000003333333300001000000010000000300033333311
-- 175:1222222125555552255555522555555225555552255555522555555212222221
-- 176:2333323225323232253233322533323225323232233232322532323225333332
-- 177:0000000000000000000000001111333300000000000000000000000011333111
-- 178:0000000000000000000000003311133300000001000000010000000133333111
-- 179:0000000000000000000000001111333300000000000000000000000011333111
-- 180:0000000000000000000000003311133300000001000000010000000133333111
-- 181:0000000000000000000000001111333300000000000000000000000011333111
-- 182:0000000000000000000000003311133300000001000000010000000133333111
-- 183:0000000000000000000000001111333300000000000000000000000011333111
-- 184:0000000000000000000000003311133300000001000000010000000133333111
-- 185:0022222202444444244444442444444424444444244444442444444424444444
-- 186:2444444444444444444444444444444444444444444444444444444444444444
-- 187:1444222214421112142111121211111221111112211111122111111221111112
-- 188:0000000000000000000000003311133300000001000000010000000133333111
-- 189:0000000000000000000000001111333300000000000000000000000011333111
-- 190:0000000000000000000000003311133300000001000000010000000133333111
-- 191:2222222223333333232322322333333323222322233333332355355522222222
-- 192:2222222223333332253232322532333223323232253332322532333225323232
-- 193:0000000000000000000000003333111100000000000000000000000033313333
-- 194:2222222224444444244444442444444424444444244444442444444424444444
-- 195:2222222244444442444444424444444244444442444444424444444244444442
-- 196:0000000000000000000000003333333300001000000010000000300033333311
-- 197:0000000000000000000000003333111100000000000000000000000033313333
-- 198:0000000000000000000000003333333300001000000010000000300033333311
-- 199:0000000000000000000000003333111100000000000000000000000033313333
-- 200:0000000000000000000000003333333300001000000010000000300033333311
-- 201:2222222221111111211111112111111121111111211111110222222233222233
-- 202:2222222211111111111111111111111111111111111111112222222233333311
-- 203:1111111211111112111111121111111211111112111111122222222033222233
-- 204:0000000000000000000000003333333300001000000010000000300033333311
-- 205:0000000000000000000000003333111100000000000000000000000033313333
-- 206:0000000000000000000000003333333300001000000010000000300033333311
-- 207:2222222223333332233232322353533223323232232353322333333222222222
-- 208:2333323225323232253233322533323225323232233232322532323225333332
-- 209:0000000000000000000000001111333300000000000000000000000011333111
-- 210:2444444424444444244444442444444424444444244444442444444424444444
-- 211:4444444244444442444444424444444244444442444444424444444244444442
-- 212:0000000000000000000000003311133300000001000000010000000133333111
-- 213:0000000000000000000000001111333300000000000000000000000011333111
-- 214:0000000000000000000000003311133300000001000000010000000133333111
-- 215:0000000000000000000000001111333300000000000000000000000011333111
-- 216:0000000000000000000000003311133300000001000000010000000133333111
-- 217:0000000000000000000000001111333300000000000000000000000011333111
-- 218:0000000000000000000000003311133300000001000000010000000133333111
-- 219:0000000000000000000000001111333300000000000000000000000011333111
-- 220:0000000000000000000000003311133300000001000000010000000133333111
-- 221:0000000000000000000000001111333300000000000000000000000011333111
-- 222:0000000000000000000000003311133300000001000000010000000133333111
-- 223:1222222112555521125555211255552112555521125555211255552112555521
-- 224:2222222223333332253232322532333223323232253332322532333225323232
-- 225:0000000000000000000000003333111100000000000000000000000033313333
-- 226:2222222221111111211111112111111121111111211111112111111122222222
-- 227:2222222211111112111111121111111211111112111111121111111222222222
-- 228:0000000000000000000000003333333300001000000010000000300033333311
-- 229:0000000000000000000000003333111100000000000000000000000033313333
-- 230:0000000000000000000000003333333300001000000010000000300033333311
-- 231:0000000000000000000000003333111100000000000000000000000033313333
-- 232:0000000000000000000000003333333300001000000010000000300033333311
-- 233:0000000000000000000000003333111100000000000000000000000033313333
-- 234:0000000000000000000000003333333300001000000010000000300033333311
-- 235:0000000000000000000000003333111100000000000000000000000033313333
-- 236:0000000000000000000000003333333300001000000010000000300033333311
-- 237:0000000000000000000000003333111100000000000000000000000033313333
-- 238:0000000000000000000000003333333300001000000010000000300033333311
-- 239:1255552112555521125555211255552112555521125555211255552112222221
-- 240:2333323225323232253233322533323225323232233232322532323225333332
-- 241:0000000000000000000000001111333300000000000000000000000011333111
-- 242:0000000000000000000000003311133300000001000000010000000133333111
-- 243:0000000000000000000000001111333300000000000000000000000011333111
-- 244:0000000000000000000000003311133300000001000000010000000133333111
-- 245:0000000000000000000000001111333300000000000000000000000011333111
-- 246:0000000000000000000000003311133300000001000000010000000133333111
-- 247:0000000000000000000000001111333300000000000000000000000011333111
-- 248:0000000000000000000000003311133300000001000000010000000133333111
-- 249:0000000000000000000000001111333300000000000000000000000011333111
-- 250:0000000000000000000000003311133300000001000000010000000133333111
-- 251:0000000000000000000000001111333300000000000000000000000011333111
-- 252:0000000000000000000000003311133300000001000000010000000133333111
-- 253:0000000000000000000000001111333300000000000000000000000011333111
-- 254:0000000000000000000000003311133300000001000000010000000133333111
-- 255:1222222125555552255555522555555225555552255555522555555212222221
-- </TILES>
-- <MAP>
-- 000:111111001020102010201020102010201020102010201020102000111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 001:111111001111111111111111111111111111111111111111111100111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 002:11111100112232115262721192a2b2c21111111111111111111100111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 003:1111110011233311536373119411b3c31111111111111111111100111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 004:1111110011243411546474119411b4c41111111111111111111100111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 005:11111100112535115565758595a5b5c51111111111111111111100111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 006:1111110016261626566676861626b6c61626162616261626162600111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 007:111111001727372b57677787172b172b172b172b172b172b172b00111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 008:1111110016283826582678881626b8261626162616261626162600111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 009:1111110017293949592b798917a9b92b172b172b172b172b172b00111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 010:11111100162a3a4a5a267a8a16aaba261626162616261626162600111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 011:11111100172b172b172b172b9babbb2b172b172b172b172b172b00111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 012:11111100162c3c26162616269cacbc261626162616261626162600111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 013:11111100172d3d2b172b172b172b172b172b172b172b172b172b00111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 014:11111100162e3e2616261626162616261626162616261626162600111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 015:11111100172b172b172b172b172b172b172b172b172b172b172b00111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- 016:111111001020102010201020102010201020102010201020102000111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
-- </MAP>

View File

@@ -1,8 +1,8 @@
-- title: Definitely not an Impostor -- title: Definitely not an Impostor
-- name: impostor -- name: impostor
-- author: Teletype Games -- author: Teletype Games
-- desc: Life of a programmer -- desc: Life of a programmer in the Vector
-- site: https://git.teletype.hu/games/impostor -- site: https://git.teletype.hu/games/impostor
-- license: MIT License -- license: MIT License
-- version: 0.8 -- version: 0.1
-- script: lua -- script: lua

View File

@@ -4,12 +4,6 @@ Screen.register({
decisions = { decisions = {
"go_to_toilet", "go_to_toilet",
"go_to_walking_to_office", "go_to_walking_to_office",
"go_to_sleep",
}, },
background = "bedroom", background = "bedroom",
draw = function()
if Context.home_norman_visible and Window.get_current_id() == "game" then
Sprite.draw_at("norman", 100, 80)
end
end
}) })

View File

@@ -1,17 +1,13 @@
--- @section Screen
local _screens = {} local _screens = {}
--- Registers a screen definition. --- Registers a new screen definition with the Screen manager.
--- @within Screen -- Overwrites existing screen if an ID conflict occurs.
--- @param screen_data table The screen data table. -- @param screen_data table A table containing the screen definition.
--- @param screen_data.id string Unique screen identifier. -- Must include an `id` field (string).
--- @param screen_data.name string Display name of the screen. -- Optional fields:
--- @param screen_data.decisions table Array of decision ID strings available on this screen. -- - `situations` (table): A table of situation IDs allowed on this screen. Defaults to an empty table.
--- @param screen_data.background string Map ID used as background. -- - `init` (function): Function to execute once when the screen becomes active. Defaults to an empty function.
--- @param[opt] screen_data.situations table Array of situation ID strings. Defaults to {}. -- - `update` (function): Function to execute each frame while the screen is active. Defaults to an empty function.
--- @param[opt] screen_data.init function Called when the screen is entered. Defaults to noop.
--- @param[opt] screen_data.update function Called each frame while screen is active. Defaults to noop.
--- @param[opt] screen_data.draw function Called after the focus overlay to draw screen-specific overlays. Defaults to noop.
function Screen.register(screen_data) function Screen.register(screen_data)
if _screens[screen_data.id] then if _screens[screen_data.id] then
trace("Warning: Overwriting screen with id: " .. screen_data.id) trace("Warning: Overwriting screen with id: " .. screen_data.id)
@@ -25,39 +21,12 @@ function Screen.register(screen_data)
if not screen_data.update then if not screen_data.update then
screen_data.update = function() end screen_data.update = function() end
end end
if not screen_data.draw then
screen_data.draw = function() end
end
_screens[screen_data.id] = screen_data _screens[screen_data.id] = screen_data
end end
--- Gets a screen by ID. --- Retrieves a registered screen by its ID.
--- @within Screen -- @param screen_id string The ID of the screen to retrieve.
--- @param screen_id string The ID of the screen. -- @return table The screen table, or `nil` if not found.
--- @return table|nil screen The screen table or nil. </br>
--- Fields: </br>
--- * id (string) Unique screen identifier.<br/>
--- * name (string) Display name.<br/>
--- * decisions (table) Array of decision ID strings.<br/>
--- * background (string) Map ID used as background.<br/>
--- * situations (table) Array of situation ID strings.<br/>
--- * init (function) Called when the screen is entered.<br/>
--- * update (function) Called each frame while screen is active.
function Screen.get_by_id(screen_id) function Screen.get_by_id(screen_id)
return _screens[screen_id] return _screens[screen_id]
end end
--- Gets all registered screens.
--- @within Screen
--- @return result table A table containing all registered screen data, indexed by their IDs or nil. </br>
--- Fields: </br>
--- * id (string) Unique screen identifier.<br/>
--- * name (string) Display name of the screen.<br/>
--- * decisions (table) Array of decision ID strings available on this screen.<br/>
--- * background (string) Map ID used as background.<br/>
--- * situations (table) Array of situation ID strings.<br/>
--- * init (function) Called when the screen is entered.<br/>
--- * update (function) Called each frame while screen is active.<br/>
function Screen.get_all()
return _screens
end

View File

@@ -2,12 +2,13 @@ Screen.register({
id = "office", id = "office",
name = "Office", name = "Office",
decisions = { decisions = {
"do_work", "play_button_mash",
"play_rhythm",
"play_ddr",
"go_to_walking_to_home", "go_to_walking_to_home",
"have_a_coffee", "have_a_coffee",
}, },
situations = { situations = {
"drink_coffee", "drink_coffee",
}, },
background = "office"
}) })

View File

@@ -3,77 +3,5 @@ Screen.register({
name = "Toilet", name = "Toilet",
decisions = { decisions = {
"go_to_home", "go_to_home",
}, }
background = "bedroom",
init = function()
Context.stat_screen_active = true
Meter.hide()
local cx = Config.screen.width * 0.75
local cy = Config.screen.height * 0.75
Focus.start_driven(cx, cy)
Focus.set_percentage(0.15)
end,
update = function()
if not Context.stat_screen_active then return end
if Input.select() or Input.player_interact() then
Focus.stop()
Context.stat_screen_active = false
Meter.show()
end
end,
draw = function()
if not Context.stat_screen_active then return end
local sw = Config.screen.width
local cx = sw / 2
local norman_x = math.floor(sw * 0.75)
local norman_y = math.floor(Config.screen.height * 0.75)
local bar_w = math.floor(sw * 0.75)
local bar_x = math.floor((sw - bar_w) / 2)
local bar_h = 4
Sprite.draw_at("norman", norman_x, norman_y)
Print.text_center("day " .. Context.day_count, cx, 10, Config.colors.white)
local narrative = "reflecting on my past and present\n...\nboth eventually flushed."
local wrapped = UI.word_wrap(narrative, 38)
local text_y = 24
for _, line in ipairs(wrapped) do
Print.text_center(line, cx, text_y, Config.colors.light_grey)
text_y = text_y + 8
end
local m = Context.meters
local max_val = Meter.get_max()
local decay_pct = Meter.get_decay_percentage()
local decay_text = string.format("-%d%%", decay_pct)
local combo_mult = Meter.get_combo_multiplier()
local combo_pct = math.floor((combo_mult - 1) * 100)
local mult_text = string.format("+%d%%", combo_pct)
local meter_start_y = text_y + 10
local meter_list = {
{ key = "wpm", label = "Work Productivity Meter" },
{ key = "ism", label = "Impostor Syndrome Meter" },
{ key = "bm", label = "Burnout Meter" },
}
for i, meter in ipairs(meter_list) do
local y = meter_start_y + (i - 1) * 20
Print.text_center(meter.label, cx, y, Config.colors.white)
local bar_y = y + 8
local fill_w = math.max(0, math.floor((m[meter.key] / max_val) * bar_w))
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
if fill_w > 0 then
rect(bar_x, bar_y, fill_w, bar_h, Config.colors.blue)
end
local decay_w = print(decay_text, 0, -6, 0, false, 1)
Print.text(decay_text, bar_x - decay_w - 4, bar_y, Config.colors.light_blue)
Print.text(mult_text, bar_x + bar_w + 4, bar_y, Config.colors.light_blue)
end
end,
}) })

View File

@@ -4,6 +4,5 @@ Screen.register({
decisions = { decisions = {
"go_to_home", "go_to_home",
"go_to_office", "go_to_office",
}, }
background = "street"
}) })

View File

@@ -4,7 +4,5 @@ Screen.register({
decisions = { decisions = {
"go_to_home", "go_to_home",
"go_to_office", "go_to_office",
"start_discussion", }
},
background = "street"
}) })

View File

@@ -1,6 +0,0 @@
Screen.register({
id = "work",
name = "Work",
decisions = {},
background_color = Config.colors.blue,
})

View File

@@ -1,13 +1,12 @@
--- @section Situation
local _situations = {} local _situations = {}
--- Registers a situation definition. --- Registers a new situation with the Situation manager.
--- @within Situation -- Overwrites existing situation if an ID conflict occurs.
--- @param situation table The situation data table. -- @param situation table A table containing the situation definition.
--- @param situation.id string Unique situation identifier.<br/> -- Must include an `id` field (string).
--- @param[opt] situation.screen_id string ID of the screen this situation belongs to.<br/> -- Optional fields:
--- @param[opt] situation.handle function Called when the situation is applied. Defaults to noop.<br/> -- - `handle` (function): Function to execute when the situation is applied. Defaults to an empty function.
--- @param[opt] situation.update function Called each frame while situation is active. Defaults to noop.<br/> -- - `update` (function): Function to execute each frame while the situation is active. Defaults to an empty function.
function Situation.register(situation) function Situation.register(situation)
if not situation or not situation.id then if not situation or not situation.id then
PopupWindow.show({"Error: Invalid situation object registered (missing id)!"}) PopupWindow.show({"Error: Invalid situation object registered (missing id)!"})
@@ -25,60 +24,32 @@ function Situation.register(situation)
_situations[situation.id] = situation _situations[situation.id] = situation
end end
--- Gets a situation by ID. --- Retrieves a registered situation by its ID.
--- @within Situation -- @param id string The ID of the situation to retrieve.
--- @param id string The situation ID. -- @return table The situation table, or `nil` if not found.
--- @return result table The situation table or nil. </br> function Situation.get(id)
--- Fields: </br>
--- * id (string) Unique situation identifier.<br/>
--- * screen_id (string) ID of the screen this situation belongs to.<br/>
--- * handle (function) Called when the situation is applied.<br/>
--- * update (function) Called each frame while situation is active.<br/>
function Situation.get_by_id(id)
return _situations[id] return _situations[id]
end end
--- Gets all registered situations, optionally filtered by screen ID. --- Applies a situation, making it the current active situation.
--- @within Situation -- The situation's `handle` function is executed.
--- @param screen_id string Optional. If provided, returns situations associated with this screen ID. -- This function first checks if the situation is valid and if it's allowed
--- @return result table A table containing all registered situation data, indexed by their IDs, or an array filtered by screen_id. </br> -- on the current screen (as defined in `Context.screens[Context.current_screen].situations`).
--- Fields: </br> -- If successful, `Context.current_situation` is updated with the ID of the applied situation.
--- * id (string) Unique situation identifier.<br/> -- @param id string The ID of the situation to apply.
--- * screen_id (string) ID of the screen this situation belongs to.<br/> function Situation.apply(id)
--- * handle (function) Called when the situation is applied.<br/> local situation = Situation.get(id)
--- * update (function) Called each frame while situation is active.<br/>
function Situation.get_all(screen_id)
if screen_id then
local filtered_situations = {}
for _, situation in pairs(_situations) do
if situation.screen_id == screen_id then
table.insert(filtered_situations, situation)
end
end
return filtered_situations
end
return _situations
end
--- Applies a situation, checking screen compatibility and returning the new situation ID if successful.
--- @within Situation
--- @param id string The situation ID to apply.
--- @param current_screen_id string The ID of the currently active screen.
--- @return string|nil The ID of the applied situation if successful, otherwise nil.
function Situation.apply(id, current_screen_id)
local situation = Situation.get_by_id(id)
local screen = Screen.get_by_id(current_screen_id)
if not situation then if not situation then
trace("Error: No situation found with id: " .. id) trace("Error: No situation found with id: " .. id)
return nil return
end end
if Util.contains(screen.situations, id) then local current_screen_obj = Screen.get_by_id(Context.current_screen)
situation.handle() if current_screen_obj and not current_screen_obj.situations[id] then
return id trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. Context.current_screen .. ").")
else return
trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. current_screen_id .. ").")
return nil
end end
Context.current_situation = id
situation.handle()
end end

View File

@@ -1,44 +1,15 @@
--- @section Sprite
local _sprites = {} local _sprites = {}
local _active_sprites = {}
local function draw_sprite_instance(sprite_data, params) --- Registers a new sprite or complex sprite definition with the Sprite manager.
local colorkey = params.colorkey or sprite_data.colorkey or 0 -- Overwrites existing sprite if an ID conflict occurs.
local scale = params.scale or sprite_data.scale or 1 -- @param sprite_data table A table containing the sprite definition.
local flip_x = params.flip_x or sprite_data.flip_x or 0 -- Must include an `id` field (string) and either an `s` field (number, for simple sprites)
local flip_y = params.flip_y or sprite_data.flip_y or 0 -- or a `sprites` field (table, for complex sprites).
local rot = params.rot or sprite_data.rot or 0 -- For complex sprites, `sprites` is a table of sub-sprite definitions, each with:
-- - `s` (number): Sprite index.
if sprite_data.sprites then -- - `x_offset` (number): X-offset relative to the parent sprite's position.
for i = 1, #sprite_data.sprites do -- - `y_offset` (number): Y-offset relative to the parent sprite's position.
local sub_sprite = sprite_data.sprites[i] -- - Optional `colorkey`, `scale`, `flip_x`, `flip_y`, `rot` for individual sub-sprites.
spr(
sub_sprite.s,
params.x + (sub_sprite.x_offset or 0),
params.y + (sub_sprite.y_offset or 0),
sub_sprite.colorkey or colorkey,
sub_sprite.scale or scale,
sub_sprite.flip_x or flip_x,
sub_sprite.flip_y or flip_y,
sub_sprite.rot or rot
)
end
else
spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot)
end
end
--- Registers a sprite definition.
--- @within Sprite
--- @param sprite_data table A table containing the sprite definition.
--- @param sprite_data.id string Unique sprite identifier.<br/>
--- @param[opt] sprite_data.s number Sprite index for single-sprite mode.<br/>
--- @param[opt] sprite_data.colorkey number Default color index for transparency.<br/>
--- @param[opt] sprite_data.scale number Default scaling factor.<br/>
--- @param[opt] sprite_data.flip_x number Set to 1 to flip horizontally by default.<br/>
--- @param[opt] sprite_data.flip_y number Set to 1 to flip vertically by default.<br/>
--- @param[opt] sprite_data.rot number Default rotation in degrees.<br/>
--- @param[opt] sprite_data.sprites table Array of sub-sprite tables for composite sprites. Each entry has: `s` (number) sprite index, `x_offset` (number) horizontal offset, `y_offset` (number) vertical offset, and optional `colorkey`, `scale`, `flip_x`, `flip_y`, `rot` overrides.<br/>
function Sprite.register(sprite_data) function Sprite.register(sprite_data)
if not sprite_data or not sprite_data.id then if not sprite_data or not sprite_data.id then
trace("Error: Invalid sprite object registered (missing id)!") trace("Error: Invalid sprite object registered (missing id)!")
@@ -50,23 +21,25 @@ function Sprite.register(sprite_data)
_sprites[sprite_data.id] = sprite_data _sprites[sprite_data.id] = sprite_data
end end
--- Schedules a sprite for drawing. --- Schedules a registered sprite to be drawn at a specific position with optional transformations.
--- @within Sprite -- The sprite's parameters are stored in `Context.sprites` for deferred rendering by `Sprite.draw()`.
--- @param id string The unique identifier of the sprite.<br/> -- If the sprite with the given `id` is already scheduled, its parameters will be updated.
--- @param x number The x-coordinate.<br/> -- @param id string The unique identifier of the sprite to show. Must be registered via `Sprite.register`.
--- @param y number The y-coordinate.<br/> -- @param x number The x-coordinate on the screen where the sprite will be drawn.
--- @param[opt] colorkey number The color index for transparency.<br/> -- @param y number The y-coordinate on the screen where the sprite will be drawn.
--- @param[opt] scale number The scaling factor.<br/> -- @param[opt] colorkey number The color index to be treated as transparent (default: 0).
--- @param[opt] flip_x number Set to 1 to flip horizontally.<br/> -- @param[opt] scale number The scaling factor for the sprite (default: 1).
--- @param[opt] flip_y number Set to 1 to flip vertically.<br/> -- @param[opt] flip_x number Set to 1 to flip the sprite horizontally (default: 0).
--- @param[opt] rot number The rotation in degrees.<br/> -- @param[opt] flip_y number Set to 1 to flip the sprite vertically (default: 0).
-- @param[opt] rot number The rotation of the sprite in degrees (default: 0).
function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot) function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot)
-- Ensure the sprite exists before attempting to show it
if not _sprites[id] then if not _sprites[id] then
trace("Error: Attempted to show non-registered sprite with id: " .. id) trace("Error: Attempted to show non-registered sprite with id: " .. id)
return return
end end
_active_sprites[id] = { Context.sprites[id] = {
id = id, id = id,
x = x, x = x,
y = y, y = y,
@@ -78,52 +51,49 @@ function Sprite.show(id, x, y, colorkey, scale, flip_x, flip_y, rot)
} }
end end
--- Hides a displayed sprite. --- Hides a currently displayed sprite by removing it from the `Context.sprites` table.
--- @within Sprite -- The sprite will no longer be drawn in subsequent frames.
--- @param id string The unique identifier of the sprite.<br/> -- @param id string The unique identifier of the sprite to hide.
function Sprite.hide(id) function Sprite.hide(id)
_active_sprites[id] = nil Context.sprites[id] = nil
end end
--- Draws a sprite immediately without scheduling it. --- Draws all sprites currently scheduled in `Context.sprites`.
--- @within Sprite -- This function retrieves the registered sprite definitions and applies the stored
--- @param id string The unique identifier of the sprite.<br/> -- position and transformation parameters. It handles both simple and complex sprites.
--- @param x number The x-coordinate.<br/>
--- @param y number The y-coordinate.<br/>
--- @param[opt] colorkey number The color index for transparency.<br/>
--- @param[opt] scale number The scaling factor.<br/>
--- @param[opt] flip_x number Set to 1 to flip horizontally.<br/>
--- @param[opt] flip_y number Set to 1 to flip vertically.<br/>
--- @param[opt] rot number The rotation in degrees.<br/>
function Sprite.draw_at(id, x, y, colorkey, scale, flip_x, flip_y, rot)
local sprite_data = _sprites[id]
if not sprite_data then
trace("Error: Attempted to draw non-registered sprite with id: " .. id)
return
end
draw_sprite_instance(sprite_data, {
x = x,
y = y,
colorkey = colorkey,
scale = scale,
flip_x = flip_x,
flip_y = flip_y,
rot = rot,
})
end
--- Draws all scheduled sprites.
--- @within Sprite
function Sprite.draw() function Sprite.draw()
for id, params in pairs(_active_sprites) do for id, params in pairs(Context.sprites) do
local sprite_data = _sprites[id] local sprite_data = _sprites[id]
if not sprite_data then if not sprite_data then
trace("Error: Sprite id " .. id .. " in _active_sprites is not registered.") trace("Error: Sprite id " .. id .. " in Context.sprites is not registered.")
_active_sprites[id] = nil Context.sprites[id] = nil -- Clean up invalid entry
-- We should probably continue to the next sprite instead of returning
-- so that other valid sprites can still be drawn.
end end
if sprite_data then
draw_sprite_instance(sprite_data, params) -- Use parameters from Context.sprites, or fall back to sprite_data, then to defaults
local colorkey = params.colorkey or sprite_data.colorkey or 0
local scale = params.scale or sprite_data.scale or 1
local flip_x = params.flip_x or sprite_data.flip_x or 0
local flip_y = params.flip_y or sprite_data.flip_y or 0
local rot = params.rot or sprite_data.rot or 0
if sprite_data.sprites then -- Complex sprite
for i = 1, #sprite_data.sprites do
local sub_sprite = sprite_data.sprites[i]
spr(
sub_sprite.s,
params.x + (sub_sprite.x_offset or 0),
params.y + (sub_sprite.y_offset or 0),
sub_sprite.colorkey or colorkey,
sub_sprite.scale or scale,
sub_sprite.flip_x or flip_x,
sub_sprite.flip_y or flip_y,
sub_sprite.rot or rot
)
end
else -- Simple sprite
spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot)
end end
end end
end end

View File

@@ -1,23 +1,17 @@
Sprite.register({ Sprite.register({
id = "norman", id = "norman",
sprites = { sprites = {
{ s = 272, x_offset = -4, y_offset = -4 }, -- Body (sprite index 0)
{ s = 273, x_offset = 4, y_offset = -4 }, { s = 0, x_offset = 0, y_offset = 0 },
{ s = 288, x_offset = -4, y_offset = 4 }, -- Head (sprite index 1)
{ s = 289, x_offset = 4, y_offset = 4 }, { s = 1, x_offset = 0, y_offset = -8 },
{ s = 304, x_offset = -4, y_offset = 12 }, -- Left Arm (sprite index 2)
{ s = 305, x_offset = 4, y_offset = 12 } { s = 2, x_offset = -4, y_offset = 4 },
} -- Right Arm (sprite index 3, flipped)
}) { s = 3, x_offset = 4, y_offset = 4, flip_x = 1 }, -- Flipped arm
-- Left Leg (sprite index 4)
Sprite.register({ { s = 4, x_offset = -2, y_offset = 8 },
id = "sleeping_norman", -- Right Leg (sprite index 5, flipped)
sprites = { { s = 5, x_offset = 2, y_offset = 8, flip_x = 1 } -- Flipped leg
{ s = 272, x_offset = 12, y_offset = -4, flip_y = 1 },
{ s = 273, x_offset = 12, y_offset = 4, flip_y = 1 },
{ s = 288, x_offset = 4, y_offset = -4, flip_y = 1 },
{ s = 289, x_offset = 4, y_offset = 4, flip_y = 1 },
{ s = 304, x_offset = -4, y_offset = -4, flip_y = 1 },
{ s = 305, x_offset = -4, y_offset = 4, flip_y = 1 }
} }
}) })

View File

@@ -1,63 +0,0 @@
--- @section AsciiArt
AsciiArt = {}
--- Draws ASCII art text using rectangles.
--- @param text string The ASCII art text to draw.
--- @param options table Configuration options (char_w, char_h, line_gap, word_gap, color, x, y).
function AsciiArt.draw(text, options)
options = options or {}
local char_w = options.char_w or 4
local char_h = options.char_h or 5
local line_gap = options.line_gap or 0
local word_gap = options.word_gap or 6
local color = options.color or Config.colors.light_blue
local lines = {}
local max_len = 0
-- Get all lines and find max length
for line in (text .. "\n"):gmatch("(.-)\n") do
table.insert(lines, line)
if #line > max_len then max_len = #line end
end
-- Clean up empty lines from the start/end
while #lines > 0 and lines[1]:gsub("%s+", "") == "" do table.remove(lines, 1) end
while #lines > 0 and lines[#lines]:gsub("%s+", "") == "" do table.remove(lines, #lines) end
if #lines == 0 then return end
local total_h = 0
for _, line in ipairs(lines) do
if line:find("#") then
total_h = total_h + char_h + line_gap
else
total_h = total_h + word_gap
end
end
total_h = total_h - line_gap
local current_y = options.y or (Config.screen.height - total_h) / 2
local x_offset = options.x or (Config.screen.width - (max_len * char_w)) / 2
for _, line in ipairs(lines) do
if line:find("#") then
for j = 1, #line do
local char = line:sub(j, j)
if char == "#" then
rect(x_offset + (j - 1) * char_w, current_y, char_w - 1, char_h - 1, color)
end
end
current_y = current_y + char_h + line_gap
else
current_y = current_y + word_gap
end
end
return {
x = x_offset,
y = options.y or (Config.screen.height - total_h) / 2,
width = max_len * char_w,
height = total_h,
bottom = (options.y or (Config.screen.height - total_h) / 2) + total_h
}
end

View File

@@ -0,0 +1,14 @@
function Audio.music_stop() music() end
function Audio.music_play_mainmenu() end
function Audio.music_play_wakingup() end
function Audio.music_play_room_morning() end
function Audio.music_play_room_street_1() end
function Audio.music_play_room_street_2() end
function Audio.music_play_room_() end
function Audio.music_play_room_work() end
function Audio.sfx_select() sfx(17, 'C-7', 30) end
function Audio.sfx_deselect() sfx(18, 'C-7', 30) end
function Audio.sfx_beep() sfx(19, 'C-6', 30) end
function Audio.sfx_success() sfx(16, 'C-7', 60) end
function Audio.sfx_bloop() sfx(21, 'C-3', 60) end

View File

@@ -1,39 +1,17 @@
--- @section Input
local INPUT_KEY_UP = 0 local INPUT_KEY_UP = 0
local INPUT_KEY_DOWN = 1 local INPUT_KEY_DOWN = 1
local INPUT_KEY_LEFT = 2 local INPUT_KEY_LEFT = 2
local INPUT_KEY_RIGHT = 3 local INPUT_KEY_RIGHT = 3
local INPUT_KEY_A = 4 local INPUT_KEY_A = 4 local INPUT_KEY_B = 5 local INPUT_KEY_Y = 7
local INPUT_KEY_B = 5
local INPUT_KEY_Y = 7
local INPUT_KEY_SPACE = 48 local INPUT_KEY_SPACE = 48
local INPUT_KEY_BACKSPACE = 51 local INPUT_KEY_BACKSPACE = 51
local INPUT_KEY_ENTER = 50 local INPUT_KEY_ENTER = 50
--- Checks if Up is pressed.
--- @within Input
function Input.up() return btnp(INPUT_KEY_UP) end function Input.up() return btnp(INPUT_KEY_UP) end
--- Checks if Down is pressed.
--- @within Input
function Input.down() return btnp(INPUT_KEY_DOWN) end function Input.down() return btnp(INPUT_KEY_DOWN) end
--- Checks if Left is pressed.
--- @within Input
function Input.left() return btnp(INPUT_KEY_LEFT) end function Input.left() return btnp(INPUT_KEY_LEFT) end
--- Checks if Right is pressed.
--- @within Input
function Input.right() return btnp(INPUT_KEY_RIGHT) end function Input.right() return btnp(INPUT_KEY_RIGHT) end
--- Checks if Select is pressed.
--- @within Input
function Input.select() 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
--- Checks if Menu Confirm is pressed.
--- @within Input
function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end function Input.menu_confirm() return btnp(INPUT_KEY_A) or keyp(INPUT_KEY_ENTER) end
--- Checks if Player Interact is pressed. function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end
--- @within Input
function Input.player_interact() return btnp(INPUT_KEY_B) or keyp(INPUT_KEY_ENTER) end
--- Checks if Menu Back is pressed.
--- @within Input
function Input.menu_back() return btnp(INPUT_KEY_Y) or keyp(INPUT_KEY_BACKSPACE) end
--- Checks if Toggle Popup is pressed.
--- @within Input
function Input.toggle_popup() return keyp(INPUT_KEY_ENTER) end function Input.toggle_popup() return keyp(INPUT_KEY_ENTER) end

View File

@@ -1,33 +1,65 @@
--- @section Main local STATE_HANDLERS = {
[WINDOW_SPLASH] = function()
SplashWindow.update()
SplashWindow.draw()
end,
[WINDOW_INTRO] = function()
IntroWindow.update()
IntroWindow.draw()
end,
[WINDOW_MENU] = function()
MenuWindow.update()
MenuWindow.draw()
end,
[WINDOW_GAME] = function()
GameWindow.update()
GameWindow.draw()
end,
[WINDOW_POPUP] = function()
GameWindow.draw()
PopupWindow.update()
PopupWindow.draw()
end,
[WINDOW_CONFIGURATION] = function()
ConfigurationWindow.update()
ConfigurationWindow.draw()
end,
[WINDOW_AUDIOTEST] = function()
AudioTestWindow.update()
AudioTestWindow.draw()
end,
[WINDOW_MINIGAME_BUTTON_MASH] = function()
MinigameButtonMashWindow.update()
MinigameButtonMashWindow.draw()
end,
[WINDOW_MINIGAME_RHYTHM] = function()
MinigameRhythmWindow.update()
MinigameRhythmWindow.draw()
end,
[WINDOW_MINIGAME_DDR] = function()
MinigameDDRWindow.update()
MinigameDDRWindow.draw()
end,
}
local initialized_game = false local initialized_game = false
--- Initializes game state.
--- @within Main
--- @return boolean initialized_game True if game has been initialized, false otherwise.
local function init_game() local function init_game()
if initialized_game then return false end if initialized_game then return end
Context.reset()
Window.set_current("intro_title") -- Set initial window using new manager
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
initialized_game = true initialized_game = true
return true
end end
--- Main game loop (TIC-80 callback).
--- @within Main
function TIC() function TIC()
init_game() init_game()
cls(Config.colors.black) cls(Config.colors.black)
local handler = Window.get_current_handler() -- Get handler from Window manager local handler = STATE_HANDLERS[Context.active_window]
if handler then if handler then
handler() handler()
end end
Meter.update() Meters.update()
Timer.update()
Trigger.update()
Glitch.draw()
if Context.game_in_progress then if Context.game_in_progress then
Meter.draw() UI.draw_meters()
Timer.draw()
end end
end end

View File

@@ -1,11 +1,4 @@
--- Prints text with shadow.
--- @within Print
--- @param text string The text to print.<br/>
--- @param x number The x-coordinate.<br/>
--- @param y number The y-coordinate.<br/>
--- @param color number The color of the text.<br/>
--- @param[opt] fixed boolean If true, uses fixed-width font.<br/>
--- @param[opt] scale number The scaling factor.<br/>
function Print.text(text, x, y, color, fixed, scale) function Print.text(text, x, y, color, fixed, scale)
local shadow_color = Config.colors.black local shadow_color = Config.colors.black
if color == shadow_color then shadow_color = Config.colors.light_grey end if color == shadow_color then shadow_color = Config.colors.light_grey end
@@ -14,14 +7,6 @@ function Print.text(text, x, y, color, fixed, scale)
print(text, x, y, color, fixed, scale) print(text, x, y, color, fixed, scale)
end end
--- Prints centered text with shadow.
--- @within Print
--- @param text string The text to print.<br/>
--- @param x number The x-coordinate for centering.<br/>
--- @param y number The y-coordinate.<br/>
--- @param color number The color of the text.<br/>
--- @param[opt] fixed boolean If true, uses fixed-width font.<br/>
--- @param[opt] scale number The scaling factor.<br/>
function Print.text_center(text, x, y, color, fixed, scale) function Print.text_center(text, x, y, color, fixed, scale)
scale = scale or 1 scale = scale or 1
local text_width = print(text, 0, -6, 0, fixed, scale) local text_width = print(text, 0, -6, 0, fixed, scale)

View File

@@ -1,44 +1,22 @@
--- @section UI
--- Draws the top bar.
--- @within UI
--- @param title string The title text to display.<br/>
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.text(title, 3, 2, Config.colors.light_blue) Print.text(title, 3, 2, Config.colors.green)
end end
--- Draws a menu. function UI.draw_dialog()
--- @within UI PopupWindow.draw()
--- @param items table A table of menu items.<br/> end
--- @param selected_item number The index of the currently selected item.<br/>
--- @param x number The x-coordinate for the menu (ignored if centered is true).<br/>
--- @param y number The y-coordinate for the menu.<br/>
--- @param[opt] centered boolean Whether to center the menu block horizontally. Defaults to false.<br/>
function UI.draw_menu(items, selected_item, x, y, centered)
if centered then
local max_w = 0
for _, item in ipairs(items) do
local w = print(item.label, 0, -10, 0, false, 1, false)
if w > max_w then max_w = w end
end
x = (Config.screen.width - max_w) / 2
end
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.text(">", x - 8, current_y, Config.colors.light_blue) Print.text(">", x - 8, current_y, Config.colors.green)
end end
Print.text(item.label, x, current_y, Config.colors.light_blue) Print.text(item.label, x, current_y, Config.colors.green)
end end
end end
--- Updates menu selection.
--- @within UI
--- @param items table A table of menu items.<br/>
--- @param selected_item number The current index of the selected item.<br/>
--- @return number selected_item The updated index of the selected item.
function UI.update_menu(items, selected_item) function UI.update_menu(items, selected_item)
if Input.up() then if Input.up() then
Audio.sfx_beep() Audio.sfx_beep()
@@ -56,108 +34,108 @@ function UI.update_menu(items, selected_item)
return selected_item return selected_item
end end
--- Draws a bordered textbox with scrolling text.
--- @within UI
--- @param text string The text to display (multi-line supported).<br/>
--- @param box_x number The x-coordinate of the box.<br/>
--- @param box_y number The y-coordinate of the box.<br/>
--- @param box_w number The width of the box.<br/>
--- @param box_h number The height of the box.<br/>
--- @param scroll_y number The vertical scroll offset for the text (0 = top, increases to scroll up).<br/>
--- @param[opt] color number The text color (default: Config.colors.white).<br/>
--- @param[opt] bg_color number The background fill color (default: Config.colors.dark_grey).<br/>
--- @param[opt] border_color number The border color (default: Config.colors.white).<br/>
--- @param[opt] center_text boolean Whether to center each line inside the box. Defaults to false.<br/>
function UI.draw_textbox(text, box_x, box_y, box_w, box_h, scroll_y, color, bg_color, border_color, center_text)
color = color or Config.colors.white
bg_color = bg_color or Config.colors.dark_grey
border_color = border_color or Config.colors.white
center_text = center_text or false
local padding = 4
local line_height = 8
local inner_x = box_x + padding
local inner_y = box_y + padding
local inner_center_x = box_x + (box_w / 2)
local visible_height = box_h - padding * 2
local lines = UI.word_wrap(text, 30)
local text_height = #lines * line_height
local base_y = inner_y
if center_text and text_height < visible_height then
base_y = inner_y + math.floor((visible_height - text_height) / 2)
end
rect(box_x, box_y, box_w, box_h, bg_color)
for i, line in ipairs(lines) do
local ly = base_y + (i - 1) * line_height - scroll_y
if ly >= inner_y and ly + line_height <= inner_y + visible_height then
if center_text then
Print.text_center(line, inner_center_x, ly, color)
else
Print.text(line, inner_x, ly, color)
end
end
end
rectb(box_x, box_y, box_w, box_h, border_color)
end
--- Wraps text.
--- @within UI
--- @param text string The text to wrap.<br/>
--- @param max_chars_per_line number The maximum characters per line.<br/>
--- @return result table A table of wrapped lines.
function UI.word_wrap(text, max_chars_per_line) function UI.word_wrap(text, max_chars_per_line)
if text == nil then return {""} end if text == nil then return {""} end
local lines = {}
local lines = {} for input_line in (text .. "\n"):gmatch("(.-)\n") do
local current_line = ""
local function trim(s) local words_in_line = 0
return (s:gsub("^%s+", ""):gsub("%s+$", "")) for word in input_line:gmatch("%S+") do
end words_in_line = words_in_line + 1
if #current_line == 0 then
local function previous_whitespace_index(s, target) current_line = word
if s:sub(target, target):match("%s") then elseif #current_line + #word + 1 <= max_chars_per_line then
return target current_line = current_line .. " " .. word
end else
table.insert(lines, current_line)
for i = target - 1, 1, -1 do current_line = word
if s:sub(i, i):match("%s") then end
return i end
end if words_in_line > 0 then
end table.insert(lines, current_line)
else
return nil table.insert(lines, "")
end
for input_line in (text .. "\n"):gmatch("(.-)\n") do
local remaining = trim(input_line)
if remaining == "" then
table.insert(lines, "")
else
while #remaining > max_chars_per_line do
local split_at = previous_whitespace_index(remaining, max_chars_per_line)
local line = trim(remaining:sub(1, split_at))
if not split_at or line == "" then
line = remaining:sub(1, max_chars_per_line)
split_at = max_chars_per_line
end end
table.insert(lines, line)
remaining = trim(remaining:sub(split_at + 1))
end
table.insert(lines, remaining)
end end
end if #lines == 0 then
return {""}
if #lines == 0 then end
return {""} return lines
end
return lines
end end
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
return {
label = label,
get = value_getter,
set = value_setter,
min = min,
max = max,
step = step,
format = format or "%.1f",
type = "numeric_stepper"
}
end
function UI.create_action_item(label, action)
return {
label = label,
action = action,
type = "action_item"
}
end
function UI.draw_decision_selector(decisions, selected_decision_index)
local bar_height = 16
local bar_y = Config.screen.height - bar_height
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
if #decisions > 0 then
local selected_decision = decisions[selected_decision_index]
local decision_label = selected_decision.label
local text_width = #decision_label * 4 local text_y = bar_y + 4
local text_x = (Config.screen.width - text_width) / 2
Print.text("<", 2, text_y, Config.colors.green)
Print.text(decision_label, text_x, text_y, Config.colors.item) Print.text(">", Config.screen.width - 6, text_y, Config.colors.green) end
end
function UI.draw_meters()
if not Context or not Context.game_in_progress or not Context.meters then return end
if Context.meters.hidden then return end
local m = Context.meters
local max = Meters.get_max()
local bar_w = 44
local bar_h = 2
local bar_x = 182
local label_x = 228
local line_h = 5
local start_y = 11
local bar_offset = math.floor((line_h - bar_h) / 2)
local meter_list = {
{ key = "wpm", label = "WPM", color = Meters.COLOR_WPM, row = 0 },
{ key = "ism", label = "ISM", color = Meters.COLOR_ISM, row = 1 },
{ key = "bm", label = "BM", color = Meters.COLOR_BM, row = 2 },
}
for _, meter in ipairs(meter_list) do
local label_y = start_y + meter.row * line_h
local bar_y = label_y + bar_offset
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
rect(bar_x, bar_y, bar_w, bar_h, Meters.COLOR_BG)
if fill_w > 0 then
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
end
print(meter.label, label_x, label_y, meter.color, false, 1, true)
end
end
function UI.update_decision_selector(decisions, selected_decision_index)
if Input.left() then
Audio.sfx_beep()
selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1)
elseif Input.right() then
Audio.sfx_beep()
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
end
return selected_decision_index
end

View File

@@ -1,38 +1,14 @@
Util = {}
--- @section Util
--- Safely wraps an index for an array.
--- @within Util
--- @param array table The array to index.
--- @param index number The desired index (can be out of bounds).
--- @return number index The wrapped index within the array's bounds.
function Util.safeindex(array, index) function Util.safeindex(array, index)
return ((index - 1 + #array) % #array) + 1 return ((index - 1 + #array) % #array) + 1
end end
--- Navigates to a screen by its ID.
--- @within Util
--- @param screen_id string The ID of the screen to go to.<br/>
function Util.go_to_screen_by_id(screen_id) function Util.go_to_screen_by_id(screen_id)
local screen = Screen.get_by_id(screen_id) local screen_index = Context.screen_indices_by_id[screen_id]
if screen then if screen_index then
Context.game.current_screen = screen_id Context.current_screen = screen_index
screen.init() Context.selected_decision_index = 1 else
else PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found or not indexed!"})
PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found!"})
end end
end
--- Checks if a table contains a specific value.
--- @within Util
--- @param t table The table to check.
--- @param value any The value to look for.<br/>
--- @return boolean true if the value is found, false otherwise.
function Util.contains(t, value)
for i = 1, #t do
if t[i] == value then
return true
end
end
return false
end end

View File

@@ -1,18 +1,11 @@
--- @section AudioTestWindow AudioTestWindow = {
AudioTestWindow.index_menu = 1 index_menu = 1,
AudioTestWindow.index_func = 1 index_func = 1,
AudioTestWindow.list_func = {} list_func = {},
AudioTestWindow.menuitems = {} menuitems = {},
AudioTestWindow.last_pressed = false last_pressed = false
}
--- Generates menu items for audio test.
--- @within AudioTestWindow
--- @param list_func table List of audio functions.<br/>
--- @param index_func number Current index of selected function.<br/>
--- @return result table Generated menu items, an array of menu item tables or nil. </br>
--- Fields: </br>
--- * label (string) Display text for the menu item.<br/>
--- * decision (function) Called when the menu item is selected.<br/>
function AudioTestWindow.generate_menuitems(list_func, index_func) function AudioTestWindow.generate_menuitems(list_func, index_func)
return { return {
{ {
@@ -22,7 +15,7 @@ function AudioTestWindow.generate_menuitems(list_func, index_func)
if current_func then if current_func then
current_func() current_func()
else else
trace("Invalid Audio function: " .. list_func[index_func]) trace("Invalid Audio function: " .. list_func[index_menu])
end end
end end
}, },
@@ -41,9 +34,6 @@ function AudioTestWindow.generate_menuitems(list_func, index_func)
} }
end end
--- Generates list of audio functions.
--- @within AudioTestWindow
--- @return result table A sorted list of audio function names.
function AudioTestWindow.generate_listfunc() function AudioTestWindow.generate_listfunc()
local result = {} local result = {}
@@ -58,15 +48,11 @@ function AudioTestWindow.generate_listfunc()
return result return result
end end
--- Navigates back from audio test window.
--- @within AudioTestWindow
function AudioTestWindow.back() function AudioTestWindow.back()
Audio.sfx_deselect() Audio.sfx_deselect()
GameWindow.set_state("menu") GameWindow.set_state(WINDOW_MENU)
end end
--- Initializes audio test window.
--- @within AudioTestWindow
function AudioTestWindow.init() function AudioTestWindow.init()
AudioTestWindow.last_pressed = false AudioTestWindow.last_pressed = false
AudioTestWindow.index_menu = 1 AudioTestWindow.index_menu = 1
@@ -77,15 +63,11 @@ function AudioTestWindow.init()
) )
end end
--- Draws audio test window.
--- @within AudioTestWindow
function AudioTestWindow.draw() function AudioTestWindow.draw()
UI.draw_top_bar("Audio test") UI.draw_top_bar("Audio test")
UI.draw_menu(AudioTestWindow.menuitems, AudioTestWindow.index_menu, 20, 50) UI.draw_menu(AudioTestWindow.menuitems, AudioTestWindow.index_menu, 20, 50)
end end
--- Updates audio test window logic.
--- @within AudioTestWindow
function AudioTestWindow.update() function AudioTestWindow.update()
if Input.up() then if Input.up() then
AudioTestWindow.index_menu = Util.safeindex(AudioTestWindow.menuitems, AudioTestWindow.index_menu - 1) AudioTestWindow.index_menu = Util.safeindex(AudioTestWindow.menuitems, AudioTestWindow.index_menu - 1)

View File

@@ -1,57 +1,50 @@
--- @section ConfigurationWindow ConfigurationWindow = {
ConfigurationWindow.controls = {} controls = {},
ConfigurationWindow.selected_control = 1 selected_control = 1,
}
--- Initializes configuration window.
--- @within ConfigurationWindow
function ConfigurationWindow.init() function ConfigurationWindow.init()
ConfigurationWindow.controls = { ConfigurationWindow.controls = {
{ UI.create_decision_item(
label = "Save", "Save",
action = function() Config.save() end, function() Config.save() end
type = "action_item" ),
}, UI.create_decision_item(
{ "Restore Defaults",
label = "Restore Defaults", function() Config.restore_defaults() end
action = function() Config.reset() end, ),
type = "action_item"
},
} }
end end
--- Draws configuration window.
--- @within ConfigurationWindow
function ConfigurationWindow.draw() function ConfigurationWindow.draw()
UI.draw_top_bar("Configuration") UI.draw_top_bar("Configuration")
local x_start = 10 local x_start = 10 local y_start = 40
local y_start = 40
local x_value_right_align = Config.screen.width - 10 local x_value_right_align = Config.screen.width - 10
local char_width = 4 local char_width = 4
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.light_blue local color = Config.colors.green
if control.type == "numeric_stepper" then if control.type == "numeric_stepper" then
local value = control.get() local value = control.get()
local label_text = control.label local label_text = control.label
local value_text = string.format(control.format, value) local value_text = string.format(control.format, value)
local value_x = x_value_right_align - (#value_text * char_width)
local value_x = x_value_right_align - (#value_text * char_width)
if i == ConfigurationWindow.selected_control then if i == ConfigurationWindow.selected_control then
color = Config.colors.item color = Config.colors.item
Print.text("<", x_start - 8, current_y, color) Print.text("<", x_start -8, current_y, color)
Print.text(label_text, x_start, current_y, color) Print.text(label_text, x_start, current_y, color) Print.text(value_text, value_x, current_y, color)
Print.text(value_text, value_x, current_y, color) Print.text(">", x_value_right_align + 4, current_y, color) else
Print.text(">", x_value_right_align + 4, current_y, color)
else
Print.text(label_text, x_start, current_y, color) Print.text(label_text, x_start, current_y, color)
Print.text(value_text, value_x, current_y, color) Print.text(value_text, value_x, current_y, color)
end end
elseif control.type == "action_item" then elseif control.type == "decision_item" then
local label_text = control.label local label_text = control.label
if i == ConfigurationWindow.selected_control then if i == ConfigurationWindow.selected_control then
color = Config.colors.item color = Config.colors.item
Print.text("<", x_start - 8, current_y, color) Print.text("<", x_start -8, current_y, color)
Print.text(label_text, x_start, 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) Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color)
else else
@@ -62,11 +55,9 @@ function ConfigurationWindow.draw()
Print.text("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
--- Updates configuration window logic.
--- @within ConfigurationWindow
function ConfigurationWindow.update() function ConfigurationWindow.update()
if Input.menu_back() then if Input.menu_back() then
GameWindow.set_state("menu") GameWindow.set_state(WINDOW_MENU)
return return
end end
@@ -86,17 +77,15 @@ function ConfigurationWindow.update()
if control then if control then
if control.type == "numeric_stepper" then if control.type == "numeric_stepper" then
local current_value = control.get() local current_value = control.get()
if Input.left() then if btnp(2) then local new_value = math.max(control.min, current_value - control.step)
local new_value = math.max(control.min, current_value - control.step)
control.set(new_value) control.set(new_value)
elseif Input.right() then elseif btnp(3) then local new_value = math.min(control.max, current_value + control.step)
local new_value = math.min(control.max, current_value + control.step)
control.set(new_value) control.set(new_value)
end end
elseif control.type == "action_item" then elseif control.type == "decision_item" then
if Input.menu_confirm() then if Input.menu_confirm() then
control.action() control.decision()
end end
end end
end end
end end

View File

@@ -1,33 +0,0 @@
--- @section ContinuedWindow
ContinuedWindow.timer = 300 -- 5 seconds at 60fps
ContinuedWindow.text = [[
### ### ### ###
# # # # # #
# # # ### ##
# # # # # #
# ### ### ###
### ### # # ### ### # # # # ### ##
# # # ## # # # ## # # # # # #
# # # # ## # # # ## # # ## # #
# # # # # # # # # # # # # #
### ### # # # ### # # ### ### ##
]]
--- Draws the continued window.
--- @within ContinuedWindow
function ContinuedWindow.draw()
cls(Config.colors.black)
AsciiArt.draw(ContinuedWindow.text, {})
end
--- Updates the continued window logic.
--- @within ContinuedWindow
function ContinuedWindow.update()
ContinuedWindow.timer = ContinuedWindow.timer - 1
if ContinuedWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
Window.set_current("menu")
MenuWindow.refresh_menu_items()
end
end

View File

@@ -1,107 +0,0 @@
--- @section DiscussionWindow
local TEXTBOX_W = math.floor(Config.screen.width * 0.7)
local TEXTBOX_H = math.floor(Config.screen.height * 0.3)
local TEXTBOX_X = math.floor((Config.screen.width - TEXTBOX_W) / 2)
local TEXTBOX_Y = math.floor((Config.screen.height - TEXTBOX_H) / 2 - 8)
local TEXTBOX_MAX_CHARS = 30
local DISCUSSION_LINE_HEIGHT = 8
local PADDING = 4
local AUTO_SCROLL_DELAY = 12
local AUTO_SCROLL_STEP = 1
--- Draws the discussion window.
--- @within DiscussionWindow
function DiscussionWindow.draw()
GameWindow.draw()
local step = Discussion.get_current_step()
if not step then return end
UI.draw_textbox(
step.question,
TEXTBOX_X, TEXTBOX_Y,
TEXTBOX_W, TEXTBOX_H,
Context.discussion.scroll_y,
Config.colors.white,
Config.colors.dark_grey,
Config.colors.light_blue,
true
)
local answers = step.answers
if #answers > 0 then
local bar_height = 16
local bar_y = Config.screen.height - bar_height
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
local selected = answers[Context.discussion.selected_answer]
local label = selected.label
local answer_text_y = bar_y + 4
Print.text("<", 2, answer_text_y, Config.colors.light_blue)
Print.text_center(label, Config.screen.width / 2, answer_text_y, Config.colors.item)
Print.text(">", Config.screen.width - 6, answer_text_y, Config.colors.light_blue)
end
end
--- Updates the discussion window logic.
--- @within DiscussionWindow
function DiscussionWindow.update()
local step = Discussion.get_current_step()
if not step then return end
local lines = UI.word_wrap(step.question, TEXTBOX_MAX_CHARS)
local text_height = #lines * DISCUSSION_LINE_HEIGHT
local visible_height = TEXTBOX_H - PADDING * 2
local max_scroll = text_height - visible_height
if max_scroll < 0 then max_scroll = 0 end
if max_scroll > 0 then
if Context.discussion.auto_scroll then
Context.discussion.scroll_timer = Context.discussion.scroll_timer + 1
if Context.discussion.scroll_timer >= AUTO_SCROLL_DELAY then
Context.discussion.scroll_timer = 0
Context.discussion.scroll_y = Context.discussion.scroll_y + AUTO_SCROLL_STEP
if Context.discussion.scroll_y > max_scroll then
Context.discussion.scroll_y = max_scroll
end
end
end
else
Context.discussion.scroll_y = 0
Context.discussion.scroll_timer = 0
end
if Input.up() then
Context.discussion.auto_scroll = false
Context.discussion.scroll_y = Context.discussion.scroll_y - DISCUSSION_LINE_HEIGHT
if Context.discussion.scroll_y < 0 then
Context.discussion.scroll_y = 0
end
elseif Input.down() then
Context.discussion.auto_scroll = false
Context.discussion.scroll_y = Context.discussion.scroll_y + DISCUSSION_LINE_HEIGHT
if Context.discussion.scroll_y > max_scroll then
Context.discussion.scroll_y = max_scroll
end
end
local answers = step.answers
if #answers > 0 then
if Input.left() then
Audio.sfx_beep()
Context.discussion.selected_answer = Util.safeindex(answers, Context.discussion.selected_answer - 1)
elseif Input.right() then
Audio.sfx_beep()
Context.discussion.selected_answer = Util.safeindex(answers, Context.discussion.selected_answer + 1)
end
if Input.select() then
Audio.sfx_select()
local selected = answers[Context.discussion.selected_answer]
if selected.on_select then
selected.on_select()
end
Discussion.go_to_step(selected.next_step)
end
end
end

View File

@@ -1,77 +0,0 @@
--- @section EndWindow
--- Draws the end screen window.
--- @within EndWindow
function EndWindow.draw()
cls(Config.colors.black)
if Context._end.state == "choice" then
local lines = {
"This is not a workplace.",
"This is a cycle.",
"And if it is a cycle...",
"it can be broken."
}
local y = 40
for _, line in ipairs(lines) do
Print.text_center(line, Config.screen.width / 2, y, Config.colors.white)
y = y + 10
end
y = y + 20
local yes_color = Context._end.selection == 1 and Config.colors.light_blue or Config.colors.white
local no_color = Context._end.selection == 2 and Config.colors.light_blue or Config.colors.white
local yes_text = (Context._end.selection == 1 and "> YES" or " YES")
local no_text = (Context._end.selection == 2 and "> NO" or " NO")
local centerX = Config.screen.width / 2
Print.text(yes_text, centerX - 40, y, yes_color)
Print.text(no_text, centerX + 10, y, no_color)
elseif Context._end.state == "ending" then
Print.text_center("Game over -- good ending.", Config.screen.width / 2, 50, Config.colors.light_blue)
Print.text_center("Congratulations!", Config.screen.width / 2, 70, Config.colors.white)
Print.text_center("Press Z to return to menu", Config.screen.width / 2, 110, Config.colors.light_grey)
end
end
--- Updates the end screen logic.
--- @within EndWindow
function EndWindow.update()
if Context._end.state == "choice" then
if Input.left() or Input.up() then
if Context._end.selection == 2 then
Audio.sfx_beep()
Context._end.selection = 1
end
elseif Input.right() or Input.down() then
if Context._end.selection == 1 then
Audio.sfx_beep()
Context._end.selection = 2
end
end
if Input.menu_confirm() then
Audio.sfx_select()
if Context._end.selection == 1 then
Context._end.state = "ending"
else
-- NO: increment day and go home
Day.increase()
Context.game.current_screen = "home"
Window.set_current("game")
-- Initialize home screen
local home_screen = Screen.get_by_id("home")
if home_screen and home_screen.init then
home_screen.init()
end
end
end
elseif Context._end.state == "ending" then
if Input.menu_confirm() then
Window.set_current("menu")
MenuWindow.refresh_menu_items()
end
end
end

View File

@@ -1,96 +1,95 @@
--- @section GameWindow --- Draws the main game window content.
local _available_decisions = {} -- This includes the current screen's background, top bar, decisions, and all active sprites.
local _selected_decision_index = 1 -- @function GameWindow.draw
function GameWindow.draw()
local function draw_game_scene(underlay_draw) local screen = Context.screens[Context.current_screen]
local screen = Screen.get_by_id(Context.game.current_screen) Map.draw(screen.background)
if not screen then return end UI.draw_top_bar(screen.name)
if screen.background then if screen and screen.decisions and #screen.decisions > 0 then
Map.draw(screen.background) local available_decisions = {}
elseif screen.background_color then for _, decision_id in ipairs(screen.decisions) do
rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color) local decision = Decision.get(decision_id)
end if decision and decision.condition() then
if underlay_draw then table.insert(available_decisions, decision)
underlay_draw() end
end end
if not Context.stat_screen_active and #_available_decisions > 0 then if #available_decisions > 0 then
Decision.draw(_available_decisions, _selected_decision_index) UI.draw_decision_selector(available_decisions, Context.selected_decision_index)
end
end end
Sprite.draw() Sprite.draw()
Focus.draw()
screen.draw()
end end
--- Draws the game window. --- Updates the logic for the main game window.
--- @within GameWindow -- Handles input, navigates between screens, calls the current screen's and situation's update functions,
function GameWindow.draw() -- and processes player decisions.
draw_game_scene() -- @function GameWindow.update
end
--- Draws the game window with a custom underlay.
--- @within GameWindow
--- @param underlay_draw function A draw callback rendered after the background but before overlays.<br/>
function GameWindow.draw_with_underlay(underlay_draw)
draw_game_scene(underlay_draw)
end
--- Updates the game window logic.
--- @within GameWindow
function GameWindow.update() function GameWindow.update()
Focus.update() local previous_screen_index = Context.current_screen
if Input.menu_back() then if Input.menu_back() then
Window.set_current("menu") Context.active_window = WINDOW_MENU
MenuWindow.refresh_menu_items() MenuWindow.refresh_menu_items()
return return
end end
local screen = Screen.get_by_id(Context.game.current_screen) if Input.up() then
if not screen or not screen.update then return end Context.current_screen = Context.current_screen - 1
if Context.current_screen < 1 then
Context.current_screen = #Context.screens
end
Context.selected_decision_index = 1 elseif Input.down() then
Context.current_screen = Context.current_screen + 1
if Context.current_screen > #Context.screens then
Context.current_screen = 1
end
Context.selected_decision_index = 1 end
local screen = Context.screens[Context.current_screen]
screen.update() screen.update()
-- Handle current situation updates if previous_screen_index ~= Context.current_screen then
if Context.game.current_situation then screen.init()
local current_situation_obj = Situation.get_by_id(Context.game.current_situation) end
if current_situation_obj and type(current_situation_obj.update) == "function" then
if Context.current_situation then
local current_situation_obj = Situation.get(Context.current_situation)
if current_situation_obj and current_situation_obj.update then
current_situation_obj.update() current_situation_obj.update()
end end
end end
if Context.stat_screen_active then return end if screen and screen.decisions and #screen.decisions > 0 then
local available_decisions = {}
for _, decision_id in ipairs(screen.decisions) do
local decision = Decision.get(decision_id)
if decision and decision.condition() then table.insert(available_decisions, decision)
end
end
-- Fetch and filter decisions locally if #available_decisions == 0 then return end
local all_decisions_for_screen = Decision.get_for_screen(screen)
_available_decisions = Decision.filter_available(all_decisions_for_screen)
if #_available_decisions == 0 then return end local new_selected_decision_index = UI.update_decision_selector(
available_decisions,
Context.selected_decision_index
)
if _selected_decision_index > #_available_decisions then if new_selected_decision_index ~= Context.selected_decision_index then
_selected_decision_index = 1 Context.selected_decision_index = new_selected_decision_index
end end
local new_selected_decision_index = Decision.update( if Input.select() then
_available_decisions, local selected_decision = available_decisions[Context.selected_decision_index]
_selected_decision_index if selected_decision and selected_decision.handle then Audio.sfx_select() selected_decision.handle()
) end
if new_selected_decision_index ~= _selected_decision_index then
_selected_decision_index = new_selected_decision_index
end
if Input.select() then
local selected_decision = _available_decisions[_selected_decision_index]
if selected_decision and selected_decision.handle then
Audio.sfx_select()
selected_decision.handle()
end end
end end
end end
--- Sets the active window. --- Sets the active window state for the game.
--- @within GameWindow -- This function is typically called when transitioning between different game states (e.g., to a minigame).
--- @param new_state string The ID of the new active window.</br> -- @param new_state number The ID of the new active window (e.g., `WINDOW_MENU`, `WINDOW_GAME`).
-- @function GameWindow.set_state
function GameWindow.set_state(new_state) function GameWindow.set_state(new_state)
Window.set_current(new_state) Context.active_window = new_state
end end

View File

@@ -1,37 +0,0 @@
--- @section BriefIntroWindow
BriefIntroWindow.y = Config.screen.height
BriefIntroWindow.speed = 0.5
BriefIntroWindow.text = [[
Norman Reds everyday life
seems ordinary: work,
meetings, coffee, and
endless notifications.
But beneath him, or around
him — something is
constantly building, and
it soon becomes clear
that there is more going
on than meets the eye.
]]
--- Draws the brief intro window.
--- @within BriefIntroWindow
function BriefIntroWindow.draw()
local x = (Config.screen.width - 132) / 2
Print.text(BriefIntroWindow.text, x, BriefIntroWindow.y, Config.colors.light_blue)
end
--- Updates the brief intro window logic.
--- @within BriefIntroWindow
function BriefIntroWindow.update()
BriefIntroWindow.y = BriefIntroWindow.y - BriefIntroWindow.speed
local lines = 1
for _ in string.gmatch(BriefIntroWindow.text, "\n") do
lines = lines + 1
end
if BriefIntroWindow.y < -lines * 8 or Input.select() or Input.menu_confirm() then
Window.set_current("menu")
end
end

View File

@@ -0,0 +1,21 @@
function IntroWindow.draw()
local x = (Config.screen.width - 132) / 2
Print.text(Context.intro.text, x, Context.intro.y, Config.colors.green)
end
function IntroWindow.update()
Context.intro.y = Context.intro.y - Context.intro.speed
local lines = 1
for _ in string.gmatch(Context.intro.text, "\n") do
lines = lines + 1
end
if Context.intro.y < -lines * 8 then
GameWindow.set_state(WINDOW_MENU)
end
if Input.menu_confirm() then
GameWindow.set_state(WINDOW_MENU)
end
end

View File

@@ -1,36 +0,0 @@
--- @section TitleIntroWindow
TitleIntroWindow.timer = 180 -- 3 seconds at 60fps
TitleIntroWindow.text = [[
## ### ### ### ### ### ### ### ## # #
# # # # # # # # # # # # # #
# # ### ### # # # # # ### # # #
# # # # # # # # # # # # #
## ### # ### # # ### # ### ## #
# # ### ### ## # #
## # # # # # # ## #
# ## # # # #### # ##
# # # # # # # # #
# # ### # # # # #
### # # ### #### ### ### #### ###
# ## ## # # # # # # # # # #
# # # # ### # # ### # # # ###
# # # # # # # # # # # #
### # # # #### ### # #### # #
]]
--- Draws the title intro window.
--- @within TitleIntroWindow
function TitleIntroWindow.draw()
AsciiArt.draw(TitleIntroWindow.text, {})
end
--- Updates the title intro window logic.
--- @within TitleIntroWindow
function TitleIntroWindow.update()
TitleIntroWindow.timer = TitleIntroWindow.timer - 1
if TitleIntroWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
Window.set_current("intro_ttg")
end
end

View File

@@ -1,33 +0,0 @@
--- @section TTGIntroWindow
TTGIntroWindow.timer = 180
TTGIntroWindow.glitch_started = false
TTGIntroWindow.text = [[
###### ###### ######
## ## #
## ## # ####
## ## # #
## ## ######
]]
--- Draws the TTG intro window.
--- @within TTGIntroWindow
function TTGIntroWindow.draw()
local bounds = AsciiArt.draw(TTGIntroWindow.text, {})
if not bounds then return end
Print.text_center("Teletype Games", (Config.screen.width / 2 + 3) , (bounds.bottom + 4), Config.colors.light_blue)
end
--- Updates the TTG intro window logic.
--- @within TTGIntroWindow
function TTGIntroWindow.update()
if not TTGIntroWindow.glitch_started then
Glitch.show()
TTGIntroWindow.glitch_started = true
end
TTGIntroWindow.timer = TTGIntroWindow.timer - 1
if TTGIntroWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
Glitch.hide()
Window.set_current("intro_brief")
end
end

View File

@@ -1,53 +0,0 @@
--- @section Window
local _windows = {}
--- Registers a window table.
--- @within Window
--- @param id string The ID of the window (e.g., "splash", "menu").</br>
--- @param window_table table The actual window module table (e.g., GameWindow).</br>
function Window.register(id, window_table)
_windows[id] = window_table
end
--- Retrieves a registered window table by its ID.
--- @within Window
--- @param id string The ID of the window.
--- @return result table The window module table or nil. </br>
--- Fields: </br>
--- * update (function) Called each frame to update window logic.<br/>
--- * draw (function) Called each frame to draw the window.<br/>
function Window.get(id)
return _windows[id]
end
--- Sets the currently active window.
--- @within Window
--- @param id string The ID of the window to activate.</br>
function Window.set_current(id)
Context.current_window = id
end
--- Gets the ID of the currently active window.
--- This function is used by the main game loop to update and draw the active window.
--- @within Window
--- @return string The ID of the active window.
function Window.get_current_id()
return Context.current_window
end
--- Gets the handler function for the currently active window.
-- This function is used by the main game loop to update and draw the active window.
--- @within Window
--- @return function A function that updates and draws the current window.
function Window.get_current_handler()
local window_table = Window.get(Context.current_window)
if window_table and window_table.update and window_table.draw then
return function()
window_table.update()
window_table.draw()
end
else
-- Fallback handler for unregistered or incomplete windows
return function() trace("Error: No handler for window: " .. tostring(Context.current_window)) end
end
end

View File

@@ -1,27 +1,13 @@
--- @section MenuWindow
local _menu_items = {}
--- Draws the menu window.
--- @within MenuWindow
function MenuWindow.draw() function MenuWindow.draw()
UI.draw_top_bar("Definitely not an Impostor") UI.draw_top_bar("Main Menu")
UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70)
local menu_h = #_menu_items * 10
local y = 10 + (Config.screen.height - 10 - 10 - menu_h) / 2
UI.draw_menu(_menu_items, Context.current_menu_item, 0, y, true)
local ttg_text = "TTG"
local ttg_w = print(ttg_text, 0, -10, 0, false, 1, false)
Print.text(ttg_text, Config.screen.width - ttg_w - 5, Config.screen.height - 10, Config.colors.light_blue)
end end
--- Updates the menu window logic.
--- @within MenuWindow
function MenuWindow.update() function MenuWindow.update()
Context.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item) Context.selected_menu_item = UI.update_menu(Context.menu_items, Context.selected_menu_item)
if Input.menu_confirm() then if Input.menu_confirm() then
local selected_item = _menu_items[Context.current_menu_item] local selected_item = Context.menu_items[Context.selected_menu_item]
if selected_item and selected_item.decision then if selected_item and selected_item.decision then
Audio.sfx_select() Audio.sfx_select()
selected_item.decision() selected_item.decision()
@@ -29,73 +15,46 @@ function MenuWindow.update()
end end
end end
--- Starts a new game from the menu.
--- @within MenuWindow
function MenuWindow.new_game() function MenuWindow.new_game()
Context.new_game() Context.new_game() GameWindow.set_state(WINDOW_GAME)
end end
--- Loads a game from the menu.
--- @within MenuWindow
function MenuWindow.load_game() function MenuWindow.load_game()
Context.load_game() Context.load_game() GameWindow.set_state(WINDOW_GAME)
GameWindow.set_state("game")
end end
--- Saves the current game from the menu.
--- @within MenuWindow
function MenuWindow.save_game() function MenuWindow.save_game()
Context.save_game() Context.save_game() end
end
--- Resumes the game from the menu.
--- @within MenuWindow
function MenuWindow.resume_game() function MenuWindow.resume_game()
GameWindow.set_state("game") GameWindow.set_state(WINDOW_GAME)
end end
--- Exits the game.
--- @within MenuWindow
function MenuWindow.exit() function MenuWindow.exit()
exit() exit()
end end
--- Opens the configuration menu.
--- @within MenuWindow
function MenuWindow.configuration() function MenuWindow.configuration()
ConfigurationWindow.init() ConfigurationWindow.init()
GameWindow.set_state("configuration") GameWindow.set_state(WINDOW_CONFIGURATION)
end end
--- Opens the audio test menu.
--- @within MenuWindow
function MenuWindow.audio_test() function MenuWindow.audio_test()
AudioTestWindow.init() AudioTestWindow.init()
GameWindow.set_state("audiotest") GameWindow.set_state(WINDOW_AUDIOTEST)
end end
--- Opens the continued screen.
--- @within MenuWindow
function MenuWindow.continued()
ContinuedWindow.timer = 300
GameWindow.set_state("continued")
end
--- Refreshes menu items.
--- @within MenuWindow
function MenuWindow.refresh_menu_items() function MenuWindow.refresh_menu_items()
_menu_items = {} Context.menu_items = {}
if Context.game_in_progress then if Context.game_in_progress then
table.insert(_menu_items, {label = "Resume Game", decision = MenuWindow.resume_game}) table.insert(Context.menu_items, {label = "Resume Game", decision = MenuWindow.resume_game})
table.insert(_menu_items, {label = "Save Game", decision = MenuWindow.save_game}) table.insert(Context.menu_items, {label = "Save Game", decision = MenuWindow.save_game})
end end
table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game}) table.insert(Context.menu_items, {label = "New Game", decision = MenuWindow.new_game})
table.insert(_menu_items, {label = "Load Game", decision = MenuWindow.load_game}) table.insert(Context.menu_items, {label = "Load Game", decision = MenuWindow.load_game})
table.insert(_menu_items, {label = "Configuration", decision = MenuWindow.configuration}) table.insert(Context.menu_items, {label = "Configuration", decision = MenuWindow.configuration})
table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test}) table.insert(Context.menu_items, {label = "Audio Test", decision = MenuWindow.audio_test})
table.insert(_menu_items, {label = "To Be Continued...", decision = MenuWindow.continued}) table.insert(Context.menu_items, {label = "Exit", decision = MenuWindow.exit})
table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit})
Context.current_menu_item = 1 Context.selected_menu_item = 1 end
end

View File

@@ -1,70 +1,10 @@
--- @section MinigameDDRWindow
--- Gets initial DDR minigame configuration.
--- @within MinigameDDRWindow
--- @return result table The default DDR minigame configuration.
function MinigameDDRWindow.init_context()
local arrow_size = 12
local arrow_spacing = 30
local total_width = (4 * arrow_size) + (3 * arrow_spacing)
local start_x = (Config.screen.width - total_width) / 2
return {
bar_fill = 0,
max_fill = 100,
fill_per_hit = 10,
miss_penalty = 5,
bar_x = 20,
bar_y = 10,
bar_width = 200,
bar_height = 12,
arrow_size = arrow_size,
arrow_spawn_timer = 0,
arrow_spawn_interval = 45,
arrow_fall_speed = 1.5,
arrows = {},
target_y = 115,
target_arrows = {
{ dir = "left", x = start_x },
{ dir = "down", x = start_x + arrow_size + arrow_spacing },
{ dir = "up", x = start_x + (arrow_size + arrow_spacing) * 2 },
{ dir = "right", x = start_x + (arrow_size + arrow_spacing) * 3 }
},
hit_threshold = 8,
button_pressed_timers = {},
button_press_duration = 8,
input_cooldowns = { left = 0, down = 0, up = 0, right = 0 },
input_cooldown_duration = 10,
frame_counter = 0,
current_song = nil,
pattern_index = 1,
use_pattern = false,
return_window = nil,
win_timer = 0,
on_win = nil
}
end
--- Initializes DDR minigame state.
--- @within MinigameDDRWindow
--- @param params table Optional parameters for configuration.<br/>
function MinigameDDRWindow.init(params) function MinigameDDRWindow.init(params)
local defaults = MinigameDDRWindow.init_context() Context.minigame_ddr = Minigames.configure_ddr(params)
if params then
for k, v in pairs(params) do
defaults[k] = v
end
end
Context.minigame_ddr = defaults
end end
--- Starts the DDR minigame.
--- @within MinigameDDRWindow
--- @param return_window string The window ID to return to after the minigame.</br>
--- @param[opt] song_key string The key of the song to play.</br>
--- @param[opt] params table Optional parameters for minigame configuration.</br>
function MinigameDDRWindow.start(return_window, song_key, params) function MinigameDDRWindow.start(return_window, song_key, params)
MinigameDDRWindow.init(params) MinigameDDRWindow.init(params)
Context.minigame_ddr.return_window = return_window or "game" Context.minigame_ddr.return_window = return_window or WINDOW_GAME
Context.minigame_ddr.debug_song_key = song_key Context.minigame_ddr.debug_song_key = song_key
if song_key and Songs and Songs[song_key] then if song_key and Songs and Songs[song_key] then
Context.minigame_ddr.current_song = Songs[song_key] Context.minigame_ddr.current_song = Songs[song_key]
@@ -79,11 +19,9 @@ function MinigameDDRWindow.start(return_window, song_key, params)
Context.minigame_ddr.debug_status = "Random mode" Context.minigame_ddr.debug_status = "Random mode"
end end
end end
Window.set_current("minigame_ddr") Context.active_window = WINDOW_MINIGAME_DDR
end end
--- Spawns a random arrow.
--- @within MinigameDDRWindow
local function spawn_arrow() local function spawn_arrow()
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
local target = mg.target_arrows[math.random(1, 4)] local target = mg.target_arrows[math.random(1, 4)]
@@ -94,9 +32,6 @@ local function spawn_arrow()
}) })
end end
--- Spawns an arrow in a specific direction.
--- @within MinigameDDRWindow
--- @param direction string The direction of the arrow ("left", "down", "up", "right").
local function spawn_arrow_dir(direction) local function spawn_arrow_dir(direction)
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
for _, target in ipairs(mg.target_arrows) do for _, target in ipairs(mg.target_arrows) do
@@ -111,31 +46,17 @@ local function spawn_arrow_dir(direction)
end end
end end
--- Checks if an arrow is hit.
--- @within MinigameDDRWindow
--- @param arrow table The arrow data.
--- @return boolean True if the arrow is hit, false otherwise.
local function check_hit(arrow) local function check_hit(arrow)
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
local distance = math.abs(arrow.y - mg.target_y) local distance = math.abs(arrow.y - mg.target_y)
return distance <= mg.hit_threshold return distance <= mg.hit_threshold
end end
--- Checks if an arrow is missed.
--- @within MinigameDDRWindow
--- @param arrow table The arrow data.
--- @return boolean True if the arrow is missed, false otherwise.
local function check_miss(arrow) local function check_miss(arrow)
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
return arrow.y > mg.target_y + mg.hit_threshold return arrow.y > mg.target_y + mg.hit_threshold
end end
--- Draws an arrow.
--- @within MinigameDDRWindow
--- @param x number The x-coordinate.
--- @param y number The y-coordinate.
--- @param direction string The direction of the arrow.
--- @param color number The color of the arrow.
local function draw_arrow(x, y, direction, color) local function draw_arrow(x, y, direction, color)
local size = 12 local size = 12
local half = size / 2 local half = size / 2
@@ -154,33 +75,20 @@ local function draw_arrow(x, y, direction, color)
end end
end end
--- Updates DDR minigame logic.
--- @within MinigameDDRWindow
function MinigameDDRWindow.update() function MinigameDDRWindow.update()
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
if mg.win_timer > 0 then
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Meter.on_minigame_complete()
if mg.on_win then
mg.on_win()
else
Meter.show()
Window.set_current(mg.return_window)
end
end
return
end
if mg.bar_fill >= mg.max_fill then if mg.bar_fill >= mg.max_fill then
mg.win_timer = Config.timing.minigame_win_duration Meters.on_minigame_complete()
Meters.show()
Context.active_window = mg.return_window
return return
end end
mg.frame_counter = mg.frame_counter + 1 mg.frame_counter = mg.frame_counter + 1
if mg.use_pattern and mg.current_song and mg.current_song.end_frame then if mg.use_pattern and mg.current_song and mg.current_song.end_frame then
if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then
mg.win_timer = Config.timing.minigame_win_duration Meters.on_minigame_complete()
Meters.show()
Context.active_window = mg.return_window
return return
end end
end end
@@ -259,8 +167,6 @@ function MinigameDDRWindow.update()
end end
end end
--- Draws DDR minigame.
--- @within MinigameDDRWindow
function MinigameDDRWindow.draw() function MinigameDDRWindow.draw()
local mg = Context.minigame_ddr local mg = Context.minigame_ddr
if not mg then if not mg then
@@ -268,11 +174,11 @@ function MinigameDDRWindow.draw()
print("DDR ERROR: Context not initialized", 10, 10, 12) print("DDR ERROR: Context not initialized", 10, 10, 12)
print("Press Z to return", 10, 20, 12) print("Press Z to return", 10, 20, 12)
if Input.select() then if Input.select() then
Window.set_current("game") Context.active_window = WINDOW_GAME
end end
return return
end end
if mg.return_window == "game" then if mg.return_window == WINDOW_GAME then
GameWindow.draw() GameWindow.draw()
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black) rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
@@ -280,7 +186,7 @@ function MinigameDDRWindow.draw()
rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey)
local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width
if fill_width > 0 then if fill_width > 0 then
local bar_color = Config.colors.light_blue local bar_color = Config.colors.green
if mg.bar_fill > 66 then if mg.bar_fill > 66 then
bar_color = Config.colors.item bar_color = Config.colors.item
elseif mg.bar_fill > 33 then elseif mg.bar_fill > 33 then
@@ -293,7 +199,7 @@ function MinigameDDRWindow.draw()
if mg.target_arrows then if mg.target_arrows then
for _, target in ipairs(mg.target_arrows) do for _, target in ipairs(mg.target_arrows) do
local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0 local is_pressed = mg.button_pressed_timers[target.dir] and mg.button_pressed_timers[target.dir] > 0
local color = is_pressed and Config.colors.light_blue or Config.colors.light_grey local color = is_pressed and Config.colors.green or Config.colors.light_grey
draw_arrow(target.x, mg.target_y, target.dir, color) draw_arrow(target.x, mg.target_y, target.dir, color)
end end
end end
@@ -313,20 +219,17 @@ function MinigameDDRWindow.draw()
"PATTERN MODE - Frame:" .. mg.frame_counter, "PATTERN MODE - Frame:" .. mg.frame_counter,
Config.screen.width / 2, Config.screen.width / 2,
debug_y, debug_y,
Config.colors.light_blue Config.colors.green
) )
if mg.current_song and mg.current_song.pattern then if mg.current_song and mg.current_song.pattern then
Print.text_center( Print.text_center(
"Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index, "Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index,
Config.screen.width / 2, Config.screen.width / 2,
debug_y + 10, debug_y + 10,
Config.colors.light_blue Config.colors.green
) )
end end
else else
Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue) Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue)
end end
if mg.win_timer > 0 then
Minigame.draw_win_overlay()
end end
end

View File

@@ -1,94 +1,26 @@
--- Gets initial button mash minigame configuration.
--- @within MinigameButtonMashWindow
--- @return result table The default button mash minigame configuration.
function MinigameButtonMashWindow.init_context()
return {
bar_fill = 0,
target_points = 100,
fill_per_press = 8,
base_degradation = 0.15,
degradation_multiplier = 0.006,
button_pressed_timer = 0,
button_press_duration = 8,
instruction_text = "MASH Z!",
show_progress_text = true,
return_window = nil,
bar_x = 20,
bar_y = 10,
bar_width = 200,
bar_height = 12,
button_x = 20,
button_y = 110,
button_size = 12,
focus_center_x = nil,
focus_center_y = nil,
focus_initial_radius = 0,
win_timer = 0,
on_win = nil
}
end
--- Initializes button mash minigame state.
--- @within MinigameButtonMashWindow
--- @param params table Optional parameters for configuration.<br/>
function MinigameButtonMashWindow.init(params) function MinigameButtonMashWindow.init(params)
local defaults = MinigameButtonMashWindow.init_context() Context.minigame_button_mash = Minigames.configure_button_mash(params)
if params then
for k, v in pairs(params) do
defaults[k] = v
end
if params.max_fill and not params.target_points then
defaults.target_points = params.max_fill
end
end
Context.minigame_button_mash = defaults
end end
--- Starts the button mash minigame.
--- @within MinigameButtonMashWindow
--- @param return_window string The window ID to return to after the minigame.<br/>
--- @param[opt] params table Optional parameters for minigame configuration.<br/>
function MinigameButtonMashWindow.start(return_window, params) function MinigameButtonMashWindow.start(return_window, params)
MinigameButtonMashWindow.init(params) MinigameButtonMashWindow.init(params)
local mg = Context.minigame_button_mash Context.minigame_button_mash.return_window = return_window or WINDOW_GAME
mg.return_window = return_window or "game" Context.active_window = WINDOW_MINIGAME_BUTTON_MASH
if mg.focus_center_x then
Focus.start_driven(mg.focus_center_x, mg.focus_center_y, {
initial_radius = mg.focus_initial_radius
})
end
Window.set_current("minigame_button_mash")
end end
--- Updates button mash minigame logic.
--- @within MinigameButtonMashWindow
function MinigameButtonMashWindow.update() function MinigameButtonMashWindow.update()
local mg = Context.minigame_button_mash local mg = Context.minigame_button_mash
if mg.win_timer > 0 then
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Meter.on_minigame_complete()
if mg.focus_center_x then Focus.stop() end
if mg.on_win then
mg.on_win()
else
Meter.show()
Window.set_current(mg.return_window)
end
end
return
end
if Input.select() then if Input.select() then
mg.bar_fill = mg.bar_fill + mg.fill_per_press mg.bar_fill = mg.bar_fill + mg.fill_per_press
mg.button_pressed_timer = mg.button_press_duration mg.button_pressed_timer = mg.button_press_duration
if mg.bar_fill > mg.target_points then if mg.bar_fill > mg.max_fill then
mg.bar_fill = mg.target_points mg.bar_fill = mg.max_fill
end end
end end
if mg.bar_fill >= mg.target_points then if mg.bar_fill >= mg.max_fill then
mg.win_timer = Config.timing.minigame_win_duration Meters.on_minigame_complete()
Meters.show()
Context.active_window = mg.return_window
return return
end end
local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier) local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier)
@@ -99,28 +31,19 @@ function MinigameButtonMashWindow.update()
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
mg.button_pressed_timer = mg.button_pressed_timer - 1 mg.button_pressed_timer = mg.button_pressed_timer - 1
end end
if mg.focus_center_x then
Focus.set_percentage(mg.bar_fill / mg.target_points)
end
end end
--- Draws button mash minigame.
--- @within MinigameButtonMashWindow
function MinigameButtonMashWindow.draw() function MinigameButtonMashWindow.draw()
local mg = Context.minigame_button_mash local mg = Context.minigame_button_mash
if mg.return_window == "game" then if mg.return_window == WINDOW_GAME then
GameWindow.draw_with_underlay(function() GameWindow.draw()
Sprite.draw_at("sleeping_norman", (Config.screen.width / 2) - 30, (Config.screen.height / 2) - 22)
end)
end
if not mg.focus_center_x then
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey)
rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey)
local fill_width = (mg.bar_fill / mg.target_points) * mg.bar_width local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width
if fill_width > 0 then if fill_width > 0 then
local bar_color = Config.colors.light_blue local bar_color = Config.colors.green
if mg.bar_fill > 66 then if mg.bar_fill > 66 then
bar_color = Config.colors.item bar_color = Config.colors.item
elseif mg.bar_fill > 33 then elseif mg.bar_fill > 33 then
@@ -130,20 +53,14 @@ function MinigameButtonMashWindow.draw()
end end
local button_color = Config.colors.light_grey local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
button_color = Config.colors.light_blue button_color = Config.colors.green
end end
circb(mg.button_x, mg.button_y, mg.button_size, button_color) circb(mg.button_x, mg.button_y, mg.button_size, button_color)
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color) circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color)
end end
Print.text_center("Z", mg.button_x, mg.button_y - 3, button_color) Print.text_center(" Z", mg.button_x - 2, mg.button_y - 3, Config.colors.light_grey)
Print.text_center(mg.instruction_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey) Print.text_center("MASH Z!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
if mg.show_progress_text then local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100)
local points_text = math.floor(mg.bar_fill) .. "/" .. mg.target_points Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black)
Print.text_center(points_text, mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black)
end
if mg.win_timer > 0 then
Minigame.draw_win_overlay()
end
end end

View File

@@ -1,89 +1,15 @@
--- @section MinigameRhythmWindow
--- Gets initial rhythm minigame configuration.
--- @within MinigameRhythmWindow
--- @return result table The default rhythm minigame configuration.
function MinigameRhythmWindow.init_context()
return {
line_position = 0,
line_speed = 0.015,
line_direction = 1,
target_center = 0.5,
target_width = 0.3,
initial_target_width = 0.3,
min_target_width = 0.08,
target_shrink_rate = 0.9,
score = 0,
max_score = 10,
button_pressed_timer = 0,
button_press_duration = 10,
return_window = nil,
bar_x = 20,
bar_y = 10,
bar_width = 200,
bar_height = 12,
button_x = 210,
button_y = 110,
button_size = 10,
press_cooldown = 0,
press_cooldown_duration = 15,
focus_center_x = nil,
focus_center_y = nil,
focus_initial_radius = 0,
win_timer = 0,
on_win = nil
}
end
--- Initializes rhythm minigame state.
--- @within MinigameRhythmWindow
--- @param params table Optional parameters for configuration.<br/>
function MinigameRhythmWindow.init(params) function MinigameRhythmWindow.init(params)
local defaults = MinigameRhythmWindow.init_context() Context.minigame_rhythm = Minigames.configure_rhythm(params)
if params then
for k, v in pairs(params) do
defaults[k] = v
end
end
Context.minigame_rhythm = defaults
end end
--- Starts the rhythm minigame.
--- @within MinigameRhythmWindow
--- @param return_window string The window ID to return to after the minigame.<br/>
--- @param[opt] params table Optional parameters for minigame configuration.<br/>
function MinigameRhythmWindow.start(return_window, params) function MinigameRhythmWindow.start(return_window, params)
MinigameRhythmWindow.init(params) MinigameRhythmWindow.init(params)
local mg = Context.minigame_rhythm Context.minigame_rhythm.return_window = return_window or WINDOW_GAME
mg.return_window = return_window or "game" Context.active_window = WINDOW_MINIGAME_RHYTHM
if mg.focus_center_x then
Focus.start_driven(mg.focus_center_x, mg.focus_center_y, {
initial_radius = mg.focus_initial_radius
})
end
Window.set_current("minigame_rhythm")
end end
--- Updates rhythm minigame logic.
--- @within MinigameRhythmWindow
function MinigameRhythmWindow.update() function MinigameRhythmWindow.update()
local mg = Context.minigame_rhythm local mg = Context.minigame_rhythm
if mg.win_timer > 0 then
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
Meter.on_minigame_complete()
if mg.focus_center_x then Focus.stop() end
if mg.on_win then
mg.on_win()
else
Meter.show()
Window.set_current(mg.return_window)
end
end
return
end
mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction) mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction)
if mg.line_position > 1 then if mg.line_position > 1 then
mg.line_position = 1 mg.line_position = 1
@@ -114,55 +40,46 @@ function MinigameRhythmWindow.update()
end end
end end
if mg.score >= mg.max_score then if mg.score >= mg.max_score then
mg.win_timer = Config.timing.minigame_win_duration Meters.on_minigame_complete()
Meters.show()
Context.active_window = mg.return_window
return return
end end
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
mg.button_pressed_timer = mg.button_pressed_timer - 1 mg.button_pressed_timer = mg.button_pressed_timer - 1
end end
if mg.focus_center_x then
Focus.set_percentage(1 - mg.score / mg.max_score)
end
end end
--- Draws rhythm minigame.
--- @within MinigameRhythmWindow
function MinigameRhythmWindow.draw() function MinigameRhythmWindow.draw()
local mg = Context.minigame_rhythm local mg = Context.minigame_rhythm
if mg.return_window == "game" then if mg.return_window == WINDOW_GAME then
GameWindow.draw_with_underlay(function() GameWindow.draw()
Sprite.draw_at("sleeping_norman", (Config.screen.width / 2) - 30, (Config.screen.height / 2) - 22)
end)
end
if not mg.focus_center_x then
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
end end
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey) rect(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.light_grey)
rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey) rectb(mg.bar_x - 2, mg.bar_y - 2, mg.bar_width + 4, mg.bar_height + 4, Config.colors.dark_grey)
rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey) rect(mg.bar_x, mg.bar_y, mg.bar_width, mg.bar_height, Config.colors.dark_grey)
local target_left = mg.target_center - (mg.target_width / 2) local target_left = mg.target_center - (mg.target_width / 2)
local target_x = mg.bar_x + (target_left * mg.bar_width) local target_x = mg.bar_x + (target_left * mg.bar_width)
local target_width_pixels = mg.target_width * mg.bar_width local target_width_pixels = mg.target_width * mg.bar_width
rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.light_blue) rect(target_x, mg.bar_y, target_width_pixels, mg.bar_height, Config.colors.green)
local line_x = mg.bar_x + (mg.line_position * mg.bar_width) local line_x = mg.bar_x + (mg.line_position * mg.bar_width)
rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.item) rect(line_x - 1, mg.bar_y, 2, mg.bar_height, Config.colors.item)
local score_text = "SCORE: " .. mg.score .. " / " .. mg.max_score
Print.text_center(score_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 8, Config.colors.light_grey)
Print.text_center( Print.text_center(
"Sleep Norman ... Sleep!", "Press Z when line is in green!",
Config.screen.width / 2, Config.screen.width / 2,
mg.bar_y + mg.bar_height + 14, mg.bar_y + mg.bar_height + 20,
Config.colors.light_grey Config.colors.light_grey
) )
local button_color = Config.colors.light_grey local button_color = Config.colors.light_grey
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
button_color = Config.colors.light_blue button_color = Config.colors.green
end end
circb(mg.button_x, mg.button_y, mg.button_size, button_color) circb(mg.button_x, mg.button_y, mg.button_size, button_color)
if mg.button_pressed_timer > 0 then if mg.button_pressed_timer > 0 then
circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color) circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color)
end end
Print.text_center("Z", mg.button_x, mg.button_y - 3, button_color) Print.text_center("Z", mg.button_x - 2, mg.button_y - 3, button_color)
if mg.win_timer > 0 then
Minigame.draw_win_overlay()
end
end end

View File

@@ -1,160 +0,0 @@
--- @section MysteriousManWindow
local STATE_TEXT = "text"
local STATE_DAY = "day"
local STATE_CHOICE = "choice"
local DEFAULT_TEXT = [[
Misterious man appears
during your sleep.
He says nothing.
He doesn't need to.
He says nothing.
]]
local state = STATE_TEXT
local text_y = Config.screen.height
local text_speed = 0.4
local day_timer = 0
local day_display_frames = 120
local selected_choice = 1
local text = DEFAULT_TEXT
local day_text_override = nil
local on_text_complete = nil
local choices = {
{
label = "Wake Up",
},
{
label = "Stay in Bed",
},
}
--- Sets the scrolling text content.
--- @within MysteriousManWindow
--- @param new_text string The text to display.
function MysteriousManWindow.set_text(new_text)
text = new_text
end
--- Starts the mysterious man window.
--- @within MysteriousManWindow
--- @param[opt] options table Optional window configuration.</br>
--- Fields: </br>
--- * text (string) Override for the scrolling text.<br/>
--- * day_text (string) Override for the centered day label.<br/>
--- * on_text_complete (function) Callback fired once when the text phase ends.<br/>
function MysteriousManWindow.start(options)
options = options or {}
state = STATE_TEXT
text_y = Config.screen.height
day_timer = 0
selected_choice = 1
text = options.text or DEFAULT_TEXT
day_text_override = options.day_text
on_text_complete = options.on_text_complete
Meter.hide()
Window.set_current("mysterious_man")
end
local function go_to_day_state()
if on_text_complete then
on_text_complete()
on_text_complete = nil
end
if Window.get_current_id() ~= "mysterious_man" then
return
end
state = STATE_DAY
day_timer = day_display_frames
end
local function wake_up()
Context.home_norman_visible = false
Util.go_to_screen_by_id("home")
MinigameButtonMashWindow.start("game", {
focus_center_x = (Config.screen.width / 2) - 22,
focus_center_y = (Config.screen.height / 2) - 18,
focus_initial_radius = 0,
target_points = 100,
instruction_text = "Wake up Norman!",
show_progress_text = false,
on_win = function()
Audio.music_play_wakingup()
Context.home_norman_visible = true
Meter.show()
Window.set_current("game")
end,
})
end
local function stay_in_bed()
Day.increase()
state = STATE_DAY
day_timer = day_display_frames
end
--- Updates the mysterious man window logic.
--- @within MysteriousManWindow
function MysteriousManWindow.update()
if state == STATE_TEXT then
text_y = text_y - text_speed
local lines = 1
for _ in string.gmatch(text, "\n") do
lines = lines + 1
end
if text_y < -lines * 8 then
go_to_day_state()
end
if Input.select() then
go_to_day_state()
end
elseif state == STATE_DAY then
day_timer = day_timer - 1
if day_timer <= 0 or Input.select() then
state = STATE_CHOICE
selected_choice = 1
end
elseif state == STATE_CHOICE then
selected_choice = UI.update_menu(choices, selected_choice)
if Input.select() then
Audio.sfx_select()
if selected_choice == 1 then
wake_up()
else
stay_in_bed()
end
end
end
end
--- Draws the mysterious man window.
--- @within MysteriousManWindow
function MysteriousManWindow.draw()
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
if state == STATE_TEXT then
local x = (Config.screen.width - 132) / 2
Print.text(text, x, text_y, Config.colors.light_grey)
elseif state == STATE_DAY then
local day_text = day_text_override or ("Day " .. Context.day_count)
Print.text_center(
day_text,
Config.screen.width / 2,
Config.screen.height / 2 - 3,
Config.colors.white
)
elseif state == STATE_CHOICE then
local menu_x = (Config.screen.width - 60) / 2
local menu_y = (Config.screen.height - 20) / 2
UI.draw_menu(choices, selected_choice, menu_x, menu_y)
end
end

View File

@@ -1,4 +1,3 @@
--- @section PopupWindow
local POPUP_X = 40 local POPUP_X = 40
local POPUP_Y = 40 local POPUP_Y = 40
local POPUP_WIDTH = 160 local POPUP_WIDTH = 160
@@ -6,40 +5,25 @@ local POPUP_HEIGHT = 80
local TEXT_MARGIN_X = POPUP_X + 10 local TEXT_MARGIN_X = POPUP_X + 10
local TEXT_MARGIN_Y = POPUP_Y + 10 local TEXT_MARGIN_Y = POPUP_Y + 10
local LINE_HEIGHT = 8 local LINE_HEIGHT = 8
--- Displays a popup window.
--- @within PopupWindow
--- @param content_strings table A table of strings to display in the popup.</br>
function PopupWindow.show(content_strings) function PopupWindow.show(content_strings)
Context.popup.show = true Context.popup.show = true
Context.popup.content = content_strings or {} Context.popup.content = content_strings or {} GameWindow.set_state(WINDOW_POPUP) end
GameWindow.set_state("popup")
end
--- Hides the popup window.
--- @within PopupWindow
function PopupWindow.hide() function PopupWindow.hide()
Context.popup.show = false Context.popup.show = false
Context.popup.content = {} Context.popup.content = {} GameWindow.set_state(WINDOW_GAME) end
GameWindow.set_state("game")
end
--- Updates popup window logic.
--- @within PopupWindow
function PopupWindow.update() function PopupWindow.update()
if Context.popup.show then if Context.popup.show then
if Input.menu_confirm() or Input.menu_back() then if Input.menu_confirm() or Input.menu_back() then PopupWindow.hide()
PopupWindow.hide()
end end
end end
end end
--- Draws the popup window.
--- @within PopupWindow
function PopupWindow.draw() function PopupWindow.draw()
if Context.popup.show then if Context.popup.show then
rect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.black) rect(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.black)
rectb(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.light_blue) rectb(POPUP_X, POPUP_Y, POPUP_WIDTH, POPUP_HEIGHT, Config.colors.green)
local current_y = TEXT_MARGIN_Y local current_y = TEXT_MARGIN_Y
for _, line in ipairs(Context.popup.content) do for _, line in ipairs(Context.popup.content) do
@@ -47,6 +31,6 @@ function PopupWindow.draw()
current_y = current_y + LINE_HEIGHT current_y = current_y + LINE_HEIGHT
end end
Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.light_blue) Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.green)
end end
end end

View File

@@ -1,44 +0,0 @@
TitleIntroWindow = {}
Window.register("intro_title", TitleIntroWindow)
TTGIntroWindow = {}
Window.register("intro_ttg", TTGIntroWindow)
BriefIntroWindow = {}
Window.register("intro_brief", BriefIntroWindow)
MenuWindow = {}
Window.register("menu", MenuWindow)
GameWindow = {}
Window.register("game", GameWindow)
PopupWindow = {}
Window.register("popup", PopupWindow)
ConfigurationWindow = {}
Window.register("configuration", ConfigurationWindow)
AudioTestWindow = {}
Window.register("audiotest", AudioTestWindow)
MinigameButtonMashWindow = {}
Window.register("minigame_button_mash", MinigameButtonMashWindow)
MinigameRhythmWindow = {}
Window.register("minigame_rhythm", MinigameRhythmWindow)
MinigameDDRWindow = {}
Window.register("minigame_ddr", MinigameDDRWindow)
MysteriousManWindow = {}
Window.register("mysterious_man", MysteriousManWindow)
EndWindow = {}
Window.register("end", EndWindow)
DiscussionWindow = {}
Window.register("discussion", DiscussionWindow)
ContinuedWindow = {}
Window.register("continued", ContinuedWindow)

View File

@@ -0,0 +1,14 @@
function SplashWindow.draw()
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()
Context.splash_timer = Context.splash_timer - 1
if Context.splash_timer <= 0 or Input.menu_confirm() then
GameWindow.set_state(WINDOW_INTRO)
end
end

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.