Compare commits

...

159 Commits

Author SHA1 Message Date
e12021a432 Displays the characters on street and in office.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-18 12:02:34 +01:00
29e7361303 Fix the dev_guard and the pizza_vendor. 2026-03-18 12:01:51 +01:00
2311232290 Includes the character registrations.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-18 11:11:53 +01:00
c0e4562971 Fix the architect and the oraculum. 2026-03-18 11:10:25 +01:00
2079de587a Renames Morphesu for Sumphore.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-18 09:50:26 +01:00
ec19e7fe12 Add 17 characters.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-18 09:13:57 +01:00
10e99ad25d Register a character named Morpheus. 2026-03-17 21:05:15 +01:00
37967639f4 Creates a character tile board in general. 2026-03-17 21:04:28 +01:00
e5a942dd00 Add some new characters to the meta.assets.lua.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-15 19:46:49 +01:00
7a162d86b9 Add new characters to the sprites. 2026-03-15 19:40:59 +01:00
mr.one
a208f0d27a New music: work activity
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-15 09:03:44 +01:00
e2c5b38ecc Merge pull request 'feature/imp-90-new-intro-sequence' (#34) from feature/imp-90-new-intro-sequence into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/34
2026-03-12 17:17:53 +00:00
Zoltan Timar
e40214c45b fix: lint fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-12 18:14:42 +01:00
Zoltan Timar
b349ded281 feat: added intro sequence, fixed norman's sprite, placed him in various places
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2026-03-12 18:10:50 +01:00
c897cdcbd5 Merge pull request 'feature/imp-89-textbox-impl' (#33) from feature/imp-89-textbox-impl into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/33
2026-03-12 16:11:44 +00:00
Zoltan Timar
5346281c5c fix: lint fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-12 17:10:24 +01:00
Zoltan Timar
5daf98fd06 feat: added new discussion functionality, implemented textbox draw functionality, added first discussion with sumphore to the street
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2026-03-12 17:08:08 +01:00
5a0c8ef19d set version to 0.8
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-12 07:52:42 +01:00
1c25a077a5 lint fixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-12 07:49:36 +01:00
fd983dd6e1 doc fixes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-03-12 07:42:11 +01:00
220ca27128 Merge pull request 'glitch' (#32) from feature/glitch into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/32
2026-03-11 23:59:18 +00:00
3bb1fb7941 glitch
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/pull_request_closed/woodpecker Pipeline failed
2026-03-12 00:58:38 +01:00
24ce240f97 Merge pull request 'feature/end-window' (#31) from feature/end-window into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/31
2026-03-11 23:45:51 +00:00
5d78cffc99 end window
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/pull_request_closed/woodpecker Pipeline failed
2026-03-12 00:44:26 +01:00
d1e7704d66 Day documentation 2026-03-11 23:41:27 +01:00
71a21f3f86 remove top bar in game window 2026-03-11 23:41:27 +01:00
5383fa3240 Merge pull request 'feature/imp-x-implement-standard-day-cycle' (#30) from feature/imp-x-implement-standard-day-cycle into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/30
2026-03-09 15:26:26 +00:00
Zoltan Timar
f3846a283e fix: fixing linter
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-09 16:25:11 +01:00
Zoltan Timar
732c8b34c8 feat: added standard day cycle, added mysterious man screen, added work screen, added new decisions, placed minigames in their proposed space
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2026-03-09 16:20:50 +01:00
53a3f37c8e Merge pull request 'feature/imp-61-time-based-trigger' (#29) from feature/imp-61-time-based-trigger into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/29
2026-03-05 19:39:36 +00:00
Zoltan Timar
9b49e13a5d fix: adding Trigger to luacheckrc
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-05 20:38:16 +01:00
Zoltan Timar
3f2df7d512 feat: trigger logic implementation
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2026-03-05 20:26:23 +01:00
3db1ae1064 Merge pull request 'minigame init refact' (#28) from feature/minigames-init-refact into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/28
2026-03-05 18:17:30 +00:00
f800cfff9b minigame init refact
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-05 19:14:12 +01:00
649b73b3f8 UPDATE_SERVER url update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-04 15:04:04 +01:00
cd41cb3312 Merge branch 'master' of https://git.teletype.hu/games/impostor
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-03 23:38:45 +01:00
ecd094e2d7 - makefile:
- added PATTERNS and TRACKS for music
- audio and configuration windows fixed
- there might be some rudimentary music in there
2026-03-03 23:36:51 +01:00
5b4f3052a6 Merge pull request 'well done splash' (#27) from feature/minigame-win-splash into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/27
2026-03-03 19:58:42 +00:00
83301a1a8e well done splash
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-03 20:55:53 +01:00
77bd76f6f9 Merge pull request 'feature/day-counter' (#26) from feature/day-counter into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/26
2026-03-03 19:37:31 +00:00
aa7d0e902d color fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-03 20:36:42 +01:00
cb19251556 Meter.draw
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-03 20:32:08 +01:00
83f801210e Decision.update & draw
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-03 20:23:56 +01:00
a732b5cbfa logic layer
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-03 20:14:57 +01:00
c39e0ece35 move meter to system (temporary)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-03 20:05:49 +01:00
03b6567c08 Timer and Day modules
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-03 19:51:22 +01:00
41eea638c0 Merge pull request 'Add the office background.' (#25) from feature/task22_programming_office into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/25
2026-03-01 09:52:03 +00:00
67de284598 Add the office background.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-01 10:51:35 +01:00
8a7fa96bcd Merge pull request 'Add the street background.' (#24) from feature/task46_programming_street into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/24
2026-03-01 09:39:19 +00:00
6a3ef5d81e Add the street background.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-01 10:36:44 +01:00
c334f644de Merge pull request 'feature/task21_programming_home' (#23) from feature/task21_programming_home into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/23
2026-03-01 08:18:15 +00:00
7cc886623b Add the tiles, sprites and map to meta.assets.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-03-01 09:07:13 +01:00
8ac5c5c3d9 Correct the palette of the characters and sprites. 2026-03-01 09:06:33 +01:00
43e943278b Add the assets of the characters and background.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-03-01 07:52:49 +01:00
47efe91f58 Merge pull request 'cicd improvements' (#22) from feature/pipeline-improvements into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/22
2026-02-27 14:54:39 +00:00
Zsolt Tasnadi
fd7b5650d3 cicd improvements
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-27 13:11:01 +01:00
0e956ec7a9 Merge pull request 'feature/task71_drawing_street' (#15) from feature/task71_drawing_street into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/15
Reviewed-by: mr.two <zoltan.timar31@gmail.com>
2026-02-27 07:51:43 +00:00
0c2999f596 Merge pull request 'feature/imp-27-add-minifier' (#21) from feature/imp-27-add-minifier into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/21
2026-02-26 23:49:35 +00:00
e46c48b2ec tic80pro image pull always
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-27 00:46:55 +01:00
99ace8a1e8 add minify to export
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline failed
2026-02-26 23:39:00 +01:00
15bf66f1ca minify.lua from github
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-26 23:27:41 +01:00
Zoltan Timar
41f75da8c3 feat: added minifier, added minify step to makefile, added unminify step to makefile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-26 22:41:58 +01:00
e05018d637 Merge pull request 'feature/ldoc-return-fixes' (#20) from feature/ldoc-return-fixes into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/20
2026-02-26 16:50:14 +00:00
8e104b1ff9 Merge branch 'master' into feature/ldoc-return-fixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 17:49:21 +01:00
e07eeb466b vscode settings update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-26 17:44:41 +01:00
337f1fc132 ldoc return fixes 2026-02-26 17:41:06 +01:00
e14160114b Merge pull request 'feature/imp-30-stat-screen' (#19) from feature/imp-30-stat-screen into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/19
2026-02-26 16:06:54 +00:00
Zoltan Timar
1b64fd2392 Merge branch 'master' into feature/imp-30-stat-screen
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 16:54:24 +01:00
Zoltan Timar
aaf1479a78 feat: stat screen on toilet with Focus overlay, screen draw callback added to manager, meter decay now only on timer revolution, timer visible on stat screen not minigames, Meter.get_timer_decay_percentage() added, Context.stat_screen_active flag added
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 16:54:00 +01:00
e56662f6ad linter and doc fix for Focus
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 16:21:25 +01:00
2d25537abb Merge pull request 'feature/imp-42-time-indicator' (#18) from feature/imp-42-time-indicator into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/18
2026-02-26 14:49:00 +00:00
Zoltan Timar
66af47c483 feat: ring timer drawn at top-left of screen, Meter.set_timer_duration(f) controls speed, Meter.set_timer_decay(a) controls decay amount, all decay pauses during any minigame window
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 15:43:39 +01:00
8f9e044a17 Merge pull request 'feature/imp-62-focus-handling' (#17) from feature/imp-62-focus-handling into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/17
2026-02-26 13:58:51 +00:00
Zoltan Timar
954a39aef1 feat: added new functionality with focus, added base background to screens, created Focus.close(), Focus.start(), Focus.driven() methods for different use-cases, added focus to screens
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-26 14:53:22 +01:00
Zsolt Tasnadi
226d75d905 return table details in docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 11:25:20 +01:00
Zsolt Tasnadi
8f34cbf875 docs for table properties
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-26 10:21:48 +01:00
64de41a940 Merge pull request 'section and within annotations for ldoc' (#16) from feature/ldoc-sections into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/16
2026-02-25 22:25:12 +00:00
777c27aa54 section and within annotations for ldoc
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-25 23:24:53 +01:00
c321fbc19a Rename the backgrounds (bedroom, office) to a uniform filename format.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-25 06:33:53 +01:00
dc8a82d583 Delete the unnecessary version of the bedroom background.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-25 06:30:56 +01:00
c565213e44 Delete the unnecessary versions of the office background.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-25 06:29:47 +01:00
035a2bc37e Add the drawing of street background in ase format.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-25 06:26:47 +01:00
297ee8b622 Merge pull request 'refact by claude' (#14) from claude-refact into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/14
2026-02-23 09:44:07 +00:00
Zsolt Tasnadi
7deeffa8d6 refact by claude
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-23 10:40:14 +01:00
272a54ea87 fix colors
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-22 21:32:24 +01:00
14b14ffc0c window.register.lua
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-22 21:26:59 +01:00
7e87a78a15 Merge pull request 'feature/context-refactoring' (#13) from feature/context-refactoring into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/13
2026-02-22 19:19:02 +00:00
62d4863a1a refact
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-22 20:18:40 +01:00
d9febf16e0 remove prular defitions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-22 15:20:10 +01:00
6f5b17147c Merge pull request 'feature/docs' (#12) from feature/docs into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/12
2026-02-21 23:31:08 +00:00
76964f872d docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-22 00:30:12 +01:00
3b137fd48e docs make target 2026-02-21 23:44:03 +01:00
b7791fb9ce Merge pull request 'screen init, update, optional decision condition' (#11) from feature/screen-init-update into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/11
2026-02-21 22:35:23 +00:00
7e1dd28808 screen init, update, optional decision condition
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-21 23:35:00 +01:00
0b25ecc793 set window objects to global
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-21 23:18:42 +01:00
f08e4ad1d4 Merge pull request 'sprite handling' (#10) from feature/sprite-handling into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/10
2026-02-21 22:13:31 +00:00
9ae6c12582 sprite handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-21 23:12:46 +01:00
4e35cd4bd3 Merge pull request 'situation handling' (#9) from feature/situation-handling into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/9
2026-02-21 21:34:10 +00:00
ed2354b0fa situation handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-21 22:33:24 +01:00
7854dc8a9f situation handling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-21 22:33:01 +01:00
3b9b67e2fa Merge pull request 'remove manager postfixes' (#8) from feature-remove-manager-postfixes into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/8
2026-02-21 20:36:29 +00:00
1a4565428d remove manager postfixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-21 21:35:48 +01:00
f02bb75e4f Merge pull request 'feat/imp-28-context-minigame-meter-table' (#7) from feat/imp-28-context-minigame-meter-table into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/7
2026-02-18 22:24:11 +00:00
Zoltan Timar
e0c3b446af fix: make lint
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-18 23:07:22 +01:00
Zoltan Timar
8832b6c833 feat: added meters, changed colors, removed unnecessary comments 2026-02-18 22:58:14 +01:00
Zoltan Timar
9014e36014 feat: moved minigames to their separate context
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-18 21:43:16 +01:00
Zoltan Timar
7b263bb454 fix: desition -> decision
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-02-18 21:42:40 +01:00
0640964ee4 Merge pull request 'feature/background-manager' (#6) from feature/background-manager into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/6
2026-02-18 18:29:51 +00:00
1cf09de1fb remove ai generated comments
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-18 19:29:06 +01:00
6303781534 tweaks
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-18 19:17:18 +01:00
e2bd1711c0 MapManager
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-18 19:13:32 +01:00
60a6c73a32 Merge pull request 'add precommit hook' (#5) from lint-recommit-hook into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/5
2026-02-18 09:28:31 +00:00
c88562ae69 add precommit hook
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-18 10:28:07 +01:00
1d06376826 Merge pull request 'make lint target' (#4) from feature/linter into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/4
2026-02-18 07:53:24 +00:00
7a57c15eeb header file path fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2026-02-18 08:52:56 +01:00
3922f51c8e linter fixes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed
2026-02-18 08:49:54 +01:00
f589b9bbca make lint target
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed
2026-02-17 23:38:31 +01:00
3abe426e3a Merge pull request 'feature/refactor-npc-handling-to-desition-handling' (#3) from feature/refactor-npc-handling-to-desition-handling into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/3
2026-02-17 20:58:47 +00:00
9eef8fd6e0 Util.go_to_screen_by_id
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/pull_request_closed/woodpecker Pipeline failed
2026-02-17 21:58:22 +01:00
0357eb415e desition and screen magement refactor 2026-02-17 21:54:49 +01:00
11b92f64d6 DesitionManager 2026-02-17 21:02:20 +01:00
d1720a0a50 named desition handler functions 2026-02-17 20:56:06 +01:00
31b98dcdf6 fix DesitionSelector arrows 2026-02-17 20:48:58 +01:00
ec84f23ad5 base loop 2026-02-17 20:45:57 +01:00
99b178262f DesitionSelector 2026-02-17 20:36:04 +01:00
d20ef85ceb rename action to desition 2026-02-17 20:26:42 +01:00
ef4876b083 remove npc and item handling 2026-02-17 20:23:11 +01:00
9a65891afa remove items and npcs 2026-02-17 16:51:30 +01:00
971acb02ca Merge branch 'master' of https://git.teletype.hu/games/impostor
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-02-16 22:13:30 +01:00
9fff21826b - added music/sfc subsystem\n- added basic audio\n- added music/sound test screen\n- added some basic vscode tasks 2026-02-16 22:10:53 +01:00
06d257a335 Merge pull request 'fix/imp-13-ddr-song-load' (#2) from fix/imp-13-ddr-song-load into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/2
2026-02-16 14:15:19 +00:00
Zoltan Timar
f553b09004 fix: added minigames description to Gemini.md, fixed song end note, fixed custom songs not loading, added another test song
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/pull_request_closed/woodpecker Pipeline failed
2026-02-13 15:25:01 +01:00
facf37dc96 Merge pull request 'feature/imp-13-minigames' (#1) from feature/imp-13-minigames into master
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Reviewed-on: http://git.teletype.hu/games/impostor/pulls/1
2026-02-12 22:05:11 +00:00
bbe24cebf2 Merge branch 'master' into feature/imp-13-minigames
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/pull_request_closed/woodpecker Pipeline failed
2026-02-12 22:05:02 +00:00
5872a27535 Makefile update
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-02-12 20:12:58 +01:00
cd279803ac Makefile update
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed
2026-02-12 20:12:25 +01:00
Zoltan Timar
c9db82cce7 feat: added minigames (button_mash, rhythm, ddr), correction in makefiles readline, placed games in init.context
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-02-12 16:31:03 +01:00
3dc28849c4 #24 Assets Import-export fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 23:21:00 +01:00
8a6214e893 restore asset constants
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 21:49:18 +01:00
d943b6deaa branch into version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-29 20:08:14 +01:00
b3b2159d75 add name to header
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-27 22:12:11 +01:00
ae56cf3555 pipeline update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-01-26 22:16:02 +01:00
2fc241fee7 Add some versions of office.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-20 22:47:23 +01:00
4e0145982f Add the firest version of the office.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-20 22:34:35 +01:00
1c987fa08b Add the player's home.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-20 21:24:24 +01:00
b6d0823875 restore import_assets
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 22:26:34 +01:00
ffa82e8f92 export_assets make target
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 21:25:00 +01:00
cfc07afe59 asset importer
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 18:14:47 +01:00
3fbce5aced purge
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-01-17 00:10:55 +01:00
b3158bdc37 rename project to impostor 2026-01-12 21:19:45 +01:00
4907c9cadf tic80 image change 2025-12-13 22:52:22 +01:00
45f2e746d6 new version 2025-12-13 20:13:08 +01:00
8e8d181c34 shadowed text 2025-12-13 20:11:50 +01:00
5379e30cf3 keyboard button fixes 2025-12-12 15:30:47 +01:00
d3fb12703c v0.13 2025-12-11 20:40:02 +01:00
f9c539a854 resume button 2025-12-11 20:39:26 +01:00
a777241d7a context init fix 2025-12-11 20:36:11 +01:00
34340d9664 save/load functionality (WIP) 2025-12-11 20:00:43 +01:00
6a39128962 v0.11 2025-12-11 18:57:30 +01:00
111 changed files with 4953 additions and 1084 deletions

7
.gitignore vendored
View File

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

74
.luacheckrc Normal file
View File

@@ -0,0 +1,74 @@
-- .luacheckrc
-- Configuration for luacheck
globals = {
"Focus",
"Day",
"Timer",
"Glitch",
"Trigger",
"Discussion",
"Util",
"Decision",
"Situation",
"Screen",
"Sprite",
"UI",
"Print",
"Input",
"Audio",
"Config",
"Context",
"Meter",
"Minigame",
"Window",
"SplashWindow",
"IntroWindow",
"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
View File

@@ -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
View 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"
}
]
}

View File

@@ -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
View File

@@ -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
![button_mash.png](/button_mash.png)
```
Progress Bar (200x12px at 20,10):
┌────────────────────────────────────┐
│████████████████░░░░░░░░░░░░░░ 45%│
└────────────────────────────────────┘
Button Indicator (Bottom):
╔═══╗
║ Z ║ ← Press repeatedly!
╚═══╝
Text: "MASH Z!"
```
#### Configuration Parameters
```lua
bar_fill = 0 -- Current fill level (0-100)
max_fill = 100 -- Target fill level
fill_per_press = 8 -- Points gained per button press
base_degradation = 0.15 -- Base degradation per frame
degradation_multiplier = 0.006 -- Increases degradation with bar fill
button_press_duration = 8 -- Visual feedback duration (frames)
```
#### Usage Example
```lua
-- Start button mash minigame
MinigameButtonMashWindow.start(WINDOW_GAME)
-- Or from any window
MinigameButtonMashWindow.start(WINDOW_POPUP)
```
#### Color Coding
- **Green**: Low fill (0-33%)
- **Medium (NPC color)**: Medium fill (34-66%)
- **Yellow (Item color)**: High fill (67-100%)
---
## Rhythm Minigame
**File**: `inc/window/window.minigame.rhythm.lua`
### Description
A timing-based minigame where the player must press Z when a moving line crosses a green target zone. The target zone shrinks with each successful hit, making the game progressively more challenging.
### Gameplay Mechanics
- **Objective**: Score 10 points by timing button presses correctly

266
Makefile
View File

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

View File

@@ -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
View File

0
assets/sfx/.keep Normal file
View File

0
assets/sprites/.keep Normal file
View File

0
assets/tiles/.keep Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets_src/map.map Normal file

Binary file not shown.

BIN
assets_src/mapimg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

View File

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

BIN
assets_src/sprites.ase Normal file

Binary file not shown.

BIN
assets_src/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
assets_src/tiles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

78
impostor.inc Normal file
View File

@@ -0,0 +1,78 @@
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
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
sprite/sprite.sumphore.lua
sprite/sprite.pizza_vendor.lua
sprite/sprite.dev_boy.lua
sprite/sprite.dev_buddy.lua
sprite/sprite.dev_extrovert.lua
sprite/sprite.dev_girl.lua
sprite/sprite.dev_guard.lua
sprite/sprite.dev_guru.lua
sprite/sprite.dev_hr_girl.lua
sprite/sprite.dev_introvert.lua
sprite/sprite.dev_operator.lua
sprite/sprite.dev_project_manager.lua
sprite/sprite.matrix_architect.lua
sprite/sprite.matrix_neo.lua
sprite/sprite.matrix_oraculum.lua
sprite/sprite.matrix_trinity.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.splash.lua
window/window.intro.lua
window/window.menu.lua
window/window.configuration.lua
window/window.audiotest.lua
window/window.popup.lua
window/window.minigame.mash.lua
window/window.minigame.rhythm.lua
window/window.minigame.ddr.lua
window/window.mysterious_man.lua
window/window.discussion.lua
window/window.game.lua
system/system.main.lua
meta/meta.assets.lua

View 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
View 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)
}
]]

View 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,
})

View File

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

View 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,
})

View File

@@ -0,0 +1,8 @@
Decision.register({
id = "go_to_office",
label = "Go to Office",
handle = function()
Glitch.show()
Util.go_to_screen_by_id("office")
end,
})

View 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,
})

View 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,
})

View 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,
})

View 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,
})

View 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,
})

View 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

View 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,
})

View File

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

View 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,
})

View 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,
})

View 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 },
},
},
},
})

View File

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

View File

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

View File

@@ -1,98 +0,0 @@
function Player.draw()
spr(Context.player.sprite_id, Context.player.x, Context.player.y, 0)
end
function Player.update()
-- Handle input
if Input.left() then
Context.player.vx = -Config.physics.move_speed
elseif Input.right() then
Context.player.vx = Config.physics.move_speed
else
Context.player.vx = 0
end
if Input.player_jump() and Context.player.jumps < Config.physics.max_jumps then
Context.player.vy = Config.physics.jump_power
Context.player.jumps = Context.player.jumps + 1
end
-- Update player position
Context.player.x = Context.player.x + Context.player.vx
Context.player.y = Context.player.y + Context.player.vy
-- Screen transition
if Context.player.x > Config.screen.width - Context.player.w then
if Context.current_screen < #Context.screens then
Context.current_screen = Context.current_screen + 1
Context.player.x = 0
else
Context.player.x = Config.screen.width - Context.player.w
end
elseif Context.player.x < 0 then
if Context.current_screen > 1 then
Context.current_screen = Context.current_screen - 1
Context.player.x = Config.screen.width - Context.player.w
else
Context.player.x = 0
end
end
-- Apply gravity
Context.player.vy = Context.player.vy + Config.physics.gravity
local currentScreenData = Context.screens[Context.current_screen]
-- Collision detection with platforms
for _, p in ipairs(currentScreenData.platforms) do
if Context.player.vy > 0 and Context.player.y + Context.player.h >= p.y and Context.player.y + Context.player.h <= p.y + p.h and Context.player.x + Context.player.w > p.x and Context.player.x < p.x + p.w then
Context.player.y = p.y - Context.player.h
Context.player.vy = 0
Context.player.jumps = 0
end
end
-- Collision detection with ground
if Context.player.y + Context.player.h > Context.ground.y then
Context.player.y = Context.ground.y - Context.player.h
Context.player.vy = 0
Context.player.jumps = 0
end
-- Entity interaction
if Input.player_interact() then
local interaction_found = false
-- NPC interaction
for _, npc in ipairs(currentScreenData.npcs) do
if math.abs(Context.player.x - npc.x) < Config.physics.interaction_radius_npc and math.abs(Context.player.y - npc.y) < Config.physics.interaction_radius_npc then
PopupWindow.show_menu_dialog(npc, {
{label = "Talk to", action = NPC.talk_to},
{label = "Fight", action = NPC.fight},
{label = "Go back", action = NPC.go_back}
}, WINDOW_POPUP)
interaction_found = true
break
end
end
if not interaction_found then
-- Item interaction
for _, item in ipairs(currentScreenData.items) do
if math.abs(Context.player.x - item.x) < Config.physics.interaction_radius_item and math.abs(Context.player.y - item.y) < Config.physics.interaction_radius_item then
PopupWindow.show_menu_dialog(item, {
{label = "Use", action = Item.use},
{label = "Look at", action = Item.look_at},
{label = "Put away", action = Item.put_away},
{label = "Go back", action = Item.go_back_from_item_dialog}
}, WINDOW_POPUP)
interaction_found = true
break
end
end
end
-- If no interaction happened, open inventory
if not interaction_found then
GameWindow.set_state(WINDOW_INVENTORY)
end
end
end

View File

@@ -1,32 +1,60 @@
local Config = {
Config = {}
--- Return initial data for Config
--- @within Config
function Config.initial_data()
return {
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
light_grey = 2,
dark_grey = 1,
red = 13,
light_blue = 9,
blue = 3,
white = 4,
item = 7,
meter_bg = 1
},
timing = {
splash_duration = 120
splash_duration = 120,
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_SPLASH_DURATION_ADDRESS = 3
local CONFIG_MAGIC_VALUE = 0xDE
--- Saves the current configuration.
--- @within Config
function Config.save()
mset(CONFIG_MAGIC_VALUE, CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK)
mset(Config.timing.splash_duration, CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
end
--- Loads saved configuration.
--- @within Config
function Config.load()
if mget(CONFIG_MAGIC_VALUE_ADDRESS, CONFIG_SAVE_BANK) == CONFIG_MAGIC_VALUE then
Config.timing.splash_duration = mget(CONFIG_SPLASH_DURATION_ADDRESS, CONFIG_SAVE_BANK)
else
Config.reset()
end
end
Config.load()

View File

@@ -1,432 +1,138 @@
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,
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/>
--- * splash_timer (number) Remaining frames for the splash screen timer.<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,
splash_timer = Config.timing.splash_duration,
dialog = {
text = "",
menu_items = {},
selected_menu_item = 1,
active_entity = nil,
showing_description = false,
current_node_key = nil
popup = {
show = false,
content = {}
},
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
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,
},
ground = {
x = 0,
y = Config.screen.height,
w = Config.screen.width,
h = 8
day_count = 1,
glitch = {
enabled = false,
state = "active",
timer = 0,
},
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
_end = {
state = "choice",
selection = 1,
},
{
x = 160,
y = 90,
w = 40,
h = 8
}
discussion = {
active = false,
id = nil,
step = 1,
selected_answer = 1,
scroll_y = 0,
scroll_timer = 0,
auto_scroll = true,
return_window = nil,
},
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."
}
}
},
{
-- Screen 2
name = "Screen 2",
platforms = {
{
x = 30,
y = 100,
w = 50,
h = 8
},
{
x = 100,
y = 80,
w = 50,
h = 8
},
{
x = 170,
y = 60,
w = 50,
h = 8
}
},
npcs = {
{
x = 120,
y = 72,
name = "Morpheus",
sprite_id = 5,
dialog = {
start = {
text = "At last. Welcome, Neo. As you no doubt have guessed, I am Morpheus.",
options = {
{label = "It's an honor to meet you.", next_node = "honor"},
{label = "You've been looking for me.", next_node = "looking_for_me"}
}
},
honor = {
text = "No, the honor is mine.",
options = {
{label = "What is this place?", next_node = "what_is_this_place"}
}
},
looking_for_me = {
text = "I have. For some time.",
options = {
{label = "What is this place?", next_node = "what_is_this_place"}
}
},
what_is_this_place = {
text = "This is the construct. It's our loading program. We can load anything from clothing, to equipment, weapons, training simulations. Anything we need.",
options = {
{label = "Right.", next_node = "dialog_end"}
}
},
dialog_end = {
text = "I've been waiting for you, Neo. We have much to discuss.",
options = {} -- Ends conversation
}
}
},
{
x = 40,
y = 92,
name = "Tank",
sprite_id = 6,
dialog = {
start = {
text = "Hey, Neo! Welcome to the construct. I'm Tank.",
options = {
{label = "Good to meet you.", next_node = "good_to_meet_you"},
{label = "This place is incredible.", next_node = "incredible"}
}
},
good_to_meet_you = {
text = "You too! We've been waiting for you. Need anything? Training? Weapons?",
options = {
{label = "Training?", next_node = "training"},
{label = "I'm good for now.", next_node = "dialog_end"}
}
},
incredible = {
text = "Isn't it? The boss's design. We can load anything we need. What do you want to learn?",
options = {
{label = "Show me.", next_node = "training"}
}
},
training = {
text = "Jujitsu? Kung Fu? How about... all of them?",
options = {
{label = "All of them.", next_node = "all_of_them"}
}
},
all_of_them = {
text = "Operator, load the combat training program.",
options = {
{label = "...", next_node = "dialog_end"}
}
},
dialog_end = {
text = "Just holler if you need anything. Anything at all.",
options = {}
}
}
}
},
items = {
{
x = 180,
y = 52,
w = 8,
h = 8,
name = "Potion",
sprite_id = 7,
desc = "A glowing red potion. It looks potent."
}
}
},
{
-- Screen 3
name = "Screen 3",
platforms = {
{
x = 50,
y = 110,
w = 30,
h = 8
},
{
x = 100,
y = 90,
w = 30,
h = 8
},
{
x = 150,
y = 70,
w = 30,
h = 8
},
{
x = 200,
y = 50,
w = 30,
h = 8
}
},
npcs = {
{
x = 210,
y = 42,
name = "Agent Smith",
sprite_id = 8,
dialog = {
start = {
text = "Mr. Anderson. We've been expecting you.",
options = {
{label = "My name is Neo.", next_node = "name_is_neo"},
{label = "...", next_node = "silent"}
}
},
name_is_neo = {
text = "Whatever you say. You're here for a reason.",
options = {
{label = "What reason?", next_node = "what_reason"}
}
},
silent = {
text = "The silent type. It doesn't matter. You are an anomaly.",
options = {
{label = "What do you want?", next_node = "what_reason"}
}
},
what_reason = {
text = "To be deleted. The system has no place for your kind.",
options = {
{label = "I won't let you.", next_node = "wont_let_you"}
}
},
wont_let_you = {
text = "You hear that, Mr. Anderson? That is the sound of inevitability.",
options = {
{label = "...", next_node = "dialog_end"}
}
},
dialog_end = {
text = "It is purpose that created us. Purpose that connects us. Purpose that pulls us. That guides us. That drives us. It is purpose that defines. Purpose that binds us.",
options = {}
}
}
},
{
x = 160,
y = 62,
name = "Cypher",
sprite_id = 9,
dialog = {
start = {
text = "Well, well. The new messiah. Welcome to the real world.",
options = {
{label = "You don't seem happy.", next_node = "not_happy"},
{label = "...", next_node = "silent"}
}
},
not_happy = {
text = "Happy? Ignorance is bliss, Neo. We've been fighting this war for years. For what?",
options = {
{label = "For freedom.", next_node = "freedom"}
}
},
silent = {
text = "Not a talker, huh? Smart. Less to regret later. Want a drink?",
options = {
{label = "Sure.", next_node = "drink"},
{label = "No thanks.", next_node = "no_drink"}
}
},
drink = {
text = "Good stuff. The little things you miss, you know? Like a good steak.",
options = {
{label = "I guess.", next_node = "dialog_end"}
}
},
no_drink = {
text = "Your loss. More for me.",
options = {
{label = "...", next_node = "dialog_end"}
}
},
freedom = {
text = "Freedom... right. If Morpheus told you you could fly, would you believe him?",
options = {
{label = "He's our leader.", next_node = "dialog_end"}
}
},
dialog_end = {
text = "Just be careful who you trust.",
options = {}
}
}
}
},
items = {}
}
}
}
end
--- 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

18
inc/init/init.module.lua Normal file
View File

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

View File

@@ -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 = {}

View File

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

View 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
View 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

View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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,
})

View File

@@ -1,29 +1,442 @@
-- <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
-- 008:0000005a00005a55000055a50000a55a00005a5700005533000575f30000757f
-- 009:55a50000a55a50005a55a00057555500fff7a5003f335a00fff3f500fffffa00
-- 016:0000000000000000000000000000003000000353000035350003535100353535
-- 017:0000000000000000000000003000000053300000151300005151300015151300
-- 018:004f99ff000fffff0000ff3300000fff00003666000355550035652503163555
-- 019:99f41000fff26000ff600000f600000063300000555330005555530055535530
-- 020:00356f6f003655f60365511f3653122f3531222f363221220532232203322322
-- 021:f6f530006f563000f1156300f2215300f2226300221233002232130022121300
-- 022:000000000000005100000155000055150000157f0000551300057f3f00017fff
-- 023:00000000555000005155000015515000ff7f5100ff31f200fff3ff0033fff200
-- 024:0000057f00000005000000050000559900055911005599110055119900551199
-- 025:f3ff5000fff500005f5000004415100077995100449915007711955044119550
-- 026:00000000000001330001331300031333001333330031f7f7001133390031999f
-- 027:0000000013310000131330003333130033333100f7f113003337310099975300
-- 028:000000000000000000000a77000057770005777700577777005775a50077a7ff
-- 029:0000000000000000777a0000777750007777750077777750a5a57750fff7a770
-- 030:000000000000051500001151000555550001517f000557ff0001ff3f0007ff1f
-- 031:00000000150000005110000055500000f7110000ff750000f3f10000f1f50000
-- 032:0033535100353533003351ff00351f3f0003ff3f0003ffff00003ff3000323ff
-- 033:5555530033555300ff155300f3f15300f3ff3000ffff30003ff30000ff323000
-- 034:036135250316355503613525031633110333331103f333330333333300033333
-- 035:55565530555355305556553013335530133333303333ff303333333033330000
-- 036:00322322003223330033331a003ff31a003ff3a1003333a100003a1a00003a1a
-- 037:223213003332130011133300111f300011133000111300001113000011130000
-- 038:000007ff0001117f0016171f0155175501561755155117555f7157555ff15755
-- 039:fffff700333f2000ffff7100555571105555716155557111555575ff555575ff
-- 040:005599110055991100ff533300fff511007fff110007f5110000311300003113
-- 041:479915504499155047333f7014113ff011113f70111130003311300003113000
-- 042:0035777f0001776600005777000000550088885508bbb84508bbb8448bb8b844
-- 043:77775300677500007750000055880000558b8000548bb800228bbb80428b8b80
-- 044:005a7f3f0057ff3f07f7ffff07757fdf077a57fd0575357f005a66a70006aa6f
-- 045:ff3f7a50ff3ff750fffff777ffdf7577ddf75a77ff75157577a66a50ff6aa600
-- 046:0005ffff00005ff3000005ff0017775500242777002121710024277100212171
-- 047:fff500003f500000f50000005771000077720000171200001772000017120000
-- 048:00323123003231430032314300313339000f3333000033300000333000033330
-- 049:324303003443030034430300933313003333f000033300000333000003333000
-- 050:0003333000033330000333300003333000053530003311300031113000333330
-- 051:3333000033330000333300003333000035350000311330003111300033333000
-- 052:000031a1000031a100003a1a00003a1a00003333000003f3000003f300000330
-- 053:111300001113000011130000111300003333000003f3000003f3000003300000
-- 054:5ff7646607f59999000991330003993000099130000133300001221000011100
-- 055:666646ff999993f7339913000039930000991300001333000012210000111000
-- 056:0000311300003113000031130000311300003113000033330000165100001111
-- 057:0311300003113000031130000311300003113000033330000165100001111000
-- 058:8888b8445f78b8445f58884405031330000331300003133000053550000577f0
-- 059:218b8880428b87f5218885f5031330500331300003133000053550000577f000
-- 060:0056aa66001f6aaa001fa666007f6aaa00006aaa0000a666000007d7000001d1
-- 061:666aa650aaaa6f106666af10aaaa6f70aaaa60006666a000007d7000001d1000
-- 062:0024277700ff611100fff333005f533300003333000033300000333000001110
-- 063:77720000116f0000335ff000333f000033300000333000003330000001110000
-- 064:00000000000000000000011100003311000111170003317f000117ff00553333
-- 065:0000000000000000110000001110000077110000ff710000ff77000013330000
-- 066:00000000000000330000033300003333000033f7000035ff000535ff0007f333
-- 067:0000000030000000333300003333000037f30000ffff0000ffff0000f333f000
-- 068:000000000000000000000000000000000000000000000122000012440001447f
-- 069:00000000000000000000000000000000000000002210000044210000f7441000
-- 070:000000000000000000000333000038880003888800388881038388310338835f
-- 071:00000000000000003330000088880000883380003355380016665300f7ff7300
-- 072:0000000000000000000000550000055500005577000057ff00007f55000fff33
-- 073:0000000000000000555550005555550077777550ffffff507f755f703f333fff
-- 074:0000000000000011000001130000113100011313000131310013137c003131ff
-- 075:000000001110000013110000313110001313100031313100ffff7100fffff100
-- 076:0000000000000055000005550000055500005577000057ff0007ff55000fff99
-- 077:0000000055550000555550005555500075775500fffff5005f555f703f993ff0
-- 078:0000000000000000000000000000000000001111000014440000144400001555
-- 079:0000000000000000000000000000000011100000441000004410000055100000
-- 080:0077533f000ff77f0005fff500005ff70000015f0005f315005ff3ff00ff3333
-- 081:f33f0000f77f00005ff000007f500000f500000053f00000f3f50000333f0000
-- 082:000fff330005ffff000057f50000007f000333350031313f0031313303113133
-- 083:ff33f000ffff100055f10000ff70000055330000ff3130003331300033311300
-- 084:000447ff000fffff0007f33f000ff11f000f7fff0000ff750000042400001142
-- 085:ff744000ffff1000f33f7000f11f5000fff7f00057ff00004240000044110000
-- 086:038383390338833b038383130338337f0383835f003838350003838300003333
-- 087:99539900bb33bb0099513900fffff530ff66f1007fff53001555310033333000
-- 088:000f7ff300005fff000007ff0000007f0000021f000024410002444400124244
-- 089:fff3ff7ff1ffff50fffff700111f7000ffff1000111142004244442042442410
-- 090:005f5733005f1f2300015fff0000077f00011117001244210144414201421444
-- 091:ff33f500ff23f500fffff00011ff500077710000112410004442410042441410
-- 092:00005fff000005ff0000001f0000000100000114000018140000181400018114
-- 093:fffff500111f5000fff500001110000042411000424180004241810042411100
-- 094:00001fff00001f3f0000cf3f0000cfff00000cf3000023cc000131ff001f1f11
-- 095:ff1000003f1000003ff00000fff00000fc000000c3200000f13100001f1f1000
-- 096:00ff33330033133305f333330ff31333033333330f5313330ff1311107f13333
-- 097:333f0000333f5000333f5000333ff000333330003335f500111ff5003337f000
-- 098:03113133031131330333313303113131033331320555313107ff313307ff3131
-- 099:333113003331130033333300213113004233330021317f0033317f5001317500
-- 100:0002441400244441024444240242442414424424144144241441442414414424
-- 101:4144210014444200424444204244422042444241424441414244414142444141
-- 102:000333330033a3530032a353003a332303a23333032a333303a23333032a3333
-- 103:3333300055353300553533002232323033333130333332303333313333333233
-- 104:001441440014424400144144001441240017f311001ff133001fff330007f333
-- 105:4244142042442410424414204244122012113f7031333ff033333ff030333f00
-- 106:01421444014414440142144431421444011111110fff1a5a0ff755a505755a51
-- 107:44441410424414104444141042441410111111105a5a55f5a5a5a5f5111a5170
-- 108:0001181400018114000118140001811300011333005fff33000f773300055333
-- 109:424181104241181042418110222318101113310033335ff03333f77033335500
-- 112:0553333300033330000333000033330000333300003333000011110000121200
-- 113:3330000033300000333000003330000033300000333000001111000012120000
-- 114:05f5313103003131033311310311113103111131003111300003333000012120
-- 115:0131130001311300013113000131130001311300003113000033330000121200
-- 116:0fff242407f72211005544200002442000024420000244200002222000024440
-- 117:424441f1112227f1024425500244200002442000024420000222200002444000
-- 118:0333333307ff333307f733330070333000003330000033300000111000001110
-- 119:33333575333331f1333335750033300000333000003330000011100000111100
-- 120:0000033300000333000003330000033300000333000003330000033300000111
-- 121:0033300000333000003330000033300000333000003330000033300010111100
-- 122:005015a100001a51000015a100001a51000015a1000022510000924900009999
-- 123:0315a150001a51000015a100001a51000015a100002251000092490000999900
-- 124:0000033300000333000003330000033300000333000003330000033300000111
-- 125:0333000003330000033300000333000003330000033300000333000000111000
-- 129:0000000000000000000000000000000000000000011515101151515151515151
-- 144:0000000500000015000000150000057f000005ff0000017f0000331700073331
-- 145:1ffffff7ff0fff0fff1fff1fffffffffff7555fffffffffffff111ff57fffff7
-- 146:1000000071000000f5000000ff000000ff500000ff5000007513100051333700
-- 160:0077333303331333333313333331333333313333355133335ff513335ff71111
-- 161:1111111133333333333337333333373333333733333337333333373311117711
-- 162:133337003733133077731330333331333333313333333155333337ff1111175f
-- 176:0775333300031333000333130000333300001333000033330000033300000111
-- 177:3335773333333533333333331313131333333333300000033000000310000001
-- 178:3333357533331000331300001333000033310000333300003331000011110000
-- </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>

View File

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

View File

@@ -0,0 +1,16 @@
Screen.register({
id = "home",
name = "Home",
decisions = {
"go_to_toilet",
"go_to_walking_to_office",
"go_to_sleep",
"go_to_end",
},
background = "bedroom",
draw = function()
if Context.home_norman_visible and Window.get_current_id() == "game" then
Sprite.draw_at("norman", 100, 80)
end
end
})

View File

@@ -0,0 +1,63 @@
--- @section Screen
local _screens = {}
--- Registers a screen definition.
--- @within Screen
--- @param screen_data table The screen data table.
--- @param screen_data.id string Unique screen identifier.
--- @param screen_data.name string Display name of the screen.
--- @param screen_data.decisions table Array of decision ID strings available on this screen.
--- @param screen_data.background string Map ID used as background.
--- @param[opt] screen_data.situations table Array of situation ID strings. Defaults to {}.
--- @param[opt] screen_data.init function Called when the screen is entered. Defaults to noop.
--- @param[opt] screen_data.update function Called each frame while screen is active. Defaults to noop.
--- @param[opt] screen_data.draw function Called after the focus overlay to draw screen-specific overlays. Defaults to noop.
function Screen.register(screen_data)
if _screens[screen_data.id] then
trace("Warning: Overwriting screen with id: " .. screen_data.id)
end
if not screen_data.situations then
screen_data.situations = {}
end
if not screen_data.init then
screen_data.init = function() end
end
if not screen_data.update then
screen_data.update = function() end
end
if not screen_data.draw then
screen_data.draw = function() end
end
_screens[screen_data.id] = screen_data
end
--- Gets a screen by ID.
--- @within Screen
--- @param screen_id string The ID of the screen.
--- @return table|nil screen The screen table or nil. </br>
--- Fields: </br>
--- * id (string) Unique screen identifier.<br/>
--- * name (string) Display name.<br/>
--- * decisions (table) Array of decision ID strings.<br/>
--- * background (string) Map ID used as background.<br/>
--- * situations (table) Array of situation ID strings.<br/>
--- * init (function) Called when the screen is entered.<br/>
--- * update (function) Called each frame while screen is active.
function Screen.get_by_id(screen_id)
return _screens[screen_id]
end
--- Gets all registered screens.
--- @within Screen
--- @return result table A table containing all registered screen data, indexed by their IDs or nil. </br>
--- Fields: </br>
--- * id (string) Unique screen identifier.<br/>
--- * name (string) Display name of the screen.<br/>
--- * decisions (table) Array of decision ID strings available on this screen.<br/>
--- * background (string) Map ID used as background.<br/>
--- * situations (table) Array of situation ID strings.<br/>
--- * init (function) Called when the screen is entered.<br/>
--- * update (function) Called each frame while screen is active.<br/>
function Screen.get_all()
return _screens
end

View File

@@ -0,0 +1,27 @@
Screen.register({
id = "office",
name = "Office",
decisions = {
"do_work",
"go_to_walking_to_home",
"have_a_coffee",
},
situations = {
"drink_coffee",
},
background = "office",
draw = function()
if Window.get_current_id() == "game" then
Sprite.draw_at("norman", 13 * 8, 9 * 8)
Sprite.draw_at("dev_buddy", 15 * 8, 9 * 8)
Sprite.draw_at("dev_project_manager", 6 * 8, 4 * 8)
Sprite.draw_at("dev_hr_girl", 12 * 8, 4 * 8)
Sprite.draw_at("dev_introvert", -4 + 5 * 8, 9 * 8)
Sprite.draw_at("dev_extrovert", 20 * 8, 4 * 8)
Sprite.draw_at("dev_girl", 23 * 8, 5 * 8)
Sprite.draw_at("dev_boy", 10 * 8, 11 * 8 + 4)
Sprite.draw_at("dev_guru", 22 * 8, 10 * 8 + 4)
Sprite.draw_at("dev_operator", 27 * 8, 10 * 8 + 4)
end
end
})

View 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,
})

View File

@@ -0,0 +1,16 @@
Screen.register({
id = "walking_to_home",
name = "Walking to home",
decisions = {
"go_to_home",
"go_to_office",
},
background = "street",
draw = function()
if Window.get_current_id() == "game" then
Sprite.draw_at("norman", 7 * 8, 3 * 8)
Sprite.draw_at("pizza_vendor", 19 * 8, 1 * 8)
Sprite.draw_at("dev_guard", 22 * 8, 2 * 8)
end
end
})

View File

@@ -0,0 +1,22 @@
Screen.register({
id = "walking_to_office",
name = "Walking to office",
decisions = {
"go_to_home",
"go_to_office",
"start_discussion",
},
background = "street",
draw = function()
if Window.get_current_id() == "game" then
Sprite.draw_at("norman", 7 * 8, 3 * 8)
Sprite.draw_at("sumphore", 9 * 8, 2 * 8)
Sprite.draw_at("pizza_vendor", 19 * 8, 1 * 8)
Sprite.draw_at("dev_guard", 22 * 8, 2 * 8)
Sprite.draw_at("matrix_trinity", 5 * 8, 11 * 8)
Sprite.draw_at("matrix_neo", 7 * 8, 11 * 8)
Sprite.draw_at("matrix_oraculum", 9 * 8, 12 * 8)
Sprite.draw_at("matrix_architect", 11 * 8, 11 * 8)
end
end
})

View File

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

View File

@@ -0,0 +1,7 @@
Situation.register({
id = "drink_coffee",
handle = function()
Audio.sfx_select()
Sprite.show("norman", 100, 100)
end,
})

View 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

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_boy",
sprites = Sprite.generate_table(2, 3, 278, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_buddy",
sprites = Sprite.generate_table(2, 3, 286, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_extrovert",
sprites = Sprite.generate_table(2, 4, 330, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_girl",
sprites = Sprite.generate_table(2, 3, 284, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_guard",
sprites = Sprite.generate_table(3, 4, 384, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_guru",
sprites = Sprite.generate_table(2, 4, 264, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_hr_girl",
sprites = Sprite.generate_table(2, 4, 260, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_introvert",
sprites = Sprite.generate_table(2, 4, 332, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_operator",
sprites = Sprite.generate_table(2, 4, 326, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "dev_project_manager",
sprites = Sprite.generate_table(2, 4, 328, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,152 @@
--- @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
--- Generates a sprites table for a rectangular composite sprite.
--- @within Sprite
--- @param width number The number of sprites wide.<br/>
--- @param height number The number of sprites tall.<br/>
--- @param starting_s number The sprite index of the top-left tile.<br/>
--- @param x_base number The base x-offset for the leftmost column.<br/>
--- @param y_base number The base y-offset for the topmost row.<br/>
--- @param x_step number The x-offset increment per column.<br/>
--- @param y_step number The y-offset increment per row.<br/>
--- @return table The sprites table array.
function Sprite.generate_table(width, height, starting_s, x_base, y_base, x_step, y_step)
local sprites = {}
for row = 0, height - 1 do
for col = 0, width - 1 do
local s = starting_s + row * 16 + col
local x_offset = x_base + col * x_step
local y_offset = y_base + row * y_step
table.insert(sprites, { s = s, x_offset = x_offset, y_offset = y_offset })
end
end
return sprites
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

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "matrix_architect",
sprites = Sprite.generate_table(2, 4, 324, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "matrix_neo",
sprites = Sprite.generate_table(2, 4, 322, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "matrix_oraculum",
sprites = Sprite.generate_table(2, 3, 282, -4, -4, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "matrix_trinity",
sprites = Sprite.generate_table(2, 4, 320, -4, -4, 8, 8)
})

View 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 }
}
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "pizza_vendor",
sprites = Sprite.generate_table(2, 2, 334, -1, -8, 8, 8)
})

View File

@@ -0,0 +1,4 @@
Sprite.register({
id = "sumphore",
sprites = Sprite.generate_table(2, 4, 258, -4, -4, 8, 8)
})

View File

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

View File

@@ -1,44 +1,32 @@
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 end
Context.reset()
Window.set_current("splash") -- Set initial window using new manager
MenuWindow.refresh_menu_items()
initialized_game = true
end
--- Main game loop (TIC-80 callback).
--- @within Main
function TIC()
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()
if Context.game_in_progress then
Meter.draw()
Timer.draw()
Glitch.draw()
end
end

View 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

View File

@@ -1,29 +1,43 @@
--- @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)
end
function UI.draw_dialog()
PopupWindow.draw()
Print.text(title, 3, 2, Config.colors.light_blue)
end
--- Draws a menu.
--- @within UI
--- @param items table A table of menu items.<br/>
--- @param selected_item number The index of the currently selected item.<br/>
--- @param x number The x-coordinate for the menu.<br/>
--- @param y number The y-coordinate for the menu.<br/>
function UI.draw_menu(items, selected_item, x, y)
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,29 +46,102 @@ 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 = {}
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
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
for i = target - 1, 1, -1 do
if s:sub(i, i):match("%s") then
return i
end
end
if words_in_line > 0 then
table.insert(lines, current_line)
else
return nil
end
for input_line in (text .. "\n"):gmatch("(.-)\n") do
local remaining = trim(input_line)
if remaining == "" then
table.insert(lines, "")
else
while #remaining > max_chars_per_line do
local split_at = previous_whitespace_index(remaining, max_chars_per_line)
local line = trim(remaining:sub(1, split_at))
if not split_at or line == "" then
line = remaining:sub(1, max_chars_per_line)
split_at = max_chars_per_line
end
table.insert(lines, line)
remaining = trim(remaining:sub(split_at + 1))
end
table.insert(lines, remaining)
end
end
@@ -64,16 +151,3 @@ function UI.word_wrap(text, max_chars_per_line)
return lines
end
function UI.create_numeric_stepper(label, value_getter, value_setter, min, max, step, format)
return {
label = label,
get = value_getter,
set = value_setter,
min = min,
max = max,
step = step,
format = format or "%.1f",
type = "numeric_stepper"
}
end

View File

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

View 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

View File

@@ -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)
-- 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
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(label_text, x_start, current_y, color)
print(value_text, value_x, current_y, color)
Print.text(label_text, x_start, current_y, color)
Print.text(value_text, value_x, current_y, color)
end
elseif control.type == "action_item" then
local label_text = control.label
if i == ConfigurationWindow.selected_control then
color = Config.colors.item
Print.text("<", x_start - 8, current_y, color)
Print.text(label_text, x_start, current_y, color)
Print.text(">", x_start + 8 + (#label_text * char_width) + 4, current_y, color)
else
Print.text(label_text, x_start, current_y, color)
end
end
end
Print.text("Press B to go back", x_start, 120, Config.colors.light_grey)
end
print("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,9 +82,9 @@ function ConfigurationWindow.update()
end
end
-- Modify control value
local control = ConfigurationWindow.controls[ConfigurationWindow.selected_control]
if control then
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)
@@ -86,5 +93,10 @@ function ConfigurationWindow.update()
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

View 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
View 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()
Context.game.current_screen = "home"
Window.set_current("game")
-- Initialize home screen
local home_screen = Screen.get_by_id("home")
if home_screen and home_screen.init then
home_screen.init()
end
end
end
elseif Context._end.state == "ending" then
if Input.menu_confirm() then
Window.set_current("menu")
MenuWindow.refresh_menu_items()
end
end
end

View File

@@ -1,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)
draw_game_scene()
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()
--- 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
function GameWindow.set_state(new_state)
Context.active_window = new_state
-- Add any state-specific initialization/cleanup here later if needed
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)
Window.set_current(new_state)
end

View File

@@ -1,25 +1,41 @@
--- @section IntroWindow
IntroWindow.y = Config.screen.height
IntroWindow.speed = 0.5
IntroWindow.text = [[
Norman Reds everyday life
seems ordinary: work,
meetings, coffee, and
endless notifications.
But beneath him, or around
him — something is
constantly building, and
it soon becomes clear
that there is more going
on than meets the eye.
]]
--- Draws the intro window.
--- @within IntroWindow
function IntroWindow.draw()
local x = (Config.screen.width - 132) / 2 -- Centered text
print(Context.intro.text, x, Context.intro.y, Config.colors.green)
local x = (Config.screen.width - 132) / 2
Print.text(IntroWindow.text, x, IntroWindow.y, Config.colors.light_blue)
end
--- Updates the intro window logic.
--- @within IntroWindow
function IntroWindow.update()
Context.intro.y = Context.intro.y - Context.intro.speed
IntroWindow.y = IntroWindow.y - IntroWindow.speed
-- Count lines in intro text to determine when scrolling is done
local lines = 1
for _ in string.gmatch(Context.intro.text, "\n") do
for _ in string.gmatch(IntroWindow.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)
if IntroWindow.y < -lines * 8 then
Window.set_current("menu")
end
-- Skip intro by pressing A
if Input.menu_confirm() then
GameWindow.set_state(WINDOW_MENU)
Window.set_current("menu")
end
end

Some files were not shown because too many files have changed in this diff Show More