Compare commits
157 Commits
1913d7d7d4
...
screen-exi
| Author | SHA1 | Date | |
|---|---|---|---|
| 883ad5fcbf | |||
| 30c894c2f0 | |||
| eb30ac0b0b | |||
| cd85a7214c | |||
| a00db92703 | |||
| 9c18812e95 | |||
| 10b7dfd24b | |||
| 4d8f01187b | |||
|
|
a208f0d27a | ||
| e2c5b38ecc | |||
|
|
e40214c45b | ||
|
|
b349ded281 | ||
| c897cdcbd5 | |||
|
|
5346281c5c | ||
|
|
5daf98fd06 | ||
| 5a0c8ef19d | |||
| 1c25a077a5 | |||
| fd983dd6e1 | |||
| 220ca27128 | |||
| 3bb1fb7941 | |||
| 24ce240f97 | |||
| 5d78cffc99 | |||
| d1e7704d66 | |||
| 71a21f3f86 | |||
| 5383fa3240 | |||
|
|
f3846a283e | ||
|
|
732c8b34c8 | ||
| 53a3f37c8e | |||
|
|
9b49e13a5d | ||
|
|
3f2df7d512 | ||
| 3db1ae1064 | |||
| f800cfff9b | |||
| 649b73b3f8 | |||
| cd41cb3312 | |||
| ecd094e2d7 | |||
| 5b4f3052a6 | |||
| 83301a1a8e | |||
| 77bd76f6f9 | |||
| aa7d0e902d | |||
| cb19251556 | |||
| 83f801210e | |||
| a732b5cbfa | |||
| c39e0ece35 | |||
| 03b6567c08 | |||
| 41eea638c0 | |||
| 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
|
||||
77
.luacheckrc
Normal file
77
.luacheckrc
Normal file
@@ -0,0 +1,77 @@
|
||||
-- .luacheckrc
|
||||
-- Configuration for luacheck
|
||||
|
||||
globals = {
|
||||
"Focus",
|
||||
"Day",
|
||||
"Timer",
|
||||
"Glitch",
|
||||
"Trigger",
|
||||
"Discussion",
|
||||
"Util",
|
||||
"Decision",
|
||||
"Situation",
|
||||
"Screen",
|
||||
"Sprite",
|
||||
"UI",
|
||||
"Print",
|
||||
"Input",
|
||||
"Audio",
|
||||
"AsciiArt",
|
||||
"Config",
|
||||
"Context",
|
||||
"Meter",
|
||||
"Minigame",
|
||||
"Window",
|
||||
"ContinuedWindow",
|
||||
"TTGIntroWindow",
|
||||
"BriefIntroWindow",
|
||||
"TitleIntroWindow",
|
||||
"MenuWindow",
|
||||
"GameWindow",
|
||||
"PopupWindow",
|
||||
"ConfigurationWindow",
|
||||
"AudioTestWindow",
|
||||
"MinigameButtonMashWindow",
|
||||
"MinigameRhythmWindow",
|
||||
"MinigameDDRWindow",
|
||||
"MysteriousManWindow",
|
||||
"DiscussionWindow",
|
||||
"EndWindow",
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
29
.vscode/tasks.json
vendored
Normal file
29
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
// 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",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Export assets",
|
||||
"type": "shell",
|
||||
"command": "make export_assets",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Make build",
|
||||
"type": "shell",
|
||||
"command": "make build"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +1,45 @@
|
||||
environment: &environment
|
||||
GAME_NAME: mranderson
|
||||
GAME_LANG: lua
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: gitea.vps.teletype.hu/games/tic80pro:latest
|
||||
- name: version
|
||||
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
|
||||
XDG_RUNTIME_DIR: /tmp
|
||||
commands:
|
||||
- make build PROJECT=$GAME_NAME
|
||||
- make export PROJECT=$GAME_NAME
|
||||
- 'make ci-export'
|
||||
|
||||
- name: artifact
|
||||
image: alpine
|
||||
environment:
|
||||
<<: *environment
|
||||
DROPAREA_HOST: vps.teletype.hu
|
||||
DROPAREA_PORT: 2223
|
||||
DROPAREA_TARGET_PATH: /home/drop
|
||||
@@ -23,17 +47,15 @@ steps:
|
||||
DROPAREA_SSH_PASSWORD:
|
||||
from_secret: droparea_ssh_password
|
||||
commands:
|
||||
- apk add --no-cache openssh-client sshpass
|
||||
- mkdir -p /root/.ssh
|
||||
- sshpass -p $DROPAREA_SSH_PASSWORD scp -o StrictHostKeyChecking=no -P $DROPAREA_PORT $GAME_NAME.$GAME_LANG $GAME_NAME.tic $GAME_NAME.html.zip $DROPAREA_USER@$DROPAREA_HOST:$DROPAREA_TARGET_PATH
|
||||
- 'apk add --no-cache make openssh-client sshpass'
|
||||
- 'make ci-artifact'
|
||||
|
||||
- name: update
|
||||
image: alpine
|
||||
environment:
|
||||
<<: *environment
|
||||
UPDATE_SERVER: https://games.vps.teletype.hu
|
||||
UPDATE_SERVER: https://games.teletype.hu
|
||||
UPDATE_SECRET:
|
||||
from_secret: update_secret_key
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- curl "$UPDATE_SERVER/update?secret=$UPDATE_SECRET&name=$GAME_NAME&platform=tic80"
|
||||
- 'apk add --no-cache make curl'
|
||||
- 'make ci-update'
|
||||
|
||||
107
GEMINI.md
107
GEMINI.md
@@ -1,6 +1,6 @@
|
||||
# TIC-80 Lua Code Regularities
|
||||
|
||||
Based on the analysis of `mranderson.lua`, the following regularities and conventions should be followed for future modifications and development within this project:
|
||||
Based on the analysis of `impostor.lua`, the following regularities and conventions should be followed for future modifications and development within this project:
|
||||
|
||||
## General Structure & Lifecycle
|
||||
|
||||
@@ -39,7 +39,7 @@ Based on the analysis of `mranderson.lua`, the following regularities and conven
|
||||
|
||||
11. **`spr()` for Sprites:** Individual sprites should be rendered using the `spr()` function.
|
||||
12. **`map()` for Maps:** Tilemaps should be drawn using the `map()` function.
|
||||
13. **`print()` for Text:** Text display should utilize the `print()` function.
|
||||
13. **`Print.text()` for Text:** Text display should utilize the `Print.text()` function.
|
||||
|
||||
## Code Style
|
||||
|
||||
@@ -52,3 +52,106 @@ Based on the analysis of `mranderson.lua`, the following regularities and conven
|
||||
## Agent Directives
|
||||
|
||||
- **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
|
||||
|
||||
268
Makefile
268
Makefile
@@ -1,46 +1,260 @@
|
||||
# -----------------------------------------
|
||||
# Makefile – TIC-80 project builder
|
||||
# Usage:
|
||||
# make PROJECT=mranderson
|
||||
# make build PROJECT=mranderson
|
||||
# make watch PROJECT=mranderson
|
||||
# make export PROJECT=mranderson
|
||||
# -----------------------------------------
|
||||
|
||||
ifndef PROJECT
|
||||
$(error Specify the project name: make PROJECT=name)
|
||||
endif
|
||||
PROJECT = impostor
|
||||
|
||||
ORDER = $(PROJECT).inc
|
||||
OUTPUT = $(PROJECT).lua
|
||||
OUTPUT_ORIGINAL = $(PROJECT).original.lua
|
||||
OUTPUT_ZIP = $(PROJECT).html.zip
|
||||
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 = $(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.teletype.hu
|
||||
|
||||
all: build
|
||||
|
||||
build: $(OUTPUT)
|
||||
@echo "==> Build complete: $(OUTPUT)"
|
||||
|
||||
$(OUTPUT): $(SRC) $(ORDER)
|
||||
@echo "==> Building $(OUTPUT)..."
|
||||
build:
|
||||
@rm -f $(OUTPUT)
|
||||
@while read f; do \
|
||||
@sed 's/\r$$//' $(ORDER) | while read f; do \
|
||||
cat "$(SRC_DIR)/$$f" >> $(OUTPUT); \
|
||||
echo "\n" >> $(OUTPUT); \
|
||||
done < $(ORDER)
|
||||
@echo "==> Done."
|
||||
echo "" >> $(OUTPUT); \
|
||||
done
|
||||
|
||||
export: $(OUTPUT)
|
||||
@echo "==> TIC-80 export..."
|
||||
tic80 --cli --skip --fs=. \
|
||||
--cmd="load $(OUTPUT) & save $(PROJECT) & export html $(PROJECT).html & exit"
|
||||
@echo "==> HTML ZIP: $(OUTPUT_ZIP)"
|
||||
@echo "==> TIC: $(OUTPUT_TIC)"
|
||||
download-minify:
|
||||
@test -f $(MINIFY) || { echo "==> Downloading $(MINIFY)"; curl -fsSL $(MINIFY_URL) -o $(MINIFY); }
|
||||
|
||||
minify: build download-minify
|
||||
@echo "==> Minifying $(OUTPUT)"
|
||||
@cp $(OUTPUT) $(OUTPUT_ORIGINAL)
|
||||
@lua $(MINIFY) minify $(OUTPUT_ORIGINAL) > $(OUTPUT)
|
||||
|
||||
export: minify
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "ERROR: VERSION not set!"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "==> Exporting HTML for version $(VERSION)"
|
||||
@tic80 --cli --skip --fs=. \
|
||||
--cmd="load $(OUTPUT) & save $(PROJECT)-$(VERSION) & export html $(PROJECT)-$(VERSION).html & exit"
|
||||
@echo "==> Creating versioned files"
|
||||
@if [ -f "$(PROJECT)-$(VERSION).tic" ]; then \
|
||||
cp $(PROJECT)-$(VERSION).tic $(PROJECT).tic; \
|
||||
fi
|
||||
@if [ -f "$(PROJECT)-$(VERSION).html.zip" ]; then \
|
||||
cp $(PROJECT)-$(VERSION).html.zip $(PROJECT).html.zip; \
|
||||
fi
|
||||
@echo "==> Generated files:"
|
||||
@ls -lh $(PROJECT)-$(VERSION).* $(PROJECT).tic $(PROJECT).html.zip 2>/dev/null || true
|
||||
|
||||
watch:
|
||||
@echo "==> Watching project: $(PROJECT)"
|
||||
make build PROJECT=$(PROJECT)
|
||||
fswatch -o $(SRC_DIR) $(ORDER) | while read; do make build PROJECT=$(PROJECT); done
|
||||
$(MAKE) build
|
||||
fswatch -o $(SRC_DIR) $(ORDER) assets | while read; do $(MAKE) build; done
|
||||
|
||||
import_assets: 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))
|
||||
@$(call f_export_asset_awk,PATTERNS,$(OUTPUT),$(ASSETS_LUA))
|
||||
@$(call f_export_asset_awk,TRACKS,$(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
|
||||
|
||||
@@ -8,14 +8,6 @@ This game is designed for the TIC-80 fantasy computer. To play, follow these ste
|
||||
2. **Clone this repository** Clone this repository to tic80's cartridge folder (MacOS: ~Library/Application Support/com.nesbox.tic/TIC-80)
|
||||
2. **Launch TIC-80:** Start the TIC-80 application.
|
||||
3. **Load the Game:**
|
||||
* Navigate to the directory where `game.lua` is located using the TIC-80 command line (`cd mranderson`).
|
||||
* Navigate to the directory where `game.lua` is located using the TIC-80 command line (`cd impostor`).
|
||||
* Type `load game.lua` and press Enter.
|
||||
* Once loaded, type `run` and press Enter to start the game.
|
||||
|
||||
## Story: The Coder's Lament
|
||||
|
||||
Before he was "The One," before he dodged bullets and shattered the illusion, Thomas Anderson was just a software developer named Neo. Trapped in a cubicle farm of endless bugs and looming deadlines, Neo's days were a monotonous cycle of debugging legacy code, attending pointless meetings, and battling unresponsive APIs. Each line of code felt like a chain, each project a heavier burden, pulling him deeper into a digital malaise.
|
||||
|
||||
He yearned for something more, a glitch in the system, a whisper of a different reality. His fingers, calloused from countless hours on the keyboard, danced across cryptic forums late at night, searching for answers, for meaning beyond the mundane syntax of his corporate prison. The coffee flowed freely, the pizza boxes piled high, and the lines of code blurred into an indistinguishable stream of ones and zeros.
|
||||
|
||||
This game chronicles Mr. Anderson's final, desperate struggles within the software development matrix. Navigate the labyrinthine codebase, escape the relentless pursuit of project managers (Agents), and uncover the hidden truths that will lead him to question everything he knows. Will he find the "red pill" in a sea of green code, or will he forever be just another drone in the system?
|
||||
|
||||
0
assets/music/.keep
Normal file
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 |
65
impostor.inc
Normal file
65
impostor.inc
Normal file
@@ -0,0 +1,65 @@
|
||||
meta/meta.header.lua
|
||||
init/init.module.lua
|
||||
init/init.config.lua
|
||||
init/init.context.lua
|
||||
system/system.util.lua
|
||||
system/system.print.lua
|
||||
system/system.input.lua
|
||||
system/system.asciiart.lua
|
||||
logic/logic.meter.lua
|
||||
logic/logic.focus.lua
|
||||
logic/logic.day.lua
|
||||
logic/logic.timer.lua
|
||||
logic/logic.trigger.lua
|
||||
logic/logic.minigame.lua
|
||||
logic/logic.glitch.lua
|
||||
logic/logic.discussion.lua
|
||||
system/system.ui.lua
|
||||
audio/audio.manager.lua
|
||||
audio/audio.songs.lua
|
||||
sprite/sprite.manager.lua
|
||||
sprite/sprite.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_end.lua
|
||||
decision/decision.go_to_walking_to_home.lua
|
||||
decision/decision.go_to_sleep.lua
|
||||
decision/decision.do_work.lua
|
||||
decision/decision.start_discussion.lua
|
||||
discussion/discussion.sumphore.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
|
||||
screen/screen.work.lua
|
||||
window/window.manager.lua
|
||||
window/window.register.lua
|
||||
window/window.end.lua
|
||||
window/window.intro.title.lua
|
||||
window/window.intro.ttg.lua
|
||||
window/window.intro.brief.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.mysterious_man.lua
|
||||
window/window.discussion.lua
|
||||
window/window.continued.lua
|
||||
window/window.game.lua
|
||||
system/system.main.lua
|
||||
meta/meta.assets.lua
|
||||
48
inc/audio/audio.manager.lua
Normal file
48
inc/audio/audio.manager.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
--- @section Audio
|
||||
|
||||
--- Stops current music.
|
||||
--- @within Audio
|
||||
function Audio.music_stop() music() end
|
||||
--- Plays main menu music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_mainmenu() end
|
||||
--- Plays waking up music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_wakingup() end
|
||||
--- Plays room morning music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_morning() end
|
||||
--- Plays room street 1 music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_street_1() end
|
||||
--- Plays room street 2 music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_street_2() end
|
||||
--- Plays room music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_() end
|
||||
--- Plays room work music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_room_work() music(0) end
|
||||
--- Plays activity work music.
|
||||
--- @within Audio
|
||||
function Audio.music_play_activity_work() music(1) end
|
||||
|
||||
--- Plays select sound effect.
|
||||
--- @within Audio
|
||||
function Audio.sfx_select() sfx(17, 'C-7', 30) end
|
||||
--- Plays deselect sound effect.
|
||||
--- @within Audio
|
||||
function Audio.sfx_deselect() sfx(18, 'C-7', 30) end
|
||||
--- Plays beep sound effect.
|
||||
--- @within Audio
|
||||
function Audio.sfx_beep() sfx(19, 'C-6', 30) end
|
||||
--- Plays success sound effect.
|
||||
--- @within Audio
|
||||
function Audio.sfx_success() sfx(16, 'C-7', 60) end
|
||||
--- Plays bloop sound effect.
|
||||
--- @within Audio
|
||||
function Audio.sfx_bloop() sfx(21, 'C-3', 60) end
|
||||
--- Plays alarm sound effect.
|
||||
--- @within Audio
|
||||
function Audio.sfx_alarm() sfx(61) end
|
||||
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)
|
||||
}
|
||||
]]
|
||||
15
inc/decision/decision.do_work.lua
Normal file
15
inc/decision/decision.do_work.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
Decision.register({
|
||||
id = "do_work",
|
||||
label = "Do Work",
|
||||
handle = function()
|
||||
Meter.hide()
|
||||
Util.go_to_screen_by_id("work")
|
||||
MinigameDDRWindow.start("game", nil, {
|
||||
on_win = function()
|
||||
Meter.show()
|
||||
Util.go_to_screen_by_id("office")
|
||||
Window.set_current("game")
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
7
inc/decision/decision.go_to_end.lua
Normal file
7
inc/decision/decision.go_to_end.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
Decision.register({
|
||||
id = "go_to_end",
|
||||
label = "Break the cycle",
|
||||
handle = function()
|
||||
Window.set_current("end")
|
||||
end,
|
||||
})
|
||||
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,
|
||||
})
|
||||
16
inc/decision/decision.go_to_sleep.lua
Normal file
16
inc/decision/decision.go_to_sleep.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
Decision.register({
|
||||
id = "go_to_sleep",
|
||||
label = "Go to Sleep",
|
||||
handle = function()
|
||||
Meter.hide()
|
||||
Day.increase()
|
||||
MinigameRhythmWindow.start("game", {
|
||||
focus_center_x = (Config.screen.width / 2) - 22,
|
||||
focus_center_y = (Config.screen.height / 2) - 18,
|
||||
focus_initial_radius = 0,
|
||||
on_win = function()
|
||||
MysteriousManWindow.start()
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
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,
|
||||
})
|
||||
146
inc/decision/decision.manager.lua
Normal file
146
inc/decision/decision.manager.lua
Normal file
@@ -0,0 +1,146 @@
|
||||
--- @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|function Display text for the decision, or a function returning it.
|
||||
--- @param[opt] decision.condition function Returns true if decision is available. Defaults to always true.
|
||||
--- @param[opt] decision.handle function Called when the decision is selected. Defaults to noop.
|
||||
function Decision.register(decision)
|
||||
if not decision or not decision.id then
|
||||
trace("Error: Invalid decision object registered (missing id)!")
|
||||
return
|
||||
end
|
||||
if not decision.label then
|
||||
trace("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 the display label for a decision.
|
||||
--- @within Decision
|
||||
--- @param decision table The decision data table.
|
||||
--- @return string result The resolved decision label.
|
||||
function Decision.get_label(decision)
|
||||
if not decision then return "" end
|
||||
if type(decision.label) == "function" then
|
||||
return decision.label() or ""
|
||||
end
|
||||
return decision.label or ""
|
||||
end
|
||||
|
||||
--- Gets a decision by ID.
|
||||
--- @within Decision
|
||||
--- @param id string The ID of the decision.
|
||||
--- @return table|nil result The decision table or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * id (string) Unique decision identifier.<br/>
|
||||
--- * label (string) Display text for the decision.<br/>
|
||||
--- * condition (function) Returns true if decision is available.<br/>
|
||||
--- * handle (function) Called when the decision is selected.
|
||||
function Decision.get_by_id(id)
|
||||
return _decisions[id]
|
||||
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 (not decision.condition or decision.condition()) then
|
||||
table.insert(available, decision)
|
||||
end
|
||||
end
|
||||
return available
|
||||
end
|
||||
|
||||
--- Draws decision selector.
|
||||
--- @within Decision
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
--- @param selected_decision_index number The index of the selected decision.<br/>
|
||||
function Decision.draw(decisions, selected_decision_index)
|
||||
local bar_height = 16
|
||||
local bar_y = Config.screen.height - bar_height
|
||||
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
|
||||
if #decisions > 0 then
|
||||
local selected_decision = decisions[selected_decision_index]
|
||||
local decision_label = Decision.get_label(selected_decision)
|
||||
local text_y = bar_y + 4
|
||||
Print.text("<", 2, text_y, Config.colors.light_blue)
|
||||
Print.text_center(decision_label, Config.screen.width / 2, text_y, Config.colors.item)
|
||||
Print.text(">", Config.screen.width - 6, text_y, Config.colors.light_blue)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates decision selector.
|
||||
--- @within Decision
|
||||
--- @param decisions table A table of decision items.<br/>
|
||||
--- @param selected_decision_index number The current index of the selected decision.<br/>
|
||||
--- @return number selected_decision_index The updated index of the selected decision.
|
||||
function Decision.update(decisions, selected_decision_index)
|
||||
if Input.left() then
|
||||
Audio.sfx_beep()
|
||||
selected_decision_index = Util.safeindex(decisions, selected_decision_index - 1)
|
||||
elseif Input.right() then
|
||||
Audio.sfx_beep()
|
||||
selected_decision_index = Util.safeindex(decisions, selected_decision_index + 1)
|
||||
end
|
||||
return selected_decision_index
|
||||
end
|
||||
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) - 22,
|
||||
focus_center_y = (Config.screen.height / 2) - 18,
|
||||
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) - 22,
|
||||
focus_center_y = (Config.screen.height / 2) - 18,
|
||||
focus_initial_radius = 0,
|
||||
})
|
||||
end,
|
||||
})
|
||||
18
inc/decision/decision.start_discussion.lua
Normal file
18
inc/decision/decision.start_discussion.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
Decision.register({
|
||||
id = "start_discussion",
|
||||
label = function()
|
||||
if Context.day_count >= 3 then
|
||||
return "Talk to Sumphore"
|
||||
end
|
||||
return "Talk to the homeless guy"
|
||||
end,
|
||||
handle = function()
|
||||
if Context.day_count < 3 then
|
||||
Discussion.start("homeless_guy", "game")
|
||||
end
|
||||
if Context.day_count >= 3 then
|
||||
Discussion.start("sumphore_day_3", "game")
|
||||
return
|
||||
end
|
||||
end,
|
||||
})
|
||||
59
inc/discussion/discussion.sumphore.lua
Normal file
59
inc/discussion/discussion.sumphore.lua
Normal file
@@ -0,0 +1,59 @@
|
||||
Discussion.register({
|
||||
id = "sumphore_day_3",
|
||||
steps = {
|
||||
{
|
||||
question = "Are you still seeking the ox?",
|
||||
answers = {
|
||||
{ label = "Huh? What ox?", next_step = 2 },
|
||||
{ label = "Are you drunk, old man?", next_step = nil },
|
||||
},
|
||||
},
|
||||
{
|
||||
question = "Did you never think there would be more to this?",
|
||||
answers = {
|
||||
{ label = "I'm not sure what you mean.", next_step = nil },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Discussion.register({
|
||||
id = "homeless_guy",
|
||||
steps = {
|
||||
{
|
||||
question = "Sup bro, how are you?",
|
||||
answers = {
|
||||
{ label = "I'm doing great, thanks!", next_step = 2 },
|
||||
{ label = "Not as good as you", next_step = nil },
|
||||
},
|
||||
},
|
||||
{
|
||||
question = "What's your name?",
|
||||
answers = {
|
||||
{ label = "Norman Reds, nice to meet you.", next_step = 3 },
|
||||
{ label = "Mom told me not to talk to strangers.", next_step = nil },
|
||||
},
|
||||
},
|
||||
{
|
||||
question = "That name ... could it be? I know a guy with that name...",
|
||||
answers = {
|
||||
{ label = "Never met you before.", next_step = 4 },
|
||||
{ label = "I'm not sure what you mean.", next_step = nil },
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
question = "My name is Sumphore, nice to meet you.",
|
||||
answers = {
|
||||
{ label = "Nice to meet you, Sumphore.", next_step = 5 },
|
||||
},
|
||||
},
|
||||
{
|
||||
question = "You're a good guy, I can tell. You abide by the rules. Life would be so much easier if more people were like you ...",
|
||||
answers = {
|
||||
{ label = "Thanks, I try my best.", next_step = nil },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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,57 @@
|
||||
local Config = {
|
||||
screen = {
|
||||
width = 240,
|
||||
height = 136
|
||||
},
|
||||
colors = {
|
||||
black = 0,
|
||||
light_grey = 13,
|
||||
dark_grey = 14,
|
||||
green = 6,
|
||||
npc = 8,
|
||||
item = 12 -- yellow
|
||||
},
|
||||
player = {
|
||||
w = 8,
|
||||
h = 8,
|
||||
start_x = 120,
|
||||
start_y = 128,
|
||||
sprite_id = 1
|
||||
},
|
||||
physics = {
|
||||
gravity = 0.5,
|
||||
jump_power = -5,
|
||||
move_speed = 1.5,
|
||||
max_jumps = 2,
|
||||
interaction_radius_npc = 12,
|
||||
interaction_radius_item = 8
|
||||
},
|
||||
timing = {
|
||||
splash_duration = 120
|
||||
Config = {}
|
||||
|
||||
--- Return initial data for Config
|
||||
--- @within Config
|
||||
function Config.initial_data()
|
||||
return {
|
||||
screen = {
|
||||
width = 240,
|
||||
height = 136
|
||||
},
|
||||
colors = {
|
||||
black = 0,
|
||||
light_grey = 2,
|
||||
dark_grey = 1,
|
||||
red = 13,
|
||||
light_blue = 9,
|
||||
blue = 3,
|
||||
white = 4,
|
||||
item = 7,
|
||||
meter_bg = 1
|
||||
},
|
||||
timing = {
|
||||
minigame_win_duration = 180
|
||||
}
|
||||
}
|
||||
}
|
||||
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_MAGIC_VALUE = 0xDE
|
||||
|
||||
--- Saves the current configuration.
|
||||
--- @within Config
|
||||
function Config.save()
|
||||
mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_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
|
||||
return
|
||||
else
|
||||
Config.reset()
|
||||
end
|
||||
end
|
||||
|
||||
Config.load()
|
||||
|
||||
@@ -1,432 +1,136 @@
|
||||
local Context = {
|
||||
active_window = WINDOW_SPLASH,
|
||||
inventory = {},
|
||||
intro = {
|
||||
y = Config.screen.height,
|
||||
speed = 0.5,
|
||||
text = "Mr. Anderson is an average\nprogrammer. His daily life\nrevolves around debugging,\npull requests, and end-of-sprint\nmeetings, all while secretly\ndreaming of being destined\nfor something more."
|
||||
},
|
||||
current_screen = 1,
|
||||
splash_timer = Config.timing.splash_duration,
|
||||
dialog = {
|
||||
text = "",
|
||||
menu_items = {},
|
||||
selected_menu_item = 1,
|
||||
active_entity = nil,
|
||||
showing_description = false,
|
||||
current_node_key = nil
|
||||
},
|
||||
player = {
|
||||
x = Config.player.start_x,
|
||||
y = Config.player.start_y,
|
||||
w = Config.player.w,
|
||||
h = Config.player.h,
|
||||
vx = 0,
|
||||
vy = 0,
|
||||
jumps = 0,
|
||||
sprite_id = Config.player.sprite_id
|
||||
},
|
||||
ground = {
|
||||
x = 0,
|
||||
y = Config.screen.height,
|
||||
w = Config.screen.width,
|
||||
h = 8
|
||||
},
|
||||
menu_items = {},
|
||||
selected_menu_item = 1,
|
||||
selected_inventory_item = 1,
|
||||
-- Screen data
|
||||
screens = {
|
||||
{
|
||||
-- Screen 1
|
||||
name = "Screen 1",
|
||||
platforms = {
|
||||
{
|
||||
x = 80,
|
||||
y = 110,
|
||||
w = 40,
|
||||
h = 8
|
||||
},
|
||||
{
|
||||
x = 160,
|
||||
y = 90,
|
||||
w = 40,
|
||||
h = 8
|
||||
}
|
||||
},
|
||||
npcs = {
|
||||
{
|
||||
x = 180,
|
||||
y = 82,
|
||||
name = "Trinity",
|
||||
sprite_id = 2,
|
||||
dialog = {
|
||||
start = {
|
||||
text = "Hello, Neo.",
|
||||
options = {
|
||||
{label = "Who are you?", next_node = "who_are_you"},
|
||||
{label = "My name is not Neo.", next_node = "not_neo"},
|
||||
{label = "...", next_node = "silent"}
|
||||
}
|
||||
},
|
||||
who_are_you = {
|
||||
text = "I am Trinity. I've been looking for you.",
|
||||
options = {
|
||||
{label = "The famous hacker?", next_node = "famous_hacker"},
|
||||
{label = "Why me?", next_node = "why_me"}
|
||||
}
|
||||
},
|
||||
not_neo = {
|
||||
text = "I know. But you will be.",
|
||||
options = {
|
||||
{label = "What are you talking about?", next_node = "who_are_you"}
|
||||
}
|
||||
},
|
||||
silent = {
|
||||
text = "You're not much of a talker, are you?",
|
||||
options = {
|
||||
{label = "I guess not.", next_node = "dialog_end"}
|
||||
}
|
||||
},
|
||||
famous_hacker = {
|
||||
text = "The one and only.",
|
||||
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."
|
||||
}
|
||||
}
|
||||
local SAVE_GAME_BANK = 6
|
||||
local SAVE_GAME_MAGIC_VALUE_ADDRESS = 0
|
||||
local SAVE_GAME_MAGIC_VALUE = 0xCA
|
||||
|
||||
--- Global game context.
|
||||
--- @section Context
|
||||
Context = {}
|
||||
|
||||
--- Gets initial data for Context.
|
||||
--- @within Context
|
||||
--- @return result table Initial context data or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * current_menu_item (number) Index of the currently selected menu item.<br/>
|
||||
--- * popup (table) Popup window state. Contains: `show` (boolean) whether popup is visible, `content` (table) array of strings to display.<br/>
|
||||
--- * game_in_progress (boolean) Whether a game is currently active.<br/>
|
||||
--- * minigame_ddr (table) DDR minigame state (see Minigame.get_default_ddr).<br/>
|
||||
--- * minigame_button_mash (table) Button mash minigame state (see Minigame.get_default_button_mash).<br/>
|
||||
--- * minigame_rhythm (table) Rhythm minigame state (see Minigame.get_default_rhythm).<br/>
|
||||
--- * meters (table) Meter values (see Meter.get_initial).<br/>
|
||||
--- * triggers (table) Active trigger runtime state, keyed by trigger ID.<br/>
|
||||
--- * stat_screen_active (boolean) Whether the stat screen overlay is currently shown.<br/>
|
||||
--- * game (table) Current game progress state. Contains: `current_screen` (string) active screen ID, `current_situation` (string|nil) active situation ID.<br/>
|
||||
function Context.initial_data()
|
||||
return {
|
||||
current_menu_item = 1,
|
||||
popup = {
|
||||
show = false,
|
||||
content = {}
|
||||
},
|
||||
{
|
||||
-- Screen 2
|
||||
name = "Screen 2",
|
||||
platforms = {
|
||||
{
|
||||
x = 30,
|
||||
y = 100,
|
||||
w = 50,
|
||||
h = 8
|
||||
},
|
||||
{
|
||||
x = 100,
|
||||
y = 80,
|
||||
w = 50,
|
||||
h = 8
|
||||
},
|
||||
{
|
||||
x = 170,
|
||||
y = 60,
|
||||
w = 50,
|
||||
h = 8
|
||||
}
|
||||
},
|
||||
npcs = {
|
||||
{
|
||||
x = 120,
|
||||
y = 72,
|
||||
name = "Morpheus",
|
||||
sprite_id = 5,
|
||||
dialog = {
|
||||
start = {
|
||||
text = "At last. Welcome, Neo. As you no doubt have guessed, I am Morpheus.",
|
||||
options = {
|
||||
{label = "It's an honor to meet you.", next_node = "honor"},
|
||||
{label = "You've been looking for me.", next_node = "looking_for_me"}
|
||||
}
|
||||
},
|
||||
honor = {
|
||||
text = "No, the honor is mine.",
|
||||
options = {
|
||||
{label = "What is this place?", next_node = "what_is_this_place"}
|
||||
}
|
||||
},
|
||||
looking_for_me = {
|
||||
text = "I have. For some time.",
|
||||
options = {
|
||||
{label = "What is this place?", next_node = "what_is_this_place"}
|
||||
}
|
||||
},
|
||||
what_is_this_place = {
|
||||
text = "This is the construct. It's our loading program. We can load anything from clothing, to equipment, weapons, training simulations. Anything we need.",
|
||||
options = {
|
||||
{label = "Right.", next_node = "dialog_end"}
|
||||
}
|
||||
},
|
||||
dialog_end = {
|
||||
text = "I've been waiting for you, Neo. We have much to discuss.",
|
||||
options = {} -- Ends conversation
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
x = 40,
|
||||
y = 92,
|
||||
name = "Tank",
|
||||
sprite_id = 6,
|
||||
dialog = {
|
||||
start = {
|
||||
text = "Hey, Neo! Welcome to the construct. I'm Tank.",
|
||||
options = {
|
||||
{label = "Good to meet you.", next_node = "good_to_meet_you"},
|
||||
{label = "This place is incredible.", next_node = "incredible"}
|
||||
}
|
||||
},
|
||||
good_to_meet_you = {
|
||||
text = "You too! We've been waiting for you. Need anything? Training? Weapons?",
|
||||
options = {
|
||||
{label = "Training?", next_node = "training"},
|
||||
{label = "I'm good for now.", next_node = "dialog_end"}
|
||||
}
|
||||
},
|
||||
incredible = {
|
||||
text = "Isn't it? The boss's design. We can load anything we need. What do you want to learn?",
|
||||
options = {
|
||||
{label = "Show me.", next_node = "training"}
|
||||
}
|
||||
},
|
||||
training = {
|
||||
text = "Jujitsu? Kung Fu? How about... all of them?",
|
||||
options = {
|
||||
{label = "All of them.", next_node = "all_of_them"}
|
||||
}
|
||||
},
|
||||
all_of_them = {
|
||||
text = "Operator, load the combat training program.",
|
||||
options = {
|
||||
{label = "...", next_node = "dialog_end"}
|
||||
}
|
||||
},
|
||||
dialog_end = {
|
||||
text = "Just holler if you need anything. Anything at all.",
|
||||
options = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
items = {
|
||||
{
|
||||
x = 180,
|
||||
y = 52,
|
||||
w = 8,
|
||||
h = 8,
|
||||
name = "Potion",
|
||||
sprite_id = 7,
|
||||
desc = "A glowing red potion. It looks potent."
|
||||
}
|
||||
}
|
||||
game_in_progress = false,
|
||||
stat_screen_active = false,
|
||||
minigame_ddr = {},
|
||||
minigame_button_mash = {},
|
||||
minigame_rhythm = {},
|
||||
meters = Meter.get_initial(),
|
||||
timer = Timer.get_initial(),
|
||||
triggers = {},
|
||||
home_norman_visible = false,
|
||||
game = {
|
||||
current_screen = "home",
|
||||
current_situation = nil,
|
||||
},
|
||||
day_count = 1,
|
||||
glitch = {
|
||||
enabled = false,
|
||||
state = "active",
|
||||
timer = 0,
|
||||
},
|
||||
_end = {
|
||||
state = "choice",
|
||||
selection = 1,
|
||||
},
|
||||
discussion = {
|
||||
active = false,
|
||||
id = nil,
|
||||
step = 1,
|
||||
selected_answer = 1,
|
||||
scroll_y = 0,
|
||||
scroll_timer = 0,
|
||||
auto_scroll = true,
|
||||
return_window = nil,
|
||||
},
|
||||
{
|
||||
-- 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()
|
||||
MysteriousManWindow.start({
|
||||
text = [[
|
||||
Norman was never a bad
|
||||
simulation engineer, but
|
||||
we need to be careful in
|
||||
letting him improve. We
|
||||
need to distract him.
|
||||
]],
|
||||
on_text_complete = function()
|
||||
Audio.sfx_alarm()
|
||||
Context.home_norman_visible = false
|
||||
Util.go_to_screen_by_id("home")
|
||||
MinigameButtonMashWindow.start("game", {
|
||||
focus_center_x = (Config.screen.width / 2) - 22,
|
||||
focus_center_y = (Config.screen.height / 2) - 18,
|
||||
focus_initial_radius = 0,
|
||||
target_points = 100,
|
||||
instruction_text = "Wake up Norman!",
|
||||
show_progress_text = false,
|
||||
on_win = function()
|
||||
Audio.music_play_wakingup()
|
||||
Context.home_norman_visible = true
|
||||
Meter.show()
|
||||
Window.set_current("game")
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- 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
|
||||
|
||||
19
inc/init/init.module.lua
Normal file
19
inc/init/init.module.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
Window = {}
|
||||
Util = {}
|
||||
Meter = {}
|
||||
Minigame = {}
|
||||
Decision = {}
|
||||
Situation = {}
|
||||
Screen = {}
|
||||
Map = {}
|
||||
UI = {}
|
||||
Print = {}
|
||||
Input = {}
|
||||
Sprite = {}
|
||||
Audio = {}
|
||||
Focus = {}
|
||||
Day = {}
|
||||
Timer = {}
|
||||
Trigger = {}
|
||||
Discussion = {}
|
||||
AsciiArt = {}
|
||||
@@ -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
|
||||
25
inc/logic/logic.day.lua
Normal file
25
inc/logic/logic.day.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
--- @section Day
|
||||
local _day_increase_handlers = {}
|
||||
|
||||
--- Increases the day count and triggers registered handlers.
|
||||
--- @within Day
|
||||
function Day.increase()
|
||||
Context.day_count = Context.day_count + 1
|
||||
for _, handler in ipairs(_day_increase_handlers) do
|
||||
handler()
|
||||
end
|
||||
end
|
||||
|
||||
--- Registers a handler to be called when the day increases.
|
||||
--- @within Day
|
||||
--- @param handler function The function to call when the day increases.
|
||||
function Day.register_handler(handler)
|
||||
table.insert(_day_increase_handlers, handler)
|
||||
end
|
||||
|
||||
Day.register_handler(function()
|
||||
local m = Context.meters
|
||||
m.ism = math.max(0, m.ism - METER_DECAY_PER_DAY)
|
||||
m.wpm = math.max(0, m.wpm - METER_DECAY_PER_DAY)
|
||||
m.bm = math.max(0, m.bm - METER_DECAY_PER_DAY)
|
||||
end)
|
||||
103
inc/logic/logic.discussion.lua
Normal file
103
inc/logic/logic.discussion.lua
Normal file
@@ -0,0 +1,103 @@
|
||||
--- @section Discussion
|
||||
local _discussions = {}
|
||||
|
||||
--- Registers a discussion definition.
|
||||
--- @within Discussion
|
||||
--- @param discussion table The discussion data table.
|
||||
--- @param discussion.id string Unique discussion identifier.
|
||||
--- @param discussion.steps table Array of step tables, each with `question` (string) and `answers` (array of {label, next_step} tables).
|
||||
--- @param[opt] discussion.on_end function Called when the discussion ends. Defaults to noop.
|
||||
function Discussion.register(discussion)
|
||||
if not discussion or not discussion.id then
|
||||
trace("Error: Invalid discussion registered (missing id)!")
|
||||
return
|
||||
end
|
||||
if not discussion.steps or #discussion.steps == 0 then
|
||||
trace("Error: Discussion '" .. discussion.id .. "' has no steps!")
|
||||
return
|
||||
end
|
||||
if not discussion.on_end then
|
||||
discussion.on_end = function() end
|
||||
end
|
||||
if _discussions[discussion.id] then
|
||||
trace("Warning: Overwriting discussion with id: " .. discussion.id)
|
||||
end
|
||||
_discussions[discussion.id] = discussion
|
||||
end
|
||||
|
||||
--- Gets a discussion by ID.
|
||||
--- @within Discussion
|
||||
--- @param id string The discussion ID.
|
||||
--- @return table|nil result The discussion table or nil.
|
||||
function Discussion.get_by_id(id)
|
||||
return _discussions[id]
|
||||
end
|
||||
|
||||
--- Starts a discussion, switching to the discussion window.
|
||||
--- @within Discussion
|
||||
--- @param id string The discussion ID to start.
|
||||
--- @param return_window string The window ID to return to after the discussion.
|
||||
function Discussion.start(id, return_window)
|
||||
local discussion = _discussions[id]
|
||||
if not discussion then
|
||||
trace("Error: Discussion not found: " .. tostring(id))
|
||||
return
|
||||
end
|
||||
Context.discussion.active = true
|
||||
Context.discussion.id = id
|
||||
Context.discussion.step = 1
|
||||
Context.discussion.selected_answer = 1
|
||||
Context.discussion.scroll_y = 0
|
||||
Context.discussion.scroll_timer = 0
|
||||
Context.discussion.auto_scroll = true
|
||||
Context.discussion.return_window = return_window or "game"
|
||||
Meter.hide()
|
||||
Window.set_current("discussion")
|
||||
end
|
||||
|
||||
--- Gets the current step data for the active discussion.
|
||||
--- @within Discussion
|
||||
--- @return table|nil result The current step table or nil.
|
||||
function Discussion.get_current_step()
|
||||
if not Context.discussion.active or not Context.discussion.id then return nil end
|
||||
local discussion = _discussions[Context.discussion.id]
|
||||
if not discussion then return nil end
|
||||
return discussion.steps[Context.discussion.step]
|
||||
end
|
||||
|
||||
--- Advances to a specific step or ends the discussion.
|
||||
--- @within Discussion
|
||||
--- @param next_step number|nil The step index to go to, or nil to end.
|
||||
function Discussion.go_to_step(next_step)
|
||||
if not next_step then
|
||||
Discussion.finish()
|
||||
return
|
||||
end
|
||||
local discussion = _discussions[Context.discussion.id]
|
||||
if not discussion or not discussion.steps[next_step] then
|
||||
Discussion.finish()
|
||||
return
|
||||
end
|
||||
Context.discussion.step = next_step
|
||||
Context.discussion.selected_answer = 1
|
||||
Context.discussion.scroll_y = 0
|
||||
Context.discussion.scroll_timer = 0
|
||||
Context.discussion.auto_scroll = true
|
||||
end
|
||||
|
||||
--- Ends the active discussion and returns to the previous window.
|
||||
--- @within Discussion
|
||||
function Discussion.finish()
|
||||
local discussion = _discussions[Context.discussion.id]
|
||||
local return_window = Context.discussion.return_window or "game"
|
||||
Context.discussion.active = false
|
||||
Context.discussion.id = nil
|
||||
Context.discussion.scroll_y = 0
|
||||
Context.discussion.scroll_timer = 0
|
||||
Context.discussion.auto_scroll = true
|
||||
Meter.show()
|
||||
if discussion and discussion.on_end then
|
||||
discussion.on_end()
|
||||
end
|
||||
Window.set_current(return_window)
|
||||
end
|
||||
166
inc/logic/logic.focus.lua
Normal file
166
inc/logic/logic.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
|
||||
58
inc/logic/logic.glitch.lua
Normal file
58
inc/logic/logic.glitch.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
--- @section Glitch
|
||||
Glitch = {}
|
||||
|
||||
--- Shows the glitch effect.
|
||||
--- @within Glitch
|
||||
function Glitch.show()
|
||||
if Context and Context.glitch then
|
||||
Context.glitch.enabled = true
|
||||
end
|
||||
end
|
||||
|
||||
--- Hides the glitch effect.
|
||||
--- @within Glitch
|
||||
function Glitch.hide()
|
||||
if Context and Context.glitch then
|
||||
Context.glitch.enabled = false
|
||||
end
|
||||
end
|
||||
|
||||
--- Draws the glitch effect if active.
|
||||
--- @within Glitch
|
||||
function Glitch.draw()
|
||||
if not Context or not Context.glitch or not Context.glitch.enabled then return end
|
||||
|
||||
-- Update state timer
|
||||
Context.glitch.timer = Context.glitch.timer - 1
|
||||
if Context.glitch.timer <= 0 then
|
||||
if Context.glitch.state == "active" then
|
||||
Context.glitch.state = "waiting"
|
||||
Context.glitch.timer = math.random(20, 60) -- Time to stay fixed
|
||||
else
|
||||
Context.glitch.state = "active"
|
||||
Context.glitch.timer = math.random(40, 100) -- Time to stay glitchy
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw stripes only when active
|
||||
if Context.glitch.state == "active" then
|
||||
for _ = 1, 15 do
|
||||
local rx = math.random(0, Config.screen.width - 1)
|
||||
local ry = math.random(0, Config.screen.height - 1)
|
||||
|
||||
-- Sample color at the random point
|
||||
local color = pix(rx, ry)
|
||||
|
||||
-- Determine random length for the stripe (2-40)
|
||||
local length = math.random(2, 40)
|
||||
|
||||
-- Draw the vertical stripe
|
||||
for sy = 0, length - 1 do
|
||||
local dy = ry + sy
|
||||
if dy < Config.screen.height then
|
||||
pix(rx, dy, color)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
155
inc/logic/logic.meter.lua
Normal file
155
inc/logic/logic.meter.lua
Normal file
@@ -0,0 +1,155 @@
|
||||
--- @section Meter
|
||||
local METER_MAX = 1000
|
||||
local METER_DEFAULT = 500
|
||||
local METER_GAIN_PER_CHORE = 100
|
||||
local METER_DECAY_PER_DAY = 20
|
||||
local COMBO_BASE_BONUS = 0.02
|
||||
local COMBO_MAX_BONUS = 0.16
|
||||
local COMBO_TIMEOUT_FRAMES = 600
|
||||
|
||||
-- Internal meters for tracking game progress and player stats.
|
||||
Meter.COLOR_ISM = Config.colors.red
|
||||
Meter.COLOR_WPM = Config.colors.blue
|
||||
Meter.COLOR_BM = Config.colors.black
|
||||
Meter.COLOR_BG = Config.colors.meter_bg
|
||||
|
||||
--- Gets initial meter values.
|
||||
--- @within Meter
|
||||
--- @return result table Initial meter values. </br>
|
||||
--- Fields: </br>
|
||||
--- * ism (number) Initial ISM meter value.<br/>
|
||||
--- * wpm (number) Initial WPM meter value.<br/>
|
||||
--- * bm (number) Initial BM meter value.<br/>
|
||||
--- * combo (number) Current combo count.<br/>
|
||||
--- * combo_timer (number) Frames since last combo action.<br/>
|
||||
--- * hidden (boolean) Whether meters are hidden.
|
||||
function Meter.get_initial()
|
||||
return {
|
||||
ism = METER_DEFAULT,
|
||||
wpm = METER_DEFAULT,
|
||||
bm = METER_DEFAULT,
|
||||
combo = 0,
|
||||
combo_timer = 0,
|
||||
hidden = false,
|
||||
}
|
||||
end
|
||||
|
||||
--- Hides meters.
|
||||
--- @within Meter
|
||||
function Meter.hide()
|
||||
if Context and Context.meters then Context.meters.hidden = true end
|
||||
end
|
||||
|
||||
--- Shows meters.
|
||||
--- @within Meter
|
||||
function Meter.show()
|
||||
if Context and Context.meters then Context.meters.hidden = false end
|
||||
end
|
||||
|
||||
--- Gets max meter value.
|
||||
--- @within Meter
|
||||
--- @return number The maximum meter value.
|
||||
function Meter.get_max()
|
||||
return METER_MAX
|
||||
end
|
||||
|
||||
--- Sets the decay amount applied to all meters per day.
|
||||
--- @within Meter
|
||||
--- @param amount number Amount to subtract from each meter.
|
||||
function Meter.set_decay(amount)
|
||||
METER_DECAY_PER_DAY = amount
|
||||
end
|
||||
|
||||
--- Gets the meter decay as a percentage of the max meter value.
|
||||
--- @within Meter
|
||||
--- @return number The decay percentage per day.
|
||||
function Meter.get_decay_percentage()
|
||||
return math.floor(METER_DECAY_PER_DAY / METER_MAX * 100)
|
||||
end
|
||||
|
||||
--- Gets combo multiplier.
|
||||
--- @within Meter
|
||||
--- @return number The current combo multiplier.
|
||||
function Meter.get_combo_multiplier()
|
||||
if not Context or not Context.meters then return 1 end
|
||||
local combo = Context.meters.combo
|
||||
if combo == 0 then return 1 end
|
||||
return 1 + math.min(COMBO_MAX_BONUS, COMBO_BASE_BONUS * (2 ^ (combo - 1)))
|
||||
end
|
||||
|
||||
--- Updates all meters.
|
||||
--- @within Meter
|
||||
function Meter.update()
|
||||
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||
local m = Context.meters
|
||||
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
|
||||
if not in_minigame then
|
||||
if m.combo > 0 then
|
||||
m.combo_timer = m.combo_timer + 1
|
||||
if m.combo_timer >= COMBO_TIMEOUT_FRAMES then
|
||||
m.combo = 0
|
||||
m.combo_timer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Adds amount to a meter.
|
||||
--- @within Meter
|
||||
--- @param key string The meter key (e.g., "wpm", "ism", "bm").
|
||||
--- @param amount number The amount to add.
|
||||
function Meter.add(key, amount)
|
||||
if not Context or not Context.meters then return end
|
||||
local m = Context.meters
|
||||
if m[key] ~= nil then
|
||||
m[key] = math.min(METER_MAX, m[key] + amount)
|
||||
end
|
||||
end
|
||||
|
||||
--- Called on minigame completion.
|
||||
--- @within Meter
|
||||
function Meter.on_minigame_complete()
|
||||
local m = Context.meters
|
||||
local gain = math.floor(METER_GAIN_PER_CHORE * Meter.get_combo_multiplier())
|
||||
Meter.add("wpm", gain)
|
||||
Meter.add("ism", gain)
|
||||
Meter.add("bm", gain)
|
||||
m.combo = m.combo + 1
|
||||
m.combo_timer = 0
|
||||
end
|
||||
|
||||
--- Draws meters.
|
||||
--- @within Meter
|
||||
function Meter.draw()
|
||||
if not Context or not Context.game_in_progress or not Context.meters then return end
|
||||
if Context.meters.hidden then return end
|
||||
|
||||
local m = Context.meters
|
||||
local max = Meter.get_max()
|
||||
local bar_w = 44
|
||||
local bar_h = 2
|
||||
local bar_x = 182
|
||||
local label_x = 228
|
||||
local line_h = 5
|
||||
local start_y = 1
|
||||
|
||||
|
||||
local bar_offset = math.floor((line_h - bar_h) / 2)
|
||||
|
||||
local meter_list = {
|
||||
{ key = "wpm", label = "WPM", color = Meter.COLOR_WPM, row = 0 },
|
||||
{ key = "ism", label = "ISM", color = Meter.COLOR_ISM, row = 1 },
|
||||
{ key = "bm", label = "BM", color = Meter.COLOR_BM, row = 2 },
|
||||
}
|
||||
|
||||
for _, meter in ipairs(meter_list) do
|
||||
local label_y = start_y + meter.row * line_h
|
||||
local bar_y = label_y + bar_offset
|
||||
local fill_w = math.max(0, math.floor((m[meter.key] / max) * bar_w))
|
||||
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
|
||||
if fill_w > 0 then
|
||||
rect(bar_x, bar_y, fill_w, bar_h, meter.color)
|
||||
end
|
||||
print(meter.label, label_x, label_y, meter.color, false, 1, true)
|
||||
end
|
||||
end
|
||||
19
inc/logic/logic.minigame.lua
Normal file
19
inc/logic/logic.minigame.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Manages minigame configurations and initial states.
|
||||
--- @section Minigame
|
||||
|
||||
--- Draws a unified win message overlay.
|
||||
--- @within Minigame
|
||||
function Minigame.draw_win_overlay()
|
||||
local text = "SUCCESS"
|
||||
local tw = #text * 4
|
||||
local th = 6
|
||||
local padding = 4
|
||||
local box_w = tw + padding * 2
|
||||
local box_h = th + padding * 2
|
||||
local box_x = (Config.screen.width - box_w) / 2
|
||||
local box_y = (Config.screen.height - box_h) / 2
|
||||
|
||||
rect(box_x, box_y, box_w, box_h, Config.colors.dark_grey)
|
||||
rectb(box_x, box_y, box_w, box_h, Config.colors.white)
|
||||
Print.text_center(text, Config.screen.width / 2, box_y + padding, Config.colors.white)
|
||||
end
|
||||
88
inc/logic/logic.timer.lua
Normal file
88
inc/logic/logic.timer.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
--- @section Timer
|
||||
|
||||
local timer_duration = 1800
|
||||
|
||||
--- Gets initial timer values.
|
||||
--- @within Timer
|
||||
--- @return result table Initial timer values. </br>
|
||||
--- Fields: </br>
|
||||
--- * progress (number) Clock timer revolution progress (0 to 1).
|
||||
function Timer.get_initial()
|
||||
return {
|
||||
progress = 0,
|
||||
}
|
||||
end
|
||||
|
||||
--- Sets the number of frames for one full timer revolution.
|
||||
--- @within Timer
|
||||
--- @param frames number Frames per revolution.
|
||||
function Timer.set_duration(frames)
|
||||
timer_duration = frames
|
||||
end
|
||||
|
||||
--- Updates the timer and handles revolution events.
|
||||
--- @within Timer
|
||||
function Timer.update()
|
||||
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
|
||||
local t = Context.timer
|
||||
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
|
||||
|
||||
if not in_minigame then
|
||||
t.progress = t.progress + (1 / timer_duration)
|
||||
if t.progress >= 1 then
|
||||
Day.increase()
|
||||
t.progress = t.progress - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Draws the clock timer indicator as a circular progress bar.
|
||||
--- @within Timer
|
||||
function Timer.draw()
|
||||
if not Context or not Context.game_in_progress or not Context.meters or not Context.timer then return end
|
||||
if Context.meters.hidden and not Context.stat_screen_active then return end
|
||||
|
||||
local cx = 10
|
||||
local cy = 8
|
||||
local r_outer = 5
|
||||
local r_inner = 3
|
||||
local progress = Context.timer.progress
|
||||
|
||||
local fg_color
|
||||
if progress <= 0.25 then
|
||||
fg_color = Config.colors.dark_grey
|
||||
elseif progress <= 0.5 then
|
||||
fg_color = Config.colors.light_blue
|
||||
elseif progress <= 0.75 then
|
||||
fg_color = Config.colors.blue
|
||||
elseif progress <= 1 then
|
||||
fg_color = Config.colors.red
|
||||
end
|
||||
|
||||
local bg_color = Config.colors.white
|
||||
local start_angle = -math.pi * 0.5
|
||||
local progress_angle = progress * 2 * math.pi
|
||||
local r_outer_sq = r_outer * r_outer
|
||||
local r_inner_sq = r_inner * r_inner
|
||||
|
||||
for dy = -r_outer, r_outer do
|
||||
for dx = -r_outer, r_outer do
|
||||
local dist_sq = dx * dx + dy * dy
|
||||
if dist_sq <= r_outer_sq and dist_sq > r_inner_sq then
|
||||
local angle = math.atan(dy, dx)
|
||||
local relative = angle - start_angle
|
||||
if relative < 0 then relative = relative + 2 * math.pi end
|
||||
if relative <= progress_angle then
|
||||
pix(cx + dx, cy + dy, fg_color)
|
||||
else
|
||||
pix(cx + dx, cy + dy, bg_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hand_angle = start_angle + progress_angle
|
||||
local hand_x = math.floor(cx + math.cos(hand_angle) * (r_inner - 1) + 0.5)
|
||||
local hand_y = math.floor(cy + math.sin(hand_angle) * (r_inner - 1) + 0.5)
|
||||
line(cx, cy, hand_x, hand_y, Config.colors.white)
|
||||
end
|
||||
135
inc/logic/logic.trigger.lua
Normal file
135
inc/logic/logic.trigger.lua
Normal file
@@ -0,0 +1,135 @@
|
||||
--- @section Trigger
|
||||
local triggers = {}
|
||||
|
||||
--- @within Trigger
|
||||
--- @param trigger table The trigger data table.
|
||||
--- @param trigger.id string Unique trigger identifier.
|
||||
--- @param trigger.duration number Duration in frames before the trigger fires.
|
||||
--- @param[opt] trigger.on_start function Called when the trigger starts. Defaults to noop.
|
||||
--- @param[opt] trigger.on_stop function Called when the trigger fires or is manually stopped. Defaults to noop.
|
||||
--- @param[opt] trigger.repeating boolean If true, trigger restarts after firing. Defaults to false.
|
||||
function Trigger.register(trigger)
|
||||
if not trigger or not trigger.id then
|
||||
trace("Error: Invalid trigger registered (missing id)!")
|
||||
return
|
||||
end
|
||||
if not trigger.duration or trigger.duration <= 0 then
|
||||
trace("Error: Invalid trigger registered (missing or invalid duration)!")
|
||||
return
|
||||
end
|
||||
|
||||
if not trigger.on_start then
|
||||
trigger.on_start = function() end
|
||||
end
|
||||
if not trigger.on_stop then
|
||||
trigger.on_stop = function() end
|
||||
end
|
||||
if trigger.repeating == nil then
|
||||
trigger.repeating = false
|
||||
end
|
||||
if triggers[trigger.id] then
|
||||
trace("Warning: Overwriting trigger with id: " .. trigger.id)
|
||||
end
|
||||
triggers[trigger.id] = trigger
|
||||
end
|
||||
|
||||
--- @within Trigger
|
||||
--- @param id string The trigger ID.
|
||||
--- @return table|nil result The trigger definition or nil.
|
||||
function Trigger.get_by_id(id)
|
||||
return triggers[id]
|
||||
end
|
||||
|
||||
--- @within Trigger
|
||||
--- @return table result All trigger definitions keyed by ID.
|
||||
function Trigger.get_all()
|
||||
return triggers
|
||||
end
|
||||
|
||||
--- @within Trigger
|
||||
--- @param id string The trigger ID.
|
||||
--- @return boolean active True if the trigger is running.
|
||||
function Trigger.is_active(id)
|
||||
if not Context or not Context.triggers then return false end
|
||||
return Context.triggers[id] ~= nil
|
||||
end
|
||||
|
||||
--- If already active, restarts from 0.
|
||||
--- @within Trigger
|
||||
--- @param id string The trigger ID.
|
||||
function Trigger.start(id)
|
||||
if not Context or not Context.triggers then return end
|
||||
local trigger = triggers[id]
|
||||
if not trigger then
|
||||
trace("Error: Cannot start unknown trigger: " .. tostring(id))
|
||||
return
|
||||
end
|
||||
|
||||
Context.triggers[id] = { elapsed = 0 }
|
||||
trigger.on_start()
|
||||
end
|
||||
|
||||
--- @within Trigger
|
||||
--- @param id string The trigger ID.
|
||||
function Trigger.stop(id)
|
||||
if not Context or not Context.triggers then return end
|
||||
local trigger = triggers[id]
|
||||
if not trigger then
|
||||
trace("Error: Cannot stop unknown trigger: " .. tostring(id))
|
||||
return
|
||||
end
|
||||
if not Context.triggers[id] then return end
|
||||
|
||||
Context.triggers[id] = nil
|
||||
trigger.on_stop()
|
||||
end
|
||||
|
||||
--- Resets elapsed time to 0 without calling handlers. No-op if inactive.
|
||||
--- @within Trigger
|
||||
--- @param id string The trigger ID.
|
||||
function Trigger.reset(id)
|
||||
if not Context or not Context.triggers then return end
|
||||
if not triggers[id] then
|
||||
trace("Error: Cannot reset unknown trigger: " .. tostring(id))
|
||||
return
|
||||
end
|
||||
if not Context.triggers[id] then return end
|
||||
|
||||
Context.triggers[id].elapsed = 0
|
||||
end
|
||||
|
||||
--- Pauses during minigames.
|
||||
--- @within Trigger
|
||||
function Trigger.update()
|
||||
if not Context or not Context.game_in_progress or not Context.triggers then return end
|
||||
|
||||
local in_minigame = string.find(Window.get_current_id(), "^minigame_") ~= nil
|
||||
if in_minigame then return end
|
||||
|
||||
local fired = {}
|
||||
for id, state in pairs(Context.triggers) do
|
||||
local trigger = triggers[id]
|
||||
if trigger then
|
||||
state.elapsed = state.elapsed + 1
|
||||
if state.elapsed >= trigger.duration then
|
||||
table.insert(fired, id)
|
||||
end
|
||||
else
|
||||
table.insert(fired, id)
|
||||
end
|
||||
end
|
||||
|
||||
for _, id in ipairs(fired) do
|
||||
local trigger = triggers[id]
|
||||
if trigger then
|
||||
trigger.on_stop()
|
||||
if trigger.repeating then
|
||||
Context.triggers[id] = { elapsed = 0 }
|
||||
else
|
||||
Context.triggers[id] = nil
|
||||
end
|
||||
else
|
||||
Context.triggers[id] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
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,340 @@
|
||||
-- <TILES>
|
||||
-- 000:4444444444444444444444444444444444444444444444444444444444444444
|
||||
-- 001:1111111111111111111111111111111111111111111111111111111111111111
|
||||
-- 002:5555555555555555555555555555555555555555555555555555555555555555
|
||||
-- 003:6666666666666666666666666666666666666666666666666666666666666666
|
||||
-- 004:7777777777777777777777777777777777777777777777777777777777777777
|
||||
-- 005:8888888888888888888888888888888888888888888888888888888888888888
|
||||
-- 006:9999999999999999999999999999999999999999999999999999999999999999
|
||||
-- 007:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
-- 008:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
-- </TILES>
|
||||
|
||||
-- <WAVES>
|
||||
-- 000:00000000ffffffff00000000ffffffff
|
||||
-- 001:0123456789abcdeffedcba9876543210
|
||||
-- 02:0123456789abcdef0123456789abcdef
|
||||
-- </WAVES>
|
||||
|
||||
-- <SFX>
|
||||
-- 000:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000304000000000
|
||||
-- </SFX>
|
||||
|
||||
-- <TRACKS>
|
||||
-- 000:100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
-- </TRACKS>
|
||||
|
||||
-- <PALETTE>
|
||||
-- 000:1a1c2c5d275db13e53ef7d57ffcd75a7f07038b76425717929366f3b5dc941a6f673eff7f4f4f494b0c2566c86333c57
|
||||
-- 000:0404005f574fc2c3c71d2b53fff1e8ab52367e2553ffa30000875129adff83769c00e436fa77a8ff004dc3c3c7ffccaa
|
||||
-- </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
|
||||
-- 048:090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900090009000900400000000000
|
||||
-- 056:4100510061406140717081709100b100c100d100e100e100e100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f100f10058a000000600
|
||||
-- 057:000000010002000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d40000000004
|
||||
-- 058:41004110410041104100411041004110c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100c100500000080800
|
||||
-- 059:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000
|
||||
-- 060:220022002200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200820082008200100000000000
|
||||
-- 061:9f009f00bf00df00df00ef00ef00ef00ef00ef00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00400000000000
|
||||
-- 062:00000100010001000100510081008100910091009100a100a100a100a100a100b100b100b100b100c100c100c100d100d100d100e100e100e100f100484000000000
|
||||
-- 063:00b000100000000000000000100060009000b000c000d000d000e000e000e000f000f000f000f000f000f000f000f000f000f000f000f000f000f000200000000000
|
||||
-- </SFX>
|
||||
-- <WAVES>
|
||||
-- 000:bcceefceedddddc84333121268abaa99
|
||||
-- 001:6789bdd96adc83248dd6334adda7578b
|
||||
-- 002:0123456789abcdef0123456789abcdef
|
||||
-- 003:224578acdeeeeddcba95434567653100
|
||||
-- 004:00000000ffffffff00000000ffffffff
|
||||
-- 005:0123456789abcdeffedcba9876543210
|
||||
-- 006:0123456789abcdef0123456789abcdef
|
||||
-- 007:76543210123456789abcdefedcba9878
|
||||
-- 008:0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f
|
||||
-- 009:fff000fff000fff000fff000fff000ff
|
||||
-- </WAVES>
|
||||
-- <PATTERNS>
|
||||
-- 000:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000008c10000000008c10000000000000000000000000000000000000000000000000000000000000000004008b50000000000000000001008c10000004008b50000001008c10000000008c1000000e008b30000004008b50000001008c10000000008c10000000008c10000000008c10000000008c10000000008c1000000000000000000000000000000
|
||||
-- 001:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007008b50000007008b50000001008c10000007008b50000001008c10000000008c10000007008b50000009008b50000001008c10000009008b50000001008c10000009008b50000009008b50000001008c10000009008b50000001008c1000000
|
||||
-- 003:4008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d9000000000000000000000000000000000000000000
|
||||
-- 004:40088d000000e0088b000000b0088b000881e0088b00000040088d000000e0088b000881b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e00889000000
|
||||
-- 005:400881000000000881000000000881000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000
|
||||
-- </PATTERNS>
|
||||
-- <TRACKS>
|
||||
-- 000:100001200001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
-- 001:581000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
-- </TRACKS>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
-- title: Mr Anderson's Adventure
|
||||
-- author: Zsolt Tasnadi
|
||||
-- desc: Life of a programmer in the Vector
|
||||
-- site: https://github.com/rastasi/mranderson
|
||||
-- title: Definitely not an Impostor
|
||||
-- name: impostor
|
||||
-- author: Teletype Games
|
||||
-- desc: Life of a programmer
|
||||
-- site: https://git.teletype.hu/games/impostor
|
||||
-- license: MIT License
|
||||
-- version: 0.10
|
||||
-- version: 0.8
|
||||
-- script: lua
|
||||
|
||||
15
inc/screen/screen.home.lua
Normal file
15
inc/screen/screen.home.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
Screen.register({
|
||||
id = "home",
|
||||
name = "Home",
|
||||
decisions = {
|
||||
"go_to_toilet",
|
||||
"go_to_walking_to_office",
|
||||
"go_to_sleep",
|
||||
},
|
||||
background = "bedroom",
|
||||
draw = function()
|
||||
if Context.home_norman_visible and Window.get_current_id() == "game" then
|
||||
Sprite.draw_at("norman", 100, 80)
|
||||
end
|
||||
end
|
||||
})
|
||||
66
inc/screen/screen.manager.lua
Normal file
66
inc/screen/screen.manager.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
--- @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.exit then
|
||||
screen_data.exit = 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
|
||||
13
inc/screen/screen.office.lua
Normal file
13
inc/screen/screen.office.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
Screen.register({
|
||||
id = "office",
|
||||
name = "Office",
|
||||
decisions = {
|
||||
"do_work",
|
||||
"go_to_walking_to_home",
|
||||
"have_a_coffee",
|
||||
},
|
||||
situations = {
|
||||
"drink_coffee",
|
||||
},
|
||||
background = "office"
|
||||
})
|
||||
79
inc/screen/screen.toilet.lua
Normal file
79
inc/screen/screen.toilet.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
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 norman_x = math.floor(sw * 0.75)
|
||||
local norman_y = math.floor(Config.screen.height * 0.75)
|
||||
local bar_w = math.floor(sw * 0.75)
|
||||
local bar_x = math.floor((sw - bar_w) / 2)
|
||||
local bar_h = 4
|
||||
|
||||
Sprite.draw_at("norman", norman_x, norman_y)
|
||||
|
||||
Print.text_center("day " .. Context.day_count, cx, 10, Config.colors.white)
|
||||
|
||||
local narrative = "reflecting on my past and present\n...\nboth eventually flushed."
|
||||
local wrapped = UI.word_wrap(narrative, 38)
|
||||
local text_y = 24
|
||||
for _, line in ipairs(wrapped) do
|
||||
Print.text_center(line, cx, text_y, Config.colors.light_grey)
|
||||
text_y = text_y + 8
|
||||
end
|
||||
|
||||
local m = Context.meters
|
||||
local max_val = Meter.get_max()
|
||||
local decay_pct = Meter.get_decay_percentage()
|
||||
local decay_text = string.format("-%d%%", decay_pct)
|
||||
local combo_mult = Meter.get_combo_multiplier()
|
||||
local combo_pct = math.floor((combo_mult - 1) * 100)
|
||||
local mult_text = string.format("+%d%%", combo_pct)
|
||||
local meter_start_y = text_y + 10
|
||||
|
||||
local meter_list = {
|
||||
{ key = "wpm", label = "Work Productivity Meter" },
|
||||
{ key = "ism", label = "Impostor Syndrome Meter" },
|
||||
{ key = "bm", label = "Burnout Meter" },
|
||||
}
|
||||
|
||||
for i, meter in ipairs(meter_list) do
|
||||
local y = meter_start_y + (i - 1) * 20
|
||||
|
||||
Print.text_center(meter.label, cx, y, Config.colors.white)
|
||||
|
||||
local bar_y = y + 8
|
||||
local fill_w = math.max(0, math.floor((m[meter.key] / max_val) * bar_w))
|
||||
rect(bar_x, bar_y, bar_w, bar_h, Meter.COLOR_BG)
|
||||
if fill_w > 0 then
|
||||
rect(bar_x, bar_y, fill_w, bar_h, Config.colors.blue)
|
||||
end
|
||||
|
||||
local decay_w = print(decay_text, 0, -6, 0, false, 1)
|
||||
Print.text(decay_text, bar_x - decay_w - 4, bar_y, Config.colors.light_blue)
|
||||
Print.text(mult_text, bar_x + bar_w + 4, bar_y, Config.colors.light_blue)
|
||||
end
|
||||
end,
|
||||
})
|
||||
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"
|
||||
})
|
||||
10
inc/screen/screen.walking_to_office.lua
Normal file
10
inc/screen/screen.walking_to_office.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
Screen.register({
|
||||
id = "walking_to_office",
|
||||
name = "Walking to office",
|
||||
decisions = {
|
||||
"go_to_home",
|
||||
"go_to_office",
|
||||
"start_discussion",
|
||||
},
|
||||
background = "street"
|
||||
})
|
||||
6
inc/screen/screen.work.lua
Normal file
6
inc/screen/screen.work.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
Screen.register({
|
||||
id = "work",
|
||||
name = "Work",
|
||||
decisions = {},
|
||||
background_color = Config.colors.blue,
|
||||
})
|
||||
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
|
||||
129
inc/sprite/sprite.manager.lua
Normal file
129
inc/sprite/sprite.manager.lua
Normal file
@@ -0,0 +1,129 @@
|
||||
--- @section Sprite
|
||||
local _sprites = {}
|
||||
local _active_sprites = {}
|
||||
|
||||
local function draw_sprite_instance(sprite_data, params)
|
||||
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
|
||||
|
||||
--- 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 a sprite immediately without scheduling it.
|
||||
--- @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.draw_at(id, x, y, colorkey, scale, flip_x, flip_y, rot)
|
||||
local sprite_data = _sprites[id]
|
||||
if not sprite_data then
|
||||
trace("Error: Attempted to draw non-registered sprite with id: " .. id)
|
||||
return
|
||||
end
|
||||
|
||||
draw_sprite_instance(sprite_data, {
|
||||
x = x,
|
||||
y = y,
|
||||
colorkey = colorkey,
|
||||
scale = scale,
|
||||
flip_x = flip_x,
|
||||
flip_y = flip_y,
|
||||
rot = rot,
|
||||
})
|
||||
end
|
||||
|
||||
--- Draws all scheduled sprites.
|
||||
--- @within Sprite
|
||||
function Sprite.draw()
|
||||
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
|
||||
if sprite_data then
|
||||
draw_sprite_instance(sprite_data, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
23
inc/sprite/sprite.norman.lua
Normal file
23
inc/sprite/sprite.norman.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
Sprite.register({
|
||||
id = "norman",
|
||||
sprites = {
|
||||
{ s = 272, x_offset = -4, y_offset = -4 },
|
||||
{ s = 273, x_offset = 4, y_offset = -4 },
|
||||
{ s = 288, x_offset = -4, y_offset = 4 },
|
||||
{ s = 289, x_offset = 4, y_offset = 4 },
|
||||
{ s = 304, x_offset = -4, y_offset = 12 },
|
||||
{ s = 305, x_offset = 4, y_offset = 12 }
|
||||
}
|
||||
})
|
||||
|
||||
Sprite.register({
|
||||
id = "sleeping_norman",
|
||||
sprites = {
|
||||
{ s = 272, x_offset = 12, y_offset = -4, flip_y = 1 },
|
||||
{ s = 273, x_offset = 12, y_offset = 4, flip_y = 1 },
|
||||
{ s = 288, x_offset = 4, y_offset = -4, flip_y = 1 },
|
||||
{ s = 289, x_offset = 4, y_offset = 4, flip_y = 1 },
|
||||
{ s = 304, x_offset = -4, y_offset = -4, flip_y = 1 },
|
||||
{ s = 305, x_offset = -4, y_offset = 4, flip_y = 1 }
|
||||
}
|
||||
})
|
||||
63
inc/system/system.asciiart.lua
Normal file
63
inc/system/system.asciiart.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
--- @section AsciiArt
|
||||
AsciiArt = {}
|
||||
|
||||
--- Draws ASCII art text using rectangles.
|
||||
--- @param text string The ASCII art text to draw.
|
||||
--- @param options table Configuration options (char_w, char_h, line_gap, word_gap, color, x, y).
|
||||
function AsciiArt.draw(text, options)
|
||||
options = options or {}
|
||||
local char_w = options.char_w or 4
|
||||
local char_h = options.char_h or 5
|
||||
local line_gap = options.line_gap or 0
|
||||
local word_gap = options.word_gap or 6
|
||||
local color = options.color or Config.colors.light_blue
|
||||
|
||||
local lines = {}
|
||||
local max_len = 0
|
||||
-- Get all lines and find max length
|
||||
for line in (text .. "\n"):gmatch("(.-)\n") do
|
||||
table.insert(lines, line)
|
||||
if #line > max_len then max_len = #line end
|
||||
end
|
||||
|
||||
-- Clean up empty lines from the start/end
|
||||
while #lines > 0 and lines[1]:gsub("%s+", "") == "" do table.remove(lines, 1) end
|
||||
while #lines > 0 and lines[#lines]:gsub("%s+", "") == "" do table.remove(lines, #lines) end
|
||||
|
||||
if #lines == 0 then return end
|
||||
|
||||
local total_h = 0
|
||||
for _, line in ipairs(lines) do
|
||||
if line:find("#") then
|
||||
total_h = total_h + char_h + line_gap
|
||||
else
|
||||
total_h = total_h + word_gap
|
||||
end
|
||||
end
|
||||
total_h = total_h - line_gap
|
||||
|
||||
local current_y = options.y or (Config.screen.height - total_h) / 2
|
||||
local x_offset = options.x or (Config.screen.width - (max_len * char_w)) / 2
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
if line:find("#") then
|
||||
for j = 1, #line do
|
||||
local char = line:sub(j, j)
|
||||
if char == "#" then
|
||||
rect(x_offset + (j - 1) * char_w, current_y, char_w - 1, char_h - 1, color)
|
||||
end
|
||||
end
|
||||
current_y = current_y + char_h + line_gap
|
||||
else
|
||||
current_y = current_y + word_gap
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
x = x_offset,
|
||||
y = options.y or (Config.screen.height - total_h) / 2,
|
||||
width = max_len * char_w,
|
||||
height = total_h,
|
||||
bottom = (options.y or (Config.screen.height - total_h) / 2) + total_h
|
||||
}
|
||||
end
|
||||
@@ -1,8 +1,39 @@
|
||||
function Input.up() return btnp(0) end
|
||||
function Input.down() return btnp(1) end
|
||||
function Input.left() return btnp(2) end
|
||||
function Input.right() return btnp(3) end
|
||||
function Input.player_jump() return btnp(4) end
|
||||
function Input.menu_confirm() return btnp(4) end
|
||||
function Input.player_interact() return btnp(5) end -- B button
|
||||
function Input.menu_back() return btnp(5) end
|
||||
--- @section Input
|
||||
local INPUT_KEY_UP = 0
|
||||
local INPUT_KEY_DOWN = 1
|
||||
local INPUT_KEY_LEFT = 2
|
||||
local INPUT_KEY_RIGHT = 3
|
||||
local INPUT_KEY_A = 4
|
||||
local INPUT_KEY_B = 5
|
||||
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,33 @@
|
||||
local STATE_HANDLERS = {
|
||||
[WINDOW_SPLASH] = function()
|
||||
SplashWindow.update()
|
||||
SplashWindow.draw()
|
||||
end,
|
||||
[WINDOW_INTRO] = function()
|
||||
IntroWindow.update()
|
||||
IntroWindow.draw()
|
||||
end,
|
||||
[WINDOW_MENU] = function()
|
||||
MenuWindow.update()
|
||||
MenuWindow.draw()
|
||||
end,
|
||||
[WINDOW_GAME] = function()
|
||||
GameWindow.update()
|
||||
GameWindow.draw()
|
||||
end,
|
||||
[WINDOW_POPUP] = function()
|
||||
GameWindow.draw()
|
||||
PopupWindow.update()
|
||||
PopupWindow.draw()
|
||||
end,
|
||||
[WINDOW_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,
|
||||
}
|
||||
--- @section Main
|
||||
local initialized_game = false
|
||||
|
||||
--- Initializes game state.
|
||||
--- @within Main
|
||||
--- @return boolean initialized_game True if game has been initialized, false otherwise.
|
||||
local function init_game()
|
||||
if initialized_game then return false end
|
||||
Context.reset()
|
||||
Window.set_current("intro_title") -- Set initial window using new manager
|
||||
MenuWindow.refresh_menu_items()
|
||||
initialized_game = true
|
||||
return true
|
||||
end
|
||||
|
||||
--- Main game loop (TIC-80 callback).
|
||||
--- @within Main
|
||||
function TIC()
|
||||
init_game()
|
||||
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
|
||||
handler()
|
||||
end
|
||||
Meter.update()
|
||||
Timer.update()
|
||||
Trigger.update()
|
||||
Glitch.draw()
|
||||
if Context.game_in_progress then
|
||||
Meter.draw()
|
||||
Timer.draw()
|
||||
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,53 @@
|
||||
--- @section UI
|
||||
|
||||
--- Draws the top bar.
|
||||
--- @within UI
|
||||
--- @param title string The title text to display.<br/>
|
||||
function UI.draw_top_bar(title)
|
||||
rect(0, 0, Config.screen.width, 10, Config.colors.dark_grey)
|
||||
print(title, 3, 2, Config.colors.green)
|
||||
Print.text(title, 3, 2, Config.colors.light_blue)
|
||||
end
|
||||
|
||||
function UI.draw_dialog()
|
||||
PopupWindow.draw()
|
||||
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 (ignored if centered is true).<br/>
|
||||
--- @param y number The y-coordinate for the menu.<br/>
|
||||
--- @param[opt] centered boolean Whether to center the menu block horizontally. Defaults to false.<br/>
|
||||
function UI.draw_menu(items, selected_item, x, y, centered)
|
||||
if centered then
|
||||
local max_w = 0
|
||||
for _, item in ipairs(items) do
|
||||
local w = print(item.label, 0, -10, 0, false, 1, false)
|
||||
if w > max_w then max_w = w end
|
||||
end
|
||||
x = (Config.screen.width - max_w) / 2
|
||||
end
|
||||
|
||||
function UI.draw_menu(items, selected_item, x, y)
|
||||
for i, item in ipairs(items) do
|
||||
local current_y = y + (i-1)*10
|
||||
if i == selected_item then
|
||||
print(">", x - 8, current_y, Config.colors.green)
|
||||
Print.text(">", x - 8, current_y, Config.colors.light_blue)
|
||||
end
|
||||
print(item.label, x, current_y, Config.colors.green)
|
||||
Print.text(item.label, x, current_y, Config.colors.light_blue)
|
||||
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)
|
||||
if Input.up() then
|
||||
Audio.sfx_beep()
|
||||
selected_item = selected_item - 1
|
||||
if selected_item < 1 then
|
||||
selected_item = #items
|
||||
end
|
||||
elseif Input.down() then
|
||||
Audio.sfx_beep()
|
||||
selected_item = selected_item + 1
|
||||
if selected_item > #items then
|
||||
selected_item = 1
|
||||
@@ -32,48 +56,108 @@ function UI.update_menu(items, selected_item)
|
||||
return selected_item
|
||||
end
|
||||
|
||||
--- Draws a bordered textbox with scrolling text.
|
||||
--- @within UI
|
||||
--- @param text string The text to display (multi-line supported).<br/>
|
||||
--- @param box_x number The x-coordinate of the box.<br/>
|
||||
--- @param box_y number The y-coordinate of the box.<br/>
|
||||
--- @param box_w number The width of the box.<br/>
|
||||
--- @param box_h number The height of the box.<br/>
|
||||
--- @param scroll_y number The vertical scroll offset for the text (0 = top, increases to scroll up).<br/>
|
||||
--- @param[opt] color number The text color (default: Config.colors.white).<br/>
|
||||
--- @param[opt] bg_color number The background fill color (default: Config.colors.dark_grey).<br/>
|
||||
--- @param[opt] border_color number The border color (default: Config.colors.white).<br/>
|
||||
--- @param[opt] center_text boolean Whether to center each line inside the box. Defaults to false.<br/>
|
||||
function UI.draw_textbox(text, box_x, box_y, box_w, box_h, scroll_y, color, bg_color, border_color, center_text)
|
||||
color = color or Config.colors.white
|
||||
bg_color = bg_color or Config.colors.dark_grey
|
||||
border_color = border_color or Config.colors.white
|
||||
center_text = center_text or false
|
||||
|
||||
local padding = 4
|
||||
local line_height = 8
|
||||
local inner_x = box_x + padding
|
||||
local inner_y = box_y + padding
|
||||
local inner_center_x = box_x + (box_w / 2)
|
||||
local visible_height = box_h - padding * 2
|
||||
local lines = UI.word_wrap(text, 30)
|
||||
local text_height = #lines * line_height
|
||||
local base_y = inner_y
|
||||
|
||||
if center_text and text_height < visible_height then
|
||||
base_y = inner_y + math.floor((visible_height - text_height) / 2)
|
||||
end
|
||||
|
||||
rect(box_x, box_y, box_w, box_h, bg_color)
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
local ly = base_y + (i - 1) * line_height - scroll_y
|
||||
if ly >= inner_y and ly + line_height <= inner_y + visible_height then
|
||||
if center_text then
|
||||
Print.text_center(line, inner_center_x, ly, color)
|
||||
else
|
||||
Print.text(line, inner_x, ly, color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
rectb(box_x, box_y, box_w, box_h, border_color)
|
||||
end
|
||||
|
||||
--- Wraps text.
|
||||
--- @within UI
|
||||
--- @param text string The text to wrap.<br/>
|
||||
--- @param max_chars_per_line number The maximum characters per line.<br/>
|
||||
--- @return result table A table of wrapped lines.
|
||||
function UI.word_wrap(text, max_chars_per_line)
|
||||
if text == nil then return {""} end
|
||||
local lines = {}
|
||||
if text == nil then return {""} end
|
||||
|
||||
for input_line in (text .. "\n"):gmatch("(.-)\n") do
|
||||
local current_line = ""
|
||||
local words_in_line = 0
|
||||
for word in input_line:gmatch("%S+") do
|
||||
words_in_line = words_in_line + 1
|
||||
if #current_line == 0 then
|
||||
current_line = word
|
||||
elseif #current_line + #word + 1 <= max_chars_per_line then
|
||||
current_line = current_line .. " " .. word
|
||||
else
|
||||
table.insert(lines, current_line)
|
||||
current_line = word
|
||||
end
|
||||
end
|
||||
local lines = {}
|
||||
|
||||
if words_in_line > 0 then
|
||||
table.insert(lines, current_line)
|
||||
else
|
||||
table.insert(lines, "")
|
||||
end
|
||||
local function trim(s)
|
||||
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
|
||||
end
|
||||
|
||||
local function previous_whitespace_index(s, target)
|
||||
if s:sub(target, target):match("%s") then
|
||||
return target
|
||||
end
|
||||
|
||||
if #lines == 0 then
|
||||
return {""}
|
||||
for i = target - 1, 1, -1 do
|
||||
if s:sub(i, i):match("%s") then
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
|
||||
return {
|
||||
label = label,
|
||||
get = value_getter,
|
||||
set = value_setter,
|
||||
min = min,
|
||||
max = max,
|
||||
step = step,
|
||||
format = format or "%.1f",
|
||||
type = "numeric_stepper"
|
||||
}
|
||||
for input_line in (text .. "\n"):gmatch("(.-)\n") do
|
||||
local remaining = trim(input_line)
|
||||
|
||||
if remaining == "" then
|
||||
table.insert(lines, "")
|
||||
else
|
||||
while #remaining > max_chars_per_line do
|
||||
local split_at = previous_whitespace_index(remaining, max_chars_per_line)
|
||||
local line = trim(remaining:sub(1, split_at))
|
||||
|
||||
if not split_at or line == "" then
|
||||
line = remaining:sub(1, max_chars_per_line)
|
||||
split_at = max_chars_per_line
|
||||
end
|
||||
|
||||
table.insert(lines, line)
|
||||
remaining = trim(remaining:sub(split_at + 1))
|
||||
end
|
||||
|
||||
table.insert(lines, remaining)
|
||||
end
|
||||
end
|
||||
|
||||
if #lines == 0 then
|
||||
return {""}
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
42
inc/system/system.util.lua
Normal file
42
inc/system/system.util.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
--- @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 prev_screen = Screen.get_by_id(Context.game.current_screen)
|
||||
local new_screen = Screen.get_by_id(screen_id)
|
||||
if new_screen then
|
||||
Context.game.current_screen = screen_id
|
||||
if prev_screen and prev_screen.exit then
|
||||
prev_screen.exit()
|
||||
end
|
||||
new_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
|
||||
115
inc/window/window.audiotest.lua
Normal file
115
inc/window/window.audiotest.lua
Normal file
@@ -0,0 +1,115 @@
|
||||
--- @section AudioTestWindow
|
||||
AudioTestWindow.index_menu = 1
|
||||
AudioTestWindow.index_func = 1
|
||||
AudioTestWindow.list_func = {}
|
||||
AudioTestWindow.menuitems = {}
|
||||
AudioTestWindow.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 @@
|
||||
ConfigurationWindow = {
|
||||
controls = {},
|
||||
selected_control = 1,
|
||||
}
|
||||
--- @section ConfigurationWindow
|
||||
ConfigurationWindow.controls = {}
|
||||
ConfigurationWindow.selected_control = 1
|
||||
|
||||
--- Initializes configuration window.
|
||||
--- @within ConfigurationWindow
|
||||
function ConfigurationWindow.init()
|
||||
ConfigurationWindow.controls = {
|
||||
UI.create_numeric_stepper(
|
||||
"Move Speed",
|
||||
function() return Config.physics.move_speed end,
|
||||
function(v) Config.physics.move_speed = v end,
|
||||
0.5, 3, 0.1, "%.1f"
|
||||
),
|
||||
UI.create_numeric_stepper(
|
||||
"Max Jumps",
|
||||
function() return Config.physics.max_jumps end,
|
||||
function(v) Config.physics.max_jumps = v end,
|
||||
1, 5, 1, "%d"
|
||||
),
|
||||
{
|
||||
label = "Save",
|
||||
action = function() Config.save() end,
|
||||
type = "action_item"
|
||||
},
|
||||
{
|
||||
label = "Restore Defaults",
|
||||
action = function() Config.reset() end,
|
||||
type = "action_item"
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- Draws configuration window.
|
||||
--- @within ConfigurationWindow
|
||||
function ConfigurationWindow.draw()
|
||||
UI.draw_top_bar("Configuration")
|
||||
|
||||
local x_start = 10 -- Left margin for labels
|
||||
local x_start = 10
|
||||
local y_start = 40
|
||||
local x_value_right_align = Config.screen.width - 10 -- Right margin for values
|
||||
local char_width = 4 -- Approximate character width for default font
|
||||
|
||||
local x_value_right_align = Config.screen.width - 10
|
||||
local char_width = 4
|
||||
for i, control in ipairs(ConfigurationWindow.controls) do
|
||||
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()
|
||||
local label_text = control.label
|
||||
local value_text = string.format(control.format, value)
|
||||
|
||||
-- Calculate x position for right-aligned value
|
||||
local value_x = x_value_right_align - (#value_text * char_width)
|
||||
|
||||
if i == ConfigurationWindow.selected_control then
|
||||
color = Config.colors.item
|
||||
print("<", x_start -8, current_y, color)
|
||||
print(label_text, x_start, current_y, color) -- Shift label due to '<'
|
||||
print(value_text, value_x, current_y, color)
|
||||
print(">", x_value_right_align + 4, current_y, color) -- Print '>' after value
|
||||
else
|
||||
print(label_text, x_start, current_y, color)
|
||||
print(value_text, value_x, current_y, color)
|
||||
if i == ConfigurationWindow.selected_control then
|
||||
color = Config.colors.item
|
||||
Print.text("<", x_start - 8, current_y, color)
|
||||
Print.text(label_text, x_start, current_y, color)
|
||||
Print.text(value_text, value_x, current_y, color)
|
||||
Print.text(">", x_value_right_align + 4, current_y, color)
|
||||
else
|
||||
Print.text(label_text, x_start, current_y, color)
|
||||
Print.text(value_text, value_x, current_y, color)
|
||||
end
|
||||
elseif control.type == "action_item" then
|
||||
local label_text = control.label
|
||||
if i == ConfigurationWindow.selected_control then
|
||||
color = Config.colors.item
|
||||
Print.text("<", x_start - 8, current_y, color)
|
||||
Print.text(label_text, x_start, current_y, color)
|
||||
Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color)
|
||||
else
|
||||
Print.text(label_text, x_start, current_y, color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print("Press B to go back", x_start, 120, Config.colors.light_grey)
|
||||
Print.text("Press B to go back", x_start, 120, Config.colors.light_grey)
|
||||
end
|
||||
|
||||
--- Updates configuration window logic.
|
||||
--- @within ConfigurationWindow
|
||||
function ConfigurationWindow.update()
|
||||
if Input.menu_back() then
|
||||
-- I need to find out how to switch back to the menu
|
||||
-- For now, I'll assume a function GameWindow.set_state exists
|
||||
GameWindow.set_state(WINDOW_MENU)
|
||||
GameWindow.set_state("menu")
|
||||
return
|
||||
end
|
||||
|
||||
-- Navigate between controls
|
||||
if Input.up() then
|
||||
ConfigurationWindow.selected_control = ConfigurationWindow.selected_control - 1
|
||||
if ConfigurationWindow.selected_control < 1 then
|
||||
@@ -75,16 +82,21 @@ function ConfigurationWindow.update()
|
||||
end
|
||||
end
|
||||
|
||||
-- Modify control value
|
||||
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
|
||||
if control then
|
||||
local current_value = control.get()
|
||||
if Input.left() then
|
||||
local new_value = math.max(control.min, current_value - control.step)
|
||||
control.set(new_value)
|
||||
elseif Input.right() then
|
||||
local new_value = math.min(control.max, current_value + control.step)
|
||||
control.set(new_value)
|
||||
if control.type == "numeric_stepper" then
|
||||
local current_value = control.get()
|
||||
if Input.left() then
|
||||
local new_value = math.max(control.min, current_value - control.step)
|
||||
control.set(new_value)
|
||||
elseif Input.right() then
|
||||
local new_value = math.min(control.max, current_value + control.step)
|
||||
control.set(new_value)
|
||||
end
|
||||
elseif control.type == "action_item" then
|
||||
if Input.menu_confirm() then
|
||||
control.action()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
33
inc/window/window.continued.lua
Normal file
33
inc/window/window.continued.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
--- @section ContinuedWindow
|
||||
ContinuedWindow.timer = 300 -- 5 seconds at 60fps
|
||||
ContinuedWindow.text = [[
|
||||
### ### ### ###
|
||||
# # # # # #
|
||||
# # # ### ##
|
||||
# # # # # #
|
||||
# ### ### ###
|
||||
|
||||
### ### # # ### ### # # # # ### ##
|
||||
# # # ## # # # ## # # # # # #
|
||||
# # # # ## # # # ## # # ## # #
|
||||
# # # # # # # # # # # # # #
|
||||
### ### # # # ### # # ### ### ##
|
||||
|
||||
]]
|
||||
|
||||
--- Draws the continued window.
|
||||
--- @within ContinuedWindow
|
||||
function ContinuedWindow.draw()
|
||||
cls(Config.colors.black)
|
||||
AsciiArt.draw(ContinuedWindow.text, {})
|
||||
end
|
||||
|
||||
--- Updates the continued window logic.
|
||||
--- @within ContinuedWindow
|
||||
function ContinuedWindow.update()
|
||||
ContinuedWindow.timer = ContinuedWindow.timer - 1
|
||||
if ContinuedWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
|
||||
Window.set_current("menu")
|
||||
MenuWindow.refresh_menu_items()
|
||||
end
|
||||
end
|
||||
107
inc/window/window.discussion.lua
Normal file
107
inc/window/window.discussion.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
--- @section DiscussionWindow
|
||||
|
||||
local TEXTBOX_W = math.floor(Config.screen.width * 0.7)
|
||||
local TEXTBOX_H = math.floor(Config.screen.height * 0.3)
|
||||
local TEXTBOX_X = math.floor((Config.screen.width - TEXTBOX_W) / 2)
|
||||
local TEXTBOX_Y = math.floor((Config.screen.height - TEXTBOX_H) / 2 - 8)
|
||||
local TEXTBOX_MAX_CHARS = 30
|
||||
local DISCUSSION_LINE_HEIGHT = 8
|
||||
local PADDING = 4
|
||||
local AUTO_SCROLL_DELAY = 12
|
||||
local AUTO_SCROLL_STEP = 1
|
||||
|
||||
--- Draws the discussion window.
|
||||
--- @within DiscussionWindow
|
||||
function DiscussionWindow.draw()
|
||||
GameWindow.draw()
|
||||
|
||||
local step = Discussion.get_current_step()
|
||||
if not step then return end
|
||||
|
||||
UI.draw_textbox(
|
||||
step.question,
|
||||
TEXTBOX_X, TEXTBOX_Y,
|
||||
TEXTBOX_W, TEXTBOX_H,
|
||||
Context.discussion.scroll_y,
|
||||
Config.colors.white,
|
||||
Config.colors.dark_grey,
|
||||
Config.colors.light_blue,
|
||||
true
|
||||
)
|
||||
|
||||
local answers = step.answers
|
||||
if #answers > 0 then
|
||||
local bar_height = 16
|
||||
local bar_y = Config.screen.height - bar_height
|
||||
rect(0, bar_y, Config.screen.width, bar_height, Config.colors.dark_grey)
|
||||
local selected = answers[Context.discussion.selected_answer]
|
||||
local label = selected.label
|
||||
local answer_text_y = bar_y + 4
|
||||
Print.text("<", 2, answer_text_y, Config.colors.light_blue)
|
||||
Print.text_center(label, Config.screen.width / 2, answer_text_y, Config.colors.item)
|
||||
Print.text(">", Config.screen.width - 6, answer_text_y, Config.colors.light_blue)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates the discussion window logic.
|
||||
--- @within DiscussionWindow
|
||||
function DiscussionWindow.update()
|
||||
local step = Discussion.get_current_step()
|
||||
if not step then return end
|
||||
|
||||
local lines = UI.word_wrap(step.question, TEXTBOX_MAX_CHARS)
|
||||
local text_height = #lines * DISCUSSION_LINE_HEIGHT
|
||||
local visible_height = TEXTBOX_H - PADDING * 2
|
||||
local max_scroll = text_height - visible_height
|
||||
if max_scroll < 0 then max_scroll = 0 end
|
||||
|
||||
if max_scroll > 0 then
|
||||
if Context.discussion.auto_scroll then
|
||||
Context.discussion.scroll_timer = Context.discussion.scroll_timer + 1
|
||||
if Context.discussion.scroll_timer >= AUTO_SCROLL_DELAY then
|
||||
Context.discussion.scroll_timer = 0
|
||||
Context.discussion.scroll_y = Context.discussion.scroll_y + AUTO_SCROLL_STEP
|
||||
if Context.discussion.scroll_y > max_scroll then
|
||||
Context.discussion.scroll_y = max_scroll
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
Context.discussion.scroll_y = 0
|
||||
Context.discussion.scroll_timer = 0
|
||||
end
|
||||
|
||||
if Input.up() then
|
||||
Context.discussion.auto_scroll = false
|
||||
Context.discussion.scroll_y = Context.discussion.scroll_y - DISCUSSION_LINE_HEIGHT
|
||||
if Context.discussion.scroll_y < 0 then
|
||||
Context.discussion.scroll_y = 0
|
||||
end
|
||||
elseif Input.down() then
|
||||
Context.discussion.auto_scroll = false
|
||||
Context.discussion.scroll_y = Context.discussion.scroll_y + DISCUSSION_LINE_HEIGHT
|
||||
if Context.discussion.scroll_y > max_scroll then
|
||||
Context.discussion.scroll_y = max_scroll
|
||||
end
|
||||
end
|
||||
|
||||
local answers = step.answers
|
||||
if #answers > 0 then
|
||||
if Input.left() then
|
||||
Audio.sfx_beep()
|
||||
Context.discussion.selected_answer = Util.safeindex(answers, Context.discussion.selected_answer - 1)
|
||||
elseif Input.right() then
|
||||
Audio.sfx_beep()
|
||||
Context.discussion.selected_answer = Util.safeindex(answers, Context.discussion.selected_answer + 1)
|
||||
end
|
||||
|
||||
if Input.select() then
|
||||
Audio.sfx_select()
|
||||
local selected = answers[Context.discussion.selected_answer]
|
||||
if selected.on_select then
|
||||
selected.on_select()
|
||||
end
|
||||
Discussion.go_to_step(selected.next_step)
|
||||
end
|
||||
end
|
||||
end
|
||||
77
inc/window/window.end.lua
Normal file
77
inc/window/window.end.lua
Normal file
@@ -0,0 +1,77 @@
|
||||
--- @section EndWindow
|
||||
|
||||
--- Draws the end screen window.
|
||||
--- @within EndWindow
|
||||
function EndWindow.draw()
|
||||
cls(Config.colors.black)
|
||||
|
||||
if Context._end.state == "choice" then
|
||||
local lines = {
|
||||
"This is not a workplace.",
|
||||
"This is a cycle.",
|
||||
"And if it is a cycle...",
|
||||
"it can be broken."
|
||||
}
|
||||
|
||||
local y = 40
|
||||
for _, line in ipairs(lines) do
|
||||
Print.text_center(line, Config.screen.width / 2, y, Config.colors.white)
|
||||
y = y + 10
|
||||
end
|
||||
|
||||
y = y + 20
|
||||
local yes_color = Context._end.selection == 1 and Config.colors.light_blue or Config.colors.white
|
||||
local no_color = Context._end.selection == 2 and Config.colors.light_blue or Config.colors.white
|
||||
|
||||
local yes_text = (Context._end.selection == 1 and "> YES" or " YES")
|
||||
local no_text = (Context._end.selection == 2 and "> NO" or " NO")
|
||||
|
||||
local centerX = Config.screen.width / 2
|
||||
Print.text(yes_text, centerX - 40, y, yes_color)
|
||||
Print.text(no_text, centerX + 10, y, no_color)
|
||||
elseif Context._end.state == "ending" then
|
||||
Print.text_center("Game over -- good ending.", Config.screen.width / 2, 50, Config.colors.light_blue)
|
||||
Print.text_center("Congratulations!", Config.screen.width / 2, 70, Config.colors.white)
|
||||
Print.text_center("Press Z to return to menu", Config.screen.width / 2, 110, Config.colors.light_grey)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates the end screen logic.
|
||||
--- @within EndWindow
|
||||
function EndWindow.update()
|
||||
if Context._end.state == "choice" then
|
||||
if Input.left() or Input.up() then
|
||||
if Context._end.selection == 2 then
|
||||
Audio.sfx_beep()
|
||||
Context._end.selection = 1
|
||||
end
|
||||
elseif Input.right() or Input.down() then
|
||||
if Context._end.selection == 1 then
|
||||
Audio.sfx_beep()
|
||||
Context._end.selection = 2
|
||||
end
|
||||
end
|
||||
|
||||
if Input.menu_confirm() then
|
||||
Audio.sfx_select()
|
||||
if Context._end.selection == 1 then
|
||||
Context._end.state = "ending"
|
||||
else
|
||||
-- NO: increment day and go home
|
||||
Day.increase()
|
||||
Util.go_to_screen_by_id("home")
|
||||
Window.set_current("game")
|
||||
-- Initialize home screen
|
||||
local home_screen = Screen.get_by_id("home")
|
||||
if home_screen and home_screen.init then
|
||||
home_screen.init()
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif Context._end.state == "ending" then
|
||||
if Input.menu_confirm() then
|
||||
Window.set_current("menu")
|
||||
MenuWindow.refresh_menu_items()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,35 +1,96 @@
|
||||
--- @section GameWindow
|
||||
local _available_decisions = {}
|
||||
local _selected_decision_index = 1
|
||||
|
||||
local function draw_game_scene(underlay_draw)
|
||||
local screen = Screen.get_by_id(Context.game.current_screen)
|
||||
if not screen then return end
|
||||
if screen.background then
|
||||
Map.draw(screen.background)
|
||||
elseif screen.background_color then
|
||||
rect(0, 0, Config.screen.width, Config.screen.height, screen.background_color)
|
||||
end
|
||||
if underlay_draw then
|
||||
underlay_draw()
|
||||
end
|
||||
if not Context.stat_screen_active and #_available_decisions > 0 then
|
||||
Decision.draw(_available_decisions, _selected_decision_index)
|
||||
end
|
||||
Sprite.draw()
|
||||
Focus.draw()
|
||||
screen.draw()
|
||||
end
|
||||
|
||||
--- Draws the game window.
|
||||
--- @within GameWindow
|
||||
function GameWindow.draw()
|
||||
local currentScreenData = Context.screens[Context.current_screen]
|
||||
|
||||
UI.draw_top_bar(currentScreenData.name)
|
||||
|
||||
-- Draw platforms
|
||||
for _, p in ipairs(currentScreenData.platforms) do
|
||||
rect(p.x, p.y, p.w, p.h, Config.colors.green)
|
||||
end
|
||||
|
||||
-- Draw items
|
||||
for _, item in ipairs(currentScreenData.items) do
|
||||
spr(item.sprite_id, item.x, item.y, 0)
|
||||
end
|
||||
|
||||
-- Draw NPCs
|
||||
for _, npc in ipairs(currentScreenData.npcs) do
|
||||
spr(npc.sprite_id, npc.x, npc.y, 0)
|
||||
end
|
||||
|
||||
-- Draw ground
|
||||
rect(Context.ground.x, Context.ground.y, Context.ground.w, Context.ground.h, Config.colors.dark_grey)
|
||||
|
||||
-- Draw player
|
||||
Player.draw()
|
||||
draw_game_scene()
|
||||
end
|
||||
|
||||
--- Draws the game window with a custom underlay.
|
||||
--- @within GameWindow
|
||||
--- @param underlay_draw function A draw callback rendered after the background but before overlays.<br/>
|
||||
function GameWindow.draw_with_underlay(underlay_draw)
|
||||
draw_game_scene(underlay_draw)
|
||||
end
|
||||
|
||||
--- Updates the game window logic.
|
||||
--- @within GameWindow
|
||||
function GameWindow.update()
|
||||
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)
|
||||
if not screen or not screen.update then return end
|
||||
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 type(current_situation_obj.update) == "function" 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 = Decision.update(
|
||||
_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
|
||||
|
||||
--- Sets the active window.
|
||||
--- @within GameWindow
|
||||
--- @param new_state string The ID of the new active window.</br>
|
||||
function GameWindow.set_state(new_state)
|
||||
Context.active_window = new_state
|
||||
-- Add any state-specific initialization/cleanup here later if needed
|
||||
Window.set_current(new_state)
|
||||
end
|
||||
37
inc/window/window.intro.brief.lua
Normal file
37
inc/window/window.intro.brief.lua
Normal file
@@ -0,0 +1,37 @@
|
||||
--- @section BriefIntroWindow
|
||||
BriefIntroWindow.y = Config.screen.height
|
||||
BriefIntroWindow.speed = 0.5
|
||||
BriefIntroWindow.text = [[
|
||||
Norman Reds’ everyday life
|
||||
seems ordinary: work,
|
||||
meetings, coffee, and
|
||||
endless notifications.
|
||||
But beneath him, or around
|
||||
him — something is
|
||||
constantly building, and
|
||||
it soon becomes clear
|
||||
that there is more going
|
||||
on than meets the eye.
|
||||
]]
|
||||
|
||||
--- Draws the brief intro window.
|
||||
--- @within BriefIntroWindow
|
||||
function BriefIntroWindow.draw()
|
||||
local x = (Config.screen.width - 132) / 2
|
||||
Print.text(BriefIntroWindow.text, x, BriefIntroWindow.y, Config.colors.light_blue)
|
||||
end
|
||||
|
||||
--- Updates the brief intro window logic.
|
||||
--- @within BriefIntroWindow
|
||||
function BriefIntroWindow.update()
|
||||
BriefIntroWindow.y = BriefIntroWindow.y - BriefIntroWindow.speed
|
||||
|
||||
local lines = 1
|
||||
for _ in string.gmatch(BriefIntroWindow.text, "\n") do
|
||||
lines = lines + 1
|
||||
end
|
||||
|
||||
if BriefIntroWindow.y < -lines * 8 or Input.select() or Input.menu_confirm() then
|
||||
Window.set_current("menu")
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
function IntroWindow.draw()
|
||||
local x = (Config.screen.width - 132) / 2 -- Centered text
|
||||
print(Context.intro.text, x, Context.intro.y, Config.colors.green)
|
||||
end
|
||||
|
||||
function IntroWindow.update()
|
||||
Context.intro.y = Context.intro.y - Context.intro.speed
|
||||
|
||||
-- Count lines in intro text to determine when scrolling is done
|
||||
local lines = 1
|
||||
for _ in string.gmatch(Context.intro.text, "\n") do
|
||||
lines = lines + 1
|
||||
end
|
||||
|
||||
-- When text is off-screen, go to menu
|
||||
if Context.intro.y < -lines * 8 then
|
||||
GameWindow.set_state(WINDOW_MENU)
|
||||
end
|
||||
|
||||
-- Skip intro by pressing A
|
||||
if Input.menu_confirm() then
|
||||
GameWindow.set_state(WINDOW_MENU)
|
||||
end
|
||||
end
|
||||
|
||||
36
inc/window/window.intro.title.lua
Normal file
36
inc/window/window.intro.title.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
--- @section TitleIntroWindow
|
||||
TitleIntroWindow.timer = 180 -- 3 seconds at 60fps
|
||||
TitleIntroWindow.text = [[
|
||||
## ### ### ### ### ### ### ### ## # #
|
||||
# # # # # # # # # # # # # #
|
||||
# # ### ### # # # # # ### # # #
|
||||
# # # # # # # # # # # # #
|
||||
## ### # ### # # ### # ### ## #
|
||||
|
||||
# # ### ### ## # #
|
||||
## # # # # # # ## #
|
||||
# ## # # # #### # ##
|
||||
# # # # # # # # #
|
||||
# # ### # # # # #
|
||||
|
||||
### # # ### #### ### ### #### ###
|
||||
# ## ## # # # # # # # # # #
|
||||
# # # # ### # # ### # # # ###
|
||||
# # # # # # # # # # # #
|
||||
### # # # #### ### # #### # #
|
||||
]]
|
||||
|
||||
--- Draws the title intro window.
|
||||
--- @within TitleIntroWindow
|
||||
function TitleIntroWindow.draw()
|
||||
AsciiArt.draw(TitleIntroWindow.text, {})
|
||||
end
|
||||
|
||||
--- Updates the title intro window logic.
|
||||
--- @within TitleIntroWindow
|
||||
function TitleIntroWindow.update()
|
||||
TitleIntroWindow.timer = TitleIntroWindow.timer - 1
|
||||
if TitleIntroWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
|
||||
Window.set_current("intro_ttg")
|
||||
end
|
||||
end
|
||||
33
inc/window/window.intro.ttg.lua
Normal file
33
inc/window/window.intro.ttg.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
--- @section TTGIntroWindow
|
||||
TTGIntroWindow.timer = 180
|
||||
TTGIntroWindow.glitch_started = false
|
||||
TTGIntroWindow.text = [[
|
||||
###### ###### ######
|
||||
## ## #
|
||||
## ## # ####
|
||||
## ## # #
|
||||
## ## ######
|
||||
]]
|
||||
|
||||
--- Draws the TTG intro window.
|
||||
--- @within TTGIntroWindow
|
||||
function TTGIntroWindow.draw()
|
||||
local bounds = AsciiArt.draw(TTGIntroWindow.text, {})
|
||||
if not bounds then return end
|
||||
Print.text_center("Teletype Games", (Config.screen.width / 2 + 3) , (bounds.bottom + 4), Config.colors.light_blue)
|
||||
end
|
||||
|
||||
--- Updates the TTG intro window logic.
|
||||
--- @within TTGIntroWindow
|
||||
function TTGIntroWindow.update()
|
||||
if not TTGIntroWindow.glitch_started then
|
||||
Glitch.show()
|
||||
TTGIntroWindow.glitch_started = true
|
||||
end
|
||||
|
||||
TTGIntroWindow.timer = TTGIntroWindow.timer - 1
|
||||
if TTGIntroWindow.timer <= 0 or Input.select() or Input.menu_confirm() then
|
||||
Glitch.hide()
|
||||
Window.set_current("intro_brief")
|
||||
end
|
||||
end
|
||||
@@ -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., GameWindow).</br>
|
||||
function Window.register(id, window_table)
|
||||
_windows[id] = window_table
|
||||
end
|
||||
|
||||
--- Retrieves a registered window table by its ID.
|
||||
--- @within Window
|
||||
--- @param id string The ID of the window.
|
||||
--- @return result table The window module table or nil. </br>
|
||||
--- Fields: </br>
|
||||
--- * update (function) Called each frame to update window logic.<br/>
|
||||
--- * draw (function) Called each frame to draw the window.<br/>
|
||||
function Window.get(id)
|
||||
return _windows[id]
|
||||
end
|
||||
|
||||
--- Sets the currently active window.
|
||||
--- @within Window
|
||||
--- @param id string The ID of the window to activate.</br>
|
||||
function Window.set_current(id)
|
||||
Context.current_window = id
|
||||
end
|
||||
|
||||
--- Gets the ID of the currently active window.
|
||||
--- This function is used by the main game loop to update and draw the active window.
|
||||
--- @within Window
|
||||
--- @return string The ID of the active window.
|
||||
function Window.get_current_id()
|
||||
return Context.current_window
|
||||
end
|
||||
|
||||
--- Gets the handler function for the currently active window.
|
||||
-- This function is used by the main game loop to update and draw the active window.
|
||||
--- @within Window
|
||||
--- @return function A function that updates and draws the current window.
|
||||
function Window.get_current_handler()
|
||||
local window_table = Window.get(Context.current_window)
|
||||
if window_table and window_table.update and window_table.draw then
|
||||
return function()
|
||||
window_table.update()
|
||||
window_table.draw()
|
||||
end
|
||||
else
|
||||
-- Fallback handler for unregistered or incomplete windows
|
||||
return function() trace("Error: No handler for window: " .. tostring(Context.current_window)) end
|
||||
end
|
||||
end
|
||||
@@ -1,42 +1,101 @@
|
||||
--- @section MenuWindow
|
||||
local _menu_items = {}
|
||||
|
||||
--- Draws the menu window.
|
||||
--- @within MenuWindow
|
||||
function MenuWindow.draw()
|
||||
UI.draw_top_bar("Main Menu")
|
||||
UI.draw_menu(Context.menu_items, Context.selected_menu_item, 108, 70)
|
||||
UI.draw_top_bar("Definitely not an Impostor")
|
||||
|
||||
local menu_h = #_menu_items * 10
|
||||
local y = 10 + (Config.screen.height - 10 - 10 - menu_h) / 2
|
||||
UI.draw_menu(_menu_items, Context.current_menu_item, 0, y, true)
|
||||
|
||||
local ttg_text = "TTG"
|
||||
local ttg_w = print(ttg_text, 0, -10, 0, false, 1, false)
|
||||
Print.text(ttg_text, Config.screen.width - ttg_w - 5, Config.screen.height - 10, Config.colors.light_blue)
|
||||
end
|
||||
|
||||
--- Updates the menu window logic.
|
||||
--- @within MenuWindow
|
||||
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
|
||||
local selected_item = Context.menu_items[Context.selected_menu_item]
|
||||
if selected_item and selected_item.action then
|
||||
selected_item.action()
|
||||
local selected_item = _menu_items[Context.current_menu_item]
|
||||
if selected_item and selected_item.decision then
|
||||
Audio.sfx_select()
|
||||
selected_item.decision()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MenuWindow.play()
|
||||
-- Reset player state and screen for a new game
|
||||
Context.player.x = Config.player.start_x
|
||||
Context.player.y = Config.player.start_y
|
||||
Context.player.vx = 0
|
||||
Context.player.vy = 0
|
||||
Context.player.jumps = 0
|
||||
Context.current_screen = 1
|
||||
GameWindow.set_state(WINDOW_GAME)
|
||||
--- Starts a new game from the menu.
|
||||
--- @within MenuWindow
|
||||
function MenuWindow.new_game()
|
||||
Context.new_game()
|
||||
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()
|
||||
exit()
|
||||
end
|
||||
|
||||
--- Opens the configuration menu.
|
||||
--- @within MenuWindow
|
||||
function MenuWindow.configuration()
|
||||
ConfigurationWindow.init()
|
||||
GameWindow.set_state(WINDOW_CONFIGURATION)
|
||||
GameWindow.set_state("configuration")
|
||||
end
|
||||
|
||||
-- Initialize menu items after actions are defined
|
||||
Context.menu_items = {
|
||||
{label = "Play", action = MenuWindow.play},
|
||||
{label = "Configuration", action = MenuWindow.configuration},
|
||||
{label = "Exit", action = MenuWindow.exit}
|
||||
}
|
||||
--- Opens the audio test menu.
|
||||
--- @within MenuWindow
|
||||
function MenuWindow.audio_test()
|
||||
AudioTestWindow.init()
|
||||
GameWindow.set_state("audiotest")
|
||||
end
|
||||
|
||||
--- Opens the continued screen.
|
||||
--- @within MenuWindow
|
||||
function MenuWindow.continued()
|
||||
ContinuedWindow.timer = 300
|
||||
GameWindow.set_state("continued")
|
||||
end
|
||||
|
||||
--- Refreshes menu items.
|
||||
--- @within MenuWindow
|
||||
function MenuWindow.refresh_menu_items()
|
||||
_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 = "To Be Continued...", decision = MenuWindow.continued})
|
||||
table.insert(_menu_items, {label = "Exit", decision = MenuWindow.exit})
|
||||
|
||||
Context.current_menu_item = 1
|
||||
end
|
||||
|
||||
332
inc/window/window.minigame.ddr.lua
Normal file
332
inc/window/window.minigame.ddr.lua
Normal file
@@ -0,0 +1,332 @@
|
||||
--- @section MinigameDDRWindow
|
||||
|
||||
--- Gets initial DDR minigame configuration.
|
||||
--- @within MinigameDDRWindow
|
||||
--- @return result table The default DDR minigame configuration.
|
||||
function MinigameDDRWindow.init_context()
|
||||
local arrow_size = 12
|
||||
local arrow_spacing = 30
|
||||
local total_width = (4 * arrow_size) + (3 * arrow_spacing)
|
||||
local start_x = (Config.screen.width - total_width) / 2
|
||||
return {
|
||||
bar_fill = 0,
|
||||
max_fill = 100,
|
||||
fill_per_hit = 10,
|
||||
miss_penalty = 5,
|
||||
bar_x = 20,
|
||||
bar_y = 10,
|
||||
bar_width = 200,
|
||||
bar_height = 12,
|
||||
arrow_size = arrow_size,
|
||||
arrow_spawn_timer = 0,
|
||||
arrow_spawn_interval = 45,
|
||||
arrow_fall_speed = 1.5,
|
||||
arrows = {},
|
||||
target_y = 115,
|
||||
target_arrows = {
|
||||
{ dir = "left", x = start_x },
|
||||
{ dir = "down", x = start_x + arrow_size + arrow_spacing },
|
||||
{ dir = "up", x = start_x + (arrow_size + arrow_spacing) * 2 },
|
||||
{ dir = "right", x = start_x + (arrow_size + arrow_spacing) * 3 }
|
||||
},
|
||||
hit_threshold = 8,
|
||||
button_pressed_timers = {},
|
||||
button_press_duration = 8,
|
||||
input_cooldowns = { left = 0, down = 0, up = 0, right = 0 },
|
||||
input_cooldown_duration = 10,
|
||||
frame_counter = 0,
|
||||
current_song = nil,
|
||||
pattern_index = 1,
|
||||
use_pattern = false,
|
||||
return_window = nil,
|
||||
win_timer = 0,
|
||||
on_win = nil
|
||||
}
|
||||
end
|
||||
|
||||
--- Initializes DDR minigame state.
|
||||
--- @within MinigameDDRWindow
|
||||
--- @param params table Optional parameters for configuration.<br/>
|
||||
function MinigameDDRWindow.init(params)
|
||||
local defaults = MinigameDDRWindow.init_context()
|
||||
if params then
|
||||
for k, v in pairs(params) do
|
||||
defaults[k] = v
|
||||
end
|
||||
end
|
||||
Context.minigame_ddr = defaults
|
||||
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.win_timer > 0 then
|
||||
mg.win_timer = mg.win_timer - 1
|
||||
if mg.win_timer == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
if mg.on_win then
|
||||
mg.on_win()
|
||||
else
|
||||
Meter.show()
|
||||
Window.set_current(mg.return_window)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if mg.bar_fill >= mg.max_fill then
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
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
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
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
|
||||
if mg.win_timer > 0 then
|
||||
Minigame.draw_win_overlay()
|
||||
end
|
||||
end
|
||||
149
inc/window/window.minigame.mash.lua
Normal file
149
inc/window/window.minigame.mash.lua
Normal file
@@ -0,0 +1,149 @@
|
||||
--- Gets initial button mash minigame configuration.
|
||||
--- @within MinigameButtonMashWindow
|
||||
--- @return result table The default button mash minigame configuration.
|
||||
function MinigameButtonMashWindow.init_context()
|
||||
return {
|
||||
bar_fill = 0,
|
||||
target_points = 100,
|
||||
fill_per_press = 8,
|
||||
base_degradation = 0.15,
|
||||
degradation_multiplier = 0.006,
|
||||
button_pressed_timer = 0,
|
||||
button_press_duration = 8,
|
||||
instruction_text = "MASH Z!",
|
||||
show_progress_text = true,
|
||||
return_window = nil,
|
||||
bar_x = 20,
|
||||
bar_y = 10,
|
||||
bar_width = 200,
|
||||
bar_height = 12,
|
||||
button_x = 20,
|
||||
button_y = 110,
|
||||
button_size = 12,
|
||||
focus_center_x = nil,
|
||||
focus_center_y = nil,
|
||||
focus_initial_radius = 0,
|
||||
win_timer = 0,
|
||||
on_win = nil
|
||||
}
|
||||
end
|
||||
|
||||
--- Initializes button mash minigame state.
|
||||
--- @within MinigameButtonMashWindow
|
||||
--- @param params table Optional parameters for configuration.<br/>
|
||||
function MinigameButtonMashWindow.init(params)
|
||||
local defaults = MinigameButtonMashWindow.init_context()
|
||||
if params then
|
||||
for k, v in pairs(params) do
|
||||
defaults[k] = v
|
||||
end
|
||||
if params.max_fill and not params.target_points then
|
||||
defaults.target_points = params.max_fill
|
||||
end
|
||||
end
|
||||
Context.minigame_button_mash = defaults
|
||||
end
|
||||
|
||||
--- 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 mg.win_timer > 0 then
|
||||
mg.win_timer = mg.win_timer - 1
|
||||
if mg.win_timer == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
if mg.focus_center_x then Focus.stop() end
|
||||
if mg.on_win then
|
||||
mg.on_win()
|
||||
else
|
||||
Meter.show()
|
||||
Window.set_current(mg.return_window)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if Input.select() then
|
||||
mg.bar_fill = mg.bar_fill + mg.fill_per_press
|
||||
mg.button_pressed_timer = mg.button_press_duration
|
||||
if mg.bar_fill > mg.target_points then
|
||||
mg.bar_fill = mg.target_points
|
||||
end
|
||||
end
|
||||
if mg.bar_fill >= mg.target_points then
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
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.target_points)
|
||||
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_with_underlay(function()
|
||||
Sprite.draw_at("sleeping_norman", (Config.screen.width / 2) - 30, (Config.screen.height / 2) - 22)
|
||||
end)
|
||||
end
|
||||
if not mg.focus_center_x then
|
||||
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
|
||||
end
|
||||
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.target_points) * 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, mg.button_y - 3, button_color)
|
||||
Print.text_center(mg.instruction_text, Config.screen.width / 2, mg.bar_y + mg.bar_height + 10, Config.colors.light_grey)
|
||||
if mg.show_progress_text then
|
||||
local points_text = math.floor(mg.bar_fill) .. "/" .. mg.target_points
|
||||
Print.text_center(points_text, mg.bar_x + mg.bar_width / 2, mg.bar_y + 2, Config.colors.black)
|
||||
end
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
Minigame.draw_win_overlay()
|
||||
end
|
||||
end
|
||||
168
inc/window/window.minigame.rhythm.lua
Normal file
168
inc/window/window.minigame.rhythm.lua
Normal file
@@ -0,0 +1,168 @@
|
||||
--- @section MinigameRhythmWindow
|
||||
|
||||
--- Gets initial rhythm minigame configuration.
|
||||
--- @within MinigameRhythmWindow
|
||||
--- @return result table The default rhythm minigame configuration.
|
||||
function MinigameRhythmWindow.init_context()
|
||||
return {
|
||||
line_position = 0,
|
||||
line_speed = 0.015,
|
||||
line_direction = 1,
|
||||
target_center = 0.5,
|
||||
target_width = 0.3,
|
||||
initial_target_width = 0.3,
|
||||
min_target_width = 0.08,
|
||||
target_shrink_rate = 0.9,
|
||||
score = 0,
|
||||
max_score = 10,
|
||||
button_pressed_timer = 0,
|
||||
button_press_duration = 10,
|
||||
return_window = nil,
|
||||
bar_x = 20,
|
||||
bar_y = 10,
|
||||
bar_width = 200,
|
||||
bar_height = 12,
|
||||
button_x = 210,
|
||||
button_y = 110,
|
||||
button_size = 10,
|
||||
press_cooldown = 0,
|
||||
press_cooldown_duration = 15,
|
||||
focus_center_x = nil,
|
||||
focus_center_y = nil,
|
||||
focus_initial_radius = 0,
|
||||
win_timer = 0,
|
||||
on_win = nil
|
||||
}
|
||||
end
|
||||
|
||||
--- Initializes rhythm minigame state.
|
||||
--- @within MinigameRhythmWindow
|
||||
--- @param params table Optional parameters for configuration.<br/>
|
||||
function MinigameRhythmWindow.init(params)
|
||||
local defaults = MinigameRhythmWindow.init_context()
|
||||
if params then
|
||||
for k, v in pairs(params) do
|
||||
defaults[k] = v
|
||||
end
|
||||
end
|
||||
Context.minigame_rhythm = defaults
|
||||
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
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
mg.win_timer = mg.win_timer - 1
|
||||
if mg.win_timer == 0 then
|
||||
Meter.on_minigame_complete()
|
||||
if mg.focus_center_x then Focus.stop() end
|
||||
if mg.on_win then
|
||||
mg.on_win()
|
||||
else
|
||||
Meter.show()
|
||||
Window.set_current(mg.return_window)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
mg.line_position = mg.line_position + (mg.line_speed * mg.line_direction)
|
||||
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
|
||||
mg.win_timer = Config.timing.minigame_win_duration
|
||||
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_with_underlay(function()
|
||||
Sprite.draw_at("sleeping_norman", (Config.screen.width / 2) - 30, (Config.screen.height / 2) - 22)
|
||||
end)
|
||||
end
|
||||
if not mg.focus_center_x then
|
||||
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
|
||||
end
|
||||
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)
|
||||
Print.text_center(
|
||||
"Sleep Norman ... Sleep!",
|
||||
Config.screen.width / 2,
|
||||
mg.bar_y + mg.bar_height + 14,
|
||||
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, mg.button_y - 3, button_color)
|
||||
|
||||
if mg.win_timer > 0 then
|
||||
Minigame.draw_win_overlay()
|
||||
end
|
||||
end
|
||||
160
inc/window/window.mysterious_man.lua
Normal file
160
inc/window/window.mysterious_man.lua
Normal file
@@ -0,0 +1,160 @@
|
||||
--- @section MysteriousManWindow
|
||||
|
||||
local STATE_TEXT = "text"
|
||||
local STATE_DAY = "day"
|
||||
local STATE_CHOICE = "choice"
|
||||
|
||||
local DEFAULT_TEXT = [[
|
||||
Misterious man appears
|
||||
during your sleep.
|
||||
|
||||
He says nothing.
|
||||
He doesn't need to.
|
||||
|
||||
He says nothing.
|
||||
]]
|
||||
|
||||
local state = STATE_TEXT
|
||||
local text_y = Config.screen.height
|
||||
local text_speed = 0.4
|
||||
local day_timer = 0
|
||||
local day_display_frames = 120
|
||||
local selected_choice = 1
|
||||
local text = DEFAULT_TEXT
|
||||
local day_text_override = nil
|
||||
local on_text_complete = nil
|
||||
|
||||
local choices = {
|
||||
{
|
||||
label = "Wake Up",
|
||||
},
|
||||
{
|
||||
label = "Stay in Bed",
|
||||
},
|
||||
}
|
||||
|
||||
--- Sets the scrolling text content.
|
||||
--- @within MysteriousManWindow
|
||||
--- @param new_text string The text to display.
|
||||
function MysteriousManWindow.set_text(new_text)
|
||||
text = new_text
|
||||
end
|
||||
|
||||
--- Starts the mysterious man window.
|
||||
--- @within MysteriousManWindow
|
||||
--- @param[opt] options table Optional window configuration.</br>
|
||||
--- Fields: </br>
|
||||
--- * text (string) Override for the scrolling text.<br/>
|
||||
--- * day_text (string) Override for the centered day label.<br/>
|
||||
--- * on_text_complete (function) Callback fired once when the text phase ends.<br/>
|
||||
function MysteriousManWindow.start(options)
|
||||
options = options or {}
|
||||
state = STATE_TEXT
|
||||
text_y = Config.screen.height
|
||||
day_timer = 0
|
||||
selected_choice = 1
|
||||
text = options.text or DEFAULT_TEXT
|
||||
day_text_override = options.day_text
|
||||
on_text_complete = options.on_text_complete
|
||||
Meter.hide()
|
||||
Window.set_current("mysterious_man")
|
||||
end
|
||||
|
||||
local function go_to_day_state()
|
||||
if on_text_complete then
|
||||
on_text_complete()
|
||||
on_text_complete = nil
|
||||
end
|
||||
if Window.get_current_id() ~= "mysterious_man" then
|
||||
return
|
||||
end
|
||||
state = STATE_DAY
|
||||
day_timer = day_display_frames
|
||||
end
|
||||
|
||||
local function wake_up()
|
||||
Context.home_norman_visible = false
|
||||
Util.go_to_screen_by_id("home")
|
||||
MinigameButtonMashWindow.start("game", {
|
||||
focus_center_x = (Config.screen.width / 2) - 22,
|
||||
focus_center_y = (Config.screen.height / 2) - 18,
|
||||
focus_initial_radius = 0,
|
||||
target_points = 100,
|
||||
instruction_text = "Wake up Norman!",
|
||||
show_progress_text = false,
|
||||
on_win = function()
|
||||
Audio.music_play_wakingup()
|
||||
Context.home_norman_visible = true
|
||||
Meter.show()
|
||||
Window.set_current("game")
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function stay_in_bed()
|
||||
Day.increase()
|
||||
state = STATE_DAY
|
||||
day_timer = day_display_frames
|
||||
end
|
||||
|
||||
--- Updates the mysterious man window logic.
|
||||
--- @within MysteriousManWindow
|
||||
function MysteriousManWindow.update()
|
||||
if state == STATE_TEXT then
|
||||
text_y = text_y - text_speed
|
||||
|
||||
local lines = 1
|
||||
for _ in string.gmatch(text, "\n") do
|
||||
lines = lines + 1
|
||||
end
|
||||
|
||||
if text_y < -lines * 8 then
|
||||
go_to_day_state()
|
||||
end
|
||||
|
||||
if Input.select() then
|
||||
go_to_day_state()
|
||||
end
|
||||
elseif state == STATE_DAY then
|
||||
day_timer = day_timer - 1
|
||||
|
||||
if day_timer <= 0 or Input.select() then
|
||||
state = STATE_CHOICE
|
||||
selected_choice = 1
|
||||
end
|
||||
elseif state == STATE_CHOICE then
|
||||
selected_choice = UI.update_menu(choices, selected_choice)
|
||||
|
||||
if Input.select() then
|
||||
Audio.sfx_select()
|
||||
if selected_choice == 1 then
|
||||
wake_up()
|
||||
else
|
||||
stay_in_bed()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Draws the mysterious man window.
|
||||
--- @within MysteriousManWindow
|
||||
function MysteriousManWindow.draw()
|
||||
rect(0, 0, Config.screen.width, Config.screen.height, Config.colors.black)
|
||||
|
||||
if state == STATE_TEXT then
|
||||
local x = (Config.screen.width - 132) / 2
|
||||
Print.text(text, x, text_y, Config.colors.light_grey)
|
||||
elseif state == STATE_DAY then
|
||||
local day_text = day_text_override or ("Day " .. Context.day_count)
|
||||
Print.text_center(
|
||||
day_text,
|
||||
Config.screen.width / 2,
|
||||
Config.screen.height / 2 - 3,
|
||||
Config.colors.white
|
||||
)
|
||||
elseif state == STATE_CHOICE then
|
||||
local menu_x = (Config.screen.width - 60) / 2
|
||||
local menu_y = (Config.screen.height - 20) / 2
|
||||
UI.draw_menu(choices, selected_choice, menu_x, menu_y)
|
||||
end
|
||||
end
|
||||
@@ -1,102 +1,52 @@
|
||||
function PopupWindow.set_dialog_node(node_key)
|
||||
local npc = Context.dialog.active_entity
|
||||
local node = npc.dialog[node_key]
|
||||
--- @section PopupWindow
|
||||
local POPUP_X = 40
|
||||
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
|
||||
GameWindow.set_state(WINDOW_GAME)
|
||||
return
|
||||
end
|
||||
|
||||
Context.dialog.current_node_key = node_key
|
||||
Context.dialog.text = node.text
|
||||
|
||||
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)
|
||||
--- Displays a popup window.
|
||||
--- @within PopupWindow
|
||||
--- @param content_strings table A table of strings to display in the popup.</br>
|
||||
function PopupWindow.show(content_strings)
|
||||
Context.popup.show = true
|
||||
Context.popup.content = content_strings or {}
|
||||
GameWindow.set_state("popup")
|
||||
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()
|
||||
if Context.dialog.showing_description then
|
||||
if Context.popup.show then
|
||||
if Input.menu_confirm() or Input.menu_back() then
|
||||
Context.dialog.showing_description = false
|
||||
Context.dialog.text = "" -- Clear the description text
|
||||
-- No need to change active_window, as it remains in WINDOW_POPUP or WINDOW_INVENTORY_ACTION
|
||||
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)
|
||||
PopupWindow.hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PopupWindow.show_menu_dialog(entity, menu_items, dialog_active_window)
|
||||
Context.dialog.active_entity = entity
|
||||
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
|
||||
|
||||
--- Draws the popup window.
|
||||
--- @within PopupWindow
|
||||
function PopupWindow.draw()
|
||||
rect(40, 40, 160, 80, Config.colors.black)
|
||||
rectb(40, 40, 160, 80, Config.colors.green)
|
||||
if Context.popup.show then
|
||||
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
|
||||
if Context.dialog.active_entity and Context.dialog.active_entity.name then
|
||||
print(Context.dialog.active_entity.name, 120 - #Context.dialog.active_entity.name * 2, 45, Config.colors.green)
|
||||
end
|
||||
local current_y = TEXT_MARGIN_Y
|
||||
for _, line in ipairs(Context.popup.content) do
|
||||
Print.text(line, TEXT_MARGIN_X, current_y, Config.colors.light_grey)
|
||||
current_y = current_y + LINE_HEIGHT
|
||||
end
|
||||
|
||||
-- Display the dialog content (description for "look at", or initial name/dialog for others)
|
||||
local wrapped_lines = UI.word_wrap(Context.dialog.text, 25) -- Max 25 chars per line
|
||||
local current_y = 55 -- Starting Y position for the first line of content
|
||||
for _, line in ipairs(wrapped_lines) do
|
||||
print(line, 50, current_y, Config.colors.light_grey)
|
||||
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)
|
||||
Print.text("[A] Close", TEXT_MARGIN_X, POPUP_Y + POPUP_HEIGHT - LINE_HEIGHT - 2, Config.colors.light_blue)
|
||||
end
|
||||
end
|
||||
44
inc/window/window.register.lua
Normal file
44
inc/window/window.register.lua
Normal file
@@ -0,0 +1,44 @@
|
||||
TitleIntroWindow = {}
|
||||
Window.register("intro_title", TitleIntroWindow)
|
||||
|
||||
TTGIntroWindow = {}
|
||||
Window.register("intro_ttg", TTGIntroWindow)
|
||||
|
||||
BriefIntroWindow = {}
|
||||
Window.register("intro_brief", BriefIntroWindow)
|
||||
|
||||
MenuWindow = {}
|
||||
Window.register("menu", MenuWindow)
|
||||
|
||||
GameWindow = {}
|
||||
Window.register("game", GameWindow)
|
||||
|
||||
PopupWindow = {}
|
||||
Window.register("popup", PopupWindow)
|
||||
|
||||
ConfigurationWindow = {}
|
||||
Window.register("configuration", ConfigurationWindow)
|
||||
|
||||
AudioTestWindow = {}
|
||||
Window.register("audiotest", AudioTestWindow)
|
||||
|
||||
MinigameButtonMashWindow = {}
|
||||
Window.register("minigame_button_mash", MinigameButtonMashWindow)
|
||||
|
||||
MinigameRhythmWindow = {}
|
||||
Window.register("minigame_rhythm", MinigameRhythmWindow)
|
||||
|
||||
MinigameDDRWindow = {}
|
||||
Window.register("minigame_ddr", MinigameDDRWindow)
|
||||
|
||||
MysteriousManWindow = {}
|
||||
Window.register("mysterious_man", MysteriousManWindow)
|
||||
|
||||
EndWindow = {}
|
||||
Window.register("end", EndWindow)
|
||||
|
||||
DiscussionWindow = {}
|
||||
Window.register("discussion", DiscussionWindow)
|
||||
|
||||
ContinuedWindow = {}
|
||||
Window.register("continued", ContinuedWindow)
|
||||
@@ -1,11 +0,0 @@
|
||||
function SplashWindow.draw()
|
||||
print("Mr. Anderson's", 78, 60, Config.colors.green)
|
||||
print("Addventure", 90, 70, Config.colors.green)
|
||||
end
|
||||
|
||||
function SplashWindow.update()
|
||||
Context.splash_timer = Context.splash_timer - 1
|
||||
if Context.splash_timer <= 0 or Input.menu_confirm() then
|
||||
GameWindow.set_state(WINDOW_INTRO)
|
||||
end
|
||||
end
|
||||
@@ -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