Compare commits
112 Commits
1913d7d7d4
...
feature/ta
| Author | SHA1 | Date | |
|---|---|---|---|
| 67de284598 | |||
| 8a7fa96bcd | |||
| 6a3ef5d81e | |||
| c334f644de | |||
| 7cc886623b | |||
| 8ac5c5c3d9 | |||
| 43e943278b | |||
| 47efe91f58 | |||
|
|
fd7b5650d3 | ||
| 0e956ec7a9 | |||
| 0c2999f596 | |||
| e46c48b2ec | |||
| 99ace8a1e8 | |||
| 15bf66f1ca | |||
|
|
41f75da8c3 | ||
| e05018d637 | |||
| 8e104b1ff9 | |||
| e07eeb466b | |||
| 337f1fc132 | |||
| e14160114b | |||
|
|
1b64fd2392 | ||
|
|
aaf1479a78 | ||
| e56662f6ad | |||
| 2d25537abb | |||
|
|
66af47c483 | ||
| 8f9e044a17 | |||
|
|
954a39aef1 | ||
|
|
226d75d905 | ||
|
|
8f34cbf875 | ||
| 64de41a940 | |||
| 777c27aa54 | |||
| c321fbc19a | |||
| dc8a82d583 | |||
| c565213e44 | |||
| 035a2bc37e | |||
| 297ee8b622 | |||
|
|
7deeffa8d6 | ||
| 272a54ea87 | |||
| 14b14ffc0c | |||
| 7e87a78a15 | |||
| 62d4863a1a | |||
| d9febf16e0 | |||
| 6f5b17147c | |||
| 76964f872d | |||
| 3b137fd48e | |||
| b7791fb9ce | |||
| 7e1dd28808 | |||
| 0b25ecc793 | |||
| f08e4ad1d4 | |||
| 9ae6c12582 | |||
| 4e35cd4bd3 | |||
| ed2354b0fa | |||
| 7854dc8a9f | |||
| 3b9b67e2fa | |||
| 1a4565428d | |||
| f02bb75e4f | |||
|
|
e0c3b446af | ||
|
|
8832b6c833 | ||
|
|
9014e36014 | ||
|
|
7b263bb454 | ||
| 0640964ee4 | |||
| 1cf09de1fb | |||
| 6303781534 | |||
| e2bd1711c0 | |||
| 60a6c73a32 | |||
| c88562ae69 | |||
| 1d06376826 | |||
| 7a57c15eeb | |||
| 3922f51c8e | |||
| f589b9bbca | |||
| 3abe426e3a | |||
| 9eef8fd6e0 | |||
| 0357eb415e | |||
| 11b92f64d6 | |||
| d1720a0a50 | |||
| 31b98dcdf6 | |||
| ec84f23ad5 | |||
| 99b178262f | |||
| d20ef85ceb | |||
| ef4876b083 | |||
| 9a65891afa | |||
| 971acb02ca | |||
| 9fff21826b | |||
| 06d257a335 | |||
|
|
f553b09004 | ||
| facf37dc96 | |||
| bbe24cebf2 | |||
| 5872a27535 | |||
| cd279803ac | |||
|
|
c9db82cce7 | ||
| 3dc28849c4 | |||
| 8a6214e893 | |||
| d943b6deaa | |||
| b3b2159d75 | |||
| ae56cf3555 | |||
| 2fc241fee7 | |||
| 4e0145982f | |||
| 1c987fa08b | |||
| b6d0823875 | |||
| ffa82e8f92 | |||
| cfc07afe59 | |||
| 3fbce5aced | |||
| b3158bdc37 | |||
| 4907c9cadf | |||
| 45f2e746d6 | |||
| 8e8d181c34 | |||
| 5379e30cf3 | |||
| d3fb12703c | |||
| f9c539a854 | |||
| a777241d7a | |||
| 34340d9664 | |||
| 6a39128962 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1 +1,6 @@
|
|||||||
mranderson.lua
|
.local
|
||||||
|
impostor.lua
|
||||||
|
impostor.original.lua
|
||||||
|
prompts
|
||||||
|
docs
|
||||||
|
minify.lua
|
||||||
66
.luacheckrc
Normal file
66
.luacheckrc
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
-- .luacheckrc
|
||||||
|
-- Configuration for luacheck
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
"Focus",
|
||||||
|
"Util",
|
||||||
|
"Decision",
|
||||||
|
"Situation",
|
||||||
|
"Screen",
|
||||||
|
"Sprite",
|
||||||
|
"UI",
|
||||||
|
"Print",
|
||||||
|
"Input",
|
||||||
|
"Audio",
|
||||||
|
"Config",
|
||||||
|
"Context",
|
||||||
|
"Meter",
|
||||||
|
"Minigame",
|
||||||
|
"Window",
|
||||||
|
"SplashWindow",
|
||||||
|
"IntroWindow",
|
||||||
|
"MenuWindow",
|
||||||
|
"GameWindow",
|
||||||
|
"PopupWindow",
|
||||||
|
"ConfigurationWindow",
|
||||||
|
"AudioTestWindow",
|
||||||
|
"MinigameButtonMashWindow",
|
||||||
|
"MinigameRhythmWindow",
|
||||||
|
"MinigameDDRWindow",
|
||||||
|
"mset",
|
||||||
|
"mget",
|
||||||
|
"btnp",
|
||||||
|
"keyp",
|
||||||
|
"music",
|
||||||
|
"sfx",
|
||||||
|
"spr",
|
||||||
|
"rect",
|
||||||
|
"rectb",
|
||||||
|
"circ",
|
||||||
|
"circb",
|
||||||
|
"cls",
|
||||||
|
"tri",
|
||||||
|
"pix",
|
||||||
|
"line",
|
||||||
|
"Songs",
|
||||||
|
"frame_from_beat",
|
||||||
|
"beats_to_pattern",
|
||||||
|
"MapBedroom",
|
||||||
|
"TIC",
|
||||||
|
"exit",
|
||||||
|
"trace",
|
||||||
|
"index_menu",
|
||||||
|
"Map",
|
||||||
|
"map",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Exclude certain warnings globally
|
||||||
|
exclude_warnings = {
|
||||||
|
"undefined_global", -- Will be covered by 'globals' table
|
||||||
|
"redefined_loop_variable", -- Common in Lua for iterators
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Options for unused variables
|
||||||
|
std = "lua51" -- Assuming Lua 5.1, common for TIC-80
|
||||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
@@ -14,6 +14,21 @@
|
|||||||
"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": [
|
||||||
|
"${workspaceFolder}/sources/poky/bitbake/lib",
|
||||||
|
"${workspaceFolder}/sources/poky/meta/lib"
|
||||||
|
],
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"${workspaceFolder}/sources/poky/bitbake/lib",
|
||||||
|
"${workspaceFolder}/sources/poky/meta/lib"
|
||||||
|
],
|
||||||
|
"files.associations": {
|
||||||
|
"*.conf": "bitbake",
|
||||||
|
"*.inc": "bitbake"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Run TIC80",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "tic80 --fs=. impostor.lua"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build & Run TIC80",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make build && tic80 --fs=. impostor.lua"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Export assets",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make export_assets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Make build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make build"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,21 +1,45 @@
|
|||||||
environment: &environment
|
|
||||||
GAME_NAME: mranderson
|
|
||||||
GAME_LANG: lua
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: version
|
||||||
image: gitea.vps.teletype.hu/games/tic80pro:latest
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- 'apk add --no-cache make'
|
||||||
|
- 'make ci-version'
|
||||||
|
|
||||||
|
- name: lint
|
||||||
|
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
|
||||||
environment:
|
environment:
|
||||||
<<: *environment
|
|
||||||
XDG_RUNTIME_DIR: /tmp
|
XDG_RUNTIME_DIR: /tmp
|
||||||
commands:
|
commands:
|
||||||
- make build PROJECT=$GAME_NAME
|
- 'make ci-export'
|
||||||
- make export PROJECT=$GAME_NAME
|
|
||||||
|
|
||||||
- name: artifact
|
- name: artifact
|
||||||
image: alpine
|
image: alpine
|
||||||
environment:
|
environment:
|
||||||
<<: *environment
|
|
||||||
DROPAREA_HOST: vps.teletype.hu
|
DROPAREA_HOST: vps.teletype.hu
|
||||||
DROPAREA_PORT: 2223
|
DROPAREA_PORT: 2223
|
||||||
DROPAREA_TARGET_PATH: /home/drop
|
DROPAREA_TARGET_PATH: /home/drop
|
||||||
@@ -23,17 +47,15 @@ steps:
|
|||||||
DROPAREA_SSH_PASSWORD:
|
DROPAREA_SSH_PASSWORD:
|
||||||
from_secret: droparea_ssh_password
|
from_secret: droparea_ssh_password
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache openssh-client sshpass
|
- 'apk add --no-cache make openssh-client sshpass'
|
||||||
- mkdir -p /root/.ssh
|
- 'make ci-artifact'
|
||||||
- sshpass -p $DROPAREA_SSH_PASSWORD scp -o StrictHostKeyChecking=no -P $DROPAREA_PORT $GAME_NAME.$GAME_LANG $GAME_NAME.tic $GAME_NAME.html.zip $DROPAREA_USER@$DROPAREA_HOST:$DROPAREA_TARGET_PATH
|
|
||||||
|
|
||||||
- name: update
|
- name: update
|
||||||
image: alpine
|
image: alpine
|
||||||
environment:
|
environment:
|
||||||
<<: *environment
|
|
||||||
UPDATE_SERVER: https://games.vps.teletype.hu
|
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||||
UPDATE_SECRET:
|
UPDATE_SECRET:
|
||||||
from_secret: update_secret_key
|
from_secret: update_secret_key
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache curl
|
- 'apk add --no-cache make curl'
|
||||||
- curl "$UPDATE_SERVER/update?secret=$UPDATE_SECRET&name=$GAME_NAME&platform=tic80"
|
- 'make ci-update'
|
||||||
|
|||||||
107
GEMINI.md
107
GEMINI.md
@@ -1,6 +1,6 @@
|
|||||||
# TIC-80 Lua Code Regularities
|
# TIC-80 Lua Code Regularities
|
||||||
|
|
||||||
Based on the analysis of `mranderson.lua`, the following regularities and conventions should be followed for future modifications and development within this project:
|
Based on the analysis of `impostor.lua`, the following regularities and conventions should be followed for future modifications and development within this project:
|
||||||
|
|
||||||
## General Structure & Lifecycle
|
## General Structure & Lifecycle
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Based on the analysis of `mranderson.lua`, the following regularities and conven
|
|||||||
|
|
||||||
11. **`spr()` for Sprites:** Individual sprites should be rendered using the `spr()` function.
|
11. **`spr()` for Sprites:** Individual sprites should be rendered using the `spr()` function.
|
||||||
12. **`map()` for Maps:** Tilemaps should be drawn using the `map()` function.
|
12. **`map()` for Maps:** Tilemaps should be drawn using the `map()` function.
|
||||||
13. **`print()` for Text:** Text display should utilize the `print()` function.
|
13. **`Print.text()` for Text:** Text display should utilize the `Print.text()` function.
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
@@ -52,3 +52,106 @@ Based on the analysis of `mranderson.lua`, the following regularities and conven
|
|||||||
## Agent Directives
|
## Agent Directives
|
||||||
|
|
||||||
- **Git Operations:** In the future, do not perform `git add` or `git commit` operations. This responsibility will be handled by the user.
|
- **Git Operations:** In the future, do not perform `git add` or `git commit` operations. This responsibility will be handled by the user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Impostor Minigame Documentation
|
||||||
|
|
||||||
|
This document provides comprehensive documentation for all three minigames implemented in the Impostor game: Button Mash, Rhythm, and DDR (Dance Dance Revolution).
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Button Mash Minigame](#button-mash-minigame)
|
||||||
|
- [Rhythm Minigame](#rhythm-minigame)
|
||||||
|
- [DDR Minigame](#ddr-minigame)
|
||||||
|
- [Integration Guide](#integration-guide)
|
||||||
|
- [Common Features](#common-features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
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
|
||||||
|
- **Progress tracking** - Visual indicators show completion status
|
||||||
|
- **Return mechanism** - Automatic return to the calling window upon completion
|
||||||
|
- **Visual feedback** - Button presses and hits are visually indicated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Button Mash Minigame
|
||||||
|
|
||||||
|
**File**: `inc/window/window.minigame.mash.lua`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
A fast-paced minigame where the player must repeatedly press the Z button to fill a progress bar. The bar automatically degrades over time, and the degradation rate increases as the bar fills up, creating increasing difficulty.
|
||||||
|
|
||||||
|
### Gameplay Mechanics
|
||||||
|
|
||||||
|
- **Objective**: Fill the progress bar to 100% by pressing Z repeatedly
|
||||||
|
- **Challenge**: The bar degrades automatically, with degradation increasing as it fills
|
||||||
|
- **Win Condition**: Bar reaches 100%
|
||||||
|
- **No Fail State**: Player can keep trying until successful
|
||||||
|
|
||||||
|
### Visual Elements
|
||||||
|
|
||||||
|
#### Simple sketch
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
Progress Bar (200x12px at 20,10):
|
||||||
|
┌────────────────────────────────────┐
|
||||||
|
│████████████████░░░░░░░░░░░░░░ 45%│
|
||||||
|
└────────────────────────────────────┘
|
||||||
|
|
||||||
|
Button Indicator (Bottom):
|
||||||
|
╔═══╗
|
||||||
|
║ Z ║ ← Press repeatedly!
|
||||||
|
╚═══╝
|
||||||
|
|
||||||
|
Text: "MASH Z!"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configuration Parameters
|
||||||
|
|
||||||
|
```lua
|
||||||
|
bar_fill = 0 -- Current fill level (0-100)
|
||||||
|
max_fill = 100 -- Target fill level
|
||||||
|
fill_per_press = 8 -- Points gained per button press
|
||||||
|
base_degradation = 0.15 -- Base degradation per frame
|
||||||
|
degradation_multiplier = 0.006 -- Increases degradation with bar fill
|
||||||
|
button_press_duration = 8 -- Visual feedback duration (frames)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Start button mash minigame
|
||||||
|
MinigameButtonMashWindow.start(WINDOW_GAME)
|
||||||
|
|
||||||
|
-- Or from any window
|
||||||
|
MinigameButtonMashWindow.start(WINDOW_POPUP)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Color Coding
|
||||||
|
|
||||||
|
- **Green**: Low fill (0-33%)
|
||||||
|
- **Medium (NPC color)**: Medium fill (34-66%)
|
||||||
|
- **Yellow (Item color)**: High fill (67-100%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rhythm Minigame
|
||||||
|
|
||||||
|
**File**: `inc/window/window.minigame.rhythm.lua`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
A timing-based minigame where the player must press Z when a moving line crosses a green target zone. The target zone shrinks with each successful hit, making the game progressively more challenging.
|
||||||
|
|
||||||
|
### Gameplay Mechanics
|
||||||
|
|
||||||
|
- **Objective**: Score 10 points by timing button presses correctly
|
||||||
|
|||||||
266
Makefile
266
Makefile
@@ -1,46 +1,258 @@
|
|||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# Makefile – TIC-80 project builder
|
# Makefile – TIC-80 project builder
|
||||||
# Usage:
|
|
||||||
# make PROJECT=mranderson
|
|
||||||
# make build PROJECT=mranderson
|
|
||||||
# make watch PROJECT=mranderson
|
|
||||||
# make export PROJECT=mranderson
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
|
|
||||||
ifndef PROJECT
|
PROJECT = impostor
|
||||||
$(error Specify the project name: make PROJECT=name)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ORDER = $(PROJECT).inc
|
ORDER = $(PROJECT).inc
|
||||||
OUTPUT = $(PROJECT).lua
|
OUTPUT = $(PROJECT).lua
|
||||||
|
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))
|
||||||
|
|
||||||
|
ASSETS_LUA = inc/meta/meta.assets.lua
|
||||||
|
ASSETS_DIR = assets
|
||||||
|
ASSET_TYPES = tiles sprites sfx music
|
||||||
|
|
||||||
|
LINT_TMP_LUA := /tmp/_lint_combined.lua
|
||||||
|
LINT_TMP_MAP := /tmp/_lint_map.txt
|
||||||
|
|
||||||
|
# CI/CD variables
|
||||||
|
VERSION_FILE = .version
|
||||||
|
GAME_LANG ?= lua
|
||||||
|
DROPAREA_HOST ?= vps.teletype.hu
|
||||||
|
DROPAREA_PORT ?= 2223
|
||||||
|
DROPAREA_TARGET_PATH ?= /home/drop
|
||||||
|
DROPAREA_USER ?= drop
|
||||||
|
UPDATE_SERVER ?= https://games.vps.teletype.hu
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build: $(OUTPUT)
|
build:
|
||||||
@echo "==> Build complete: $(OUTPUT)"
|
|
||||||
|
|
||||||
$(OUTPUT): $(SRC) $(ORDER)
|
|
||||||
@echo "==> Building $(OUTPUT)..."
|
|
||||||
@rm -f $(OUTPUT)
|
@rm -f $(OUTPUT)
|
||||||
@while read f; do \
|
@sed 's/\r$$//' $(ORDER) | while read f; do \
|
||||||
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
|
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
|
||||||
echo "\n" >> $(OUTPUT); \
|
echo "" >> $(OUTPUT); \
|
||||||
done < $(ORDER)
|
done
|
||||||
@echo "==> Done."
|
|
||||||
|
|
||||||
export: $(OUTPUT)
|
download-minify:
|
||||||
@echo "==> TIC-80 export..."
|
@test -f $(MINIFY) || { echo "==> Downloading $(MINIFY)"; curl -fsSL $(MINIFY_URL) -o $(MINIFY); }
|
||||||
tic80 --cli --skip --fs=. \
|
|
||||||
--cmd="load $(OUTPUT) & save $(PROJECT) & export html $(PROJECT).html & exit"
|
minify: build download-minify
|
||||||
@echo "==> HTML ZIP: $(OUTPUT_ZIP)"
|
@echo "==> Minifying $(OUTPUT)"
|
||||||
@echo "==> TIC: $(OUTPUT_TIC)"
|
@cp $(OUTPUT) $(OUTPUT_ORIGINAL)
|
||||||
|
@lua $(MINIFY) minify $(OUTPUT_ORIGINAL) > $(OUTPUT)
|
||||||
|
|
||||||
|
export: minify
|
||||||
|
@if [ -z "$(VERSION)" ]; then \
|
||||||
|
echo "ERROR: VERSION not set!"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "==> Exporting HTML for version $(VERSION)"
|
||||||
|
@tic80 --cli --skip --fs=. \
|
||||||
|
--cmd="load $(OUTPUT) & save $(PROJECT)-$(VERSION) & export html $(PROJECT)-$(VERSION).html & exit"
|
||||||
|
@echo "==> Creating versioned files"
|
||||||
|
@if [ -f "$(PROJECT)-$(VERSION).tic" ]; then \
|
||||||
|
cp $(PROJECT)-$(VERSION).tic $(PROJECT).tic; \
|
||||||
|
fi
|
||||||
|
@if [ -f "$(PROJECT)-$(VERSION).html.zip" ]; then \
|
||||||
|
cp $(PROJECT)-$(VERSION).html.zip $(PROJECT).html.zip; \
|
||||||
|
fi
|
||||||
|
@echo "==> Generated files:"
|
||||||
|
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
@echo "==> Watching project: $(PROJECT)"
|
$(MAKE) build
|
||||||
make build PROJECT=$(PROJECT)
|
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do $(MAKE) build; done
|
||||||
fswatch -o $(SRC_DIR) $(ORDER) | while read; do make build PROJECT=$(PROJECT); done
|
|
||||||
|
import_assets: build
|
||||||
|
@TIC_CMD="load $(OUTPUT) &"; \
|
||||||
|
for t in $(ASSET_TYPES); do \
|
||||||
|
for f in $(ASSETS_DIR)/$$t/*.png; do \
|
||||||
|
[ -e "$$f" ] || continue; \
|
||||||
|
echo "==> Importing $$f as $$t..."; \
|
||||||
|
TIC_CMD="$${TIC_CMD} & import $$t $$f"; \
|
||||||
|
done; \
|
||||||
|
done; \
|
||||||
|
TIC_CMD="$$TIC_CMD save & exit"; \
|
||||||
|
echo $$TIC_CMD; \
|
||||||
|
tic80 --cli --skip --fs=. --cmd="$$TIC_CMD"
|
||||||
|
|
||||||
|
# export helper function
|
||||||
|
define f_export_asset_awk
|
||||||
|
cat $(2) | awk '/-- <$(1)>/,/<\/$(1)>/' >> $(3)
|
||||||
|
endef
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@echo "==> Merging..."
|
||||||
|
@rm -f $(LINT_TMP_LUA) $(LINT_TMP_MAP)
|
||||||
|
@touch $(LINT_TMP_LUA)
|
||||||
|
@line=1; \
|
||||||
|
while IFS= read -r f || [ -n "$$f" ]; do \
|
||||||
|
f=$$(printf '%s' "$$f" | tr -d '\r'); \
|
||||||
|
[ -z "$$f" ] && continue; \
|
||||||
|
before=$$(wc -l < $(LINT_TMP_LUA)); \
|
||||||
|
cat "$(SRC_DIR)/$$f" >> $(LINT_TMP_LUA); \
|
||||||
|
printf '\n' >> $(LINT_TMP_LUA); \
|
||||||
|
after=$$(wc -l < $(LINT_TMP_LUA)); \
|
||||||
|
linecount=$$((after - before)); \
|
||||||
|
echo "$$line $$linecount $(SRC_DIR)/$$f" >> $(LINT_TMP_MAP); \
|
||||||
|
line=$$((line + linecount)); \
|
||||||
|
done < $(ORDER)
|
||||||
|
@echo "==> luacheck..."
|
||||||
|
@LINT_OUTPUT=$$(luacheck --no-max-line-length $(LINT_TMP_LUA) 2>&1 | awk -v map=$(LINT_TMP_MAP) ' \
|
||||||
|
BEGIN { \
|
||||||
|
NR_map = 0; \
|
||||||
|
while ((getline line < map) > 0) { \
|
||||||
|
n = split(line, a, " "); \
|
||||||
|
start[NR_map] = a[1]+0; \
|
||||||
|
count[NR_map] = a[2]+0; \
|
||||||
|
fname[NR_map] = a[3]; \
|
||||||
|
NR_map++; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
/^[^:]+:[0-9]+:[0-9]+:/ { \
|
||||||
|
colon1 = index($$0, ":"); \
|
||||||
|
rest1 = substr($$0, colon1+1); \
|
||||||
|
colon2 = index(rest1, ":"); \
|
||||||
|
absline = substr(rest1, 1, colon2-1) + 0; \
|
||||||
|
rest2 = substr(rest1, colon2+1); \
|
||||||
|
colon3 = index(rest2, ":"); \
|
||||||
|
col = substr(rest2, 1, colon3-1); \
|
||||||
|
rest = substr(rest2, colon3); \
|
||||||
|
found = 0; \
|
||||||
|
for (i = 0; i < NR_map; i++) { \
|
||||||
|
end_line = start[i] + count[i] -1; \
|
||||||
|
if (absline >= start[i] && absline <= end_line) { \
|
||||||
|
relline = absline - start[i] + 1; \
|
||||||
|
print fname[i] ":" relline ":" col ":" rest; \
|
||||||
|
found = 1; \
|
||||||
|
break; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
if (!found) print $$0; \
|
||||||
|
next; \
|
||||||
|
} \
|
||||||
|
{ print } \
|
||||||
|
'); \
|
||||||
|
echo "$$LINT_OUTPUT"; \
|
||||||
|
NUM_ISSUES=$$(echo "$$LINT_OUTPUT" | grep -cE "^[^:]+:[0-9]+:[0-9]+:"); \
|
||||||
|
if [ "$$NUM_ISSUES" -gt 0 ]; then \
|
||||||
|
echo "Total: $$NUM_ISSUES issue(s) found, commit aborted."; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
echo "Checking /tmp/_lint_combined.lua OK"; \
|
||||||
|
echo "Total: 0 warnings / 0 errors in 1 file"; \
|
||||||
|
fi
|
||||||
|
@rm -f $(LINT_TMP_LUA) $(LINT_TMP_MAP)
|
||||||
|
|
||||||
|
export_assets:
|
||||||
|
# $(OUTPUT) would be a circular dependency
|
||||||
|
@test -e $(OUTPUT)
|
||||||
|
@echo "==> Exporting TIC-80 asset sections"
|
||||||
|
@mkdir -p inc/meta
|
||||||
|
@echo -n '' > $(ASSETS_LUA)
|
||||||
|
@$(call f_export_asset_awk,PALETTE,$(OUTPUT),$(ASSETS_LUA))
|
||||||
|
@$(call f_export_asset_awk,TILES,$(OUTPUT),$(ASSETS_LUA))
|
||||||
|
@$(call f_export_asset_awk,SPRITES,$(OUTPUT),$(ASSETS_LUA))
|
||||||
|
@$(call f_export_asset_awk,MAP,$(OUTPUT),$(ASSETS_LUA))
|
||||||
|
@$(call f_export_asset_awk,SFX,$(OUTPUT),$(ASSETS_LUA))
|
||||||
|
@$(call f_export_asset_awk,WAVES,$(OUTPUT),$(ASSETS_LUA))
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -f $(PROJECT)-*.tic $(PROJECT)-*.html.zip $(PROJECT)-*-docs.zip $(PROJECT)-docs.zip $(OUTPUT) $(OUTPUT_ORIGINAL)
|
||||||
|
@echo "==> Cleaned build artifacts"
|
||||||
|
|
||||||
|
install_precommit_hook:
|
||||||
|
@echo "Installing Git pre-commit hook (lint check)..."
|
||||||
|
@mkdir -p .git/hooks
|
||||||
|
@printf '#!/bin/bash\n' > .git/hooks/pre-commit
|
||||||
|
@printf 'echo "Running lint before commit..."\n' >> .git/hooks/pre-commit
|
||||||
|
@printf 'make lint\n' >> .git/hooks/pre-commit
|
||||||
|
@printf 'if [ $$? -ne 0 ]; then\n' >> .git/hooks/pre-commit
|
||||||
|
@printf ' echo "Lint failed! Commit aborted."\n' >> .git/hooks/pre-commit
|
||||||
|
@printf ' exit 1\n' >> .git/hooks/pre-commit
|
||||||
|
@printf 'fi\n' >> .git/hooks/pre-commit
|
||||||
|
@chmod +x .git/hooks/pre-commit
|
||||||
|
@echo "Pre-commit hook installed successfully."
|
||||||
|
|
||||||
|
docs: build
|
||||||
|
@echo "==> Checking for ldoc..."
|
||||||
|
@if ! command -v ldoc &> /dev/null; then \
|
||||||
|
echo "ldoc not found, attempting to install with luarocks..."; \
|
||||||
|
if command -v luarocks &> /dev/null; then \
|
||||||
|
luarocks install ldoc; \
|
||||||
|
else \
|
||||||
|
echo "Error: luarocks not found. Please install luarocks and then ldoc manually."; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
fi
|
||||||
|
@echo "==> Running ldoc..."
|
||||||
|
@ldoc ${OUTPUT} -d docs
|
||||||
|
@echo "==> Documentation generated."
|
||||||
|
|
||||||
|
# -----------------------------------------
|
||||||
|
# CI/CD Pipeline 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-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
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Mr. Anderson's Matrix Escape
|
# Definitely not an Impostor
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -8,14 +8,6 @@ This game is designed for the TIC-80 fantasy computer. To play, follow these ste
|
|||||||
2. **Clone this repository** Clone this repository to tic80's cartridge folder (MacOS: ~Library/Application Support/com.nesbox.tic/TIC-80)
|
2. **Clone this repository** Clone this repository to tic80's cartridge folder (MacOS: ~Library/Application Support/com.nesbox.tic/TIC-80)
|
||||||
2. **Launch TIC-80:** Start the TIC-80 application.
|
2. **Launch TIC-80:** Start the TIC-80 application.
|
||||||
3. **Load the Game:**
|
3. **Load the Game:**
|
||||||
* Navigate to the directory where `game.lua` is located using the TIC-80 command line (`cd mranderson`).
|
* Navigate to the directory where `game.lua` is located using the TIC-80 command line (`cd impostor`).
|
||||||
* Type `load game.lua` and press Enter.
|
* Type `load game.lua` and press Enter.
|
||||||
* Once loaded, type `run` and press Enter to start the game.
|
* Once loaded, type `run` and press Enter to start the game.
|
||||||
|
|
||||||
## Story: The Coder's Lament
|
|
||||||
|
|
||||||
Before he was "The One," before he dodged bullets and shattered the illusion, Thomas Anderson was just a software developer named Neo. Trapped in a cubicle farm of endless bugs and looming deadlines, Neo's days were a monotonous cycle of debugging legacy code, attending pointless meetings, and battling unresponsive APIs. Each line of code felt like a chain, each project a heavier burden, pulling him deeper into a digital malaise.
|
|
||||||
|
|
||||||
He yearned for something more, a glitch in the system, a whisper of a different reality. His fingers, calloused from countless hours on the keyboard, danced across cryptic forums late at night, searching for answers, for meaning beyond the mundane syntax of his corporate prison. The coffee flowed freely, the pizza boxes piled high, and the lines of code blurred into an indistinguishable stream of ones and zeros.
|
|
||||||
|
|
||||||
This game chronicles Mr. Anderson's final, desperate struggles within the software development matrix. Navigate the labyrinthine codebase, escape the relentless pursuit of project managers (Agents), and uncover the hidden truths that will lead him to question everything he knows. Will he find the "red pill" in a sea of green code, or will he forever be just another drone in the system?
|
|
||||||
|
|||||||
0
assets/music/.keep
Normal file
0
assets/music/.keep
Normal file
0
assets/sfx/.keep
Normal file
0
assets/sfx/.keep
Normal file
0
assets/sprites/.keep
Normal file
0
assets/sprites/.keep
Normal file
0
assets/tiles/.keep
Normal file
0
assets/tiles/.keep
Normal file
BIN
assets_src/background_bedroom.ase
Normal file
BIN
assets_src/background_bedroom.ase
Normal file
Binary file not shown.
BIN
assets_src/background_office.ase
Normal file
BIN
assets_src/background_office.ase
Normal file
Binary file not shown.
BIN
assets_src/background_street.ase
Normal file
BIN
assets_src/background_street.ase
Normal file
Binary file not shown.
BIN
assets_src/map.map
Normal file
BIN
assets_src/map.map
Normal file
Binary file not shown.
BIN
assets_src/mapimg.png
Normal file
BIN
assets_src/mapimg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
assets_src/palette-pico8.png
Normal file
BIN
assets_src/palette-pico8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 480 B |
1
assets_src/palette-pico8.txt
Normal file
1
assets_src/palette-pico8.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0404005f574fc2c3c71d2b53fff1e8ab52367e2553ffa30000875129adff83769c00e436fa77a8ff004dc3c3c7ffccaa
|
||||||
BIN
assets_src/sprites.ase
Normal file
BIN
assets_src/sprites.ase
Normal file
Binary file not shown.
BIN
assets_src/sprites.png
Normal file
BIN
assets_src/sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets_src/tiles.png
Normal file
BIN
assets_src/tiles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
51
impostor.inc
Normal file
51
impostor.inc
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
meta/meta.header.lua
|
||||||
|
init/init.module.lua
|
||||||
|
init/init.config.lua
|
||||||
|
init/init.minigame.lua
|
||||||
|
init/init.meter.lua
|
||||||
|
init/init.context.lua
|
||||||
|
system/system.util.lua
|
||||||
|
system/system.print.lua
|
||||||
|
system/system.input.lua
|
||||||
|
system/system.focus.lua
|
||||||
|
system/system.ui.lua
|
||||||
|
audio/audio.manager.lua
|
||||||
|
audio/audio.songs.lua
|
||||||
|
sprite/sprite.manager.lua
|
||||||
|
sprite/sprite.norman.lua
|
||||||
|
situation/situation.manager.lua
|
||||||
|
situation/situation.drink_coffee.lua
|
||||||
|
decision/decision.manager.lua
|
||||||
|
decision/decision.have_a_coffee.lua
|
||||||
|
decision/decision.go_to_home.lua
|
||||||
|
decision/decision.go_to_toilet.lua
|
||||||
|
decision/decision.go_to_walking_to_office.lua
|
||||||
|
decision/decision.go_to_office.lua
|
||||||
|
decision/decision.go_to_walking_to_home.lua
|
||||||
|
decision/decision.play_button_mash.lua
|
||||||
|
decision/decision.play_rhythm.lua
|
||||||
|
decision/decision.play_ddr.lua
|
||||||
|
map/map.manager.lua
|
||||||
|
map/map.bedroom.lua
|
||||||
|
map/map.street.lua
|
||||||
|
map/map.office.lua
|
||||||
|
screen/screen.manager.lua
|
||||||
|
screen/screen.home.lua
|
||||||
|
screen/screen.toilet.lua
|
||||||
|
screen/screen.walking_to_office.lua
|
||||||
|
screen/screen.office.lua
|
||||||
|
screen/screen.walking_to_home.lua
|
||||||
|
window/window.manager.lua
|
||||||
|
window/window.register.lua
|
||||||
|
window/window.splash.lua
|
||||||
|
window/window.intro.lua
|
||||||
|
window/window.menu.lua
|
||||||
|
window/window.configuration.lua
|
||||||
|
window/window.audiotest.lua
|
||||||
|
window/window.popup.lua
|
||||||
|
window/window.minigame.mash.lua
|
||||||
|
window/window.minigame.rhythm.lua
|
||||||
|
window/window.minigame.ddr.lua
|
||||||
|
window/window.game.lua
|
||||||
|
system/system.main.lua
|
||||||
|
meta/meta.assets.lua
|
||||||
43
inc/audio/audio.manager.lua
Normal file
43
inc/audio/audio.manager.lua
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
--- @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.
|
||||||
|
-- TODO: function name is incomplete, determine the correct room identifier
|
||||||
|
--- @within Audio
|
||||||
|
function Audio.music_play_room_() end
|
||||||
|
--- Plays room work music.
|
||||||
|
--- @within Audio
|
||||||
|
function Audio.music_play_room_work() 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
|
||||||
164
inc/audio/audio.songs.lua
Normal file
164
inc/audio/audio.songs.lua
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
--- @section Songs
|
||||||
|
|
||||||
|
-- DDR Arrow Spawn Patterns
|
||||||
|
-- Each song defines when arrows should spawn, synced to music beats
|
||||||
|
Songs = {
|
||||||
|
-- Example song pattern
|
||||||
|
test_song = {
|
||||||
|
name = "Test Song",
|
||||||
|
bpm = 120, -- Beats per minute (for reference)
|
||||||
|
fps = 60, -- Frames per second (TIC-80 default)
|
||||||
|
end_frame = 570, -- Frame when song ends (last note)
|
||||||
|
-- Arrow spawn pattern
|
||||||
|
-- Each entry defines when (in frames) and which direction arrow spawns
|
||||||
|
-- Formula: frame = (beat / bpm) * 60 * fps
|
||||||
|
-- For 120 BPM: 1 beat = 30 frames, 2 beats = 60 frames, etc.
|
||||||
|
pattern = {
|
||||||
|
-- Beat 1-4 (intro)
|
||||||
|
{frame = 30, dir = "left"},
|
||||||
|
{frame = 60, dir = "down"},
|
||||||
|
{frame = 90, dir = "up"},
|
||||||
|
{frame = 120, dir = "right"},
|
||||||
|
-- Beat 5-8 (faster)
|
||||||
|
{frame = 135, dir = "left"},
|
||||||
|
{frame = 150, dir = "right"},
|
||||||
|
{frame = 165, dir = "left"},
|
||||||
|
{frame = 180, dir = "right"},
|
||||||
|
-- Beat 9-12 (complex pattern)
|
||||||
|
{frame = 210, dir = "left"},
|
||||||
|
{frame = 210, dir = "right"}, -- simultaneous
|
||||||
|
{frame = 240, dir = "up"},
|
||||||
|
{frame = 240, dir = "down"}, -- simultaneous
|
||||||
|
{frame = 270, dir = "left"},
|
||||||
|
{frame = 300, dir = "right"},
|
||||||
|
-- Beat 13-16 (rapid sequence)
|
||||||
|
{frame = 330, dir = "left"},
|
||||||
|
{frame = 345, dir = "down"},
|
||||||
|
{frame = 360, dir = "up"},
|
||||||
|
{frame = 375, dir = "right"},
|
||||||
|
{frame = 390, dir = "left"},
|
||||||
|
{frame = 405, dir = "down"},
|
||||||
|
{frame = 420, dir = "up"},
|
||||||
|
{frame = 435, dir = "right"},
|
||||||
|
-- Beat 17-20 (finale)
|
||||||
|
{frame = 465, dir = "up"},
|
||||||
|
{frame = 465, dir = "down"},
|
||||||
|
{frame = 495, dir = "left"},
|
||||||
|
{frame = 495, dir = "right"},
|
||||||
|
{frame = 525, dir = "up"},
|
||||||
|
{frame = 540, dir = "down"},
|
||||||
|
{frame = 555, dir = "left"},
|
||||||
|
{frame = 570, dir = "right"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test_song_2 = {
|
||||||
|
name = "Test Song 2",
|
||||||
|
bpm = 120, -- Beats per minute (for reference)
|
||||||
|
fps = 60, -- Frames per second (TIC-80 default)
|
||||||
|
end_frame = 570, -- Frame when song ends (last note)
|
||||||
|
-- Arrow spawn pattern
|
||||||
|
-- Each entry defines when (in frames) and which direction arrow spawns
|
||||||
|
-- Formula: frame = (beat / bpm) * 60 * fps
|
||||||
|
-- For 120 BPM: 1 beat = 30 frames, 2 beats = 60 frames, etc.
|
||||||
|
pattern = {
|
||||||
|
-- Beat 1-4 (intro)
|
||||||
|
{frame = 30, dir = "left"},
|
||||||
|
{frame = 60, dir = "down"},
|
||||||
|
{frame = 90, dir = "up"},
|
||||||
|
{frame = 120, dir = "right"},
|
||||||
|
-- Beat 5-8 (faster)
|
||||||
|
{frame = 135, dir = "left"},
|
||||||
|
{frame = 150, dir = "right"},
|
||||||
|
{frame = 165, dir = "left"},
|
||||||
|
{frame = 180, dir = "right"},
|
||||||
|
-- Beat 9-12 (complex pattern)
|
||||||
|
{frame = 210, dir = "left"},
|
||||||
|
{frame = 210, dir = "right"}, -- simultaneous
|
||||||
|
{frame = 240, dir = "up"},
|
||||||
|
{frame = 240, dir = "down"}, -- simultaneous
|
||||||
|
{frame = 270, dir = "left"},
|
||||||
|
{frame = 300, dir = "right"},
|
||||||
|
-- Beat 13-16 (rapid sequence)
|
||||||
|
{frame = 330, dir = "left"},
|
||||||
|
{frame = 345, dir = "down"},
|
||||||
|
{frame = 360, dir = "up"},
|
||||||
|
{frame = 375, dir = "right"},
|
||||||
|
{frame = 390, dir = "left"},
|
||||||
|
{frame = 405, dir = "down"},
|
||||||
|
{frame = 420, dir = "up"},
|
||||||
|
{frame = 435, dir = "right"},
|
||||||
|
-- Beat 17-20 (finale)
|
||||||
|
{frame = 465, dir = "up"},
|
||||||
|
{frame = 465, dir = "down"},
|
||||||
|
{frame = 495, dir = "left"},
|
||||||
|
{frame = 495, dir = "right"},
|
||||||
|
{frame = 525, dir = "up"},
|
||||||
|
{frame = 540, dir = "down"},
|
||||||
|
{frame = 555, dir = "left"},
|
||||||
|
{frame = 570, dir = "right"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
-- Random mode (no predefined pattern, spawns randomly)
|
||||||
|
random = {
|
||||||
|
name = "Random Mode",
|
||||||
|
bpm = 0, -- Not applicable for random mode
|
||||||
|
fps = 60,
|
||||||
|
end_frame = nil, -- No end frame for random mode
|
||||||
|
pattern = {} -- Empty, will spawn randomly in game
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Converts beats to frames.
|
||||||
|
--- @within Songs
|
||||||
|
--- @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)
|
||||||
|
fps = fps or 60
|
||||||
|
local seconds_per_beat = 60 / bpm
|
||||||
|
local frames_per_beat = seconds_per_beat * fps
|
||||||
|
return math.floor(beat * frames_per_beat)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Converts beat notation to frame pattern.
|
||||||
|
--- @within Songs
|
||||||
|
--- @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)
|
||||||
|
fps = fps or 60
|
||||||
|
local pattern = {}
|
||||||
|
for _, beat_data in ipairs(beats) do
|
||||||
|
local beat = beat_data[1]
|
||||||
|
local dir = beat_data[2]
|
||||||
|
table.insert(pattern, {
|
||||||
|
frame = frame_from_beat(beat, bpm, fps),
|
||||||
|
dir = dir
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return pattern
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Example of creating a song using beat notation:
|
||||||
|
--[[
|
||||||
|
Songs.custom_song = {
|
||||||
|
name = "Custom Song",
|
||||||
|
bpm = 130,
|
||||||
|
fps = 60,
|
||||||
|
pattern = beats_to_pattern({
|
||||||
|
{1, "left"},
|
||||||
|
{2, "down"},
|
||||||
|
{3, "up"},
|
||||||
|
{4, "right"},
|
||||||
|
{4.5, "left"},
|
||||||
|
{5, "right"}
|
||||||
|
}, 130)
|
||||||
|
}
|
||||||
|
]]
|
||||||
7
inc/decision/decision.go_to_home.lua
Normal file
7
inc/decision/decision.go_to_home.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "go_to_home",
|
||||||
|
label = "Go to Home",
|
||||||
|
handle = function()
|
||||||
|
Util.go_to_screen_by_id("home")
|
||||||
|
end,
|
||||||
|
})
|
||||||
7
inc/decision/decision.go_to_office.lua
Normal file
7
inc/decision/decision.go_to_office.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "go_to_office",
|
||||||
|
label = "Go to Office",
|
||||||
|
handle = function()
|
||||||
|
Util.go_to_screen_by_id("office")
|
||||||
|
end,
|
||||||
|
})
|
||||||
7
inc/decision/decision.go_to_toilet.lua
Normal file
7
inc/decision/decision.go_to_toilet.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "go_to_toilet",
|
||||||
|
label = "Go to Toilet",
|
||||||
|
handle = function()
|
||||||
|
Util.go_to_screen_by_id("toilet")
|
||||||
|
end,
|
||||||
|
})
|
||||||
7
inc/decision/decision.go_to_walking_to_home.lua
Normal file
7
inc/decision/decision.go_to_walking_to_home.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "go_to_walking_to_home",
|
||||||
|
label = "Walking to home",
|
||||||
|
handle = function()
|
||||||
|
Util.go_to_screen_by_id("walking_to_home")
|
||||||
|
end,
|
||||||
|
})
|
||||||
7
inc/decision/decision.go_to_walking_to_office.lua
Normal file
7
inc/decision/decision.go_to_walking_to_office.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "go_to_walking_to_office",
|
||||||
|
label = "Walking to office",
|
||||||
|
handle = function()
|
||||||
|
Util.go_to_screen_by_id("walking_to_office")
|
||||||
|
end,
|
||||||
|
})
|
||||||
8
inc/decision/decision.have_a_coffee.lua
Normal file
8
inc/decision/decision.have_a_coffee.lua
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "have_a_coffee",
|
||||||
|
label = "Have a Coffee",
|
||||||
|
handle = function()
|
||||||
|
local new_situation_id = Situation.apply("drink_coffee", Context.game.current_screen)
|
||||||
|
Context.game.current_situation = new_situation_id
|
||||||
|
end,
|
||||||
|
})
|
||||||
100
inc/decision/decision.manager.lua
Normal file
100
inc/decision/decision.manager.lua
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
--- @section Decision
|
||||||
|
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 Display text for the decision.
|
||||||
|
--- @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)
|
||||||
|
if not decision or not decision.id then
|
||||||
|
PopupWindow.show({"Error: Invalid decision object registered (missing id)!"})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not decision.label then
|
||||||
|
PopupWindow.show({"Error: Invalid decision object registered (missing label)!"})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not decision.condition then
|
||||||
|
decision.condition = function() return true end
|
||||||
|
end
|
||||||
|
if not decision.handle then
|
||||||
|
decision.handle = function() end
|
||||||
|
end
|
||||||
|
if _decisions[decision.id] then
|
||||||
|
trace("Warning: Overwriting decision with id: " .. decision.id)
|
||||||
|
end
|
||||||
|
_decisions[decision.id] = decision
|
||||||
|
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]
|
||||||
|
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()
|
||||||
|
return _decisions
|
||||||
|
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 decision.condition() then
|
||||||
|
table.insert(available, decision)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return available
|
||||||
|
end
|
||||||
12
inc/decision/decision.play_button_mash.lua
Normal file
12
inc/decision/decision.play_button_mash.lua
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "play_button_mash",
|
||||||
|
label = "Play Button Mash",
|
||||||
|
handle = function()
|
||||||
|
Meter.hide()
|
||||||
|
MinigameButtonMashWindow.start("game", {
|
||||||
|
focus_center_x = Config.screen.width / 2,
|
||||||
|
focus_center_y = Config.screen.height / 2,
|
||||||
|
focus_initial_radius = 0,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
5
inc/decision/decision.play_ddr.lua
Normal file
5
inc/decision/decision.play_ddr.lua
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "play_ddr",
|
||||||
|
label = "Play DDR (Random)",
|
||||||
|
handle = function() Meter.hide() MinigameDDRWindow.start("game", nil) end,
|
||||||
|
})
|
||||||
12
inc/decision/decision.play_rhythm.lua
Normal file
12
inc/decision/decision.play_rhythm.lua
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Decision.register({
|
||||||
|
id = "play_rhythm",
|
||||||
|
label = "Play Rhythm Game",
|
||||||
|
handle = function()
|
||||||
|
Meter.hide()
|
||||||
|
MinigameRhythmWindow.start("game", {
|
||||||
|
focus_center_x = Config.screen.width / 2,
|
||||||
|
focus_center_y = Config.screen.height / 2,
|
||||||
|
focus_initial_radius = 0,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
function Item.use()
|
|
||||||
print("Used item: " .. Context.dialog.active_entity.name)
|
|
||||||
GameWindow.set_state(WINDOW_INVENTORY)
|
|
||||||
end
|
|
||||||
function Item.look_at()
|
|
||||||
PopupWindow.show_description_dialog(Context.dialog.active_entity, Context.dialog.active_entity.desc)
|
|
||||||
end
|
|
||||||
function Item.put_away()
|
|
||||||
-- Add item to inventory
|
|
||||||
table.insert(Context.inventory, Context.dialog.active_entity)
|
|
||||||
|
|
||||||
-- Remove item from screen
|
|
||||||
local currentScreenData = Context.screens[Context.current_screen]
|
|
||||||
for i, item in ipairs(currentScreenData.items) do
|
|
||||||
if item == Context.dialog.active_entity then
|
|
||||||
table.remove(currentScreenData.items, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Go back to game
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
|
||||||
function Item.go_back_from_item_dialog()
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Item.go_back_from_inventory_action()
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Item.drop()
|
|
||||||
-- Remove item from inventory
|
|
||||||
for i, item in ipairs(Context.inventory) do
|
|
||||||
if item == Context.dialog.active_entity then
|
|
||||||
table.remove(Context.inventory, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Add item to screen
|
|
||||||
local currentScreenData = Context.screens[Context.current_screen]
|
|
||||||
Context.dialog.active_entity.x = Context.player.x
|
|
||||||
Context.dialog.active_entity.y = Context.player.y
|
|
||||||
table.insert(currentScreenData.items, Context.dialog.active_entity)
|
|
||||||
|
|
||||||
-- Go back to inventory
|
|
||||||
GameWindow.set_state(WINDOW_INVENTORY)
|
|
||||||
end
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
function NPC.talk_to()
|
|
||||||
local npc = Context.dialog.active_entity
|
|
||||||
if npc.dialog and npc.dialog.start then
|
|
||||||
PopupWindow.set_dialog_node("start")
|
|
||||||
else
|
|
||||||
-- if no dialog, go back
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
function NPC.fight() end
|
|
||||||
function NPC.go_back()
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
function Player.draw()
|
|
||||||
spr(Context.player.sprite_id, Context.player.x, Context.player.y, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Player.update()
|
|
||||||
-- Handle input
|
|
||||||
if Input.left() then
|
|
||||||
Context.player.vx = -Config.physics.move_speed
|
|
||||||
elseif Input.right() then
|
|
||||||
Context.player.vx = Config.physics.move_speed
|
|
||||||
else
|
|
||||||
Context.player.vx = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if Input.player_jump() and Context.player.jumps < Config.physics.max_jumps then
|
|
||||||
Context.player.vy = Config.physics.jump_power
|
|
||||||
Context.player.jumps = Context.player.jumps + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Update player position
|
|
||||||
Context.player.x = Context.player.x + Context.player.vx
|
|
||||||
Context.player.y = Context.player.y + Context.player.vy
|
|
||||||
|
|
||||||
-- Screen transition
|
|
||||||
if Context.player.x > Config.screen.width - Context.player.w then
|
|
||||||
if Context.current_screen < #Context.screens then
|
|
||||||
Context.current_screen = Context.current_screen + 1
|
|
||||||
Context.player.x = 0
|
|
||||||
else
|
|
||||||
Context.player.x = Config.screen.width - Context.player.w
|
|
||||||
end
|
|
||||||
elseif Context.player.x < 0 then
|
|
||||||
if Context.current_screen > 1 then
|
|
||||||
Context.current_screen = Context.current_screen - 1
|
|
||||||
Context.player.x = Config.screen.width - Context.player.w
|
|
||||||
else
|
|
||||||
Context.player.x = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Apply gravity
|
|
||||||
Context.player.vy = Context.player.vy + Config.physics.gravity
|
|
||||||
|
|
||||||
local currentScreenData = Context.screens[Context.current_screen]
|
|
||||||
-- Collision detection with platforms
|
|
||||||
for _, p in ipairs(currentScreenData.platforms) do
|
|
||||||
if Context.player.vy > 0 and Context.player.y + Context.player.h >= p.y and Context.player.y + Context.player.h <= p.y + p.h and Context.player.x + Context.player.w > p.x and Context.player.x < p.x + p.w then
|
|
||||||
Context.player.y = p.y - Context.player.h
|
|
||||||
Context.player.vy = 0
|
|
||||||
Context.player.jumps = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Collision detection with ground
|
|
||||||
if Context.player.y + Context.player.h > Context.ground.y then
|
|
||||||
Context.player.y = Context.ground.y - Context.player.h
|
|
||||||
Context.player.vy = 0
|
|
||||||
Context.player.jumps = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Entity interaction
|
|
||||||
if Input.player_interact() then
|
|
||||||
local interaction_found = false
|
|
||||||
-- NPC interaction
|
|
||||||
for _, npc in ipairs(currentScreenData.npcs) do
|
|
||||||
if math.abs(Context.player.x - npc.x) < Config.physics.interaction_radius_npc and math.abs(Context.player.y - npc.y) < Config.physics.interaction_radius_npc then
|
|
||||||
PopupWindow.show_menu_dialog(npc, {
|
|
||||||
{label = "Talk to", action = NPC.talk_to},
|
|
||||||
{label = "Fight", action = NPC.fight},
|
|
||||||
{label = "Go back", action = NPC.go_back}
|
|
||||||
}, WINDOW_POPUP)
|
|
||||||
interaction_found = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not interaction_found then
|
|
||||||
-- Item interaction
|
|
||||||
for _, item in ipairs(currentScreenData.items) do
|
|
||||||
if math.abs(Context.player.x - item.x) < Config.physics.interaction_radius_item and math.abs(Context.player.y - item.y) < Config.physics.interaction_radius_item then
|
|
||||||
PopupWindow.show_menu_dialog(item, {
|
|
||||||
{label = "Use", action = Item.use},
|
|
||||||
{label = "Look at", action = Item.look_at},
|
|
||||||
{label = "Put away", action = Item.put_away},
|
|
||||||
{label = "Go back", action = Item.go_back_from_item_dialog}
|
|
||||||
}, WINDOW_POPUP)
|
|
||||||
interaction_found = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If no interaction happened, open inventory
|
|
||||||
if not interaction_found then
|
|
||||||
GameWindow.set_state(WINDOW_INVENTORY)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,32 +1,59 @@
|
|||||||
local Config = {
|
Config = {}
|
||||||
screen = {
|
|
||||||
width = 240,
|
--- Return initial data for Config
|
||||||
height = 136
|
--- @within Config
|
||||||
},
|
function Config.initial_data()
|
||||||
colors = {
|
return {
|
||||||
black = 0,
|
screen = {
|
||||||
light_grey = 13,
|
width = 240,
|
||||||
dark_grey = 14,
|
height = 136
|
||||||
green = 6,
|
},
|
||||||
npc = 8,
|
colors = {
|
||||||
item = 12 -- yellow
|
black = 2,
|
||||||
},
|
light_grey = 13,
|
||||||
player = {
|
dark_grey = 14,
|
||||||
w = 8,
|
red = 0,
|
||||||
h = 8,
|
light_blue = 7,
|
||||||
start_x = 120,
|
blue = 9,
|
||||||
start_y = 128,
|
white = 12,
|
||||||
sprite_id = 1
|
item = 12,
|
||||||
},
|
meter_bg = 12
|
||||||
physics = {
|
},
|
||||||
gravity = 0.5,
|
timing = {
|
||||||
jump_power = -5,
|
splash_duration = 120
|
||||||
move_speed = 1.5,
|
}
|
||||||
max_jumps = 2,
|
|
||||||
interaction_radius_npc = 12,
|
|
||||||
interaction_radius_item = 8
|
|
||||||
},
|
|
||||||
timing = {
|
|
||||||
splash_duration = 120
|
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
|
|
||||||
|
--- Restores default configuration settings.
|
||||||
|
--- @within Config
|
||||||
|
function Config.reset()
|
||||||
|
local initial = Config.initial_data()
|
||||||
|
Config.screen = initial.screen
|
||||||
|
Config.colors = initial.colors
|
||||||
|
Config.timing = initial.timing
|
||||||
|
end
|
||||||
|
|
||||||
|
local CONFIG_SAVE_BANK = 7
|
||||||
|
local CONFIG_MAGIC_VALUE_ADDRESS = 2
|
||||||
|
local CONFIG_SPLASH_DURATION_ADDRESS = 3
|
||||||
|
local CONFIG_MAGIC_VALUE = 0xDE
|
||||||
|
|
||||||
|
--- Saves the current configuration.
|
||||||
|
--- @within Config
|
||||||
|
function Config.save()
|
||||||
|
mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK)
|
||||||
|
mset(Config.timing.splash_duration, CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Loads saved configuration.
|
||||||
|
--- @within Config
|
||||||
|
function Config.load()
|
||||||
|
if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then
|
||||||
|
Config.timing.splash_duration = mget(CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
|
||||||
|
else
|
||||||
|
Config.reset()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Config.load()
|
||||||
|
|||||||
@@ -1,432 +1,86 @@
|
|||||||
local Context = {
|
local SAVE_GAME_BANK = 6
|
||||||
active_window = WINDOW_SPLASH,
|
local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
|
||||||
inventory = {},
|
local SAVE_GAME_MAGIC_VALUE = 0xCA
|
||||||
intro = {
|
|
||||||
y = Config.screen.height,
|
--- Global game context.
|
||||||
speed = 0.5,
|
--- @section Context
|
||||||
text = "Mr. Anderson is an average\nprogrammer. His daily life\nrevolves around debugging,\npull requests, and end-of-sprint\nmeetings, all while secretly\ndreaming of being destined\nfor something more."
|
Context = {}
|
||||||
},
|
|
||||||
current_screen = 1,
|
--- Gets initial data for Context.
|
||||||
splash_timer = Config.timing.splash_duration,
|
--- @within Context
|
||||||
dialog = {
|
--- @return result table Initial context data or nil. </br>
|
||||||
text = "",
|
--- Fields: </br>
|
||||||
menu_items = {},
|
--- * current_menu_item (number) Index of the currently selected menu item.<br/>
|
||||||
selected_menu_item = 1,
|
--- * splash_timer (number) Remaining frames for the splash screen timer.<br/>
|
||||||
active_entity = nil,
|
--- * popup (table) Popup window state. Contains: `show` (boolean) whether popup is visible, `content` (table) array of strings to display.<br/>
|
||||||
showing_description = false,
|
--- * game_in_progress (boolean) Whether a game is currently active.<br/>
|
||||||
current_node_key = nil
|
--- * 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/>
|
||||||
player = {
|
--- * minigame_rhythm (table) Rhythm minigame state (see Minigame.get_default_rhythm).<br/>
|
||||||
x = Config.player.start_x,
|
--- * meters (table) Meter values (see Meter.get_initial).<br/>
|
||||||
y = Config.player.start_y,
|
--- * stat_screen_active (boolean) Whether the stat screen overlay is currently shown.<br/>
|
||||||
w = Config.player.w,
|
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.<br/>
|
||||||
h = Config.player.h,
|
function Context.initial_data()
|
||||||
vx = 0,
|
return {
|
||||||
vy = 0,
|
current_menu_item = 1,
|
||||||
jumps = 0,
|
splash_timer = Config.timing.splash_duration,
|
||||||
sprite_id = Config.player.sprite_id
|
popup = {
|
||||||
},
|
show = false,
|
||||||
ground = {
|
content = {}
|
||||||
x = 0,
|
|
||||||
y = Config.screen.height,
|
|
||||||
w = Config.screen.width,
|
|
||||||
h = 8
|
|
||||||
},
|
|
||||||
menu_items = {},
|
|
||||||
selected_menu_item = 1,
|
|
||||||
selected_inventory_item = 1,
|
|
||||||
-- Screen data
|
|
||||||
screens = {
|
|
||||||
{
|
|
||||||
-- Screen 1
|
|
||||||
name = "Screen 1",
|
|
||||||
platforms = {
|
|
||||||
{
|
|
||||||
x = 80,
|
|
||||||
y = 110,
|
|
||||||
w = 40,
|
|
||||||
h = 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 160,
|
|
||||||
y = 90,
|
|
||||||
w = 40,
|
|
||||||
h = 8
|
|
||||||
}
|
|
||||||
},
|
|
||||||
npcs = {
|
|
||||||
{
|
|
||||||
x = 180,
|
|
||||||
y = 82,
|
|
||||||
name = "Trinity",
|
|
||||||
sprite_id = 2,
|
|
||||||
dialog = {
|
|
||||||
start = {
|
|
||||||
text = "Hello, Neo.",
|
|
||||||
options = {
|
|
||||||
{label = "Who are you?", next_node = "who_are_you"},
|
|
||||||
{label = "My name is not Neo.", next_node = "not_neo"},
|
|
||||||
{label = "...", next_node = "silent"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
who_are_you = {
|
|
||||||
text = "I am Trinity. I've been looking for you.",
|
|
||||||
options = {
|
|
||||||
{label = "The famous hacker?", next_node = "famous_hacker"},
|
|
||||||
{label = "Why me?", next_node = "why_me"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
not_neo = {
|
|
||||||
text = "I know. But you will be.",
|
|
||||||
options = {
|
|
||||||
{label = "What are you talking about?", next_node = "who_are_you"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
silent = {
|
|
||||||
text = "You're not much of a talker, are you?",
|
|
||||||
options = {
|
|
||||||
{label = "I guess not.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
famous_hacker = {
|
|
||||||
text = "The one and only.",
|
|
||||||
options = {
|
|
||||||
{label = "Wow.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
why_me = {
|
|
||||||
text = "Morpheus believes you are The One.",
|
|
||||||
options = {
|
|
||||||
{label = "The One?", next_node = "the_one"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
the_one = {
|
|
||||||
text = "The one who will save us all.",
|
|
||||||
options = {
|
|
||||||
{label = "I'm just a programmer.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dialog_end = {
|
|
||||||
text = "We'll talk later.",
|
|
||||||
options = {} -- No options, ends conversation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 90,
|
|
||||||
y = 102,
|
|
||||||
name = "Oracle",
|
|
||||||
sprite_id = 3,
|
|
||||||
dialog = {
|
|
||||||
start = {
|
|
||||||
text = "I know what you're thinking. 'Am I in the right place?'",
|
|
||||||
options = {
|
|
||||||
{label = "Who are you?", next_node = "who_are_you"},
|
|
||||||
{label = "I guess I am.", next_node = "you_are"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
who_are_you = {
|
|
||||||
text = "I'm the Oracle. And you're right on time. Want a cookie?",
|
|
||||||
options = {
|
|
||||||
{label = "Sure.", next_node = "cookie"},
|
|
||||||
{label = "No, thank you.", next_node = "no_cookie"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
you_are = {
|
|
||||||
text = "Of course you are. Sooner or later, everyone comes to see me. Want a cookie?",
|
|
||||||
options = {
|
|
||||||
{label = "Yes, please.", next_node = "cookie"},
|
|
||||||
{label = "I'm good.", next_node = "no_cookie"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cookie = {
|
|
||||||
text = "Here you go. Now, what's really on your mind?",
|
|
||||||
options = {
|
|
||||||
{label = "Am I The One?", next_node = "the_one"},
|
|
||||||
{label = "What is the Matrix?", next_node = "the_matrix"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
no_cookie = {
|
|
||||||
text = "Suit yourself. Now, what's troubling you?",
|
|
||||||
options = {
|
|
||||||
{label = "Am I The One?", next_node = "the_one"},
|
|
||||||
{label = "What is the Matrix?", next_node = "the_matrix"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
the_one = {
|
|
||||||
text = "Being The One is just like being in love. No one can tell you you're in love, you just know it. Through and through. Balls to bones.",
|
|
||||||
options = {
|
|
||||||
{label = "So I'm not?", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
the_matrix = {
|
|
||||||
text = "The Matrix is a system, Neo. That system is our enemy. But when you're inside, you look around, what do you see? The very minds of the people we are trying to save.",
|
|
||||||
options = {
|
|
||||||
{label = "I see.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dialog_end = {
|
|
||||||
text = "You have to understand, most of these people are not ready to be unplugged.",
|
|
||||||
options = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
items = {
|
|
||||||
{
|
|
||||||
x = 100,
|
|
||||||
y = 128,
|
|
||||||
w = 8,
|
|
||||||
h = 8,
|
|
||||||
name = "Key",
|
|
||||||
sprite_id = 4,
|
|
||||||
desc = "A rusty old key. It might open something."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
game_in_progress = false,
|
||||||
-- Screen 2
|
stat_screen_active = false,
|
||||||
name = "Screen 2",
|
minigame_ddr = Minigame.get_default_ddr(),
|
||||||
platforms = {
|
minigame_button_mash = Minigame.get_default_button_mash(),
|
||||||
{
|
minigame_rhythm = Minigame.get_default_rhythm(),
|
||||||
x = 30,
|
meters = Meter.get_initial(),
|
||||||
y = 100,
|
game = {
|
||||||
w = 50,
|
current_screen = "home",
|
||||||
h = 8
|
current_situation = nil,
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 100,
|
|
||||||
y = 80,
|
|
||||||
w = 50,
|
|
||||||
h = 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 170,
|
|
||||||
y = 60,
|
|
||||||
w = 50,
|
|
||||||
h = 8
|
|
||||||
}
|
|
||||||
},
|
|
||||||
npcs = {
|
|
||||||
{
|
|
||||||
x = 120,
|
|
||||||
y = 72,
|
|
||||||
name = "Morpheus",
|
|
||||||
sprite_id = 5,
|
|
||||||
dialog = {
|
|
||||||
start = {
|
|
||||||
text = "At last. Welcome, Neo. As you no doubt have guessed, I am Morpheus.",
|
|
||||||
options = {
|
|
||||||
{label = "It's an honor to meet you.", next_node = "honor"},
|
|
||||||
{label = "You've been looking for me.", next_node = "looking_for_me"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
honor = {
|
|
||||||
text = "No, the honor is mine.",
|
|
||||||
options = {
|
|
||||||
{label = "What is this place?", next_node = "what_is_this_place"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
looking_for_me = {
|
|
||||||
text = "I have. For some time.",
|
|
||||||
options = {
|
|
||||||
{label = "What is this place?", next_node = "what_is_this_place"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
what_is_this_place = {
|
|
||||||
text = "This is the construct. It's our loading program. We can load anything from clothing, to equipment, weapons, training simulations. Anything we need.",
|
|
||||||
options = {
|
|
||||||
{label = "Right.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dialog_end = {
|
|
||||||
text = "I've been waiting for you, Neo. We have much to discuss.",
|
|
||||||
options = {} -- Ends conversation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 40,
|
|
||||||
y = 92,
|
|
||||||
name = "Tank",
|
|
||||||
sprite_id = 6,
|
|
||||||
dialog = {
|
|
||||||
start = {
|
|
||||||
text = "Hey, Neo! Welcome to the construct. I'm Tank.",
|
|
||||||
options = {
|
|
||||||
{label = "Good to meet you.", next_node = "good_to_meet_you"},
|
|
||||||
{label = "This place is incredible.", next_node = "incredible"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
good_to_meet_you = {
|
|
||||||
text = "You too! We've been waiting for you. Need anything? Training? Weapons?",
|
|
||||||
options = {
|
|
||||||
{label = "Training?", next_node = "training"},
|
|
||||||
{label = "I'm good for now.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
incredible = {
|
|
||||||
text = "Isn't it? The boss's design. We can load anything we need. What do you want to learn?",
|
|
||||||
options = {
|
|
||||||
{label = "Show me.", next_node = "training"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
training = {
|
|
||||||
text = "Jujitsu? Kung Fu? How about... all of them?",
|
|
||||||
options = {
|
|
||||||
{label = "All of them.", next_node = "all_of_them"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
all_of_them = {
|
|
||||||
text = "Operator, load the combat training program.",
|
|
||||||
options = {
|
|
||||||
{label = "...", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dialog_end = {
|
|
||||||
text = "Just holler if you need anything. Anything at all.",
|
|
||||||
options = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
items = {
|
|
||||||
{
|
|
||||||
x = 180,
|
|
||||||
y = 52,
|
|
||||||
w = 8,
|
|
||||||
h = 8,
|
|
||||||
name = "Potion",
|
|
||||||
sprite_id = 7,
|
|
||||||
desc = "A glowing red potion. It looks potent."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
-- Screen 3
|
|
||||||
name = "Screen 3",
|
|
||||||
platforms = {
|
|
||||||
{
|
|
||||||
x = 50,
|
|
||||||
y = 110,
|
|
||||||
w = 30,
|
|
||||||
h = 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 100,
|
|
||||||
y = 90,
|
|
||||||
w = 30,
|
|
||||||
h = 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 150,
|
|
||||||
y = 70,
|
|
||||||
w = 30,
|
|
||||||
h = 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 200,
|
|
||||||
y = 50,
|
|
||||||
w = 30,
|
|
||||||
h = 8
|
|
||||||
}
|
|
||||||
},
|
|
||||||
npcs = {
|
|
||||||
{
|
|
||||||
x = 210,
|
|
||||||
y = 42,
|
|
||||||
name = "Agent Smith",
|
|
||||||
sprite_id = 8,
|
|
||||||
dialog = {
|
|
||||||
start = {
|
|
||||||
text = "Mr. Anderson. We've been expecting you.",
|
|
||||||
options = {
|
|
||||||
{label = "My name is Neo.", next_node = "name_is_neo"},
|
|
||||||
{label = "...", next_node = "silent"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name_is_neo = {
|
|
||||||
text = "Whatever you say. You're here for a reason.",
|
|
||||||
options = {
|
|
||||||
{label = "What reason?", next_node = "what_reason"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
silent = {
|
|
||||||
text = "The silent type. It doesn't matter. You are an anomaly.",
|
|
||||||
options = {
|
|
||||||
{label = "What do you want?", next_node = "what_reason"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
what_reason = {
|
|
||||||
text = "To be deleted. The system has no place for your kind.",
|
|
||||||
options = {
|
|
||||||
{label = "I won't let you.", next_node = "wont_let_you"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wont_let_you = {
|
|
||||||
text = "You hear that, Mr. Anderson? That is the sound of inevitability.",
|
|
||||||
options = {
|
|
||||||
{label = "...", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dialog_end = {
|
|
||||||
text = "It is purpose that created us. Purpose that connects us. Purpose that pulls us. That guides us. That drives us. It is purpose that defines. Purpose that binds us.",
|
|
||||||
options = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x = 160,
|
|
||||||
y = 62,
|
|
||||||
name = "Cypher",
|
|
||||||
sprite_id = 9,
|
|
||||||
dialog = {
|
|
||||||
start = {
|
|
||||||
text = "Well, well. The new messiah. Welcome to the real world.",
|
|
||||||
options = {
|
|
||||||
{label = "You don't seem happy.", next_node = "not_happy"},
|
|
||||||
{label = "...", next_node = "silent"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
not_happy = {
|
|
||||||
text = "Happy? Ignorance is bliss, Neo. We've been fighting this war for years. For what?",
|
|
||||||
options = {
|
|
||||||
{label = "For freedom.", next_node = "freedom"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
silent = {
|
|
||||||
text = "Not a talker, huh? Smart. Less to regret later. Want a drink?",
|
|
||||||
options = {
|
|
||||||
{label = "Sure.", next_node = "drink"},
|
|
||||||
{label = "No thanks.", next_node = "no_drink"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
drink = {
|
|
||||||
text = "Good stuff. The little things you miss, you know? Like a good steak.",
|
|
||||||
options = {
|
|
||||||
{label = "I guess.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
no_drink = {
|
|
||||||
text = "Your loss. More for me.",
|
|
||||||
options = {
|
|
||||||
{label = "...", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
freedom = {
|
|
||||||
text = "Freedom... right. If Morpheus told you you could fly, would you believe him?",
|
|
||||||
options = {
|
|
||||||
{label = "He's our leader.", next_node = "dialog_end"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dialog_end = {
|
|
||||||
text = "Just be careful who you trust.",
|
|
||||||
options = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
items = {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
end
|
||||||
|
|
||||||
|
--- Resets game context to initial state.
|
||||||
|
--- @within Context
|
||||||
|
function Context.reset()
|
||||||
|
local initial_data = Context.initial_data()
|
||||||
|
for k in pairs(Context) do
|
||||||
|
if type(Context[k]) ~= "function" then
|
||||||
|
Context[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in pairs(initial_data) do
|
||||||
|
Context[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Starts a new game.
|
||||||
|
--- @within Context
|
||||||
|
function Context.new_game()
|
||||||
|
Context.reset()
|
||||||
|
Context.game_in_progress = true
|
||||||
|
MenuWindow.refresh_menu_items()
|
||||||
|
Screen.get_by_id(Context.game.current_screen).init()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Saves the current game state.
|
||||||
|
--- @within Context
|
||||||
|
function Context.save_game()
|
||||||
|
if not Context.game_in_progress then return end
|
||||||
|
mset(SAVE_GAME_MAGIC_VALUE, SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Loads a saved game state.
|
||||||
|
--- @within Context
|
||||||
|
function Context.load_game()
|
||||||
|
if mget(SAVE_GAME_MAGIC_VALUE_ADDRESS, SAVE_GAME_BANK) ~= SAVE_GAME_MAGIC_VALUE then
|
||||||
|
Context.new_game()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
Context.reset()
|
||||||
|
Context.game_in_progress = true
|
||||||
|
MenuWindow.refresh_menu_items()
|
||||||
|
Screen.get_by_id(Context.game.current_screen).init()
|
||||||
|
end
|
||||||
|
|||||||
138
inc/init/init.meter.lua
Normal file
138
inc/init/init.meter.lua
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
--- @section Meter
|
||||||
|
local METER_MAX = 1000
|
||||||
|
local METER_DEFAULT = 500
|
||||||
|
local METER_GAIN_PER_CHORE = 100
|
||||||
|
local COMBO_BASE_BONUS = 0.02
|
||||||
|
local COMBO_MAX_BONUS = 0.16
|
||||||
|
local COMBO_TIMEOUT_FRAMES = 600
|
||||||
|
|
||||||
|
-- 1800 frames = 30 seconds (1800 ÷ 60 = 30)
|
||||||
|
local meter_timer_duration = 1800
|
||||||
|
local meter_timer_decay_per_revolution = 20
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
--- Sets the number of frames for one full timer revolution.
|
||||||
|
--- @within Meter
|
||||||
|
--- @param frames number Frames per revolution (controls degradation speed).
|
||||||
|
function Meter.set_timer_duration(frames)
|
||||||
|
meter_timer_duration = frames
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the degradation amount applied to all meters per revolution.
|
||||||
|
--- @within Meter
|
||||||
|
--- @param amount number Amount to subtract from each meter per revolution.
|
||||||
|
function Meter.set_timer_decay(amount)
|
||||||
|
meter_timer_decay_per_revolution = amount
|
||||||
|
end
|
||||||
|
|
||||||
|
--- 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.<br/>
|
||||||
|
--- * timer_progress (number) Clock timer revolution progress (0 to 1).
|
||||||
|
function Meter.get_initial()
|
||||||
|
return {
|
||||||
|
ism = METER_DEFAULT,
|
||||||
|
wpm = METER_DEFAULT,
|
||||||
|
bm = METER_DEFAULT,
|
||||||
|
combo = 0,
|
||||||
|
combo_timer = 0,
|
||||||
|
hidden = false,
|
||||||
|
timer_progress = 0,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
--- 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
|
||||||
|
m.timer_progress = m.timer_progress + (1 / meter_timer_duration)
|
||||||
|
if m.timer_progress >= 1 then
|
||||||
|
m.timer_progress = m.timer_progress - 1
|
||||||
|
m.ism = math.max(0, m.ism - meter_timer_decay_per_revolution)
|
||||||
|
m.wpm = math.max(0, m.wpm - meter_timer_decay_per_revolution)
|
||||||
|
m.bm = math.max(0, m.bm - meter_timer_decay_per_revolution)
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Gets the timer decay as a percentage of the max meter value.
|
||||||
|
--- @within Meter
|
||||||
|
--- @return number The decay percentage per revolution (e.g. 2 means -2%).
|
||||||
|
function Meter.get_timer_decay_percentage()
|
||||||
|
return math.floor(meter_timer_decay_per_revolution / METER_MAX * 100)
|
||||||
|
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
|
||||||
267
inc/init/init.minigame.lua
Normal file
267
inc/init/init.minigame.lua
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
-- Manages minigame configurations and initial states.
|
||||||
|
--- @section Minigame
|
||||||
|
|
||||||
|
--- Applies parameters to defaults
|
||||||
|
--- @within Minigame
|
||||||
|
--- @param defaults table The default configuration table.
|
||||||
|
--- @param params table The parameters to apply.
|
||||||
|
--- @return table The updated configuration table.
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Gets default DDR minigame configuration.
|
||||||
|
--- @within Minigame
|
||||||
|
--- @return result table The default DDR minigame configuration. </br>
|
||||||
|
--- Fields: </br>
|
||||||
|
--- * bar_fill (number) Current fill level of the progress bar.<br/>
|
||||||
|
--- * max_fill (number) Maximum fill value to win.<br/>
|
||||||
|
--- * fill_per_hit (number) Fill gained per successful hit.<br/>
|
||||||
|
--- * miss_penalty (number) Fill lost per miss.<br/>
|
||||||
|
--- * bar_x (number) Progress bar X position.<br/>
|
||||||
|
--- * bar_y (number) Progress bar Y position.<br/>
|
||||||
|
--- * bar_width (number) Progress bar width.<br/>
|
||||||
|
--- * bar_height (number) Progress bar height.<br/>
|
||||||
|
--- * arrow_size (number) Size of arrow sprites.<br/>
|
||||||
|
--- * arrow_spawn_timer (number) Timer for arrow spawning.<br/>
|
||||||
|
--- * arrow_spawn_interval (number) Frames between arrow spawns.<br/>
|
||||||
|
--- * arrow_fall_speed (number) Speed of falling arrows.<br/>
|
||||||
|
--- * arrows (table) Array of active arrow objects.<br/>
|
||||||
|
--- * target_y (number) Y position of the target line.<br/>
|
||||||
|
--- * target_arrows (table) Array of target arrow positions. Each entry has: `dir` (string) arrow direction, `x` (number) X position.<br/>
|
||||||
|
--- * hit_threshold (number) Pixel distance for a valid hit.<br/>
|
||||||
|
--- * button_pressed_timers (table) Per-button press animation timers.<br/>
|
||||||
|
--- * button_press_duration (number) Duration of button press animation.<br/>
|
||||||
|
--- * input_cooldowns (table) Per-direction cooldown timers (left, down, up, right).<br/>
|
||||||
|
--- * input_cooldown_duration (number) Frames of input cooldown.<br/>
|
||||||
|
--- * frame_counter (number) Global frame counter.<br/>
|
||||||
|
--- * current_song (table) Currently playing song data.<br/>
|
||||||
|
--- * pattern_index (number) Current index in song pattern.<br/>
|
||||||
|
--- * use_pattern (boolean) Whether to use song pattern for spawning.<br/>
|
||||||
|
--- * return_window (string) Window ID to return to after minigame.
|
||||||
|
function Minigame.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
|
||||||
|
|
||||||
|
--- Gets default button mash minigame configuration.
|
||||||
|
--- @within Minigame
|
||||||
|
--- @return result table The default button mash minigame configuration. </br>
|
||||||
|
--- Fields: </br>
|
||||||
|
--- * bar_fill (number) Current fill level of the progress bar.<br/>
|
||||||
|
--- * max_fill (number) Maximum fill value to win.<br/>
|
||||||
|
--- * fill_per_press (number) Fill gained per button press.<br/>
|
||||||
|
--- * base_degradation (number) Base rate of bar degradation per frame.<br/>
|
||||||
|
--- * degradation_multiplier (number) Multiplier for degradation scaling.<br/>
|
||||||
|
--- * button_pressed_timer (number) Button press animation timer.<br/>
|
||||||
|
--- * button_press_duration (number) Duration of button press animation.<br/>
|
||||||
|
--- * return_window (string) Window ID to return to after minigame.<br/>
|
||||||
|
--- * bar_x (number) Progress bar X position.<br/>
|
||||||
|
--- * bar_y (number) Progress bar Y position.<br/>
|
||||||
|
--- * bar_width (number) Progress bar width.<br/>
|
||||||
|
--- * bar_height (number) Progress bar height.<br/>
|
||||||
|
--- * button_x (number) Button indicator X position.<br/>
|
||||||
|
--- * button_y (number) Button indicator Y position.<br/>
|
||||||
|
--- * button_size (number) Button indicator size.<br/>
|
||||||
|
function Minigame.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,
|
||||||
|
focus_center_x = nil,
|
||||||
|
focus_center_y = nil,
|
||||||
|
focus_initial_radius = 0
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets default rhythm minigame configuration.
|
||||||
|
--- @within Minigame
|
||||||
|
--- @return result table The default rhythm minigame configuration. </br>
|
||||||
|
--- Fields: </br>
|
||||||
|
--- * line_position (number) Current position of the moving line (0-1).<br/>
|
||||||
|
--- * line_speed (number) Speed of the moving line per frame.<br/>
|
||||||
|
--- * line_direction (number) Direction of line movement (1 or -1).<br/>
|
||||||
|
--- * target_center (number) Center of the target zone (0-1).<br/>
|
||||||
|
--- * target_width (number) Current width of the target zone.<br/>
|
||||||
|
--- * initial_target_width (number) Starting width of the target zone.<br/>
|
||||||
|
--- * min_target_width (number) Minimum width the target zone can shrink to.<br/>
|
||||||
|
--- * target_shrink_rate (number) Multiplier applied to target width after each hit.<br/>
|
||||||
|
--- * score (number) Current score.<br/>
|
||||||
|
--- * max_score (number) Score needed to win.<br/>
|
||||||
|
--- * button_pressed_timer (number) Button press animation timer.<br/>
|
||||||
|
--- * button_press_duration (number) Duration of button press animation.<br/>
|
||||||
|
--- * return_window (string) Window ID to return to after minigame.<br/>
|
||||||
|
--- * bar_x (number) Progress bar X position.<br/>
|
||||||
|
--- * bar_y (number) Progress bar Y position.<br/>
|
||||||
|
--- * bar_width (number) Progress bar width.<br/>
|
||||||
|
--- * bar_height (number) Progress bar height.<br/>
|
||||||
|
--- * button_x (number) Button indicator X position.<br/>
|
||||||
|
--- * button_y (number) Button indicator Y position.<br/>
|
||||||
|
--- * button_size (number) Button indicator size.<br/>
|
||||||
|
--- * press_cooldown (number) Current cooldown timer.<br/>
|
||||||
|
--- * press_cooldown_duration (number) Frames of press cooldown.<br/>
|
||||||
|
function Minigame.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,
|
||||||
|
focus_center_x = nil,
|
||||||
|
focus_center_y = nil,
|
||||||
|
focus_initial_radius = 0
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Configures DDR minigame.
|
||||||
|
--- @within Minigame
|
||||||
|
--- @param params table Optional parameters to override defaults (see Minigame.get_default_ddr).
|
||||||
|
--- @param[opt] params.bar_fill number Current fill level of the progress bar.
|
||||||
|
--- @param[opt] params.max_fill number Maximum fill value to win.
|
||||||
|
--- @param[opt] params.fill_per_hit number Fill gained per successful hit.
|
||||||
|
--- @param[opt] params.miss_penalty number Fill lost per miss.
|
||||||
|
--- @param[opt] params.bar_x number Progress bar X position.
|
||||||
|
--- @param[opt] params.bar_y number Progress bar Y position.
|
||||||
|
--- @param[opt] params.bar_width number Progress bar width.
|
||||||
|
--- @param[opt] params.bar_height number Progress bar height.
|
||||||
|
--- @param[opt] params.arrow_size number Size of arrow sprites.
|
||||||
|
--- @param[opt] params.arrow_spawn_timer number Timer for arrow spawning.
|
||||||
|
--- @param[opt] params.arrow_spawn_interval number Frames between arrow spawns.
|
||||||
|
--- @param[opt] params.arrow_fall_speed number Speed of falling arrows.
|
||||||
|
--- @param[opt] params.arrows table Array of active arrow objects.
|
||||||
|
--- @param[opt] params.target_y number Y position of the target line.
|
||||||
|
--- @param[opt] params.target_arrows table Array of target arrow positions with dir and x fields.
|
||||||
|
--- @param[opt] params.hit_threshold number Pixel distance for a valid hit.
|
||||||
|
--- @param[opt] params.button_pressed_timers table Per-button press animation timers.
|
||||||
|
--- @param[opt] params.button_press_duration number Duration of button press animation.
|
||||||
|
--- @param[opt] params.input_cooldowns table Per-direction cooldown timers (left, down, up, right).
|
||||||
|
--- @param[opt] params.input_cooldown_duration number Frames of input cooldown.
|
||||||
|
--- @param[opt] params.frame_counter number Global frame counter.
|
||||||
|
--- @param[opt] params.current_song table Currently playing song data.
|
||||||
|
--- @param[opt] params.pattern_index number Current index in song pattern.
|
||||||
|
--- @param[opt] params.use_pattern boolean Whether to use song pattern for spawning.
|
||||||
|
--- @param[opt] params.return_window string Window ID to return to after minigame.
|
||||||
|
--- @return result table The configured DDR minigame state (see Minigame.get_default_ddr for fields).
|
||||||
|
function Minigame.configure_ddr(params)
|
||||||
|
return apply_params(Minigame.get_default_ddr(), params)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Configures button mash minigame.
|
||||||
|
--- @within Minigame
|
||||||
|
--- @param params table Optional parameters to override defaults (see Minigame.get_default_button_mash).
|
||||||
|
--- @param[opt] params.bar_fill number Current fill level of the progress bar.
|
||||||
|
--- @param[opt] params.max_fill number Maximum fill value to win.
|
||||||
|
--- @param[opt] params.fill_per_press number Fill gained per button press.
|
||||||
|
--- @param[opt] params.base_degradation number Base rate of bar degradation per frame.
|
||||||
|
--- @param[opt] params.degradation_multiplier number Multiplier for degradation scaling.
|
||||||
|
--- @param[opt] params.button_pressed_timer number Button press animation timer.
|
||||||
|
--- @param[opt] params.button_press_duration number Duration of button press animation.
|
||||||
|
--- @param[opt] params.return_window string Window ID to return to after minigame.
|
||||||
|
--- @param[opt] params.bar_x number Progress bar X position.
|
||||||
|
--- @param[opt] params.bar_y number Progress bar Y position.
|
||||||
|
--- @param[opt] params.bar_width number Progress bar width.
|
||||||
|
--- @param[opt] params.bar_height number Progress bar height.
|
||||||
|
--- @param[opt] params.button_x number Button indicator X position.
|
||||||
|
--- @param[opt] params.button_y number Button indicator Y position.
|
||||||
|
--- @param[opt] params.button_size number Button indicator size.
|
||||||
|
--- @return result table The configured button mash minigame state (see Minigame.get_default_button_mash for fields).
|
||||||
|
function Minigame.configure_button_mash(params)
|
||||||
|
return apply_params(Minigame.get_default_button_mash(), params)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Configures rhythm minigame.
|
||||||
|
--- @within Minigame
|
||||||
|
--- @param params table Optional parameters to override defaults (see Minigame.get_default_rhythm).
|
||||||
|
--- @param[opt] params.line_position number Current position of the moving line (0-1).
|
||||||
|
--- @param[opt] params.line_speed number Speed of the moving line per frame.
|
||||||
|
--- @param[opt] params.line_direction number Direction of line movement (1 or -1).
|
||||||
|
--- @param[opt] params.target_center number Center of the target zone (0-1).
|
||||||
|
--- @param[opt] params.target_width number Current width of the target zone.
|
||||||
|
--- @param[opt] params.initial_target_width number Starting width of the target zone.
|
||||||
|
--- @param[opt] params.min_target_width number Minimum width the target zone can shrink to.
|
||||||
|
--- @param[opt] params.target_shrink_rate number Multiplier applied to target width after each hit.
|
||||||
|
--- @param[opt] params.score number Current score.
|
||||||
|
--- @param[opt] params.max_score number Score needed to win.
|
||||||
|
--- @param[opt] params.button_pressed_timer number Button press animation timer.
|
||||||
|
--- @param[opt] params.button_press_duration number Duration of button press animation.
|
||||||
|
--- @param[opt] params.return_window string Window ID to return to after minigame.
|
||||||
|
--- @param[opt] params.bar_x number Progress bar X position.
|
||||||
|
--- @param[opt] params.bar_y number Progress bar Y position.
|
||||||
|
--- @param[opt] params.bar_width number Progress bar width.
|
||||||
|
--- @param[opt] params.bar_height number Progress bar height.
|
||||||
|
--- @param[opt] params.button_x number Button indicator X position.
|
||||||
|
--- @param[opt] params.button_y number Button indicator Y position.
|
||||||
|
--- @param[opt] params.button_size number Button indicator size.
|
||||||
|
--- @param[opt] params.press_cooldown number Current cooldown timer.
|
||||||
|
--- @param[opt] params.press_cooldown_duration number Frames of press cooldown.
|
||||||
|
--- @return result table The configured rhythm minigame state (see Minigame.get_default_rhythm for fields).
|
||||||
|
function Minigame.configure_rhythm(params)
|
||||||
|
return apply_params(Minigame.get_default_rhythm(), params)
|
||||||
|
end
|
||||||
14
inc/init/init.module.lua
Normal file
14
inc/init/init.module.lua
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Window = {}
|
||||||
|
Util = {}
|
||||||
|
Meter = {}
|
||||||
|
Minigame = {}
|
||||||
|
Decision = {}
|
||||||
|
Situation = {}
|
||||||
|
Screen = {}
|
||||||
|
Map = {}
|
||||||
|
UI = {}
|
||||||
|
Print = {}
|
||||||
|
Input = {}
|
||||||
|
Sprite = {}
|
||||||
|
Audio = {}
|
||||||
|
Focus = {}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
local SplashWindow = {}
|
|
||||||
local IntroWindow = {}
|
|
||||||
local MenuWindow = {}
|
|
||||||
local GameWindow = {}
|
|
||||||
local PopupWindow = {}
|
|
||||||
local InventoryWindow = {}
|
|
||||||
local ConfigurationWindow = {}
|
|
||||||
|
|
||||||
local UI = {}
|
|
||||||
local Input = {}
|
|
||||||
local NPC = {}
|
|
||||||
local Item = {}
|
|
||||||
local Player = {}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
local WINDOW_SPLASH = 0
|
|
||||||
local WINDOW_INTRO = 1
|
|
||||||
local WINDOW_MENU = 2
|
|
||||||
local WINDOW_GAME = 3
|
|
||||||
local WINDOW_POPUP = 4
|
|
||||||
local WINDOW_INVENTORY = 5
|
|
||||||
local WINDOW_INVENTORY_ACTION = 6
|
|
||||||
local WINDOW_CONFIGURATION = 7
|
|
||||||
9
inc/map/map.bedroom.lua
Normal file
9
inc/map/map.bedroom.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Map.register({
|
||||||
|
id = "bedroom",
|
||||||
|
from_x = 0,
|
||||||
|
from_y = 0,
|
||||||
|
width = 30,
|
||||||
|
height = 17,
|
||||||
|
to_x = 0,
|
||||||
|
to_y = 0,
|
||||||
|
})
|
||||||
75
inc/map/map.manager.lua
Normal file
75
inc/map/map.manager.lua
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
-- Manages game maps.
|
||||||
|
--- @section Map
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
local maps_array = {}
|
||||||
|
for _, map_data in pairs(_maps) do
|
||||||
|
table.insert(maps_array, map_data)
|
||||||
|
end
|
||||||
|
return maps_array
|
||||||
|
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)
|
||||||
|
if _maps[map_data.id] then
|
||||||
|
trace("Warning: Overwriting map with id: " .. map_data.id)
|
||||||
|
end
|
||||||
|
_maps[map_data.id] = map_data
|
||||||
|
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)
|
||||||
|
return _maps[map_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws a map.
|
||||||
|
--- @within Map
|
||||||
|
--- @param map_id string The ID of the map to draw.
|
||||||
|
function Map.draw(map_id)
|
||||||
|
local map_data = Map.get_by_id(map_id)
|
||||||
|
if not map_data then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
map(
|
||||||
|
map_data.from_x,
|
||||||
|
map_data.from_y,
|
||||||
|
map_data.width,
|
||||||
|
map_data.height,
|
||||||
|
map_data.to_x,
|
||||||
|
map_data.to_y
|
||||||
|
)
|
||||||
|
end
|
||||||
9
inc/map/map.office.lua
Normal file
9
inc/map/map.office.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Map.register({
|
||||||
|
id = "office",
|
||||||
|
from_x = 60,
|
||||||
|
from_y = 0,
|
||||||
|
width = 30,
|
||||||
|
height = 17,
|
||||||
|
to_x = 0,
|
||||||
|
to_y = 0,
|
||||||
|
})
|
||||||
9
inc/map/map.street.lua
Normal file
9
inc/map/map.street.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Map.register({
|
||||||
|
id = "street",
|
||||||
|
from_x = 30,
|
||||||
|
from_y = 0,
|
||||||
|
width = 30,
|
||||||
|
height = 17,
|
||||||
|
to_x = 0,
|
||||||
|
to_y = 0,
|
||||||
|
})
|
||||||
@@ -1,29 +1,327 @@
|
|||||||
-- <TILES>
|
|
||||||
-- 000:4444444444444444444444444444444444444444444444444444444444444444
|
|
||||||
-- 001:1111111111111111111111111111111111111111111111111111111111111111
|
|
||||||
-- 002:5555555555555555555555555555555555555555555555555555555555555555
|
|
||||||
-- 003:6666666666666666666666666666666666666666666666666666666666666666
|
|
||||||
-- 004:7777777777777777777777777777777777777777777777777777777777777777
|
|
||||||
-- 005:8888888888888888888888888888888888888888888888888888888888888888
|
|
||||||
-- 006:9999999999999999999999999999999999999999999999999999999999999999
|
|
||||||
-- 007:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
-- 008:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
|
||||||
-- </TILES>
|
|
||||||
|
|
||||||
-- <WAVES>
|
|
||||||
-- 000:00000000ffffffff00000000ffffffff
|
|
||||||
-- 001:0123456789abcdeffedcba9876543210
|
|
||||||
-- 02:0123456789abcdef0123456789abcdef
|
|
||||||
-- </WAVES>
|
|
||||||
|
|
||||||
-- <SFX>
|
|
||||||
-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000
|
|
||||||
-- </SFX>
|
|
||||||
|
|
||||||
-- <TRACKS>
|
|
||||||
-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
||||||
-- </TRACKS>
|
|
||||||
|
|
||||||
-- <PALETTE>
|
-- <PALETTE>
|
||||||
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
|
-- 000:0404005f574fc2c3c71d2b53fff1e8ab52367e2553ffa30000875129adff83769c00e436fa77a8ff004dc3c3c7ffccaa
|
||||||
-- </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>
|
||||||
|
-- 000:060006400600064006000640060006400600060006000600060006000600060006000600060006000600060006000600060006000600060006000600300000000900
|
||||||
|
-- 016:05000500050005400540054005700570057005400540054005700570057005c005c005c005c005c005c005c005c005c005c005c005c005c005c005c0470000000000
|
||||||
|
-- 017:040004000400040004000400046004600460046004600460146024c034c054c064c084c0a4c0b4c0c4c0c4c0d4c0d4c0e4c0f4c0f4c0f4c0f4c0f4c0400000000000
|
||||||
|
-- 018:04c004c004c004c004c004c0046004600460046004600460240034005400640084009400a400b400c400d400d400e400e400e400f400f400f400f400300000000000
|
||||||
|
-- 019:0400040004000400040004d014d014d024d034d054d074d094d0b4d0c4d0e4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0f4d0400000000000
|
||||||
|
-- 020:090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900500000000000
|
||||||
|
-- 021:01000100010001000100f10001100110011001100110f11001200120012001200120f1201130113011302130213021302130313041308130a130d130380000000000
|
||||||
|
-- 032:010001100100011001000110010001100100010001000100010001000100010001000100010001000100010001000100010001000100010001000100301000000800
|
||||||
|
-- 033:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
|
||||||
|
-- 044:0600f6000620f6000600f6000610f600f600f6000600f600f600f600f6000600060006000600060006000600060006000600060006000600060006004600000f0f00
|
||||||
|
-- 045:0000f0000020f0000000f0000010f000f000f0000000f000f000f000f0000000000000000000000000000000000000000000000000000000000000004600000f0f00
|
||||||
|
-- 057:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
|
||||||
|
-- 058:41004110410041104100411041004110c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100100000080800
|
||||||
|
-- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000
|
||||||
|
-- 060:220022002200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200100000000000
|
||||||
|
-- 061:9f009f00bf00df00df00ef00ef00ef00ef00ef00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00400000000000
|
||||||
|
-- 062:00000100010001000100510081008100910091009100a100a100a100a100a100b100b100b100b100c100c100c100d100d100d100e100e100e100f100484000000000
|
||||||
|
-- 063:00b000100000000000000000100060009000b000c000d000d000e000e000e000f000f000f000f000f000f000f000f000f000f000f000f000f000f000405000000000
|
||||||
|
-- </SFX>
|
||||||
|
-- <WAVES>
|
||||||
|
-- 000:bcceefceedddddc84333121268abaa99
|
||||||
|
-- 001:6789bdd96adc83248dd6334adda7578b
|
||||||
|
-- 002:0123456789abcdef0123456789abcdef
|
||||||
|
-- 003:224578acdeeeeddcba95434567653100
|
||||||
|
-- 004:00000000ffffffff00000000ffffffff
|
||||||
|
-- 005:0123456789abcdeffedcba9876543210
|
||||||
|
-- 006:0123456789abcdef0123456789abcdef
|
||||||
|
-- 007:76543210123456789abcdefedcba9878
|
||||||
|
-- 008:0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
|
||||||
|
-- 009:fff000fff000fff000fff000fff000ff
|
||||||
|
-- </WAVES>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
-- title: Mr Anderson's Adventure
|
-- title: Definitely not an Impostor
|
||||||
-- author: Zsolt Tasnadi
|
-- name: impostor
|
||||||
|
-- author: Teletype Games
|
||||||
-- desc: Life of a programmer in the Vector
|
-- desc: Life of a programmer in the Vector
|
||||||
-- site: https://github.com/rastasi/mranderson
|
-- site: https://git.teletype.hu/games/impostor
|
||||||
-- license: MIT License
|
-- license: MIT License
|
||||||
-- version: 0.10
|
-- version: 0.1
|
||||||
-- script: lua
|
-- script: lua
|
||||||
|
|||||||
9
inc/screen/screen.home.lua
Normal file
9
inc/screen/screen.home.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Screen.register({
|
||||||
|
id = "home",
|
||||||
|
name = "Home",
|
||||||
|
decisions = {
|
||||||
|
"go_to_toilet",
|
||||||
|
"go_to_walking_to_office",
|
||||||
|
},
|
||||||
|
background = "bedroom"
|
||||||
|
})
|
||||||
63
inc/screen/screen.manager.lua
Normal file
63
inc/screen/screen.manager.lua
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
--- @section Screen
|
||||||
|
local _screens = {}
|
||||||
|
|
||||||
|
--- Registers a screen definition.
|
||||||
|
--- @within Screen
|
||||||
|
--- @param screen_data table The screen data table.
|
||||||
|
--- @param screen_data.id string Unique screen identifier.
|
||||||
|
--- @param screen_data.name string Display name of the screen.
|
||||||
|
--- @param screen_data.decisions table Array of decision ID strings available on this screen.
|
||||||
|
--- @param screen_data.background string Map ID used as background.
|
||||||
|
--- @param[opt] screen_data.situations table Array of situation ID strings. Defaults to {}.
|
||||||
|
--- @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)
|
||||||
|
if _screens[screen_data.id] then
|
||||||
|
trace("Warning: Overwriting screen with id: " .. screen_data.id)
|
||||||
|
end
|
||||||
|
if not screen_data.situations then
|
||||||
|
screen_data.situations = {}
|
||||||
|
end
|
||||||
|
if not screen_data.init then
|
||||||
|
screen_data.init = function() end
|
||||||
|
end
|
||||||
|
if not screen_data.update then
|
||||||
|
screen_data.update = function() end
|
||||||
|
end
|
||||||
|
if not screen_data.draw then
|
||||||
|
screen_data.draw = function() end
|
||||||
|
end
|
||||||
|
_screens[screen_data.id] = screen_data
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets a screen by ID.
|
||||||
|
--- @within Screen
|
||||||
|
--- @param screen_id string The ID of the screen.
|
||||||
|
--- @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)
|
||||||
|
return _screens[screen_id]
|
||||||
|
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
|
||||||
15
inc/screen/screen.office.lua
Normal file
15
inc/screen/screen.office.lua
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Screen.register({
|
||||||
|
id = "office",
|
||||||
|
name = "Office",
|
||||||
|
decisions = {
|
||||||
|
"play_button_mash",
|
||||||
|
"play_rhythm",
|
||||||
|
"play_ddr",
|
||||||
|
"go_to_walking_to_home",
|
||||||
|
"have_a_coffee",
|
||||||
|
},
|
||||||
|
situations = {
|
||||||
|
"drink_coffee",
|
||||||
|
},
|
||||||
|
background = "office"
|
||||||
|
})
|
||||||
76
inc/screen/screen.toilet.lua
Normal file
76
inc/screen/screen.toilet.lua
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
Screen.register({
|
||||||
|
id = "toilet",
|
||||||
|
name = "Toilet",
|
||||||
|
decisions = {
|
||||||
|
"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 bar_w = math.floor(sw * 0.75)
|
||||||
|
local bar_x = math.floor((sw - bar_w) / 2)
|
||||||
|
local bar_h = 4
|
||||||
|
|
||||||
|
-- TODO: Add day counter
|
||||||
|
Print.text_center("day 1", 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_timer_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,
|
||||||
|
})
|
||||||
9
inc/screen/screen.walking_to_home.lua
Normal file
9
inc/screen/screen.walking_to_home.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Screen.register({
|
||||||
|
id = "walking_to_home",
|
||||||
|
name = "Walking to home",
|
||||||
|
decisions = {
|
||||||
|
"go_to_home",
|
||||||
|
"go_to_office",
|
||||||
|
},
|
||||||
|
background = "street"
|
||||||
|
})
|
||||||
9
inc/screen/screen.walking_to_office.lua
Normal file
9
inc/screen/screen.walking_to_office.lua
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Screen.register({
|
||||||
|
id = "walking_to_office",
|
||||||
|
name = "Walking to office",
|
||||||
|
decisions = {
|
||||||
|
"go_to_home",
|
||||||
|
"go_to_office",
|
||||||
|
},
|
||||||
|
background = "street"
|
||||||
|
})
|
||||||
7
inc/situation/situation.drink_coffee.lua
Normal file
7
inc/situation/situation.drink_coffee.lua
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Situation.register({
|
||||||
|
id = "drink_coffee",
|
||||||
|
handle = function()
|
||||||
|
Audio.sfx_select()
|
||||||
|
Sprite.show("norman", 100, 100)
|
||||||
|
end,
|
||||||
|
})
|
||||||
84
inc/situation/situation.manager.lua
Normal file
84
inc/situation/situation.manager.lua
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
--- @section Situation
|
||||||
|
local _situations = {}
|
||||||
|
|
||||||
|
--- Registers a situation definition.
|
||||||
|
--- @within Situation
|
||||||
|
--- @param situation table The situation data table.
|
||||||
|
--- @param situation.id string Unique situation identifier.<br/>
|
||||||
|
--- @param[opt] situation.screen_id string ID of the screen this situation belongs to.<br/>
|
||||||
|
--- @param[opt] situation.handle function Called when the situation is applied. Defaults to noop.<br/>
|
||||||
|
--- @param[opt] situation.update function Called each frame while situation is active. Defaults to noop.<br/>
|
||||||
|
function Situation.register(situation)
|
||||||
|
if not situation or not situation.id then
|
||||||
|
PopupWindow.show({"Error: Invalid situation object registered (missing id)!"})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not situation.handle then
|
||||||
|
situation.handle = function() end
|
||||||
|
end
|
||||||
|
if not situation.update then
|
||||||
|
situation.update = function() end
|
||||||
|
end
|
||||||
|
if _situations[situation.id] then
|
||||||
|
trace("Warning: Overwriting situation with id: " .. situation.id)
|
||||||
|
end
|
||||||
|
_situations[situation.id] = situation
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets a situation by ID.
|
||||||
|
--- @within Situation
|
||||||
|
--- @param id string The situation ID.
|
||||||
|
--- @return result table The situation table or nil. </br>
|
||||||
|
--- 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]
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets all registered situations, optionally filtered by screen ID.
|
||||||
|
--- @within Situation
|
||||||
|
--- @param screen_id string Optional. If provided, returns situations associated with this screen ID.
|
||||||
|
--- @return result table A table containing all registered situation data, indexed by their IDs, or an array filtered by screen_id. </br>
|
||||||
|
--- 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_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
|
||||||
|
trace("Error: No situation found with id: " .. id)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if Util.contains(screen.situations, id) then
|
||||||
|
situation.handle()
|
||||||
|
return id
|
||||||
|
else
|
||||||
|
trace("Info: Situation " .. id .. " cannot be applied to current screen (id: " .. current_screen_id .. ").")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
96
inc/sprite/sprite.manager.lua
Normal file
96
inc/sprite/sprite.manager.lua
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
--- @section Sprite
|
||||||
|
local _sprites = {}
|
||||||
|
local _active_sprites = {}
|
||||||
|
|
||||||
|
--- 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)
|
||||||
|
if not sprite_data or not sprite_data.id then
|
||||||
|
trace("Error: Invalid sprite object registered (missing id)!")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if _sprites[sprite_data.id] then
|
||||||
|
trace("Warning: Overwriting sprite with id: " .. sprite_data.id)
|
||||||
|
end
|
||||||
|
_sprites[sprite_data.id] = sprite_data
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Schedules a sprite for drawing.
|
||||||
|
--- @within Sprite
|
||||||
|
--- @param id string The unique identifier of the sprite.<br/>
|
||||||
|
--- @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.show(id, x, y, colorkey, scale, flip_x, flip_y, rot)
|
||||||
|
if not _sprites[id] then
|
||||||
|
trace("Error: Attempted to show non-registered sprite with id: " .. id)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
_active_sprites[id] = {
|
||||||
|
id = id,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
colorkey = colorkey,
|
||||||
|
scale = scale,
|
||||||
|
flip_x = flip_x,
|
||||||
|
flip_y = flip_y,
|
||||||
|
rot = rot,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Hides a displayed sprite.
|
||||||
|
--- @within Sprite
|
||||||
|
--- @param id string The unique identifier of the sprite.<br/>
|
||||||
|
function Sprite.hide(id)
|
||||||
|
_active_sprites[id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws all scheduled sprites.
|
||||||
|
--- @within Sprite
|
||||||
|
function Sprite.draw()
|
||||||
|
for id, params in pairs(_active_sprites) do
|
||||||
|
local sprite_data = _sprites[id]
|
||||||
|
if not sprite_data then
|
||||||
|
trace("Error: Sprite id " .. id .. " in _active_sprites is not registered.")
|
||||||
|
_active_sprites[id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
spr(sprite_data.s, params.x, params.y, colorkey, scale, flip_x, flip_y, rot)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
17
inc/sprite/sprite.norman.lua
Normal file
17
inc/sprite/sprite.norman.lua
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Sprite.register({
|
||||||
|
id = "norman",
|
||||||
|
sprites = {
|
||||||
|
-- Body (sprite index 0)
|
||||||
|
{ s = 0, x_offset = 0, y_offset = 0 },
|
||||||
|
-- Head (sprite index 1)
|
||||||
|
{ s = 1, x_offset = 0, y_offset = -8 },
|
||||||
|
-- Left Arm (sprite index 2)
|
||||||
|
{ 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)
|
||||||
|
{ s = 4, x_offset = -2, y_offset = 8 },
|
||||||
|
-- Right Leg (sprite index 5, flipped)
|
||||||
|
{ s = 5, x_offset = 2, y_offset = 8, flip_x = 1 } -- Flipped leg
|
||||||
|
}
|
||||||
|
})
|
||||||
166
inc/system/system.focus.lua
Normal file
166
inc/system/system.focus.lua
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
--- @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
|
||||||
@@ -1,8 +1,39 @@
|
|||||||
function Input.up() return btnp(0) end
|
--- @section Input
|
||||||
function Input.down() return btnp(1) end
|
local INPUT_KEY_UP = 0
|
||||||
function Input.left() return btnp(2) end
|
local INPUT_KEY_DOWN = 1
|
||||||
function Input.right() return btnp(3) end
|
local INPUT_KEY_LEFT = 2
|
||||||
function Input.player_jump() return btnp(4) end
|
local INPUT_KEY_RIGHT = 3
|
||||||
function Input.menu_confirm() return btnp(4) end
|
local INPUT_KEY_A = 4
|
||||||
function Input.player_interact() return btnp(5) end -- B button
|
local INPUT_KEY_B = 5
|
||||||
function Input.menu_back() return btnp(5) end
|
local INPUT_KEY_Y = 7
|
||||||
|
local INPUT_KEY_SPACE = 48
|
||||||
|
local INPUT_KEY_BACKSPACE = 51
|
||||||
|
local INPUT_KEY_ENTER = 50
|
||||||
|
|
||||||
|
--- Checks if Up is pressed.
|
||||||
|
--- @within Input
|
||||||
|
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
|
||||||
|
--- Checks if Left is pressed.
|
||||||
|
--- @within Input
|
||||||
|
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
|
||||||
|
--- Checks if Select is pressed.
|
||||||
|
--- @within Input
|
||||||
|
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
|
||||||
|
--- Checks if Player Interact is pressed.
|
||||||
|
--- @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
|
||||||
|
|||||||
@@ -1,44 +1,29 @@
|
|||||||
local STATE_HANDLERS = {
|
--- @section Main
|
||||||
[WINDOW_SPLASH] = function()
|
local initialized_game = false
|
||||||
SplashWindow.update()
|
|
||||||
SplashWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_INTRO] = function()
|
|
||||||
IntroWindow.update()
|
|
||||||
IntroWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_MENU] = function()
|
|
||||||
MenuWindow.update()
|
|
||||||
MenuWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_GAME] = function()
|
|
||||||
GameWindow.update()
|
|
||||||
GameWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_POPUP] = function()
|
|
||||||
GameWindow.draw()
|
|
||||||
PopupWindow.update()
|
|
||||||
PopupWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_INVENTORY] = function()
|
|
||||||
InventoryWindow.update()
|
|
||||||
InventoryWindow.draw()
|
|
||||||
end,
|
|
||||||
[WINDOW_INVENTORY_ACTION] = function()
|
|
||||||
InventoryWindow.draw()
|
|
||||||
PopupWindow.draw()
|
|
||||||
PopupWindow.update()
|
|
||||||
end,
|
|
||||||
[WINDOW_CONFIGURATION] = function()
|
|
||||||
ConfigurationWindow.update()
|
|
||||||
ConfigurationWindow.draw()
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
--- Initializes game state.
|
||||||
|
--- @within Main
|
||||||
|
--- @return boolean initialized_game True if game has been initialized, false otherwise.
|
||||||
|
local function init_game()
|
||||||
|
if initialized_game then return end
|
||||||
|
Context.reset()
|
||||||
|
Window.set_current("splash") -- Set initial window using new manager
|
||||||
|
MenuWindow.refresh_menu_items()
|
||||||
|
initialized_game = true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Main game loop (TIC-80 callback).
|
||||||
|
--- @within Main
|
||||||
function TIC()
|
function TIC()
|
||||||
|
init_game()
|
||||||
cls(Config.colors.black)
|
cls(Config.colors.black)
|
||||||
local handler = STATE_HANDLERS[Context.active_window]
|
local handler = Window.get_current_handler() -- Get handler from Window manager
|
||||||
if handler then
|
if handler then
|
||||||
handler()
|
handler()
|
||||||
end
|
end
|
||||||
|
Meter.update()
|
||||||
|
if Context.game_in_progress then
|
||||||
|
UI.draw_meters()
|
||||||
|
UI.draw_timer()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
30
inc/system/system.print.lua
Normal file
30
inc/system/system.print.lua
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
--- 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)
|
||||||
|
local shadow_color = Config.colors.black
|
||||||
|
if color == shadow_color then shadow_color = Config.colors.light_grey end
|
||||||
|
scale = scale or 1
|
||||||
|
print(text, x + 1, y + 1, shadow_color, fixed, scale)
|
||||||
|
print(text, x, y, color, fixed, scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- 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)
|
||||||
|
scale = scale or 1
|
||||||
|
local text_width = print(text, 0, -6, 0, fixed, scale)
|
||||||
|
local centered_x = x - (text_width / 2)
|
||||||
|
Print.text(text, centered_x, y, color, fixed, scale)
|
||||||
|
end
|
||||||
@@ -1,29 +1,49 @@
|
|||||||
|
--- @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(title, 3, 2, Config.colors.green)
|
Print.text(title, 3, 2, Config.colors.light_blue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Draws dialog window.
|
||||||
|
--- @within UI
|
||||||
function UI.draw_dialog()
|
function UI.draw_dialog()
|
||||||
PopupWindow.draw()
|
PopupWindow.draw()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Draws a menu.
|
||||||
|
--- @within UI
|
||||||
|
--- @param items table A table of menu items.<br/>
|
||||||
|
--- @param selected_item number The index of the currently selected item.<br/>
|
||||||
|
--- @param x number The x-coordinate for the menu.<br/>
|
||||||
|
--- @param y number The y-coordinate for the menu.<br/>
|
||||||
function UI.draw_menu(items, selected_item, x, y)
|
function UI.draw_menu(items, selected_item, x, y)
|
||||||
for i, item in ipairs(items) do
|
for i, item in ipairs(items) do
|
||||||
local current_y = y + (i-1)*10
|
local current_y = y + (i-1)*10
|
||||||
if i == selected_item then
|
if i == selected_item then
|
||||||
print(">", x - 8, current_y, Config.colors.green)
|
Print.text(">", x - 8, current_y, Config.colors.light_blue)
|
||||||
end
|
end
|
||||||
print(item.label, x, current_y, Config.colors.green)
|
Print.text(item.label, x, current_y, Config.colors.light_blue)
|
||||||
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()
|
||||||
selected_item = selected_item - 1
|
selected_item = selected_item - 1
|
||||||
if selected_item < 1 then
|
if selected_item < 1 then
|
||||||
selected_item = #items
|
selected_item = #items
|
||||||
end
|
end
|
||||||
elseif Input.down() then
|
elseif Input.down() then
|
||||||
|
Audio.sfx_beep()
|
||||||
selected_item = selected_item + 1
|
selected_item = selected_item + 1
|
||||||
if selected_item > #items then
|
if selected_item > #items then
|
||||||
selected_item = 1
|
selected_item = 1
|
||||||
@@ -32,10 +52,14 @@ function UI.update_menu(items, selected_item)
|
|||||||
return selected_item
|
return selected_item
|
||||||
end
|
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
|
for input_line in (text .. "\n"):gmatch("(.-)\n") do
|
||||||
local current_line = ""
|
local current_line = ""
|
||||||
local words_in_line = 0
|
local words_in_line = 0
|
||||||
@@ -50,21 +74,37 @@ function UI.word_wrap(text, max_chars_per_line)
|
|||||||
current_line = word
|
current_line = word
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if words_in_line > 0 then
|
if words_in_line > 0 then
|
||||||
table.insert(lines, current_line)
|
table.insert(lines, current_line)
|
||||||
else
|
else
|
||||||
table.insert(lines, "")
|
table.insert(lines, "")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if #lines == 0 then
|
if #lines == 0 then
|
||||||
return {""}
|
return {""}
|
||||||
end
|
end
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates a numeric stepper.
|
||||||
|
--- @within UI
|
||||||
|
--- @param label string The label for the stepper.<br/>
|
||||||
|
--- @param value_getter function Function to get the current value.<br/>
|
||||||
|
--- @param value_setter function Function to set the current value.<br/>
|
||||||
|
--- @param min number The minimum value.<br/>
|
||||||
|
--- @param max number The maximum value.<br/>
|
||||||
|
--- @param step number The step increment.<br/>
|
||||||
|
--- @param[opt] format string The format string for displaying the value.<br/>
|
||||||
|
--- @return result table A numeric stepper control definition or nil. </br>
|
||||||
|
--- Fields: </br>
|
||||||
|
--- * label (string) The label for the stepper.<br/>
|
||||||
|
--- * get (function) Function to get the current value.<br/>
|
||||||
|
--- * set (function) Function to set the current value.<br/>
|
||||||
|
--- * min (number) The minimum value.<br/>
|
||||||
|
--- * max (number) The maximum value.<br/>
|
||||||
|
--- * step (number) The step increment.<br/>
|
||||||
|
--- * format (string) The format string for displaying the value.<br/>
|
||||||
|
--- * type (string) Control type identifier ("numeric_stepper").<br/>
|
||||||
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
|
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
|
||||||
return {
|
return {
|
||||||
label = label,
|
label = label,
|
||||||
@@ -77,3 +117,144 @@ function UI.create_numeric_stepper(label, value_getter, value_setter, min, max,
|
|||||||
type = "numeric_stepper"
|
type = "numeric_stepper"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates an action item.
|
||||||
|
--- @within UI
|
||||||
|
--- @param label string The label for the action item.<br/>
|
||||||
|
--- @param action function The function to execute when the item is selected.<br/>
|
||||||
|
--- @return result table An action item control definition or nil. </br>
|
||||||
|
--- Fields: </br>
|
||||||
|
--- * label (string) The label for the action item.<br/>
|
||||||
|
--- * action (function) The function to execute when the item is selected.<br/>
|
||||||
|
--- * type (string) Control type identifier ("action_item").<br/>
|
||||||
|
function UI.create_action_item(label, action)
|
||||||
|
return {
|
||||||
|
label = label,
|
||||||
|
action = action,
|
||||||
|
type = "action_item"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws decision selector.
|
||||||
|
--- @within UI
|
||||||
|
--- @param decisions table A table of decision items.<br/>
|
||||||
|
--- @param selected_decision_index number The index of the selected decision.<br/>
|
||||||
|
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.light_blue)
|
||||||
|
Print.text(decision_label, text_x, text_y, Config.colors.item)
|
||||||
|
Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws the clock timer indicator as a circular progress bar in the top-left area.
|
||||||
|
--- Color transitions: white (0-50%), yellow (50-75%), red (75-100%).
|
||||||
|
--- @within UI
|
||||||
|
function UI.draw_timer()
|
||||||
|
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||||
|
if Context.meters.hidden and not Context.stat_screen_active then return end
|
||||||
|
|
||||||
|
local m = Context.meters
|
||||||
|
local cx = 10
|
||||||
|
local cy = 20
|
||||||
|
local r_outer = 5
|
||||||
|
local r_inner = 3
|
||||||
|
local progress = m.timer_progress
|
||||||
|
|
||||||
|
local fg_color
|
||||||
|
if progress <= 0.25 then
|
||||||
|
fg_color = Config.colors.white
|
||||||
|
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.dark_grey
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Draws meters.
|
||||||
|
--- @within UI
|
||||||
|
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 = 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 = 11
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Updates decision selector.
|
||||||
|
--- @within UI
|
||||||
|
--- @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 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
|
||||||
38
inc/system/system.util.lua
Normal file
38
inc/system/system.util.lua
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
--- @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)
|
||||||
|
return ((index - 1 + #array) % #array) + 1
|
||||||
|
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)
|
||||||
|
local screen = Screen.get_by_id(screen_id)
|
||||||
|
if screen then
|
||||||
|
Context.game.current_screen = screen_id
|
||||||
|
screen.init()
|
||||||
|
else
|
||||||
|
PopupWindow.show({"Error: Screen '" .. screen_id .. "' not found!"})
|
||||||
|
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
|
||||||
117
inc/window/window.audiotest.lua
Normal file
117
inc/window/window.audiotest.lua
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
--- @section AudioTestWindow
|
||||||
|
AudioTestWindow = {
|
||||||
|
index_menu = 1,
|
||||||
|
index_func = 1,
|
||||||
|
list_func = {},
|
||||||
|
menuitems = {},
|
||||||
|
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)
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
label = "Play music/sound: " .. (list_func[index_func] or "?"),
|
||||||
|
decision = function()
|
||||||
|
local current_func = Audio[list_func[index_func]]
|
||||||
|
if current_func then
|
||||||
|
current_func()
|
||||||
|
else
|
||||||
|
trace("Invalid Audio function: " .. list_func[index_func])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Stop playing music",
|
||||||
|
decision = function()
|
||||||
|
Audio.music_stop()
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Back",
|
||||||
|
decision = function()
|
||||||
|
AudioTestWindow.back()
|
||||||
|
end
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Generates list of audio functions.
|
||||||
|
--- @within AudioTestWindow
|
||||||
|
--- @return result table A sorted list of audio function names.
|
||||||
|
function AudioTestWindow.generate_listfunc()
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
for k, v in pairs(Audio) do
|
||||||
|
if type(v) == "function" then
|
||||||
|
result[#result + 1] = k
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Navigates back from audio test window.
|
||||||
|
--- @within AudioTestWindow
|
||||||
|
function AudioTestWindow.back()
|
||||||
|
Audio.sfx_deselect()
|
||||||
|
GameWindow.set_state("menu")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Initializes audio test window.
|
||||||
|
--- @within AudioTestWindow
|
||||||
|
function AudioTestWindow.init()
|
||||||
|
AudioTestWindow.last_pressed = false
|
||||||
|
AudioTestWindow.index_menu = 1
|
||||||
|
AudioTestWindow.index_func = 1
|
||||||
|
AudioTestWindow.list_func = AudioTestWindow.generate_listfunc()
|
||||||
|
AudioTestWindow.menuitems = AudioTestWindow.generate_menuitems(
|
||||||
|
AudioTestWindow.list_func, AudioTestWindow.index_func
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws audio test window.
|
||||||
|
--- @within AudioTestWindow
|
||||||
|
function AudioTestWindow.draw()
|
||||||
|
UI.draw_top_bar("Audio test")
|
||||||
|
UI.draw_menu(AudioTestWindow.menuitems, AudioTestWindow.index_menu, 20, 50)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Updates audio test window logic.
|
||||||
|
--- @within AudioTestWindow
|
||||||
|
function AudioTestWindow.update()
|
||||||
|
if Input.up() then
|
||||||
|
AudioTestWindow.index_menu = Util.safeindex(AudioTestWindow.menuitems, AudioTestWindow.index_menu - 1)
|
||||||
|
elseif Input.down() then
|
||||||
|
AudioTestWindow.index_menu = Util.safeindex(AudioTestWindow.menuitems, AudioTestWindow.index_menu + 1)
|
||||||
|
elseif Input.left() then
|
||||||
|
AudioTestWindow.index_func = Util.safeindex(
|
||||||
|
AudioTestWindow.list_func,
|
||||||
|
AudioTestWindow.index_func - 1
|
||||||
|
)
|
||||||
|
AudioTestWindow.menuitems = AudioTestWindow.generate_menuitems(
|
||||||
|
AudioTestWindow.list_func, AudioTestWindow.index_func
|
||||||
|
)
|
||||||
|
elseif Input.right() then
|
||||||
|
AudioTestWindow.index_func = Util.safeindex(
|
||||||
|
AudioTestWindow.list_func,
|
||||||
|
AudioTestWindow.index_func + 1
|
||||||
|
)
|
||||||
|
AudioTestWindow.menuitems = AudioTestWindow.generate_menuitems(
|
||||||
|
AudioTestWindow.list_func, AudioTestWindow.index_func
|
||||||
|
)
|
||||||
|
elseif Input.menu_confirm() then
|
||||||
|
AudioTestWindow.menuitems[AudioTestWindow.index_menu].decision()
|
||||||
|
elseif Input.menu_back() then
|
||||||
|
AudioTestWindow.back()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,68 +1,75 @@
|
|||||||
|
--- @section ConfigurationWindow
|
||||||
ConfigurationWindow = {
|
ConfigurationWindow = {
|
||||||
controls = {},
|
controls = {},
|
||||||
selected_control = 1,
|
selected_control = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- Initializes configuration window.
|
||||||
|
--- @within ConfigurationWindow
|
||||||
function ConfigurationWindow.init()
|
function ConfigurationWindow.init()
|
||||||
ConfigurationWindow.controls = {
|
ConfigurationWindow.controls = {
|
||||||
UI.create_numeric_stepper(
|
UI.create_action_item(
|
||||||
"Move Speed",
|
"Save",
|
||||||
function() return Config.physics.move_speed end,
|
function() Config.save() end
|
||||||
function(v) Config.physics.move_speed = v end,
|
|
||||||
0.5, 3, 0.1, "%.1f"
|
|
||||||
),
|
),
|
||||||
UI.create_numeric_stepper(
|
UI.create_action_item(
|
||||||
"Max Jumps",
|
"Restore Defaults",
|
||||||
function() return Config.physics.max_jumps end,
|
function() Config.reset() end
|
||||||
function(v) Config.physics.max_jumps = v end,
|
|
||||||
1, 5, 1, "%d"
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
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 -- Left margin for labels
|
local x_start = 10
|
||||||
local y_start = 40
|
local y_start = 40
|
||||||
local x_value_right_align = Config.screen.width - 10 -- Right margin for values
|
local x_value_right_align = Config.screen.width - 10
|
||||||
local char_width = 4 -- Approximate character width for default font
|
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.green
|
local color = Config.colors.light_blue
|
||||||
|
if control.type == "numeric_stepper" then
|
||||||
|
local value = control.get()
|
||||||
|
local label_text = control.label
|
||||||
|
local value_text = string.format(control.format, value)
|
||||||
|
local value_x = x_value_right_align - (#value_text * char_width)
|
||||||
|
|
||||||
local value = control.get()
|
if i == ConfigurationWindow.selected_control then
|
||||||
local label_text = control.label
|
color = Config.colors.item
|
||||||
local value_text = string.format(control.format, value)
|
Print.text("<", x_start - 8, current_y, color)
|
||||||
|
Print.text(label_text, x_start, current_y, color)
|
||||||
-- Calculate x position for right-aligned value
|
Print.text(value_text, value_x, current_y, color)
|
||||||
local value_x = x_value_right_align - (#value_text * char_width)
|
Print.text(">", x_value_right_align + 4, current_y, color)
|
||||||
|
else
|
||||||
if i == ConfigurationWindow.selected_control then
|
Print.text(label_text, x_start, current_y, color)
|
||||||
color = Config.colors.item
|
Print.text(value_text, value_x, current_y, color)
|
||||||
print("<", x_start -8, current_y, color)
|
end
|
||||||
print(label_text, x_start, current_y, color) -- Shift label due to '<'
|
elseif control.type == "action_item" then
|
||||||
print(value_text, value_x, current_y, color)
|
local label_text = control.label
|
||||||
print(">", x_value_right_align + 4, current_y, color) -- Print '>' after value
|
if i == ConfigurationWindow.selected_control then
|
||||||
else
|
color = Config.colors.item
|
||||||
print(label_text, x_start, current_y, color)
|
Print.text("<", x_start - 8, current_y, color)
|
||||||
print(value_text, value_x, current_y, color)
|
Print.text(label_text, x_start, current_y, color)
|
||||||
|
Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color)
|
||||||
|
else
|
||||||
|
Print.text(label_text, x_start, current_y, color)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Print.text("Press B to go back", x_start, 120, Config.colors.light_grey)
|
||||||
print("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
|
||||||
-- I need to find out how to switch back to the menu
|
GameWindow.set_state("menu")
|
||||||
-- For now, I'll assume a function GameWindow.set_state exists
|
|
||||||
GameWindow.set_state(WINDOW_MENU)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Navigate between controls
|
|
||||||
if Input.up() then
|
if Input.up() then
|
||||||
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1
|
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1
|
||||||
if ConfigurationWindow.selected_control < 1 then
|
if ConfigurationWindow.selected_control < 1 then
|
||||||
@@ -75,16 +82,21 @@ function ConfigurationWindow.update()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Modify control value
|
|
||||||
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
|
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
|
||||||
if control then
|
if control then
|
||||||
local current_value = control.get()
|
if control.type == "numeric_stepper" then
|
||||||
if Input.left() then
|
local current_value = control.get()
|
||||||
local new_value = math.max(control.min, current_value - control.step)
|
if Input.left() then
|
||||||
control.set(new_value)
|
local new_value = math.max(control.min, current_value - control.step)
|
||||||
elseif Input.right() then
|
control.set(new_value)
|
||||||
local new_value = math.min(control.max, current_value + control.step)
|
elseif Input.right() then
|
||||||
control.set(new_value)
|
local new_value = math.min(control.max, current_value + control.step)
|
||||||
|
control.set(new_value)
|
||||||
|
end
|
||||||
|
elseif control.type == "action_item" then
|
||||||
|
if Input.menu_confirm() then
|
||||||
|
control.action()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,35 +1,81 @@
|
|||||||
|
--- @section GameWindow
|
||||||
|
local _available_decisions = {}
|
||||||
|
local _selected_decision_index = 1
|
||||||
|
|
||||||
|
--- Draws the game window.
|
||||||
|
--- @within GameWindow
|
||||||
function GameWindow.draw()
|
function GameWindow.draw()
|
||||||
local currentScreenData = Context.screens[Context.current_screen]
|
local screen = Screen.get_by_id(Context.game.current_screen)
|
||||||
|
if screen.background then
|
||||||
UI.draw_top_bar(currentScreenData.name)
|
Map.draw(screen.background)
|
||||||
|
elseif screen.background_color then
|
||||||
-- Draw platforms
|
rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color)
|
||||||
for _, p in ipairs(currentScreenData.platforms) do
|
|
||||||
rect(p.x, p.y, p.w, p.h, Config.colors.green)
|
|
||||||
end
|
end
|
||||||
|
UI.draw_top_bar(screen.name)
|
||||||
-- Draw items
|
if not Context.stat_screen_active and #_available_decisions > 0 then
|
||||||
for _, item in ipairs(currentScreenData.items) do
|
UI.draw_decision_selector(_available_decisions, _selected_decision_index)
|
||||||
spr(item.sprite_id, item.x, item.y, 0)
|
|
||||||
end
|
end
|
||||||
|
Sprite.draw()
|
||||||
-- Draw NPCs
|
Focus.draw()
|
||||||
for _, npc in ipairs(currentScreenData.npcs) do
|
screen.draw()
|
||||||
spr(npc.sprite_id, npc.x, npc.y, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Draw ground
|
|
||||||
rect(Context.ground.x, Context.ground.y, Context.ground.w, Context.ground.h, Config.colors.dark_grey)
|
|
||||||
|
|
||||||
-- Draw player
|
|
||||||
Player.draw()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Updates the game window logic.
|
||||||
|
--- @within GameWindow
|
||||||
function GameWindow.update()
|
function GameWindow.update()
|
||||||
Player.update() -- Call the encapsulated player update logic
|
Focus.update()
|
||||||
|
if Input.menu_back() then
|
||||||
|
Window.set_current("menu")
|
||||||
|
MenuWindow.refresh_menu_items()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local screen = Screen.get_by_id(Context.game.current_screen)
|
||||||
|
screen.update()
|
||||||
|
|
||||||
|
-- Handle current situation updates
|
||||||
|
if Context.game.current_situation then
|
||||||
|
local current_situation_obj = Situation.get_by_id(Context.game.current_situation)
|
||||||
|
if current_situation_obj and current_situation_obj.update then
|
||||||
|
current_situation_obj.update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if Context.stat_screen_active then return end
|
||||||
|
|
||||||
|
-- Fetch and filter decisions locally
|
||||||
|
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
|
||||||
|
|
||||||
|
if _selected_decision_index > #_available_decisions then
|
||||||
|
_selected_decision_index = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_selected_decision_index = UI.update_decision_selector(
|
||||||
|
_available_decisions,
|
||||||
|
_selected_decision_index
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Sets the active window.
|
||||||
|
--- @within GameWindow
|
||||||
|
--- @param new_state string The ID of the new active window.</br>
|
||||||
function GameWindow.set_state(new_state)
|
function GameWindow.set_state(new_state)
|
||||||
Context.active_window = new_state
|
Window.set_current(new_state)
|
||||||
-- Add any state-specific initialization/cleanup here later if needed
|
|
||||||
end
|
end
|
||||||
@@ -1,25 +1,41 @@
|
|||||||
|
--- @section IntroWindow
|
||||||
|
IntroWindow.y = Config.screen.height
|
||||||
|
IntroWindow.speed = 0.5
|
||||||
|
IntroWindow.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 intro window.
|
||||||
|
--- @within IntroWindow
|
||||||
function IntroWindow.draw()
|
function IntroWindow.draw()
|
||||||
local x = (Config.screen.width - 132) / 2 -- Centered text
|
local x = (Config.screen.width - 132) / 2
|
||||||
print(Context.intro.text, x, Context.intro.y, Config.colors.green)
|
Print.text(IntroWindow.text, x, IntroWindow.y, Config.colors.light_blue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Updates the intro window logic.
|
||||||
|
--- @within IntroWindow
|
||||||
function IntroWindow.update()
|
function IntroWindow.update()
|
||||||
Context.intro.y = Context.intro.y - Context.intro.speed
|
IntroWindow.y = IntroWindow.y - IntroWindow.speed
|
||||||
|
|
||||||
-- Count lines in intro text to determine when scrolling is done
|
|
||||||
local lines = 1
|
local lines = 1
|
||||||
for _ in string.gmatch(Context.intro.text, "\n") do
|
for _ in string.gmatch(IntroWindow.text, "\n") do
|
||||||
lines = lines + 1
|
lines = lines + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
-- When text is off-screen, go to menu
|
if IntroWindow.y < -lines * 8 then
|
||||||
if Context.intro.y < -lines * 8 then
|
Window.set_current("menu")
|
||||||
GameWindow.set_state(WINDOW_MENU)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Skip intro by pressing A
|
|
||||||
if Input.menu_confirm() then
|
if Input.menu_confirm() then
|
||||||
GameWindow.set_state(WINDOW_MENU)
|
Window.set_current("menu")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
function InventoryWindow.draw()
|
|
||||||
UI.draw_top_bar("Inventory")
|
|
||||||
|
|
||||||
if #Context.inventory == 0 then
|
|
||||||
print("Inventory is empty.", 70, 70, Config.colors.light_grey)
|
|
||||||
else
|
|
||||||
for i, item in ipairs(Context.inventory) do
|
|
||||||
local color = Config.colors.light_grey
|
|
||||||
if i == Context.selected_inventory_item then
|
|
||||||
color = Config.colors.green
|
|
||||||
print(">", 60, 20 + i * 10, color)
|
|
||||||
end
|
|
||||||
print(item.name, 70, 20 + i * 10, color)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function InventoryWindow.update()
|
|
||||||
Context.selected_inventory_item = UI.update_menu(Context.inventory, Context.selected_inventory_item)
|
|
||||||
|
|
||||||
if Input.menu_confirm() and #Context.inventory > 0 then
|
|
||||||
local selected_item = Context.inventory[Context.selected_inventory_item]
|
|
||||||
PopupWindow.show_menu_dialog(selected_item, {
|
|
||||||
{label = "Use", action = Item.use},
|
|
||||||
{label = "Drop", action = Item.drop},
|
|
||||||
{label = "Look at", action = Item.look_at},
|
|
||||||
{label = "Go back", action = Item.go_back_from_inventory_action}
|
|
||||||
}, WINDOW_INVENTORY_ACTION)
|
|
||||||
end
|
|
||||||
|
|
||||||
if Input.menu_back() then
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
53
inc/window/window.manager.lua
Normal file
53
inc/window/window.manager.lua
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
--- @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., SplashWindow).</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
|
||||||
@@ -1,42 +1,87 @@
|
|||||||
|
--- @section MenuWindow
|
||||||
|
local _menu_items = {}
|
||||||
|
|
||||||
|
--- Draws the menu window.
|
||||||
|
--- @within MenuWindow
|
||||||
function MenuWindow.draw()
|
function MenuWindow.draw()
|
||||||
UI.draw_top_bar("Main Menu")
|
UI.draw_top_bar("Main Menu")
|
||||||
UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70)
|
UI.draw_menu(_menu_items, Context.current_menu_item, 108, 70)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Updates the menu window logic.
|
||||||
|
--- @within MenuWindow
|
||||||
function MenuWindow.update()
|
function MenuWindow.update()
|
||||||
Context.selected_menu_item = UI.update_menu(Context.menu_items, Context.selected_menu_item)
|
Context.current_menu_item = UI.update_menu(_menu_items, Context.current_menu_item)
|
||||||
|
|
||||||
if Input.menu_confirm() then
|
if Input.menu_confirm() then
|
||||||
local selected_item = Context.menu_items[Context.selected_menu_item]
|
local selected_item = _menu_items[Context.current_menu_item]
|
||||||
if selected_item and selected_item.action then
|
if selected_item and selected_item.decision then
|
||||||
selected_item.action()
|
Audio.sfx_select()
|
||||||
|
selected_item.decision()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function MenuWindow.play()
|
--- Starts a new game from the menu.
|
||||||
-- Reset player state and screen for a new game
|
--- @within MenuWindow
|
||||||
Context.player.x = Config.player.start_x
|
function MenuWindow.new_game()
|
||||||
Context.player.y = Config.player.start_y
|
Context.new_game()
|
||||||
Context.player.vx = 0
|
GameWindow.set_state("game")
|
||||||
Context.player.vy = 0
|
|
||||||
Context.player.jumps = 0
|
|
||||||
Context.current_screen = 1
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Loads a game from the menu.
|
||||||
|
--- @within MenuWindow
|
||||||
|
function MenuWindow.load_game()
|
||||||
|
Context.load_game()
|
||||||
|
GameWindow.set_state("game")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Saves the current game from the menu.
|
||||||
|
--- @within MenuWindow
|
||||||
|
function MenuWindow.save_game()
|
||||||
|
Context.save_game()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Resumes the game from the menu.
|
||||||
|
--- @within MenuWindow
|
||||||
|
function MenuWindow.resume_game()
|
||||||
|
GameWindow.set_state("game")
|
||||||
|
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(WINDOW_CONFIGURATION)
|
GameWindow.set_state("configuration")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Initialize menu items after actions are defined
|
--- Opens the audio test menu.
|
||||||
Context.menu_items = {
|
--- @within MenuWindow
|
||||||
{label = "Play", action = MenuWindow.play},
|
function MenuWindow.audio_test()
|
||||||
{label = "Configuration", action = MenuWindow.configuration},
|
AudioTestWindow.init()
|
||||||
{label = "Exit", action = MenuWindow.exit}
|
GameWindow.set_state("audiotest")
|
||||||
}
|
end
|
||||||
|
|
||||||
|
--- Refreshes menu items.
|
||||||
|
--- @within MenuWindow
|
||||||
|
function MenuWindow.refresh_menu_items()
|
||||||
|
_menu_items = {}
|
||||||
|
if Context.game_in_progress then
|
||||||
|
table.insert(_menu_items, {label = "Resume Game", decision = MenuWindow.resume_game})
|
||||||
|
table.insert(_menu_items, {label = "Save Game", decision = MenuWindow.save_game})
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(_menu_items, {label = "New Game", decision = MenuWindow.new_game})
|
||||||
|
table.insert(_menu_items, {label = "Load Game", decision = MenuWindow.load_game})
|
||||||
|
table.insert(_menu_items, {label = "Configuration", decision = MenuWindow.configuration})
|
||||||
|
table.insert(_menu_items, {label = "Audio Test", decision = MenuWindow.audio_test})
|
||||||
|
table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit})
|
||||||
|
|
||||||
|
Context.current_menu_item = 1
|
||||||
|
end
|
||||||
|
|||||||
268
inc/window/window.minigame.ddr.lua
Normal file
268
inc/window/window.minigame.ddr.lua
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
--- @section MinigameDDRWindow
|
||||||
|
|
||||||
|
--- Initializes DDR minigame state.
|
||||||
|
--- @within MinigameDDRWindow
|
||||||
|
--- @param params table Optional parameters for configuration.<br/>
|
||||||
|
function MinigameDDRWindow.init(params)
|
||||||
|
Context.minigame_ddr = Minigame.configure_ddr(params)
|
||||||
|
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)
|
||||||
|
MinigameDDRWindow.init(params)
|
||||||
|
Context.minigame_ddr.return_window = return_window or "game"
|
||||||
|
Context.minigame_ddr.debug_song_key = song_key
|
||||||
|
if song_key and Songs and Songs[song_key] then
|
||||||
|
Context.minigame_ddr.current_song = Songs[song_key]
|
||||||
|
Context.minigame_ddr.use_pattern = true
|
||||||
|
Context.minigame_ddr.pattern_index = 1
|
||||||
|
Context.minigame_ddr.debug_status = "Pattern loaded: " .. song_key
|
||||||
|
else
|
||||||
|
Context.minigame_ddr.use_pattern = false
|
||||||
|
if song_key then
|
||||||
|
Context.minigame_ddr.debug_status = "Song not found: " .. tostring(song_key)
|
||||||
|
else
|
||||||
|
Context.minigame_ddr.debug_status = "Random mode"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Window.set_current("minigame_ddr")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Spawns a random arrow.
|
||||||
|
--- @within MinigameDDRWindow
|
||||||
|
local function spawn_arrow()
|
||||||
|
local mg = Context.minigame_ddr
|
||||||
|
local target = mg.target_arrows[math.random(1, 4)]
|
||||||
|
table.insert(mg.arrows, {
|
||||||
|
dir = target.dir,
|
||||||
|
x = target.x,
|
||||||
|
y = mg.bar_y + mg.bar_height + 10
|
||||||
|
})
|
||||||
|
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 mg = Context.minigame_ddr
|
||||||
|
for _, target in ipairs(mg.target_arrows) do
|
||||||
|
if target.dir == direction then
|
||||||
|
table.insert(mg.arrows, {
|
||||||
|
dir = direction,
|
||||||
|
x = target.x,
|
||||||
|
y = mg.bar_y + mg.bar_height + 10
|
||||||
|
})
|
||||||
|
break
|
||||||
|
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 mg = Context.minigame_ddr
|
||||||
|
local distance = math.abs(arrow.y - mg.target_y)
|
||||||
|
return distance <= mg.hit_threshold
|
||||||
|
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 mg = Context.minigame_ddr
|
||||||
|
return arrow.y > mg.target_y + mg.hit_threshold
|
||||||
|
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 size = 12
|
||||||
|
local half = size / 2
|
||||||
|
if direction == "left" then
|
||||||
|
tri(x + half, y, x, y + half, x + half, y + size, color)
|
||||||
|
rect(x + half, y + half - 2, half, 4, color)
|
||||||
|
elseif direction == "right" then
|
||||||
|
tri(x + half, y, x + size, y + half, x + half, y + size, color)
|
||||||
|
rect(x, y + half - 2, half, 4, color)
|
||||||
|
elseif direction == "up" then
|
||||||
|
tri(x, y + half, x + half, y, x + size, y + half, color)
|
||||||
|
rect(x + half - 2, y + half, 4, half, color)
|
||||||
|
elseif direction == "down" then
|
||||||
|
tri(x, y + half, x + half, y + size, x + size, y + half, color)
|
||||||
|
rect(x + half - 2, y, 4, half, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Updates DDR minigame logic.
|
||||||
|
--- @within MinigameDDRWindow
|
||||||
|
function MinigameDDRWindow.update()
|
||||||
|
local mg = Context.minigame_ddr
|
||||||
|
if mg.bar_fill >= mg.max_fill then
|
||||||
|
Meter.on_minigame_complete()
|
||||||
|
Meter.show()
|
||||||
|
Window.set_current(mg.return_window)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
mg.frame_counter = mg.frame_counter + 1
|
||||||
|
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
|
||||||
|
Meter.on_minigame_complete()
|
||||||
|
Meter.show()
|
||||||
|
Window.set_current(mg.return_window)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if mg.use_pattern and mg.current_song and mg.current_song.pattern then
|
||||||
|
local pattern = mg.current_song.pattern
|
||||||
|
while mg.pattern_index <= #pattern do
|
||||||
|
local spawn_entry = pattern[mg.pattern_index]
|
||||||
|
if mg.frame_counter >= spawn_entry.frame then
|
||||||
|
spawn_arrow_dir(spawn_entry.dir)
|
||||||
|
mg.pattern_index = mg.pattern_index + 1
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mg.arrow_spawn_timer = mg.arrow_spawn_timer + 1
|
||||||
|
if mg.arrow_spawn_timer >= mg.arrow_spawn_interval then
|
||||||
|
spawn_arrow()
|
||||||
|
mg.arrow_spawn_timer = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local arrows_to_remove = {}
|
||||||
|
for i, arrow in ipairs(mg.arrows) do
|
||||||
|
arrow.y = arrow.y + mg.arrow_fall_speed
|
||||||
|
if check_miss(arrow) then
|
||||||
|
table.insert(arrows_to_remove, i)
|
||||||
|
mg.bar_fill = mg.bar_fill - mg.miss_penalty
|
||||||
|
if mg.bar_fill < 0 then
|
||||||
|
mg.bar_fill = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- iterate backwards to avoid index shift issues
|
||||||
|
for i = #arrows_to_remove, 1, -1 do
|
||||||
|
table.remove(mg.arrows, arrows_to_remove[i])
|
||||||
|
end
|
||||||
|
for dir, _ in pairs(mg.input_cooldowns) do
|
||||||
|
if mg.input_cooldowns[dir] > 0 then
|
||||||
|
mg.input_cooldowns[dir] = mg.input_cooldowns[dir] - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for dir, _ in pairs(mg.button_pressed_timers) do
|
||||||
|
if mg.button_pressed_timers[dir] > 0 then
|
||||||
|
mg.button_pressed_timers[dir] = mg.button_pressed_timers[dir] - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local input_map = {
|
||||||
|
left = Input.left(),
|
||||||
|
down = Input.down(),
|
||||||
|
up = Input.up(),
|
||||||
|
right = Input.right()
|
||||||
|
}
|
||||||
|
for dir, pressed in pairs(input_map) do
|
||||||
|
if pressed and mg.input_cooldowns[dir] == 0 then
|
||||||
|
mg.input_cooldowns[dir] = mg.input_cooldown_duration
|
||||||
|
mg.button_pressed_timers[dir] = mg.button_press_duration
|
||||||
|
local hit = false
|
||||||
|
for i, arrow in ipairs(mg.arrows) do
|
||||||
|
if arrow.dir == dir and check_hit(arrow) then
|
||||||
|
mg.bar_fill = mg.bar_fill + mg.fill_per_hit
|
||||||
|
if mg.bar_fill > mg.max_fill then
|
||||||
|
mg.bar_fill = mg.max_fill
|
||||||
|
end
|
||||||
|
table.remove(mg.arrows, i)
|
||||||
|
hit = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not hit then
|
||||||
|
mg.bar_fill = mg.bar_fill - 2
|
||||||
|
if mg.bar_fill < 0 then
|
||||||
|
mg.bar_fill = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws DDR minigame.
|
||||||
|
--- @within MinigameDDRWindow
|
||||||
|
function MinigameDDRWindow.draw()
|
||||||
|
local mg = Context.minigame_ddr
|
||||||
|
if not mg then
|
||||||
|
cls(0)
|
||||||
|
print("DDR ERROR: Context not initialized", 10, 10, 12)
|
||||||
|
print("Press Z to return", 10, 20, 12)
|
||||||
|
if Input.select() then
|
||||||
|
Window.set_current("game")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if mg.return_window == "game" then
|
||||||
|
GameWindow.draw()
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
if fill_width > 0 then
|
||||||
|
local bar_color = Config.colors.light_blue
|
||||||
|
if mg.bar_fill > 66 then
|
||||||
|
bar_color = Config.colors.item
|
||||||
|
elseif mg.bar_fill > 33 then
|
||||||
|
bar_color = Config.colors.blue
|
||||||
|
end
|
||||||
|
rect(mg.bar_x, mg.bar_y, fill_width, mg.bar_height, bar_color)
|
||||||
|
end
|
||||||
|
local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100)
|
||||||
|
Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black)
|
||||||
|
if mg.target_arrows then
|
||||||
|
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 color = is_pressed and Config.colors.light_blue or Config.colors.light_grey
|
||||||
|
draw_arrow(target.x, mg.target_y, target.dir, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if mg.arrows then
|
||||||
|
for _, arrow in ipairs(mg.arrows) do
|
||||||
|
draw_arrow(arrow.x, arrow.y, arrow.dir, Config.colors.blue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Print.text_center("Hit the arrows!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
|
||||||
|
local debug_y = 60
|
||||||
|
if mg.debug_status then
|
||||||
|
Print.text_center(mg.debug_status, Config.screen.width / 2, debug_y, Config.colors.item)
|
||||||
|
debug_y = debug_y + 10
|
||||||
|
end
|
||||||
|
if mg.use_pattern then
|
||||||
|
Print.text_center(
|
||||||
|
"PATTERN MODE - Frame:" .. mg.frame_counter,
|
||||||
|
Config.screen.width / 2,
|
||||||
|
debug_y,
|
||||||
|
Config.colors.light_blue
|
||||||
|
)
|
||||||
|
if mg.current_song and mg.current_song.pattern then
|
||||||
|
Print.text_center(
|
||||||
|
"Pattern Len:" .. #mg.current_song.pattern .. " Index:" .. mg.pattern_index,
|
||||||
|
Config.screen.width / 2,
|
||||||
|
debug_y + 10,
|
||||||
|
Config.colors.light_blue
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Print.text_center("RANDOM MODE", Config.screen.width / 2, debug_y, Config.colors.blue)
|
||||||
|
end
|
||||||
|
end
|
||||||
89
inc/window/window.minigame.mash.lua
Normal file
89
inc/window/window.minigame.mash.lua
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
--- Initializes button mash minigame state.
|
||||||
|
--- @within MinigameButtonMashWindow
|
||||||
|
--- @param params table Optional parameters for configuration.<br/>
|
||||||
|
function MinigameButtonMashWindow.init(params)
|
||||||
|
Context.minigame_button_mash = Minigame.configure_button_mash(params)
|
||||||
|
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)
|
||||||
|
MinigameButtonMashWindow.init(params)
|
||||||
|
local mg = Context.minigame_button_mash
|
||||||
|
mg.return_window = return_window or "game"
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Updates button mash minigame logic.
|
||||||
|
--- @within MinigameButtonMashWindow
|
||||||
|
function MinigameButtonMashWindow.update()
|
||||||
|
local mg = Context.minigame_button_mash
|
||||||
|
if Input.select() then
|
||||||
|
mg.bar_fill = mg.bar_fill + mg.fill_per_press
|
||||||
|
mg.button_pressed_timer = mg.button_press_duration
|
||||||
|
if mg.bar_fill > mg.max_fill then
|
||||||
|
mg.bar_fill = mg.max_fill
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if mg.bar_fill >= mg.max_fill then
|
||||||
|
Meter.on_minigame_complete()
|
||||||
|
Meter.show()
|
||||||
|
if mg.focus_center_x then Focus.stop() end
|
||||||
|
Window.set_current(mg.return_window)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local degradation = mg.base_degradation + (mg.bar_fill * mg.degradation_multiplier)
|
||||||
|
mg.bar_fill = mg.bar_fill - degradation
|
||||||
|
if mg.bar_fill < 0 then
|
||||||
|
mg.bar_fill = 0
|
||||||
|
end
|
||||||
|
if mg.button_pressed_timer > 0 then
|
||||||
|
mg.button_pressed_timer = mg.button_pressed_timer - 1
|
||||||
|
end
|
||||||
|
if mg.focus_center_x then
|
||||||
|
Focus.set_percentage(mg.bar_fill / mg.max_fill)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws button mash minigame.
|
||||||
|
--- @within MinigameButtonMashWindow
|
||||||
|
function MinigameButtonMashWindow.draw()
|
||||||
|
local mg = Context.minigame_button_mash
|
||||||
|
if mg.return_window == "game" then
|
||||||
|
GameWindow.draw()
|
||||||
|
end
|
||||||
|
if not mg.focus_center_x then
|
||||||
|
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
local fill_width = (mg.bar_fill / mg.max_fill) * mg.bar_width
|
||||||
|
if fill_width > 0 then
|
||||||
|
local bar_color = Config.colors.light_blue
|
||||||
|
if mg.bar_fill > 66 then
|
||||||
|
bar_color = Config.colors.item
|
||||||
|
elseif mg.bar_fill > 33 then
|
||||||
|
bar_color = Config.colors.blue
|
||||||
|
end
|
||||||
|
rect(mg.bar_x, mg.bar_y, fill_width, mg.bar_height, bar_color)
|
||||||
|
end
|
||||||
|
local button_color = Config.colors.light_grey
|
||||||
|
if mg.button_pressed_timer > 0 then
|
||||||
|
button_color = Config.colors.light_blue
|
||||||
|
end
|
||||||
|
circb(mg.button_x, mg.button_y, mg.button_size, button_color)
|
||||||
|
if mg.button_pressed_timer > 0 then
|
||||||
|
circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color)
|
||||||
|
end
|
||||||
|
Print.text_center(" Z", mg.button_x - 2, mg.button_y - 3, Config.colors.light_grey)
|
||||||
|
Print.text_center("MASH Z!", Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
|
||||||
|
local percentage = math.floor((mg.bar_fill / mg.max_fill) * 100)
|
||||||
|
Print.text_center(percentage .. "%", mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black)
|
||||||
|
end
|
||||||
110
inc/window/window.minigame.rhythm.lua
Normal file
110
inc/window/window.minigame.rhythm.lua
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
--- @section MinigameRhythmWindow
|
||||||
|
|
||||||
|
--- Initializes rhythm minigame state.
|
||||||
|
--- @within MinigameRhythmWindow
|
||||||
|
--- @param params table Optional parameters for configuration.<br/>
|
||||||
|
function MinigameRhythmWindow.init(params)
|
||||||
|
Context.minigame_rhythm = Minigame.configure_rhythm(params)
|
||||||
|
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)
|
||||||
|
MinigameRhythmWindow.init(params)
|
||||||
|
local mg = Context.minigame_rhythm
|
||||||
|
mg.return_window = return_window or "game"
|
||||||
|
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
|
||||||
|
|
||||||
|
--- Updates rhythm minigame logic.
|
||||||
|
--- @within MinigameRhythmWindow
|
||||||
|
function MinigameRhythmWindow.update()
|
||||||
|
local mg = Context.minigame_rhythm
|
||||||
|
mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction)
|
||||||
|
if mg.line_position > 1 then
|
||||||
|
mg.line_position = 1
|
||||||
|
mg.line_direction = -1
|
||||||
|
elseif mg.line_position < 0 then
|
||||||
|
mg.line_position = 0
|
||||||
|
mg.line_direction = 1
|
||||||
|
end
|
||||||
|
if mg.press_cooldown > 0 then
|
||||||
|
mg.press_cooldown = mg.press_cooldown - 1
|
||||||
|
end
|
||||||
|
if Input.select() and mg.press_cooldown == 0 then
|
||||||
|
mg.button_pressed_timer = mg.button_press_duration
|
||||||
|
mg.press_cooldown = mg.press_cooldown_duration
|
||||||
|
local target_left = mg.target_center - (mg.target_width / 2)
|
||||||
|
local target_right = mg.target_center + (mg.target_width / 2)
|
||||||
|
if mg.line_position >= target_left and mg.line_position <= target_right then
|
||||||
|
mg.score = mg.score + 1
|
||||||
|
else
|
||||||
|
mg.score = mg.score - 1
|
||||||
|
if mg.score < 0 then
|
||||||
|
mg.score = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
mg.target_width = mg.initial_target_width * (mg.target_shrink_rate ^ mg.score)
|
||||||
|
if mg.target_width < mg.min_target_width then
|
||||||
|
mg.target_width = mg.min_target_width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if mg.score >= mg.max_score then
|
||||||
|
Meter.on_minigame_complete()
|
||||||
|
Meter.show()
|
||||||
|
if mg.focus_center_x then Focus.stop() end
|
||||||
|
Window.set_current(mg.return_window)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if mg.button_pressed_timer > 0 then
|
||||||
|
mg.button_pressed_timer = mg.button_pressed_timer - 1
|
||||||
|
end
|
||||||
|
if mg.focus_center_x then
|
||||||
|
Focus.set_percentage(1 - mg.score / mg.max_score)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Draws rhythm minigame.
|
||||||
|
--- @within MinigameRhythmWindow
|
||||||
|
function MinigameRhythmWindow.draw()
|
||||||
|
local mg = Context.minigame_rhythm
|
||||||
|
if mg.return_window == "game" then
|
||||||
|
GameWindow.draw()
|
||||||
|
end
|
||||||
|
if not mg.focus_center_x then
|
||||||
|
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
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_x = mg.bar_x + (target_left * 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)
|
||||||
|
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)
|
||||||
|
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(
|
||||||
|
"Press Z when line is in green!",
|
||||||
|
Config.screen.width / 2,
|
||||||
|
mg.bar_y + mg.bar_height + 20,
|
||||||
|
Config.colors.light_grey
|
||||||
|
)
|
||||||
|
local button_color = Config.colors.light_grey
|
||||||
|
if mg.button_pressed_timer > 0 then
|
||||||
|
button_color = Config.colors.light_blue
|
||||||
|
end
|
||||||
|
circb(mg.button_x, mg.button_y, mg.button_size, button_color)
|
||||||
|
if mg.button_pressed_timer > 0 then
|
||||||
|
circ(mg.button_x, mg.button_y, mg.button_size - 2, button_color)
|
||||||
|
end
|
||||||
|
Print.text_center("Z", mg.button_x - 2, mg.button_y - 3, button_color)
|
||||||
|
end
|
||||||
@@ -1,102 +1,52 @@
|
|||||||
function PopupWindow.set_dialog_node(node_key)
|
--- @section PopupWindow
|
||||||
local npc = Context.dialog.active_entity
|
local POPUP_X = 40
|
||||||
local node = npc.dialog[node_key]
|
local POPUP_Y = 40
|
||||||
|
local POPUP_WIDTH = 160
|
||||||
|
local POPUP_HEIGHT = 80
|
||||||
|
local TEXT_MARGIN_X = POPUP_X + 10
|
||||||
|
local TEXT_MARGIN_Y = POPUP_Y + 10
|
||||||
|
local LINE_HEIGHT = 8
|
||||||
|
|
||||||
if not node then
|
--- Displays a popup window.
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
--- @within PopupWindow
|
||||||
return
|
--- @param content_strings table A table of strings to display in the popup.</br>
|
||||||
end
|
function PopupWindow.show(content_strings)
|
||||||
|
Context.popup.show = true
|
||||||
Context.dialog.current_node_key = node_key
|
Context.popup.content = content_strings or {}
|
||||||
Context.dialog.text = node.text
|
GameWindow.set_state("popup")
|
||||||
|
|
||||||
local menu_items = {}
|
|
||||||
if node.options then
|
|
||||||
for _, option in ipairs(node.options) do
|
|
||||||
table.insert(menu_items, {
|
|
||||||
label = option.label,
|
|
||||||
action = function()
|
|
||||||
PopupWindow.set_dialog_node(option.next_node)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- if no options, it's the end of this branch.
|
|
||||||
if #menu_items == 0 then
|
|
||||||
table.insert(menu_items, {
|
|
||||||
label = "Go back",
|
|
||||||
action = function() GameWindow.set_state(WINDOW_GAME) end
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
Context.dialog.menu_items = menu_items
|
|
||||||
Context.dialog.selected_menu_item = 1
|
|
||||||
Context.dialog.showing_description = false
|
|
||||||
GameWindow.set_state(WINDOW_POPUP)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Hides the popup window.
|
||||||
|
--- @within PopupWindow
|
||||||
|
function PopupWindow.hide()
|
||||||
|
Context.popup.show = false
|
||||||
|
Context.popup.content = {}
|
||||||
|
GameWindow.set_state("game")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Updates popup window logic.
|
||||||
|
--- @within PopupWindow
|
||||||
function PopupWindow.update()
|
function PopupWindow.update()
|
||||||
if Context.dialog.showing_description then
|
if Context.popup.show then
|
||||||
if Input.menu_confirm() or Input.menu_back() then
|
if Input.menu_confirm() or Input.menu_back() then
|
||||||
Context.dialog.showing_description = false
|
PopupWindow.hide()
|
||||||
Context.dialog.text = "" -- Clear the description text
|
|
||||||
-- No need to change active_window, as it remains in WINDOW_POPUP or WINDOW_INVENTORY_ACTION
|
|
||||||
end
|
|
||||||
else
|
|
||||||
Context.dialog.selected_menu_item = UI.update_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item)
|
|
||||||
|
|
||||||
if Input.menu_confirm() then
|
|
||||||
local selected_item = Context.dialog.menu_items[Context.dialog.selected_menu_item]
|
|
||||||
if selected_item and selected_item.action then
|
|
||||||
selected_item.action()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if Input.menu_back() then
|
|
||||||
GameWindow.set_state(WINDOW_GAME)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function PopupWindow.show_menu_dialog(entity, menu_items, dialog_active_window)
|
--- Draws the popup window.
|
||||||
Context.dialog.active_entity = entity
|
--- @within PopupWindow
|
||||||
Context.dialog.text = "" -- Initial dialog text is empty, name is title
|
|
||||||
GameWindow.set_state(dialog_active_window or WINDOW_POPUP)
|
|
||||||
Context.dialog.showing_description = false
|
|
||||||
Context.dialog.menu_items = menu_items
|
|
||||||
Context.dialog.selected_menu_item = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function PopupWindow.show_description_dialog(entity, description_text)
|
|
||||||
Context.dialog.active_entity = entity
|
|
||||||
Context.dialog.text = description_text
|
|
||||||
GameWindow.set_state(WINDOW_POPUP)
|
|
||||||
Context.dialog.showing_description = true
|
|
||||||
-- No menu items needed for description dialog
|
|
||||||
end
|
|
||||||
|
|
||||||
function PopupWindow.draw()
|
function PopupWindow.draw()
|
||||||
rect(40, 40, 160, 80, Config.colors.black)
|
if Context.popup.show then
|
||||||
rectb(40, 40, 160, 80, Config.colors.green)
|
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)
|
||||||
|
|
||||||
-- Display the entity's name as the dialog title
|
local current_y = TEXT_MARGIN_Y
|
||||||
if Context.dialog.active_entity and Context.dialog.active_entity.name then
|
for _, line in ipairs(Context.popup.content) do
|
||||||
print(Context.dialog.active_entity.name, 120 - #Context.dialog.active_entity.name * 2, 45, Config.colors.green)
|
Print.text(line, TEXT_MARGIN_X, current_y, Config.colors.light_grey)
|
||||||
end
|
current_y = current_y + LINE_HEIGHT
|
||||||
|
end
|
||||||
|
|
||||||
-- Display the dialog content (description for "look at", or initial name/dialog for others)
|
Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.light_blue)
|
||||||
local wrapped_lines = UI.word_wrap(Context.dialog.text, 25) -- Max 25 chars per line
|
|
||||||
local current_y = 55 -- Starting Y position for the first line of content
|
|
||||||
for _, line in ipairs(wrapped_lines) do
|
|
||||||
print(line, 50, current_y, Config.colors.light_grey)
|
|
||||||
current_y = current_y + 8 -- Move to the next line (8 pixels for default font height + padding)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Adjust menu position based on the number of wrapped lines
|
|
||||||
if not Context.dialog.showing_description then
|
|
||||||
UI.draw_menu(Context.dialog.menu_items, Context.dialog.selected_menu_item, 50, current_y + 2)
|
|
||||||
else
|
|
||||||
print("[A] Go Back", 50, current_y + 10, Config.colors.green)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
29
inc/window/window.register.lua
Normal file
29
inc/window/window.register.lua
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
SplashWindow = {}
|
||||||
|
Window.register("splash", SplashWindow)
|
||||||
|
|
||||||
|
IntroWindow = {}
|
||||||
|
Window.register("intro", IntroWindow)
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
|
--- Draws the splash window.
|
||||||
|
--- @within SplashWindow
|
||||||
function SplashWindow.draw()
|
function SplashWindow.draw()
|
||||||
print("Mr. Anderson's", 78, 60, Config.colors.green)
|
local txt = "Definitely not an Impostor"
|
||||||
print("Addventure", 90, 70, Config.colors.green)
|
local y = (Config.screen.height - 6) / 2
|
||||||
|
Print.text_center(txt, Config.screen.width / 2, y, Config.colors.white)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Updates the splash window logic.
|
||||||
|
--- @within SplashWindow
|
||||||
function SplashWindow.update()
|
function SplashWindow.update()
|
||||||
Context.splash_timer = Context.splash_timer - 1
|
Context.splash_timer = Context.splash_timer - 1
|
||||||
if Context.splash_timer <= 0 or Input.menu_confirm() then
|
if Context.splash_timer <= 0 or Input.menu_confirm() then
|
||||||
GameWindow.set_state(WINDOW_INTRO)
|
Window.set_current("intro")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
289
infra.md
Normal file
289
infra.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# Server
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Internet --> Nginx
|
||||||
|
|
||||||
|
Nginx --> Traefik
|
||||||
|
Traefik --> Gitea
|
||||||
|
Gitea --> GiteaDB[(Gitea data / SQLite)]
|
||||||
|
|
||||||
|
Traefik --> WoodpeckerServer
|
||||||
|
WoodpeckerServer --> WoodpeckerDB[(Woodpecker data / SQLite)]
|
||||||
|
WoodpeckerServer --> WoodpeckerAgent
|
||||||
|
WoodpeckerAgent --> DockerSocket[(Docker)]
|
||||||
|
|
||||||
|
Traefik --> WebApp
|
||||||
|
WebApp --> MySQL[(MySQL)]
|
||||||
|
WebApp --> Softwares[(Volume)]
|
||||||
|
|
||||||
|
Droparea --> Softwares
|
||||||
|
|
||||||
|
Nginx --> Discourse
|
||||||
|
Discourse --> ForumDB[(Postgres)]
|
||||||
|
Discourse --> Redis[(Redis)]
|
||||||
|
|
||||||
|
Nginx --> Wiki
|
||||||
|
Wiki --> WikiDB[(Postgres)]
|
||||||
|
```
|
||||||
|
|
||||||
|
# TIC-80 Pipeline
|
||||||
|
|
||||||
|
This document describes the Woodpecker CI pipeline used to build, export, upload, and publish a TIC-80 game project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The pipeline performs the following steps:
|
||||||
|
|
||||||
|
1. **Build** the TIC-80 project using a custom Docker image
|
||||||
|
2. **Export** the game to `.tic` and HTML formats
|
||||||
|
3. **Upload artifacts** to a remote server via SCP
|
||||||
|
4. **Notify an update server** to publish the new version
|
||||||
|
|
||||||
|
The pipeline is driven by environment variables so it can be reused across projects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Global Environment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment: &environment
|
||||||
|
GAME_NAME: mranderson
|
||||||
|
GAME_LANG: lua
|
||||||
|
```
|
||||||
|
|
||||||
|
- **GAME_NAME**: Project name (used for all outputs)
|
||||||
|
- **GAME_LANG**: Source language used by TIC-80 (Lua)
|
||||||
|
|
||||||
|
The anchor (`&environment`) allows reuse across steps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Build & Export
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: build
|
||||||
|
image: git.teletype.hu/internal/tic80pro:latest
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
XDG_RUNTIME_DIR: /tmp
|
||||||
|
commands:
|
||||||
|
- make build
|
||||||
|
- make export
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Uses a custom TIC-80 Pro Docker image hosted in Gitea
|
||||||
|
- Runs the Makefile `build` target to assemble source files
|
||||||
|
- Runs the `export` target to generate:
|
||||||
|
- `.tic` cartridge
|
||||||
|
- `.html.zip` web build
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Artifact Upload
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: artifact
|
||||||
|
image: alpine
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
DROPAREA_HOST: vps.teletype.hu
|
||||||
|
DROPAREA_PORT: 2223
|
||||||
|
DROPAREA_TARGET_PATH: /home/drop
|
||||||
|
DROPAREA_USER: drop
|
||||||
|
DROPAREA_SSH_PASSWORD:
|
||||||
|
from_secret: droparea_ssh_password
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache openssh-client sshpass
|
||||||
|
- mkdir -p /root/.ssh
|
||||||
|
- sshpass -p $DROPAREA_SSH_PASSWORD scp -o StrictHostKeyChecking=no -P $DROPAREA_PORT \
|
||||||
|
$GAME_NAME.$GAME_LANG \
|
||||||
|
$GAME_NAME.tic \
|
||||||
|
$GAME_NAME.html.zip \
|
||||||
|
$DROPAREA_USER@$DROPAREA_HOST:$DROPAREA_TARGET_PATH
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Installs SCP tooling in a minimal Alpine container
|
||||||
|
- Uploads:
|
||||||
|
- Source file
|
||||||
|
- TIC-80 cartridge
|
||||||
|
- HTML export ZIP
|
||||||
|
- Uses secrets for SSH authentication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Update Notification
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: update
|
||||||
|
image: alpine
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||||
|
UPDATE_SECRET:
|
||||||
|
from_secret: update_secret_key
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- curl "$UPDATE_SERVER/update?secret=$UPDATE_SECRET&name=$GAME_NAME&platform=tic80"
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
- Sends an HTTP request to the update server
|
||||||
|
- Notifies that a new TIC-80 build is available
|
||||||
|
- Uses a secret key to authorize the update
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
After a successful run:
|
||||||
|
|
||||||
|
- The game is built and exported
|
||||||
|
- Artifacts are uploaded to the server
|
||||||
|
- The public game index is updated automatically
|
||||||
|
|
||||||
|
This pipeline enables **fully automated TIC-80 releases** using open tools and infrastructure.
|
||||||
|
|
||||||
|
|
||||||
|
# TIC-80 Makefile Project Builder
|
||||||
|
|
||||||
|
This Makefile provides a simple, reproducible workflow for building **TIC-80 Lua projects** from multiple source files. It is designed for small indie or experimental projects where the source code is split into logical parts and then merged into a single `.lua` cartridge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The workflow is based on four core ideas:
|
||||||
|
|
||||||
|
- Source code is split into multiple Lua files inside an `inc/` directory
|
||||||
|
- A project-specific `.inc` file defines the **build order**
|
||||||
|
- All source files are concatenated into one final `.lua` file
|
||||||
|
- TIC-80 is used in CLI mode to export runnable artifacts
|
||||||
|
|
||||||
|
This approach keeps the codebase modular while remaining compatible with TIC-80’s single-file cartridge model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
project-root/
|
||||||
|
├── inc/
|
||||||
|
│ ├── core.lua
|
||||||
|
│ ├── player.lua
|
||||||
|
│ └── world.lua
|
||||||
|
├── mranderson.inc
|
||||||
|
├── Makefile
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- `inc/` contains all Lua source fragments
|
||||||
|
- `<project>.inc` defines the order in which files are merged
|
||||||
|
- `<project>.lua` is generated automatically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The `.inc` File
|
||||||
|
|
||||||
|
The `.inc` file is a **plain text file** listing Lua source files in build order:
|
||||||
|
|
||||||
|
```text
|
||||||
|
core.lua
|
||||||
|
player.lua
|
||||||
|
world.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
The order matters. Files listed earlier are concatenated first and must define any globals used later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Build (default)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
- Reads `mranderson.inc`
|
||||||
|
- Concatenates files from `inc/`
|
||||||
|
- Produces `mranderson.lua`
|
||||||
|
|
||||||
|
### Export (TIC-80)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make export
|
||||||
|
```
|
||||||
|
|
||||||
|
- Loads the generated Lua file into TIC-80 (CLI mode)
|
||||||
|
- Saves a `.tic` cartridge
|
||||||
|
- Exports an HTML build
|
||||||
|
|
||||||
|
### Export Assets
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make export_assets
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Purpose**: Extracts asset sections (PALETTE, TILES, SPRITES, MAP, SFX, MUSIC) from the compiled `<project>.lua` file.
|
||||||
|
- **Mechanism**: Uses `sed` to directly parse the generated `<project>.lua` and saves the extracted data into `inc/meta/meta.assets.lua`. This file can then be used to embed the asset data directly into other parts of the project or for version control of visual assets.
|
||||||
|
|
||||||
|
### Import Assets
|
||||||
|
|
||||||
|
The `import_assets` target was considered during development but is currently not part of the build workflow. Asset handling for TIC-80 projects within this Makefile relies solely on direct extraction (`export_assets`) from the built Lua cartridge, rather than importing external asset definitions. This target may be implemented in the future if a need for pre-build asset injection arises.
|
||||||
|
|
||||||
|
### Watch Mode
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make watch
|
||||||
|
```
|
||||||
|
|
||||||
|
- Performs an initial build
|
||||||
|
- Watches the `inc/` directory and `.inc` file
|
||||||
|
- Rebuilds automatically on any change
|
||||||
|
|
||||||
|
Requires `fswatch` to be installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generated Artifacts
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `<project>.lua` | Merged Lua source (input for TIC-80) |
|
||||||
|
| `<project>.tic` | TIC-80 cartridge |
|
||||||
|
| `<project>.html` | Web export |
|
||||||
|
| `<project>.html.zip` | Packaged HTML build |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
|
||||||
|
- Keep TIC-80 projects modular
|
||||||
|
- Avoid manual copy-paste between files
|
||||||
|
- Enable fast iteration and experimentation
|
||||||
|
- Remain fully compatible with open-source tooling
|
||||||
|
|
||||||
|
This Makefile is intentionally minimal and transparent, favoring simplicity over abstraction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `make`
|
||||||
|
- `tic80` available in PATH
|
||||||
|
- `fswatch` (only for watch mode)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License — free to use, modify, and redistribute.
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
meta/meta.header.lua
|
|
||||||
init/init.config.lua
|
|
||||||
init/init.windows.lua
|
|
||||||
init/init.modules.lua
|
|
||||||
init/init.context.lua
|
|
||||||
entity/entity.npc.lua
|
|
||||||
entity/entity.item.lua
|
|
||||||
entity/entity.player.lua
|
|
||||||
system/system.input.lua
|
|
||||||
system/system.ui.lua
|
|
||||||
window/window.splash.lua
|
|
||||||
window/window.intro.lua
|
|
||||||
window/window.menu.lua
|
|
||||||
window/window.configuration.lua
|
|
||||||
window/window.popup.lua
|
|
||||||
window/window.inventory.lua
|
|
||||||
window/window.game.lua
|
|
||||||
system/system.main.lua
|
|
||||||
meta/meta.assets.lua
|
|
||||||
Reference in New Issue
Block a user