spriteforge 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spriteforge/__init__.py +1 -0
- spriteforge/__main__.py +3 -0
- spriteforge/agents/__init__.py +0 -0
- spriteforge/agents/assets.py +109 -0
- spriteforge/agents/cancellation.py +12 -0
- spriteforge/agents/director.py +50 -0
- spriteforge/agents/models.py +63 -0
- spriteforge/agents/roles/art-director.md +15 -0
- spriteforge/agents/roles/art-reviewer.md +14 -0
- spriteforge/agents/roles/director.md +14 -0
- spriteforge/agents/roles/game-designer.md +15 -0
- spriteforge/agents/roles/level-tile-designer.md +16 -0
- spriteforge/agents/roles/librarian.md +13 -0
- spriteforge/agents/roles/menu-designer.md +15 -0
- spriteforge/agents/roles/pixel-artist.md +14 -0
- spriteforge/agents/roles/programmer.md +14 -0
- spriteforge/agents/roles/qa-playtester.md +13 -0
- spriteforge/agents/roles/sound-director.md +14 -0
- spriteforge/agents/roles.py +39 -0
- spriteforge/agents/router.py +233 -0
- spriteforge/agents/router_eval.py +145 -0
- spriteforge/agents/routing_state.py +133 -0
- spriteforge/agents/runtime.py +430 -0
- spriteforge/agents/workflow.py +389 -0
- spriteforge/app/__init__.py +0 -0
- spriteforge/app/agents_view.py +248 -0
- spriteforge/app/chat.py +114 -0
- spriteforge/app/extensions.py +115 -0
- spriteforge/app/index.html +279 -0
- spriteforge/app/local_only.py +49 -0
- spriteforge/app/server.py +358 -0
- spriteforge/app/static/assets/index-L2s1wlcp.css +1 -0
- spriteforge/app/static/assets/index-aBfzgLQl.js +71 -0
- spriteforge/app/static/assets/press-start-2p-latin-400-normal-BKfmdmKK.woff +0 -0
- spriteforge/app/static/assets/press-start-2p-latin-400-normal-xj9lRvVs.woff2 +0 -0
- spriteforge/app/static/index.html +13 -0
- spriteforge/app/static/press-start-2p-LICENSE.txt +93 -0
- spriteforge/app/studio.py +278 -0
- spriteforge/app/workers.py +73 -0
- spriteforge/cli.py +241 -0
- spriteforge/config.py +193 -0
- spriteforge/config.yaml +157 -0
- spriteforge/core/__init__.py +0 -0
- spriteforge/core/acting.py +16 -0
- spriteforge/core/cost.py +236 -0
- spriteforge/core/credentials.py +24 -0
- spriteforge/core/demo.py +79 -0
- spriteforge/core/forms.py +20 -0
- spriteforge/core/gates.py +175 -0
- spriteforge/core/security.py +42 -0
- spriteforge/core/state.py +222 -0
- spriteforge/core/team.py +15 -0
- spriteforge/core/usage.py +68 -0
- spriteforge/game/__init__.py +0 -0
- spriteforge/game/actors.py +377 -0
- spriteforge/game/agent_tools.py +178 -0
- spriteforge/game/art.py +2097 -0
- spriteforge/game/art_checks.py +190 -0
- spriteforge/game/art_review.py +518 -0
- spriteforge/game/build.py +134 -0
- spriteforge/game/bundle.py +1178 -0
- spriteforge/game/cli.py +119 -0
- spriteforge/game/codegen.py +433 -0
- spriteforge/game/codeinfo.py +154 -0
- spriteforge/game/dbsource.py +137 -0
- spriteforge/game/design.py +342 -0
- spriteforge/game/export.py +62 -0
- spriteforge/game/gamedb.py +191 -0
- spriteforge/game/image_cache.py +61 -0
- spriteforge/game/lessons.py +259 -0
- spriteforge/game/level_gen.py +118 -0
- spriteforge/game/level_preview.py +97 -0
- spriteforge/game/levels.py +934 -0
- spriteforge/game/loops.py +38 -0
- spriteforge/game/menus.py +712 -0
- spriteforge/game/mock_game.js +109 -0
- spriteforge/game/mock_platformer.js +58 -0
- spriteforge/game/motion.py +881 -0
- spriteforge/game/pipeline.py +1768 -0
- spriteforge/game/plan.py +502 -0
- spriteforge/game/playtest.py +318 -0
- spriteforge/game/qa.py +664 -0
- spriteforge/game/qa_checks.py +223 -0
- spriteforge/game/qa_runtime.py +565 -0
- spriteforge/game/reach.py +807 -0
- spriteforge/game/record.py +138 -0
- spriteforge/game/research.py +262 -0
- spriteforge/game/revision.py +225 -0
- spriteforge/game/sdk/API.md +104 -0
- spriteforge/game/sdk/FLOW.md +19 -0
- spriteforge/game/sdk/LEVELS.md +65 -0
- spriteforge/game/sdk/sf.js +418 -0
- spriteforge/game/sdk/sfflow.js +442 -0
- spriteforge/game/sdk/sflevel.js +683 -0
- spriteforge/game/sdk/sfsound-editor.js +225 -0
- spriteforge/game/sdk/sfsound.js +260 -0
- spriteforge/game/sdk/sounds.html +67 -0
- spriteforge/game/sounds.py +793 -0
- spriteforge/game/story.py +188 -0
- spriteforge/game/structure.py +98 -0
- spriteforge/game/team_reports.py +394 -0
- spriteforge/game/tilesets.py +70 -0
- spriteforge/game/title_art.py +186 -0
- spriteforge/genres/__init__.py +0 -0
- spriteforge/library/__init__.py +15 -0
- spriteforge/library/cli.py +126 -0
- spriteforge/library/describe.py +242 -0
- spriteforge/library/looks.py +154 -0
- spriteforge/library/models.py +79 -0
- spriteforge/library/store.py +422 -0
- spriteforge/library/tilesets/README.md +32 -0
- spriteforge/library/tilesets/gandalf-seasons.json +111 -0
- spriteforge/library/tilesets/pixel-crawler-dungeon.json +66 -0
- spriteforge/mcp/__init__.py +0 -0
- spriteforge/mcp/cli.py +58 -0
- spriteforge/mcp/client.py +57 -0
- spriteforge/mcp/filesystem_read.py +32 -0
- spriteforge/mcp/gamedb.py +56 -0
- spriteforge/mcp/library.py +179 -0
- spriteforge/mcp/memory.py +37 -0
- spriteforge/mcp/pixel_tools.py +145 -0
- spriteforge/mcp/registry.py +145 -0
- spriteforge/mcp/service.py +224 -0
- spriteforge/mcp/templates/context7.json +6 -0
- spriteforge/mcp/templates/fetch.json +6 -0
- spriteforge/mcp/templates/filesystem-read.json +6 -0
- spriteforge/mcp/templates/pixel-tools.json +6 -0
- spriteforge/memory/README.md +10 -0
- spriteforge/memory/__init__.py +0 -0
- spriteforge/memory/cli.py +131 -0
- spriteforge/memory/okf/__init__.py +1 -0
- spriteforge/memory/okf/catalog.py +331 -0
- spriteforge/memory/okf/codec.py +63 -0
- spriteforge/memory/okf/git.py +104 -0
- spriteforge/memory/okf/indexes.py +50 -0
- spriteforge/memory/okf/models.py +114 -0
- spriteforge/memory/okf/search.py +53 -0
- spriteforge/memory/okf/seed.py +81 -0
- spriteforge/memory/okf/seeds/genres.yaml +121 -0
- spriteforge/memory/okf/seeds/other.yaml +150 -0
- spriteforge/memory/okf/store.py +402 -0
- spriteforge/memory/okf/validate.py +115 -0
- spriteforge/pixel/__init__.py +17 -0
- spriteforge/pixel/atlas.py +110 -0
- spriteforge/pixel/grid.py +174 -0
- spriteforge/pixel/palette.py +156 -0
- spriteforge/pixel/palettes/dungeon.json +9 -0
- spriteforge/pixel/postprocess.py +170 -0
- spriteforge/pixel/preview.py +40 -0
- spriteforge/pixel/schemas.py +139 -0
- spriteforge/pixel/source.py +237 -0
- spriteforge/pixel/terrain.py +48 -0
- spriteforge/pixel/validate.py +138 -0
- spriteforge/prompts/anim_frame.txt +1 -0
- spriteforge/prompts/character.txt +1 -0
- spriteforge/prompts/item.txt +1 -0
- spriteforge/prompts/tile.txt +1 -0
- spriteforge/prompts/ui.txt +1 -0
- spriteforge/providers/__init__.py +0 -0
- spriteforge/providers/base.py +97 -0
- spriteforge/providers/http.py +38 -0
- spriteforge/providers/mock.py +111 -0
- spriteforge/providers/muse_image.py +60 -0
- spriteforge/providers/muse_search.py +91 -0
- spriteforge/providers/muse_spark.py +66 -0
- spriteforge/skills/__init__.py +0 -0
- spriteforge/skills/builtin/LICENSE-awesome-gamedev-agent-skills.txt +201 -0
- spriteforge/skills/builtin/NOTICE-awesome-gamedev-agent-skills.txt +15 -0
- spriteforge/skills/builtin/THIRD_PARTY.md +21 -0
- spriteforge/skills/builtin/audio-design/SKILL.md +190 -0
- spriteforge/skills/builtin/game-feel/SKILL.md +174 -0
- spriteforge/skills/builtin/game-ui-ux/SKILL.md +167 -0
- spriteforge/skills/builtin/level-design/SKILL.md +169 -0
- spriteforge/skills/builtin/phaser-arcade-physics/SKILL.md +17 -0
- spriteforge/skills/builtin/physics-tuning/SKILL.md +164 -0
- spriteforge/skills/builtin/pixel-art-prompting/SKILL.md +12 -0
- spriteforge/skills/builtin/platformer/SKILL.md +153 -0
- spriteforge/skills/builtin/puzzle/SKILL.md +143 -0
- spriteforge/skills/builtin/sprite-animation-frames/SKILL.md +11 -0
- spriteforge/skills/builtin/tileset-design/SKILL.md +10 -0
- spriteforge/skills/catalog.py +200 -0
- spriteforge/skills/cli.py +41 -0
- spriteforge/stages/__init__.py +0 -0
- spriteforge/stages/asset_context.py +301 -0
- spriteforge/stages/asset_models.py +192 -0
- spriteforge/stages/asset_render.py +366 -0
- spriteforge/stages/asset_review.py +140 -0
- spriteforge/stages/asset_store.py +94 -0
- spriteforge/stages/chat_changes.py +236 -0
- spriteforge/stages/concept.py +42 -0
- spriteforge/stages/game.py +1699 -0
- spriteforge/stages/game_confirm.py +112 -0
- spriteforge/stages/game_final.py +220 -0
- spriteforge/stages/game_record.py +246 -0
- spriteforge/stages/gameplay.py +218 -0
- spriteforge/stages/intake.py +372 -0
- spriteforge/stages/preview_assets.py +212 -0
- spriteforge/stages/preview_models.py +167 -0
- spriteforge/stages/preview_render.py +169 -0
- spriteforge/stages/preview_repair.py +61 -0
- spriteforge/stages/preview_review.py +169 -0
- spriteforge/stages/production.py +170 -0
- spriteforge/stages/references.py +105 -0
- spriteforge/stages/rewind.py +128 -0
- spriteforge/stages/workflow.py +440 -0
- spriteforge/templates/phaser/audit.js +45 -0
- spriteforge/templates/phaser/font.png +0 -0
- spriteforge/templates/phaser/font.xml +1 -0
- spriteforge/templates/phaser/preview.html +9 -0
- spriteforge/templates/phaser/room.js +49 -0
- spriteforge/templates/phaser/vendor/LICENSE.md +20 -0
- spriteforge/templates/phaser/vendor/phaser.min.js +1 -0
- spriteforge-0.1.1.dist-info/METADATA +364 -0
- spriteforge-0.1.1.dist-info/RECORD +218 -0
- spriteforge-0.1.1.dist-info/WHEEL +5 -0
- spriteforge-0.1.1.dist-info/entry_points.txt +2 -0
- spriteforge-0.1.1.dist-info/licenses/LICENSE +27 -0
- spriteforge-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/* SpriteForge flow and menus (L2.5): the screens around the game, drawn from data.
|
|
2
|
+
|
|
3
|
+
A build with menus loads menus.js (window.SF_MENUS: the Menu Designer's screens fitted by
|
|
4
|
+
code, the theme in the art's colours, the levels, controls and credits) and this file after
|
|
5
|
+
sf.js (and sflevel.js). The game itself is one scene that starts straight in play; the SDK
|
|
6
|
+
adds its own scene `sf-menu` on top and runs the flow:
|
|
7
|
+
|
|
8
|
+
title (Press Enter) -> main (Start, Continue, Level select, Settings, Controls, Credits)
|
|
9
|
+
play: Escape pauses (Resume, Restart level, Settings, Quit to title; game time stops)
|
|
10
|
+
a level finished -> results (Next level, Title); the last one -> victory (Credits, Title)
|
|
11
|
+
lose -> game over (Retry, Title)
|
|
12
|
+
|
|
13
|
+
Keys: arrows/WASD move the highlight, Enter/Space pick, Escape goes back (resumes on pause),
|
|
14
|
+
digits pick directly; the mouse works too. Events: title, menu_open {screen}, menu_select
|
|
15
|
+
{screen, item, action}, pause, resume, level_start {n} (single-scene games; sflevel.js emits
|
|
16
|
+
it for levels as data), level_complete {n}, level_failed {n}, game_complete.
|
|
17
|
+
sf.menu = {screen, title, options, actions, index} while a screen is open (QA tests read it).
|
|
18
|
+
sf.progress (localStorage, per game): levels unlocked, the best result per level, settings;
|
|
19
|
+
?reset=1 clears it. ?play=1, ?level=n and autoplay skip the title (bots and playtests);
|
|
20
|
+
in autoplay a screen that ends a level picks its first item by itself. */
|
|
21
|
+
(function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
const sf = window.sf;
|
|
24
|
+
const M = window.SF_MENUS || null;
|
|
25
|
+
const params = new URLSearchParams(location.search);
|
|
26
|
+
const flow = (sf.flow = {active: !!M, menus: M, state: 'boot', screen: null});
|
|
27
|
+
if (!M) return;
|
|
28
|
+
|
|
29
|
+
const ITEM = 8, HEAD = 12, BIG = 16, CHAR_EM = 0.62, PANEL_MAX = 260, PAD = 10, LINE = 12;
|
|
30
|
+
const DEPTH = 6000;
|
|
31
|
+
const theme = M.theme || {};
|
|
32
|
+
const col = h => parseInt(String(h || '#ffffff').slice(1), 16);
|
|
33
|
+
const levelGame = () => !!(sf.level && sf.level.data);
|
|
34
|
+
const levelCount = () => (M.levels || []).length || 1;
|
|
35
|
+
|
|
36
|
+
/* ---------- saved progress and settings ---------------------------------------------------- */
|
|
37
|
+
const KEY = 'sf-progress:' + (M.title || document.title || 'game');
|
|
38
|
+
const store = {
|
|
39
|
+
read() { try { return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {}; } catch (e) { return {}; } },
|
|
40
|
+
write(v) { try { window.localStorage.setItem(KEY, JSON.stringify(v)); } catch (e) { /* private mode */ } },
|
|
41
|
+
clear() { try { window.localStorage.removeItem(KEY); } catch (e) { /* private mode */ } },
|
|
42
|
+
};
|
|
43
|
+
if (params.has('reset')) store.clear();
|
|
44
|
+
const P = (sf.progress = Object.assign({unlocked: 1, best: {}, settings: {}, completed: false}, store.read()));
|
|
45
|
+
P.unlocked = Math.max(1, Math.min(levelCount(), P.unlocked | 0 || 1));
|
|
46
|
+
const save = () => store.write(P);
|
|
47
|
+
function applySettings() {
|
|
48
|
+
const d = (P.settings.difficulty in (M.difficulty || {})) ? P.settings.difficulty : 'normal';
|
|
49
|
+
sf.settings.difficulty = d;
|
|
50
|
+
sf.settings.multiplier = (M.difficulty || {})[d] || 1;
|
|
51
|
+
sf.settings.fewerFlashes = !!P.settings.fewerFlashes;
|
|
52
|
+
sf.settings.sound = P.settings.sound !== false; // sfsound.js plays nothing when off
|
|
53
|
+
if (sf.state) sf.state.difficulty = d;
|
|
54
|
+
}
|
|
55
|
+
applySettings();
|
|
56
|
+
|
|
57
|
+
/* ---------- the flow ---------------------------------------------------------------------- */
|
|
58
|
+
const skip = sf.flags.autoplay || params.has('play') || params.has('level');
|
|
59
|
+
let menu = null; // the sf-menu scene
|
|
60
|
+
let pending = null; // a screen waiting to open after the game's own end message
|
|
61
|
+
let shownAt = 0; // when the open screen opened (autoplay picks a little later)
|
|
62
|
+
flow.current = 1; // the level being played (1 for single-scene games)
|
|
63
|
+
|
|
64
|
+
flow.boot = function (boot, game, ready) {
|
|
65
|
+
flow.gameKey = game.sys.settings.key;
|
|
66
|
+
game.events.on('create', onGameCreate);
|
|
67
|
+
if (skip) {
|
|
68
|
+
flow.state = 'play';
|
|
69
|
+
game.events.once('create', ready);
|
|
70
|
+
boot.scene.launch('sf-menu');
|
|
71
|
+
boot.scene.start(flow.gameKey);
|
|
72
|
+
} else {
|
|
73
|
+
flow.state = 'title';
|
|
74
|
+
flow.onMenuReady = ready;
|
|
75
|
+
boot.scene.start('sf-menu');
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function onGameCreate() {
|
|
80
|
+
pending = null;
|
|
81
|
+
flow.state = 'play';
|
|
82
|
+
sf.paused = false;
|
|
83
|
+
if (flow.screen) close();
|
|
84
|
+
flow.current = levelGame() ? sf.level.n : 1;
|
|
85
|
+
if (!levelGame()) sf.event('level_start', {n: 1});
|
|
86
|
+
intro(flow.current);
|
|
87
|
+
}
|
|
88
|
+
// (sf.js emits restart whenever the game's scene starts again, except for the next level.)
|
|
89
|
+
|
|
90
|
+
// Events the game emits drive the screens after play: lose -> game over, win -> victory.
|
|
91
|
+
const event = sf.event;
|
|
92
|
+
sf.event = function (name, data) {
|
|
93
|
+
event(name, data);
|
|
94
|
+
if (flow.state !== 'play') return;
|
|
95
|
+
if (name === 'lose') {
|
|
96
|
+
event('level_failed', {n: flow.current});
|
|
97
|
+
if (M.screens.game_over) later('game_over');
|
|
98
|
+
} else if (name === 'win') {
|
|
99
|
+
if (!levelGame()) event('level_complete', {n: 1});
|
|
100
|
+
record(flow.current);
|
|
101
|
+
P.completed = true;
|
|
102
|
+
save();
|
|
103
|
+
event('game_complete');
|
|
104
|
+
later('victory');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function record(n) {
|
|
109
|
+
const st = sf.state || {}, best = P.best[n] || {};
|
|
110
|
+
const time = +st.time || 0, score = +st.score || 0;
|
|
111
|
+
P.best[n] = {time: best.time ? Math.min(best.time, time) : time, score: Math.max(best.score || 0, score)};
|
|
112
|
+
flow.last = {n, time, score, best: best.time ? best : null};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The sflevel engine finished level n (not the last): results, then the next level.
|
|
116
|
+
flow.levelDone = function (n) {
|
|
117
|
+
record(n);
|
|
118
|
+
P.unlocked = Math.max(P.unlocked, Math.min(levelCount(), n + 1));
|
|
119
|
+
save();
|
|
120
|
+
later(M.screens.results ? 'results' : null);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function later(screen) { // let the game's own message show first
|
|
124
|
+
flow.state = 'ending';
|
|
125
|
+
pending = {screen, at: 0.9};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function gameScene() { return sf.game.scene.getScene(flow.gameKey); }
|
|
129
|
+
const gameScenes = () => sf.game.scene.scenes.filter(sc => !String(sc.sys.settings.key).startsWith('sf-'));
|
|
130
|
+
|
|
131
|
+
let stopped = []; // the game's scenes a screen paused
|
|
132
|
+
function stopPlay(state) { // the game stands still under a screen
|
|
133
|
+
flow.state = state;
|
|
134
|
+
sf.paused = true;
|
|
135
|
+
for (const sc of gameScenes()) {
|
|
136
|
+
if (sc.sys.isActive()) { sc.sys.pause(); stopped.push(sc); }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
flow.pause = function () {
|
|
141
|
+
if (flow.state !== 'play' || (sf.menu && !sf.menu.screen)) return; // not over an sf.choose
|
|
142
|
+
stopPlay('paused');
|
|
143
|
+
sf.event('pause');
|
|
144
|
+
open('pause', true);
|
|
145
|
+
};
|
|
146
|
+
flow.resume = function () {
|
|
147
|
+
if (flow.state !== 'paused') return;
|
|
148
|
+
close();
|
|
149
|
+
flow.state = 'play';
|
|
150
|
+
sf.paused = false;
|
|
151
|
+
for (const sc of stopped) if (sc.sys.isPaused()) sc.sys.resume();
|
|
152
|
+
stopped = [];
|
|
153
|
+
sf.event('resume');
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Start level n (a new game, Continue, a level from Level select, the next level).
|
|
157
|
+
flow.play = function (n, advancing) {
|
|
158
|
+
close();
|
|
159
|
+
n = Math.max(1, Math.min(levelCount(), n || 1));
|
|
160
|
+
if (levelGame()) { sf.level.n = n; sf.level.first = n; sf.level.advancing = !!advancing; }
|
|
161
|
+
flow.current = n;
|
|
162
|
+
flow.state = 'play';
|
|
163
|
+
sf.paused = false;
|
|
164
|
+
stopped = [];
|
|
165
|
+
menu.scene.launch(flow.gameKey); // starts it, or starts it again when it runs
|
|
166
|
+
};
|
|
167
|
+
flow.newGame = n => flow.play(n || 1, false);
|
|
168
|
+
flow.restartLevel = () => flow.play(flow.current, false);
|
|
169
|
+
flow.toTitle = function () {
|
|
170
|
+
close();
|
|
171
|
+
for (const sc of gameScenes()) {
|
|
172
|
+
if (sc.sys.isActive() || sc.sys.isPaused()) menu.scene.stop(sc.sys.settings.key);
|
|
173
|
+
}
|
|
174
|
+
stopped = [];
|
|
175
|
+
flow.state = 'title';
|
|
176
|
+
sf.paused = false;
|
|
177
|
+
pending = null;
|
|
178
|
+
open('title', true);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/* ---------- screens ----------------------------------------------------------------------- */
|
|
182
|
+
const stack = [];
|
|
183
|
+
function open(id, reset) {
|
|
184
|
+
if (!M.screens[id]) return;
|
|
185
|
+
if (reset) stack.length = 0;
|
|
186
|
+
else if (flow.screen && flow.screen !== id) stack.push(flow.screen);
|
|
187
|
+
flow.screen = id;
|
|
188
|
+
flow.index = 0;
|
|
189
|
+
shownAt = 0;
|
|
190
|
+
if (id === 'title') sf.event('title');
|
|
191
|
+
sf.event('menu_open', {screen: id});
|
|
192
|
+
draw();
|
|
193
|
+
}
|
|
194
|
+
flow.open = id => open(id, false);
|
|
195
|
+
// Screenshots (G3): any screen as the player sees it; the ones over play stop the game first.
|
|
196
|
+
flow.preview = function (id) {
|
|
197
|
+
if (['pause', 'results', 'game_over', 'victory'].includes(id)) {
|
|
198
|
+
const st = sf.state || {};
|
|
199
|
+
if (!flow.last) flow.last = {n: flow.current, time: +st.time || 0, score: +st.score || 0, best: null};
|
|
200
|
+
if (flow.state === 'play' || flow.state === 'ending') stopPlay(id === 'pause' ? 'paused' : 'over');
|
|
201
|
+
pending = null;
|
|
202
|
+
}
|
|
203
|
+
open(id, true);
|
|
204
|
+
};
|
|
205
|
+
function close() {
|
|
206
|
+
flow.screen = null;
|
|
207
|
+
stack.length = 0;
|
|
208
|
+
if (sf.menu && sf.menu.screen) sf.menu = null;
|
|
209
|
+
if (menu) menu.clear();
|
|
210
|
+
}
|
|
211
|
+
function back() {
|
|
212
|
+
const id = flow.screen;
|
|
213
|
+
if (id === 'pause') return flow.resume();
|
|
214
|
+
if (['results', 'game_over', 'victory'].includes(id)) return flow.toTitle();
|
|
215
|
+
if (id === 'title') return;
|
|
216
|
+
const prev = stack.pop() || (flow.state === 'title' ? 'title' : null);
|
|
217
|
+
if (prev) { flow.screen = null; open(prev, false); return; }
|
|
218
|
+
close();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The items of the open screen: [{label, action, value, locked, level}].
|
|
222
|
+
function items(id) {
|
|
223
|
+
const out = (M.screens[id].items || []).map(i => Object.assign({}, i));
|
|
224
|
+
if (id === 'level_select') {
|
|
225
|
+
const lv = (M.levels || []).map(l => ({label: l.n + '. ' + l.name, action: 'level', level: l.n,
|
|
226
|
+
locked: l.n > P.unlocked})); // fmt: skip
|
|
227
|
+
out.unshift(...lv);
|
|
228
|
+
}
|
|
229
|
+
if (id === 'main' && P.unlocked <= 1) { // nothing to continue yet
|
|
230
|
+
out.forEach(i => { if (i.action === 'continue') i.locked = true; });
|
|
231
|
+
}
|
|
232
|
+
for (const i of out) {
|
|
233
|
+
if (i.action === 'difficulty') i.value = sf.settings.difficulty;
|
|
234
|
+
if (i.action === 'fewer_flashes') i.value = sf.settings.fewerFlashes ? 'on' : 'off';
|
|
235
|
+
if (i.action === 'sound') i.value = sf.settings.sound ? 'on' : 'off';
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function pick(index) {
|
|
241
|
+
const id = flow.screen;
|
|
242
|
+
if (!id) return;
|
|
243
|
+
const list = items(id), it = list[index];
|
|
244
|
+
if (!it || it.locked) return;
|
|
245
|
+
sf.event('menu_select', {screen: id, item: it.label, action: it.action});
|
|
246
|
+
const act = it.action;
|
|
247
|
+
if (act === 'main') return open('main', true);
|
|
248
|
+
if (act === 'start') return flow.newGame(1);
|
|
249
|
+
if (act === 'continue') return flow.newGame(P.unlocked);
|
|
250
|
+
if (act === 'level') return flow.newGame(it.level);
|
|
251
|
+
if (['level_select', 'settings', 'controls', 'credits'].includes(act)) return open(act, false);
|
|
252
|
+
if (act === 'resume') return flow.resume();
|
|
253
|
+
if (act === 'restart' || act === 'retry') return flow.restartLevel();
|
|
254
|
+
if (act === 'next_level') return flow.play(flow.current + 1, true);
|
|
255
|
+
if (act === 'title') return flow.toTitle();
|
|
256
|
+
if (act === 'back') return back();
|
|
257
|
+
if (act === 'difficulty') {
|
|
258
|
+
const names = Object.keys(M.difficulty || {normal: 1});
|
|
259
|
+
P.settings.difficulty = names[(names.indexOf(sf.settings.difficulty) + 1) % names.length];
|
|
260
|
+
}
|
|
261
|
+
if (act === 'fewer_flashes') P.settings.fewerFlashes = !sf.settings.fewerFlashes;
|
|
262
|
+
if (act === 'sound') P.settings.sound = !sf.settings.sound;
|
|
263
|
+
applySettings();
|
|
264
|
+
save();
|
|
265
|
+
flow.index = index;
|
|
266
|
+
draw();
|
|
267
|
+
}
|
|
268
|
+
flow.pick = pick;
|
|
269
|
+
// The sound on or off (the settings item, or M anywhere: sfsound.js), saved with the settings.
|
|
270
|
+
flow.setSound = function (on) {
|
|
271
|
+
P.settings.sound = !!on;
|
|
272
|
+
applySettings();
|
|
273
|
+
save();
|
|
274
|
+
if (flow.screen) draw();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
function onKey(e) {
|
|
278
|
+
const k = e.key;
|
|
279
|
+
if (!flow.screen) {
|
|
280
|
+
if (k === 'Escape') flow.pause();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const n = items(flow.screen).length;
|
|
284
|
+
if (!n) return;
|
|
285
|
+
const digit = Number(k);
|
|
286
|
+
if (digit >= 1 && digit <= Math.min(9, n)) return pick(digit - 1);
|
|
287
|
+
if (k === 'ArrowUp' || k === 'w' || k === 'W') flow.index = (flow.index + n - 1) % n;
|
|
288
|
+
else if (k === 'ArrowDown' || k === 's' || k === 'S') flow.index = (flow.index + 1) % n;
|
|
289
|
+
else if (k === 'Enter' || k === ' ') return pick(flow.index);
|
|
290
|
+
else if (k === 'Escape') return back();
|
|
291
|
+
else return;
|
|
292
|
+
if (sf.sound && sf.sound.moment) sf.sound.moment('menu_move');
|
|
293
|
+
draw();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* ---------- drawing ----------------------------------------------------------------------- */
|
|
297
|
+
const fit = (text, px, room) => {
|
|
298
|
+
const max = Math.max(1, Math.floor(room / (px * CHAR_EM)));
|
|
299
|
+
text = String(text || '');
|
|
300
|
+
return text.length <= max ? text : text.slice(0, Math.max(1, max - 1)) + '~';
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
function draw() {
|
|
304
|
+
if (!menu || !flow.screen) return;
|
|
305
|
+
const scene = menu, id = flow.screen, def = M.screens[id];
|
|
306
|
+
scene.clear();
|
|
307
|
+
const [w, h] = sf.screen(), s = sf.assets.scale || 1;
|
|
308
|
+
const add = o => { scene.parts.push(o); return o.setScrollFactor ? o.setScrollFactor(0) : o; };
|
|
309
|
+
const text = (x, y, str, px, color, originX) => add(sf.text(scene, x, y, str, {
|
|
310
|
+
size: px, color: color, originX: originX == null ? 0.5 : originX, depth: DEPTH + 2, sdk: true})); // fmt: skip
|
|
311
|
+
const behindTitle = flow.state === 'title';
|
|
312
|
+
if (behindTitle) {
|
|
313
|
+
if (M.art && M.art.title && sf.has(M.art.title)) {
|
|
314
|
+
const img = add(scene.add.image(w / 2, h / 2, M.art.title).setDepth(DEPTH));
|
|
315
|
+
img.setScale(Math.max(w / img.width, h / img.height));
|
|
316
|
+
} else {
|
|
317
|
+
add(scene.add.rectangle(0, 0, w, h, col(theme.panel)).setOrigin(0).setDepth(DEPTH));
|
|
318
|
+
add(scene.add.rectangle(0, h * 0.62, w, h * 0.38, col(theme.border), 0.18).setOrigin(0).setDepth(DEPTH));
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
add(scene.add.rectangle(0, 0, w, h, 0x000000, 0.55).setOrigin(0).setDepth(DEPTH));
|
|
322
|
+
}
|
|
323
|
+
const list = items(id);
|
|
324
|
+
const panelW = Math.min(w - 32 * s, PANEL_MAX * s), room = panelW - 2 * PAD * s;
|
|
325
|
+
let top = null;
|
|
326
|
+
const lines = [];
|
|
327
|
+
if (def.title) lines.push({str: def.title, px: HEAD, color: theme.highlight});
|
|
328
|
+
// (The controls screen lists the controls itself: a subtitle repeating them is left out.)
|
|
329
|
+
if (def.subtitle && id !== 'controls') lines.push({str: def.subtitle, px: ITEM, color: theme.text});
|
|
330
|
+
if (id === 'results' && flow.last) {
|
|
331
|
+
lines.push({str: 'Level ' + flow.last.n + ': ' + ((M.levels || [])[flow.last.n - 1] || {}).name, px: ITEM, color: theme.text});
|
|
332
|
+
lines.push({str: 'Time ' + flow.last.time.toFixed(1) + ' s' + (flow.last.score ? ' Score ' + flow.last.score : ''), px: ITEM, color: theme.text});
|
|
333
|
+
if (flow.last.best) lines.push({str: 'Best ' + flow.last.best.time.toFixed(1) + ' s', px: ITEM, color: theme.dim});
|
|
334
|
+
}
|
|
335
|
+
if (id === 'controls') (M.controls || []).forEach(c => lines.push({str: c, px: ITEM, color: theme.text}));
|
|
336
|
+
if (id === 'credits') (M.credits || []).forEach(c => lines.push({str: c, px: ITEM, color: theme.text}));
|
|
337
|
+
const headH = lines.reduce((a, l) => a + (l.px + 4) * s, 0);
|
|
338
|
+
const panelH = headH + (list.length * LINE) * s + 2 * PAD * s;
|
|
339
|
+
if (id === 'title' || id === 'main') {
|
|
340
|
+
// The panel sits at the bottom; the game's title, its tagline and the emblem share what is
|
|
341
|
+
// left above it: the emblem shrinks (or goes) first, then the tagline (live 2026-11-02: a
|
|
342
|
+
// six-item main menu under a full-size emblem ran off the screen).
|
|
343
|
+
top = h - panelH - 4 * s;
|
|
344
|
+
const room = top - 2 * s;
|
|
345
|
+
const sub = !!M.subtitle && room >= (BIG + 4 + LINE + 4) * s;
|
|
346
|
+
const words = (BIG + 4) * s + (sub ? LINE * s : 0);
|
|
347
|
+
let y = Math.max(2 * s, Math.min(h * 0.12, room - words));
|
|
348
|
+
if (M.art && M.art.emblem && sf.has(M.art.emblem)) {
|
|
349
|
+
const space = Math.min(h * 0.3, room - words - y - 4 * s);
|
|
350
|
+
if (space >= 12 * s) {
|
|
351
|
+
const em = add(scene.add.image(w / 2, y, M.art.emblem).setOrigin(0.5, 0).setDepth(DEPTH + 1));
|
|
352
|
+
em.setScale(Math.min(1, space / em.height));
|
|
353
|
+
y += em.displayHeight + 4 * s;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
text(w / 2, y, fit(M.title, BIG * s, w - 24 * s), BIG, theme.highlight);
|
|
357
|
+
y += (BIG + 4) * s;
|
|
358
|
+
if (sub) { text(w / 2, y, fit(M.subtitle, ITEM * s, w - 24 * s), ITEM, theme.text); y += LINE * s; }
|
|
359
|
+
top = Math.max(top, y + 2 * s);
|
|
360
|
+
}
|
|
361
|
+
if (top == null) top = Math.max(4 * s, (h - panelH) / 2);
|
|
362
|
+
const px = w / 2 - panelW / 2;
|
|
363
|
+
const panel = add(scene.add.rectangle(px, top, panelW, panelH, col(theme.panel), 0.94).setOrigin(0).setDepth(DEPTH + 1));
|
|
364
|
+
if (theme.style !== 'solid') panel.setStrokeStyle(s, col(theme.border));
|
|
365
|
+
if (theme.style === 'double') {
|
|
366
|
+
add(scene.add.rectangle(px + 2 * s, top + 2 * s, panelW - 4 * s, panelH - 4 * s).setOrigin(0)
|
|
367
|
+
.setStrokeStyle(s, col(theme.border)).setDepth(DEPTH + 1));
|
|
368
|
+
}
|
|
369
|
+
let y = top + PAD * s;
|
|
370
|
+
for (const l of lines) { text(w / 2, y, fit(l.str, l.px * s, room), l.px, l.color); y += (l.px + 4) * s; }
|
|
371
|
+
const labels = [];
|
|
372
|
+
list.forEach((it, i) => {
|
|
373
|
+
const sel = i === flow.index;
|
|
374
|
+
const label = it.label + (it.value != null ? ': ' + it.value : '');
|
|
375
|
+
const t = text(px + PAD * s, y + i * LINE * s, (sel ? '> ' : ' ') + fit(label, ITEM * s, room - 2 * ITEM * s * CHAR_EM),
|
|
376
|
+
ITEM, it.locked ? theme.dim : sel ? theme.highlight : theme.text, 0); // fmt: skip
|
|
377
|
+
if (!sf.flags.headless) t.setInteractive().on('pointerdown', () => { flow.index = i; pick(i); });
|
|
378
|
+
labels.push(it.label);
|
|
379
|
+
});
|
|
380
|
+
flow.panel = {x: px, y: top, w: panelW, h: panelH};
|
|
381
|
+
sf.menu = {screen: id, title: def.title || M.title, options: labels, index: flow.index,
|
|
382
|
+
actions: list.map(it => it.action)}; // fmt: skip
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The level's intro: its number, name and goal over the first seconds of play.
|
|
386
|
+
function intro(n) {
|
|
387
|
+
if (!menu) return;
|
|
388
|
+
const lv = (M.levels || [])[n - 1];
|
|
389
|
+
if (!lv) return;
|
|
390
|
+
const [w] = sf.screen(), s = sf.assets.scale || 1;
|
|
391
|
+
menu.clearIntro();
|
|
392
|
+
const a = sf.text(menu, w / 2, 26 * s, 'Level ' + n + ': ' + lv.name, {size: HEAD, color: theme.highlight, originX: 0.5, depth: DEPTH - 1, sdk: true});
|
|
393
|
+
const b = sf.text(menu, w / 2, 42 * s, fit(lv.goal, ITEM * s, w - 24 * s), {size: ITEM, color: theme.text, originX: 0.5, depth: DEPTH - 1, sdk: true});
|
|
394
|
+
menu.intro = {parts: [a, b], until: sf.clock.now + 1.8};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
class MenuScene extends Phaser.Scene {
|
|
398
|
+
constructor() { super('sf-menu'); }
|
|
399
|
+
create() {
|
|
400
|
+
menu = this;
|
|
401
|
+
this.parts = [];
|
|
402
|
+
this.intro = null;
|
|
403
|
+
this.scene.bringToTop();
|
|
404
|
+
if (this.input.keyboard) this.input.keyboard.on('keydown', onKey);
|
|
405
|
+
if (flow.state === 'title') {
|
|
406
|
+
open('title', true);
|
|
407
|
+
if (flow.onMenuReady) { const r = flow.onMenuReady; flow.onMenuReady = null; r(); }
|
|
408
|
+
} else if (flow.state === 'play') intro(flow.current);
|
|
409
|
+
}
|
|
410
|
+
clear() { for (const p of this.parts) p.destroy(); this.parts = []; }
|
|
411
|
+
clearIntro() { if (this.intro) this.intro.parts.forEach(p => p.destroy()); this.intro = null; }
|
|
412
|
+
update(time, delta) {
|
|
413
|
+
const dt = delta / 1000;
|
|
414
|
+
if (this.intro && (sf.clock.now >= this.intro.until || flow.state !== 'play')) this.clearIntro();
|
|
415
|
+
if (pending) {
|
|
416
|
+
pending.at -= dt;
|
|
417
|
+
if (pending.at <= 0) {
|
|
418
|
+
const p = pending;
|
|
419
|
+
pending = null;
|
|
420
|
+
if (p.screen) { stopPlay('over'); open(p.screen, true); }
|
|
421
|
+
else if (flow.state === 'ending') flow.play(flow.current + 1, true); // no results screen
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// A bot cannot read menus: a screen that ends a level picks its first item by itself.
|
|
425
|
+
if (sf.flags.autoplay && flow.screen && ['results', 'game_over', 'victory'].includes(flow.screen)) {
|
|
426
|
+
shownAt += dt;
|
|
427
|
+
if (shownAt > 1.5) { shownAt = -1e9; pick(0); }
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The SDK's scene goes last, so it draws over the game; sf.state shows the difficulty the
|
|
433
|
+
// player picked (QA tests read it).
|
|
434
|
+
const start = sf.start;
|
|
435
|
+
sf.start = function (opts) {
|
|
436
|
+
opts = Object.assign({}, opts || {});
|
|
437
|
+
opts.scenes = [...(opts.scenes || []), MenuScene];
|
|
438
|
+
const out = start(opts);
|
|
439
|
+
if (sf.game) sf.game.events.on('poststep', () => { if (sf.state) sf.state.difficulty = sf.settings.difficulty; });
|
|
440
|
+
return out;
|
|
441
|
+
};
|
|
442
|
+
})();
|