synthos 0.10.1 → 0.11.1
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.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +285 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1388 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1888 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +283 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1414 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -1,1308 +1,1281 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en"><head>
|
|
2
|
-
<meta charset="UTF-8">
|
|
3
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4
|
-
<title>SynthOS</title>
|
|
5
|
-
<script id="theme-info" src="/api/theme-info.js" data-locked="true"></script>
|
|
6
|
-
<link id="theme-css" rel="stylesheet" href="/api/theme.css" data-locked="true">
|
|
7
|
-
<style>
|
|
8
|
-
#game-wrapper{width:100%;height:100%;display:flex;align-items:flex-start;justify-content:center;background:#000;overflow:hidden}
|
|
9
|
-
#game-container{position:relative;border:1px solid rgba(0,255,255,.25);box-shadow:0 0 24px rgba(0,255,255,.1),inset 0 0 24px rgba(0,255,255,.05);overflow:hidden;flex-shrink:0}
|
|
10
|
-
#gameCanvas{display:block;width:100%;height:100%}
|
|
11
|
-
.game-ui{position:absolute;top:20px;left:20px;right:20px;display:flex;justify-content:space-between;pointer-events:none;z-index:10}
|
|
12
|
-
.lives-display,.score-display{font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px;color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px #0ff;letter-spacing:2px}
|
|
13
|
-
.mute-btn{pointer-events:auto;background:none;border:1px solid rgba(0,255,255,.4);border-radius:4px;color:#0ff;cursor:pointer;padding:2px 6px;font-size:16px;line-height:1;opacity:.7;transition:opacity .2s}.mute-btn:hover{opacity:1}
|
|
14
|
-
.player-indicator{font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px;text-shadow:0 0 10px currentColor,0 0 20px currentColor;letter-spacing:2px;margin-right:8px}
|
|
15
|
-
.game-over-screen,.start-screen{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;z-index:20}
|
|
16
|
-
.game-over-screen h1,.start-screen h1{font-family:Orbitron,'Segoe UI',sans-serif;font-size:48px;line-height:1.2;color:#f0f;text-shadow:0 0 20px #f0f,0 0 40px #f0f,0 0 60px #f0f;margin-bottom:20px;letter-spacing:5px}
|
|
17
|
-
.game-over-screen p,.start-screen p{font-size:18px;color:#0ff;text-shadow:0 0 10px #0ff;margin-bottom:30px}
|
|
18
|
-
.restart-btn,.start-btn,.mode-btn{padding:15px 40px;font-size:18px;font-family:Orbitron,'Segoe UI',sans-serif;background:0 0;border:2px solid #0ff;color:#0ff;cursor:pointer;text-transform:uppercase;letter-spacing:3px;transition:.3s;box-shadow:0 0 20px rgba(0,255,255,.3),inset 0 0 20px rgba(0,255,255,.1)}
|
|
19
|
-
.restart-btn:hover,.start-btn:hover,.mode-btn:hover{background:rgba(0,255,255,.2);box-shadow:0 0 30px rgba(0,255,255,.5),inset 0 0 30px rgba(0,255,255,.2);transform:scale(1.05)}
|
|
20
|
-
.controls-info{position:absolute;bottom:16px;right:20px;font-size:13px;color:rgba(0,255,255,.5);text-align:right;letter-spacing:1px;display:flex;align-items:center;gap:16px;font-family:'Segoe UI',sans-serif}
|
|
21
|
-
.controls-info .action-item{display:inline-flex;align-items:center;gap:4px}
|
|
22
|
-
|
|
23
|
-
/* Mode selection */
|
|
24
|
-
.mode-select{display:flex;flex-direction:column;gap:12px;align-items:center;margin-bottom:20px}
|
|
25
|
-
.mode-btn{padding:12px 36px;font-size:16px;min-width:240px}
|
|
26
|
-
.mode-btn.selected{background:rgba(0,255,255,.25);border-color:#fff;color:#fff;box-shadow:0 0 30px rgba(0,255,255,.6),inset 0 0 20px rgba(0,255,255,.3)}
|
|
27
|
-
|
|
28
|
-
/* Player count selector */
|
|
29
|
-
.player-count-select{display:none;flex-direction:column;align-items:center;gap:12px;margin-top:16px}
|
|
30
|
-
.player-count-select.visible{display:flex}
|
|
31
|
-
.player-count-row{display:flex;align-items:center;gap:16px}
|
|
32
|
-
.player-count-arrow{font-size:28px;color:#0ff;cursor:pointer;user-select:none;text-shadow:0 0 10px #0ff;padding:4px 12px;transition:.2s}
|
|
33
|
-
.player-count-arrow:hover{color:#fff;text-shadow:0 0 15px #fff}
|
|
34
|
-
.player-count-num{font-family:Orbitron,'Segoe UI',sans-serif;font-size:36px;color:#f0f;text-shadow:0 0 15px #f0f,0 0 30px #f0f;min-width:50px;text-align:center}
|
|
35
|
-
.player-count-label{font-size:14px;color:rgba(0,255,255,.6);letter-spacing:2px}
|
|
36
|
-
.player-count-confirm{margin-top:8px}
|
|
37
|
-
|
|
38
|
-
/* Side panels */
|
|
39
|
-
.side-panel{width:200px;flex-shrink:0;padding:20px 16px;display:flex;flex-direction:column;font-family:Orbitron,'Segoe UI',sans-serif;box-sizing:border-box;overflow:hidden}
|
|
40
|
-
.side-panel.hidden{display:none}
|
|
41
|
-
.side-panel h2{font-size:16px;letter-spacing:3px;margin:0 0 16px 0;text-align:center;font-weight:700}
|
|
42
|
-
#side-leaderboard{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px rgba(255,0,255,.4)}
|
|
43
|
-
#side-leaderboard h2{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px #f0f}
|
|
44
|
-
#side-controls{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px rgba(0,255,255,.4)}
|
|
45
|
-
#side-controls h2{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px #0ff}
|
|
46
|
-
.leaderboard-table{width:100%;border-collapse:collapse;font-family:'Courier New',monospace;font-size:13px}
|
|
47
|
-
.leaderboard-table td{padding:4px 0;white-space:nowrap}
|
|
48
|
-
.leaderboard-table .rank{width:24px;color:rgba(255,0,255,.6);text-align:right;padding-right:6px}
|
|
49
|
-
.leaderboard-table .initials{color:#f0f;letter-spacing:2px}
|
|
50
|
-
.leaderboard-table .hs-score{text-align:right;color:rgba(255,0,255,.7)}
|
|
51
|
-
.controls-list{list-style:none;padding:0;margin:0;font-size:12px}
|
|
52
|
-
.controls-list li{padding:6px 0;display:flex;gap:8px;align-items:center;color:#0ff}
|
|
53
|
-
.controls-list .key-icon{display:inline-block;min-width:50px;padding:2px 6px;border:1px solid rgba(0,255,255,.4);border-radius:3px;text-align:center;font-size:11px;color:#0ff;background:rgba(0,255,255,.05);flex-shrink:0}
|
|
54
|
-
.controls-list .key-desc{color:rgba(0,255,255,.7);font-family:'Segoe UI',sans-serif}
|
|
55
|
-
.bt-section{margin-top:auto;padding-top:16px;border-top:1px solid rgba(0,255,255,.15)}
|
|
56
|
-
.bt-section h3{font-size:11px;letter-spacing:2px;color:rgba(0,255,255,.5);margin:0 0 8px 0;text-align:center;font-weight:400}
|
|
57
|
-
.bt-section p{font-size:11px;color:rgba(0,255,255,.4);font-family:'Segoe UI',sans-serif;margin:0 0 4px 0;line-height:1.4}
|
|
58
|
-
.side-panel{overflow-y:auto}
|
|
59
|
-
#side-leaderboard{padding-top:20px}
|
|
60
|
-
#side-controls{padding-top:20px}
|
|
61
|
-
|
|
62
|
-
/* Gamepad status indicator */
|
|
63
|
-
#gamepadStatus{text-align:center;padding:8px 0;margin-bottom:12px;font-size:11px;letter-spacing:2px;border:1px solid rgba(0,255,255,.15);border-radius:4px;transition:all .3s;color:rgba(0,255,255,.3);text-shadow:none}
|
|
64
|
-
#gamepadStatus.connected{color:#0f0;text-shadow:0 0 8px #0f0,0 0 16px rgba(0,255,0,.4);border-color:rgba(0,255,0,.3);background:rgba(0,255,0,.05);animation:gpPulse 2s ease-in-out infinite}
|
|
65
|
-
@keyframes gpPulse{0%,100%{box-shadow:0 0 4px rgba(0,255,0,.1)}50%{box-shadow:0 0 12px rgba(0,255,0,.25)}}
|
|
66
|
-
|
|
67
|
-
/* Pause overlay */
|
|
68
|
-
#pauseOverlay{position:absolute;top:0;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;z-index:25;background:rgba(0,0,0,.5);flex-direction:column;gap:8px}
|
|
69
|
-
#pauseOverlay.visible{display:flex}
|
|
70
|
-
#pauseOverlay .pause-title{font-family:Orbitron,'Segoe UI',sans-serif;font-size:48px;color:#0ff;text-shadow:0 0 20px #0ff,0 0 40px #0ff,0 0 60px #0ff;letter-spacing:8px}
|
|
71
|
-
#pauseOverlay .pause-hint{font-family:'Segoe UI',sans-serif;font-size:14px;color:rgba(0,255,255,.5);letter-spacing:1px}
|
|
72
|
-
|
|
73
|
-
/* High score entry */
|
|
74
|
-
#highScoreEntry{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;z-index:30;display:none}
|
|
75
|
-
#highScoreEntry h2{font-family:Orbitron,'Segoe UI',sans-serif;font-size:24px;color:#f0f;text-shadow:0 0 15px #f0f,0 0 30px #f0f;margin-bottom:10px;letter-spacing:3px}
|
|
76
|
-
#highScoreEntry .hs-final-score{font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px;color:#0ff;text-shadow:0 0 10px #0ff;margin-bottom:20px}
|
|
77
|
-
.initial-slots{display:flex;justify-content:center;gap:12px;margin-bottom:20px}
|
|
78
|
-
.initial-slot{width:40px;height:50px;border:2px solid #f0f;display:flex;align-items:center;justify-content:center;font-family:Orbitron,'Segoe UI',sans-serif;font-size:28px;color:#f0f;text-shadow:0 0 10px #f0f;box-shadow:0 0 10px rgba(255,0,255,.3)}
|
|
79
|
-
.initial-slot.active{border-color:#0ff;color:#0ff;text-shadow:0 0 10px #0ff;box-shadow:0 0 15px rgba(0,255,255,.5)}
|
|
80
|
-
.hs-instructions{font-size:12px;color:rgba(0,255,255,.6);font-family:'Segoe UI',sans-serif;margin-bottom:16px}
|
|
81
|
-
|
|
82
|
-
/* Keyboard key hint (for bottom bar) */
|
|
83
|
-
.kb-key{display:inline-block;padding:1px 6px;border:1px solid rgba(0,255,255,.35);border-radius:3px;font-size:11px;color:rgba(0,255,255,.7);background:rgba(0,255,255,.05);vertical-align:middle;margin:0 2px;font-family:'Segoe UI',sans-serif}
|
|
84
|
-
/* Focused menu item */
|
|
85
|
-
.mode-btn.focused{background:rgba(0,255,255,.2);border-color:#fff;color:#fff;box-shadow:0 0 25px rgba(0,255,255,.5),inset 0 0 20px rgba(0,255,255,.2)}
|
|
86
|
-
|
|
87
|
-
/* Turn ready overlay */
|
|
88
|
-
#turnReadyOverlay{position:absolute;top:0;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px;z-index:26;background:rgba(0,0,0,.7)}
|
|
89
|
-
#turnReadyOverlay.visible{display:flex}
|
|
90
|
-
#turnReadyOverlay .turn-player{font-family:Orbitron,'Segoe UI',sans-serif;font-size:42px;text-shadow:0 0 20px currentColor,0 0 40px currentColor;letter-spacing:6px}
|
|
91
|
-
#turnReadyOverlay .turn-subtitle{font-family:Orbitron,'Segoe UI',sans-serif;font-size:22px;color:#fff;text-shadow:0 0 12px rgba(255,255,255,.6);letter-spacing:4px;margin-bottom:8px}
|
|
92
|
-
#turnReadyOverlay .turn-prompt{font-family:'Segoe UI',sans-serif;font-size:16px;color:rgba(0,255,255,.6);letter-spacing:1px}
|
|
93
|
-
|
|
94
|
-
/* Final scoreboard overlay */
|
|
95
|
-
#finalScoreboard{position:absolute;top:0;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;z-index:27;background:rgba(0,0,0,.8)}
|
|
96
|
-
#finalScoreboard.visible{display:flex}
|
|
97
|
-
#finalScoreboard .sb-title{font-family:Orbitron,'Segoe UI',sans-serif;font-size:32px;color:#f0f;text-shadow:0 0 20px #f0f,0 0 40px #f0f;letter-spacing:5px;margin-bottom:8px}
|
|
98
|
-
#finalScoreboard .sb-subtitle{font-family:Orbitron,'Segoe UI',sans-serif;font-size:14px;color:rgba(255,255,255,.5);letter-spacing:3px;margin-bottom:12px}
|
|
99
|
-
.sb-table{border-collapse:collapse;font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px}
|
|
100
|
-
.sb-table td{padding:8px 16px}
|
|
101
|
-
.sb-table .sb-rank{color:rgba(255,255,255,.5);text-align:right;font-size:14px}
|
|
102
|
-
.sb-table .sb-player{letter-spacing:3px}
|
|
103
|
-
.sb-table .sb-initials{font-size:14px;color:rgba(255,255,255,.5);letter-spacing:2px}
|
|
104
|
-
.sb-table .sb-score{text-align:right;color:#fff;letter-spacing:2px}
|
|
105
|
-
.sb-play-again{margin-top:16px}
|
|
106
|
-
</style>
|
|
107
|
-
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
|
|
108
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
109
|
-
</head>
|
|
110
|
-
|
|
111
|
-
<body>
|
|
112
|
-
<div class="
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<div class="
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
<button class="restart-btn
|
|
160
|
-
</div>
|
|
161
|
-
<div
|
|
162
|
-
<
|
|
163
|
-
<
|
|
164
|
-
<div class="
|
|
165
|
-
<
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
function
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
(
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
'<
|
|
563
|
-
'<
|
|
564
|
-
'<
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
let
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
ball.x
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
ball.
|
|
938
|
-
ball.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
// CUSTOMIZE:
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// ---
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
ctx.
|
|
1015
|
-
|
|
1016
|
-
ctx.fillStyle =
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
//
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
ctx.
|
|
1039
|
-
ctx.
|
|
1040
|
-
ctx.
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1043
|
-
ctx.
|
|
1044
|
-
ctx.
|
|
1045
|
-
ctx.
|
|
1046
|
-
ctx.
|
|
1047
|
-
ctx.
|
|
1048
|
-
ctx.
|
|
1049
|
-
|
|
1050
|
-
ctx.
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
// === CLICK HANDLERS & INIT ===================================================
|
|
1286
|
-
document.getElementById("btn1P").addEventListener("click", startSinglePlayer);
|
|
1287
|
-
document.getElementById("btnPassPlay").addEventListener("click", function() { showStartPhase("count"); });
|
|
1288
|
-
document.getElementById("btn1P").addEventListener("mouseenter", function() { menuCursor = 0; updateMenuFocus(); });
|
|
1289
|
-
document.getElementById("btnPassPlay").addEventListener("mouseenter", function() { menuCursor = 1; updateMenuFocus(); });
|
|
1290
|
-
document.getElementById("pcLeft").addEventListener("click", function() { adjustPlayerCount(-1); });
|
|
1291
|
-
document.getElementById("pcRight").addEventListener("click", function() { adjustPlayerCount(1); });
|
|
1292
|
-
document.getElementById("pcConfirm").addEventListener("click", confirmPlayerCount);
|
|
1293
|
-
document.getElementById("sbPlayAgain").addEventListener("click", function() { resetStartScreen(); });
|
|
1294
|
-
document.getElementById("restartBtn").addEventListener("click", function() {
|
|
1295
|
-
document.getElementById("gameOverScreen").style.display = "none";
|
|
1296
|
-
resetStartScreen();
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
// Initialize
|
|
1300
|
-
updateAllPrompts();
|
|
1301
|
-
initStars();
|
|
1302
|
-
gameLoop();
|
|
1303
|
-
</script>
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
<script id="page-helpers" src="/api/page-helpers.js?v=3" data-locked="true"></script>
|
|
1307
|
-
<script id="page-script" src="/api/page-script.js?v=3" data-locked="true"></script>
|
|
1308
|
-
</body></html>
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head>
|
|
2
|
+
<meta charset="UTF-8">
|
|
3
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4
|
+
<title>SynthOS</title>
|
|
5
|
+
<script id="theme-info" src="/api/theme-info.js" data-locked="true"></script>
|
|
6
|
+
<link id="theme-css" rel="stylesheet" href="/api/theme.css" data-locked="true">
|
|
7
|
+
<style>
|
|
8
|
+
#game-wrapper{width:100%;height:100%;display:flex;align-items:flex-start;justify-content:center;background:#000;overflow:hidden}
|
|
9
|
+
#game-container{position:relative;border:1px solid rgba(0,255,255,.25);box-shadow:0 0 24px rgba(0,255,255,.1),inset 0 0 24px rgba(0,255,255,.05);overflow:hidden;flex-shrink:0}
|
|
10
|
+
#gameCanvas{display:block;width:100%;height:100%}
|
|
11
|
+
.game-ui{position:absolute;top:20px;left:20px;right:20px;display:flex;justify-content:space-between;pointer-events:none;z-index:10}
|
|
12
|
+
.lives-display,.score-display{font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px;color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px #0ff;letter-spacing:2px}
|
|
13
|
+
.mute-btn{pointer-events:auto;background:none;border:1px solid rgba(0,255,255,.4);border-radius:4px;color:#0ff;cursor:pointer;padding:2px 6px;font-size:16px;line-height:1;opacity:.7;transition:opacity .2s}.mute-btn:hover{opacity:1}
|
|
14
|
+
.player-indicator{font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px;text-shadow:0 0 10px currentColor,0 0 20px currentColor;letter-spacing:2px;margin-right:8px}
|
|
15
|
+
.game-over-screen,.start-screen{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;z-index:20}
|
|
16
|
+
.game-over-screen h1,.start-screen h1{font-family:Orbitron,'Segoe UI',sans-serif;font-size:48px;line-height:1.2;color:#f0f;text-shadow:0 0 20px #f0f,0 0 40px #f0f,0 0 60px #f0f;margin-bottom:20px;letter-spacing:5px}
|
|
17
|
+
.game-over-screen p,.start-screen p{font-size:18px;color:#0ff;text-shadow:0 0 10px #0ff;margin-bottom:30px}
|
|
18
|
+
.restart-btn,.start-btn,.mode-btn{padding:15px 40px;font-size:18px;font-family:Orbitron,'Segoe UI',sans-serif;background:0 0;border:2px solid #0ff;color:#0ff;cursor:pointer;text-transform:uppercase;letter-spacing:3px;transition:.3s;box-shadow:0 0 20px rgba(0,255,255,.3),inset 0 0 20px rgba(0,255,255,.1)}
|
|
19
|
+
.restart-btn:hover,.start-btn:hover,.mode-btn:hover{background:rgba(0,255,255,.2);box-shadow:0 0 30px rgba(0,255,255,.5),inset 0 0 30px rgba(0,255,255,.2);transform:scale(1.05)}
|
|
20
|
+
.controls-info{position:absolute;bottom:16px;right:20px;font-size:13px;color:rgba(0,255,255,.5);text-align:right;letter-spacing:1px;display:flex;align-items:center;gap:16px;font-family:'Segoe UI',sans-serif}
|
|
21
|
+
.controls-info .action-item{display:inline-flex;align-items:center;gap:4px}
|
|
22
|
+
|
|
23
|
+
/* Mode selection */
|
|
24
|
+
.mode-select{display:flex;flex-direction:column;gap:12px;align-items:center;margin-bottom:20px}
|
|
25
|
+
.mode-btn{padding:12px 36px;font-size:16px;min-width:240px}
|
|
26
|
+
.mode-btn.selected{background:rgba(0,255,255,.25);border-color:#fff;color:#fff;box-shadow:0 0 30px rgba(0,255,255,.6),inset 0 0 20px rgba(0,255,255,.3)}
|
|
27
|
+
|
|
28
|
+
/* Player count selector */
|
|
29
|
+
.player-count-select{display:none;flex-direction:column;align-items:center;gap:12px;margin-top:16px}
|
|
30
|
+
.player-count-select.visible{display:flex}
|
|
31
|
+
.player-count-row{display:flex;align-items:center;gap:16px}
|
|
32
|
+
.player-count-arrow{font-size:28px;color:#0ff;cursor:pointer;user-select:none;text-shadow:0 0 10px #0ff;padding:4px 12px;transition:.2s}
|
|
33
|
+
.player-count-arrow:hover{color:#fff;text-shadow:0 0 15px #fff}
|
|
34
|
+
.player-count-num{font-family:Orbitron,'Segoe UI',sans-serif;font-size:36px;color:#f0f;text-shadow:0 0 15px #f0f,0 0 30px #f0f;min-width:50px;text-align:center}
|
|
35
|
+
.player-count-label{font-size:14px;color:rgba(0,255,255,.6);letter-spacing:2px}
|
|
36
|
+
.player-count-confirm{margin-top:8px}
|
|
37
|
+
|
|
38
|
+
/* Side panels */
|
|
39
|
+
.side-panel{width:200px;flex-shrink:0;padding:20px 16px;display:flex;flex-direction:column;font-family:Orbitron,'Segoe UI',sans-serif;box-sizing:border-box;overflow:hidden}
|
|
40
|
+
.side-panel.hidden{display:none}
|
|
41
|
+
.side-panel h2{font-size:16px;letter-spacing:3px;margin:0 0 16px 0;text-align:center;font-weight:700}
|
|
42
|
+
#side-leaderboard{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px rgba(255,0,255,.4)}
|
|
43
|
+
#side-leaderboard h2{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px #f0f}
|
|
44
|
+
#side-controls{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px rgba(0,255,255,.4)}
|
|
45
|
+
#side-controls h2{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px #0ff}
|
|
46
|
+
.leaderboard-table{width:100%;border-collapse:collapse;font-family:'Courier New',monospace;font-size:13px}
|
|
47
|
+
.leaderboard-table td{padding:4px 0;white-space:nowrap}
|
|
48
|
+
.leaderboard-table .rank{width:24px;color:rgba(255,0,255,.6);text-align:right;padding-right:6px}
|
|
49
|
+
.leaderboard-table .initials{color:#f0f;letter-spacing:2px}
|
|
50
|
+
.leaderboard-table .hs-score{text-align:right;color:rgba(255,0,255,.7)}
|
|
51
|
+
.controls-list{list-style:none;padding:0;margin:0;font-size:12px}
|
|
52
|
+
.controls-list li{padding:6px 0;display:flex;gap:8px;align-items:center;color:#0ff}
|
|
53
|
+
.controls-list .key-icon{display:inline-block;min-width:50px;padding:2px 6px;border:1px solid rgba(0,255,255,.4);border-radius:3px;text-align:center;font-size:11px;color:#0ff;background:rgba(0,255,255,.05);flex-shrink:0}
|
|
54
|
+
.controls-list .key-desc{color:rgba(0,255,255,.7);font-family:'Segoe UI',sans-serif}
|
|
55
|
+
.bt-section{margin-top:auto;padding-top:16px;border-top:1px solid rgba(0,255,255,.15)}
|
|
56
|
+
.bt-section h3{font-size:11px;letter-spacing:2px;color:rgba(0,255,255,.5);margin:0 0 8px 0;text-align:center;font-weight:400}
|
|
57
|
+
.bt-section p{font-size:11px;color:rgba(0,255,255,.4);font-family:'Segoe UI',sans-serif;margin:0 0 4px 0;line-height:1.4}
|
|
58
|
+
.side-panel{overflow-y:auto}
|
|
59
|
+
#side-leaderboard{padding-top:20px}
|
|
60
|
+
#side-controls{padding-top:20px}
|
|
61
|
+
|
|
62
|
+
/* Gamepad status indicator */
|
|
63
|
+
#gamepadStatus{text-align:center;padding:8px 0;margin-bottom:12px;font-size:11px;letter-spacing:2px;border:1px solid rgba(0,255,255,.15);border-radius:4px;transition:all .3s;color:rgba(0,255,255,.3);text-shadow:none}
|
|
64
|
+
#gamepadStatus.connected{color:#0f0;text-shadow:0 0 8px #0f0,0 0 16px rgba(0,255,0,.4);border-color:rgba(0,255,0,.3);background:rgba(0,255,0,.05);animation:gpPulse 2s ease-in-out infinite}
|
|
65
|
+
@keyframes gpPulse{0%,100%{box-shadow:0 0 4px rgba(0,255,0,.1)}50%{box-shadow:0 0 12px rgba(0,255,0,.25)}}
|
|
66
|
+
|
|
67
|
+
/* Pause overlay */
|
|
68
|
+
#pauseOverlay{position:absolute;top:0;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;z-index:25;background:rgba(0,0,0,.5);flex-direction:column;gap:8px}
|
|
69
|
+
#pauseOverlay.visible{display:flex}
|
|
70
|
+
#pauseOverlay .pause-title{font-family:Orbitron,'Segoe UI',sans-serif;font-size:48px;color:#0ff;text-shadow:0 0 20px #0ff,0 0 40px #0ff,0 0 60px #0ff;letter-spacing:8px}
|
|
71
|
+
#pauseOverlay .pause-hint{font-family:'Segoe UI',sans-serif;font-size:14px;color:rgba(0,255,255,.5);letter-spacing:1px}
|
|
72
|
+
|
|
73
|
+
/* High score entry */
|
|
74
|
+
#highScoreEntry{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;z-index:30;display:none}
|
|
75
|
+
#highScoreEntry h2{font-family:Orbitron,'Segoe UI',sans-serif;font-size:24px;color:#f0f;text-shadow:0 0 15px #f0f,0 0 30px #f0f;margin-bottom:10px;letter-spacing:3px}
|
|
76
|
+
#highScoreEntry .hs-final-score{font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px;color:#0ff;text-shadow:0 0 10px #0ff;margin-bottom:20px}
|
|
77
|
+
.initial-slots{display:flex;justify-content:center;gap:12px;margin-bottom:20px}
|
|
78
|
+
.initial-slot{width:40px;height:50px;border:2px solid #f0f;display:flex;align-items:center;justify-content:center;font-family:Orbitron,'Segoe UI',sans-serif;font-size:28px;color:#f0f;text-shadow:0 0 10px #f0f;box-shadow:0 0 10px rgba(255,0,255,.3)}
|
|
79
|
+
.initial-slot.active{border-color:#0ff;color:#0ff;text-shadow:0 0 10px #0ff;box-shadow:0 0 15px rgba(0,255,255,.5)}
|
|
80
|
+
.hs-instructions{font-size:12px;color:rgba(0,255,255,.6);font-family:'Segoe UI',sans-serif;margin-bottom:16px}
|
|
81
|
+
|
|
82
|
+
/* Keyboard key hint (for bottom bar) */
|
|
83
|
+
.kb-key{display:inline-block;padding:1px 6px;border:1px solid rgba(0,255,255,.35);border-radius:3px;font-size:11px;color:rgba(0,255,255,.7);background:rgba(0,255,255,.05);vertical-align:middle;margin:0 2px;font-family:'Segoe UI',sans-serif}
|
|
84
|
+
/* Focused menu item */
|
|
85
|
+
.mode-btn.focused{background:rgba(0,255,255,.2);border-color:#fff;color:#fff;box-shadow:0 0 25px rgba(0,255,255,.5),inset 0 0 20px rgba(0,255,255,.2)}
|
|
86
|
+
|
|
87
|
+
/* Turn ready overlay */
|
|
88
|
+
#turnReadyOverlay{position:absolute;top:0;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px;z-index:26;background:rgba(0,0,0,.7)}
|
|
89
|
+
#turnReadyOverlay.visible{display:flex}
|
|
90
|
+
#turnReadyOverlay .turn-player{font-family:Orbitron,'Segoe UI',sans-serif;font-size:42px;text-shadow:0 0 20px currentColor,0 0 40px currentColor;letter-spacing:6px}
|
|
91
|
+
#turnReadyOverlay .turn-subtitle{font-family:Orbitron,'Segoe UI',sans-serif;font-size:22px;color:#fff;text-shadow:0 0 12px rgba(255,255,255,.6);letter-spacing:4px;margin-bottom:8px}
|
|
92
|
+
#turnReadyOverlay .turn-prompt{font-family:'Segoe UI',sans-serif;font-size:16px;color:rgba(0,255,255,.6);letter-spacing:1px}
|
|
93
|
+
|
|
94
|
+
/* Final scoreboard overlay */
|
|
95
|
+
#finalScoreboard{position:absolute;top:0;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;z-index:27;background:rgba(0,0,0,.8)}
|
|
96
|
+
#finalScoreboard.visible{display:flex}
|
|
97
|
+
#finalScoreboard .sb-title{font-family:Orbitron,'Segoe UI',sans-serif;font-size:32px;color:#f0f;text-shadow:0 0 20px #f0f,0 0 40px #f0f;letter-spacing:5px;margin-bottom:8px}
|
|
98
|
+
#finalScoreboard .sb-subtitle{font-family:Orbitron,'Segoe UI',sans-serif;font-size:14px;color:rgba(255,255,255,.5);letter-spacing:3px;margin-bottom:12px}
|
|
99
|
+
.sb-table{border-collapse:collapse;font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px}
|
|
100
|
+
.sb-table td{padding:8px 16px}
|
|
101
|
+
.sb-table .sb-rank{color:rgba(255,255,255,.5);text-align:right;font-size:14px}
|
|
102
|
+
.sb-table .sb-player{letter-spacing:3px}
|
|
103
|
+
.sb-table .sb-initials{font-size:14px;color:rgba(255,255,255,.5);letter-spacing:2px}
|
|
104
|
+
.sb-table .sb-score{text-align:right;color:#fff;letter-spacing:2px}
|
|
105
|
+
.sb-play-again{margin-top:16px}
|
|
106
|
+
</style>
|
|
107
|
+
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
|
|
108
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
109
|
+
</head>
|
|
110
|
+
|
|
111
|
+
<body>
|
|
112
|
+
<div class="viewer-panel full-viewer" id="viewerPanel" style="background:#000;">
|
|
113
|
+
<div id="game-wrapper">
|
|
114
|
+
<div id="side-leaderboard" class="side-panel hidden">
|
|
115
|
+
<h2>HIGH SCORES</h2>
|
|
116
|
+
<table class="leaderboard-table">
|
|
117
|
+
<tbody id="leaderboardBody"></tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
<div id="game-container">
|
|
121
|
+
<canvas id="gameCanvas"></canvas>
|
|
122
|
+
<div class="game-ui">
|
|
123
|
+
<div class="score-display"><span id="playerIndicator" class="player-indicator" style="display:none"></span>SCORE: <span id="score">00000</span></div>
|
|
124
|
+
<div class="lives-display">LIVES: <span id="lives">3</span></div>
|
|
125
|
+
<button class="mute-btn" id="muteBtn" title="Toggle sound">🔊</button>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="pauseOverlay"><span class="pause-title">PAUSED</span><span class="pause-hint" id="pauseHint"></span></div>
|
|
128
|
+
<div id="turnReadyOverlay">
|
|
129
|
+
<div class="turn-player" id="turnPlayerName">PLAYER 1</div>
|
|
130
|
+
<div class="turn-subtitle">YOUR TURN</div>
|
|
131
|
+
<div class="turn-prompt" id="turnPrompt">Press SPACE to start</div>
|
|
132
|
+
</div>
|
|
133
|
+
<div id="finalScoreboard">
|
|
134
|
+
<div class="sb-title">GAME OVER</div>
|
|
135
|
+
<div class="sb-subtitle" id="sbSubtitle">FINAL STANDINGS</div>
|
|
136
|
+
<table class="sb-table"><tbody id="sbBody"></tbody></table>
|
|
137
|
+
<button class="restart-btn sb-play-again" id="sbPlayAgain">PLAY AGAIN</button>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="start-screen" id="startScreen">
|
|
140
|
+
<h1>PADDLE<br>BOUNCE</h1>
|
|
141
|
+
<p>Keep the ball alive! Bounce it off your paddle to score.</p>
|
|
142
|
+
<div class="mode-select" id="modeSelect">
|
|
143
|
+
<button class="mode-btn" id="btn1P">START GAME</button>
|
|
144
|
+
<button class="mode-btn" id="btnPassPlay">PASS & PLAY</button>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="player-count-select" id="playerCountSelect">
|
|
147
|
+
<div class="player-count-label">PLAYERS</div>
|
|
148
|
+
<div class="player-count-row">
|
|
149
|
+
<span class="player-count-arrow" id="pcLeft">←</span>
|
|
150
|
+
<span class="player-count-num" id="pcNum">2</span>
|
|
151
|
+
<span class="player-count-arrow" id="pcRight">→</span>
|
|
152
|
+
</div>
|
|
153
|
+
<button class="mode-btn player-count-confirm" id="pcConfirm">START</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="game-over-screen" id="gameOverScreen" style="display: none;">
|
|
157
|
+
<h1>GAME OVER</h1>
|
|
158
|
+
<p>Final Score: <span id="finalScore">0</span></p>
|
|
159
|
+
<button class="restart-btn" id="restartBtn">TRY AGAIN</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div id="highScoreEntry">
|
|
162
|
+
<h2>NEW HIGH SCORE!</h2>
|
|
163
|
+
<div class="hs-final-score">SCORE: <span id="hsScore">0</span></div>
|
|
164
|
+
<div class="initial-slots">
|
|
165
|
+
<div class="initial-slot active" id="slot0">A</div>
|
|
166
|
+
<div class="initial-slot" id="slot1">A</div>
|
|
167
|
+
<div class="initial-slot" id="slot2">A</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="hs-instructions" id="hsInstructions">LEFT/RIGHT select slot • UP/DOWN change letter • ENTER confirm</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="controls-info" id="controlsInfo">←/→ move paddle • P pause</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div id="side-controls" class="side-panel hidden">
|
|
174
|
+
<div id="gamepadStatus">NO GAMEPAD</div>
|
|
175
|
+
<h2>CONTROLS</h2>
|
|
176
|
+
<ul class="controls-list" id="controlsList">
|
|
177
|
+
<li><span class="key-icon">A/←</span><span class="key-desc">Move Left</span></li>
|
|
178
|
+
<li><span class="key-icon">D/→</span><span class="key-desc">Move Right</span></li>
|
|
179
|
+
<li><span class="key-icon">P</span><span class="key-desc">Pause</span></li>
|
|
180
|
+
</ul>
|
|
181
|
+
<div class="bt-section" id="btSection"></div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
</div>
|
|
186
|
+
<div id="instructions" style="display: none;" data-locked="true"></div>
|
|
187
|
+
<div id="thoughts" style="display: none;" data-locked="true"></div>
|
|
188
|
+
<script src="/static/retro-game.js"></script>
|
|
189
|
+
<script id="paddle-bounce-game">
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// === PADDLE BOUNCE — Retro Game Starter ======================================
|
|
192
|
+
// =============================================================================
|
|
193
|
+
// This is a template game built on the RetroGame library (retro-game.js).
|
|
194
|
+
// Look for "// CUSTOMIZE:" comments to find the key places to modify.
|
|
195
|
+
// Use neon_asteroids as a full-featured reference for more advanced patterns.
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
// === SETUP & DOM REFS ========================================================
|
|
199
|
+
// Grab references to the HTML elements the game needs.
|
|
200
|
+
RetroGame.injectCSS();
|
|
201
|
+
const canvas = document.getElementById("gameCanvas");
|
|
202
|
+
const ctx = canvas.getContext("2d");
|
|
203
|
+
const viewerPanel = document.getElementById("viewerPanel");
|
|
204
|
+
const gameContainer = document.getElementById("game-container");
|
|
205
|
+
const sideLeaderboard = document.getElementById("side-leaderboard");
|
|
206
|
+
const sideControls = document.getElementById("side-controls");
|
|
207
|
+
const pauseOverlay = document.getElementById("pauseOverlay");
|
|
208
|
+
|
|
209
|
+
// === GAME CONSTANTS ==========================================================
|
|
210
|
+
// CUSTOMIZE: Tweak these values to change how the game feels.
|
|
211
|
+
const ASPECT = 4 / 3; // Canvas aspect ratio
|
|
212
|
+
const PADDLE_WIDTH = 100; // CUSTOMIZE: Width of the paddle in pixels
|
|
213
|
+
const PADDLE_HEIGHT = 14; // CUSTOMIZE: Height of the paddle in pixels
|
|
214
|
+
const PADDLE_SPEED = 8; // CUSTOMIZE: How fast the paddle moves
|
|
215
|
+
const PADDLE_Y_OFFSET = 40; // Distance from bottom of canvas
|
|
216
|
+
const BALL_RADIUS = 8; // CUSTOMIZE: Size of the ball
|
|
217
|
+
const BALL_INITIAL_SPEED = 5; // CUSTOMIZE: Starting ball speed
|
|
218
|
+
const BALL_SPEED_INCREMENT = 0.15; // CUSTOMIZE: Speed added per paddle hit
|
|
219
|
+
const BALL_MAX_SPEED = 12; // CUSTOMIZE: Maximum ball speed
|
|
220
|
+
const STARTING_LIVES = 3; // CUSTOMIZE: Lives per game
|
|
221
|
+
// High scores stored in synthos.data table storage
|
|
222
|
+
|
|
223
|
+
// Player colors for multiplayer: P1=cyan, P2=magenta, P3=yellow, P4=green, ...
|
|
224
|
+
const PLAYER_COLORS = ["#00ffff", "#ff00ff", "#ffff00", "#00ff00", "#ff6600", "#ff3388", "#33ccff", "#ccff33"];
|
|
225
|
+
|
|
226
|
+
// === CANVAS RESIZE ===========================================================
|
|
227
|
+
// Inline resize logic — fits the canvas into the viewer panel while
|
|
228
|
+
// maintaining aspect ratio. Shows/hides side panels based on available space.
|
|
229
|
+
let resizeTimer = 0;
|
|
230
|
+
|
|
231
|
+
function resizeCanvas() {
|
|
232
|
+
const vw = viewerPanel.clientWidth;
|
|
233
|
+
const vh = viewerPanel.clientHeight;
|
|
234
|
+
if (vw < 1 || vh < 1) return;
|
|
235
|
+
const panelRatio = vw / vh;
|
|
236
|
+
let w, h;
|
|
237
|
+
if (panelRatio > ASPECT) {
|
|
238
|
+
h = vh;
|
|
239
|
+
w = Math.floor(h * ASPECT);
|
|
240
|
+
} else {
|
|
241
|
+
w = vw;
|
|
242
|
+
h = Math.floor(w / ASPECT);
|
|
243
|
+
}
|
|
244
|
+
gameContainer.style.width = w + "px";
|
|
245
|
+
gameContainer.style.height = h + "px";
|
|
246
|
+
canvas.width = w;
|
|
247
|
+
canvas.height = h;
|
|
248
|
+
const extraSpace = vw - w;
|
|
249
|
+
const chatHidden = document.body.classList.contains("chat-collapsed");
|
|
250
|
+
if (chatHidden && extraSpace > 400) {
|
|
251
|
+
sideLeaderboard.classList.remove("hidden");
|
|
252
|
+
sideControls.classList.remove("hidden");
|
|
253
|
+
} else {
|
|
254
|
+
sideLeaderboard.classList.add("hidden");
|
|
255
|
+
sideControls.classList.add("hidden");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function debouncedResize() {
|
|
260
|
+
clearTimeout(resizeTimer);
|
|
261
|
+
resizeCanvas();
|
|
262
|
+
resizeTimer = setTimeout(resizeCanvas, 300);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
resizeCanvas();
|
|
266
|
+
window.addEventListener("resize", resizeCanvas);
|
|
267
|
+
new MutationObserver(debouncedResize).observe(document.body, { attributes: true, attributeFilter: ["class"] });
|
|
268
|
+
|
|
269
|
+
// === INPUT SETUP =============================================================
|
|
270
|
+
// RetroGame.createKeyboard() tracks which keys are held each frame.
|
|
271
|
+
// RetroGame.createGamepad() reads controller state with deadzone filtering.
|
|
272
|
+
// RetroGame.createGamepadUI() provides platform-aware button glyphs.
|
|
273
|
+
const keyboard = RetroGame.createKeyboard();
|
|
274
|
+
const gamepad = RetroGame.createGamepad({ deadzone: 0.3 });
|
|
275
|
+
const B = RetroGame.BUTTON;
|
|
276
|
+
const gamepadUI = RetroGame.createGamepadUI(gamepad, { statusElement: document.getElementById("gamepadStatus") });
|
|
277
|
+
|
|
278
|
+
// Game-specific gamepad action mappings
|
|
279
|
+
function gpPause() { return gamepad.wasPressed(B.START); }
|
|
280
|
+
function gpConfirm() { return gamepad.wasPressed(B.A); }
|
|
281
|
+
function gpBack() { return gamepad.wasPressed(B.B); }
|
|
282
|
+
|
|
283
|
+
// Edge-triggered directional helpers (stick + dpad)
|
|
284
|
+
var _prevDir = { up: false, down: false, left: false, right: false };
|
|
285
|
+
function gpUp() { return gamepad.up && !_prevDir.up; }
|
|
286
|
+
function gpDown() { return gamepad.down && !_prevDir.down; }
|
|
287
|
+
function gpLeft() { return gamepad.left && !_prevDir.left; }
|
|
288
|
+
function gpRight() { return gamepad.right && !_prevDir.right; }
|
|
289
|
+
function saveDirState() { _prevDir.up = gamepad.up; _prevDir.down = gamepad.down; _prevDir.left = gamepad.left; _prevDir.right = gamepad.right; }
|
|
290
|
+
|
|
291
|
+
// Delegate to library UI for button glyphs
|
|
292
|
+
function gpBtn(name) { return gamepadUI.buttonGlyph(name); }
|
|
293
|
+
function gpBtnName(name) { return gamepadUI.buttonName(name); }
|
|
294
|
+
function gpStartName() { return gamepadUI.startName(); }
|
|
295
|
+
|
|
296
|
+
// Non-face button label (e.g., "L-Stick", "START")
|
|
297
|
+
function gpLabel(text) {
|
|
298
|
+
return '<span class="gp-label">' + text + '</span>';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Gamepad-aware text helper
|
|
302
|
+
function btnText(kbLabel, gpLabelText) {
|
|
303
|
+
return gamepad.connected ? gpLabelText : kbLabel;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Keyboard key styled span
|
|
307
|
+
function kbKey(text) {
|
|
308
|
+
return '<span class="kb-key">' + text + '</span>';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Build an action item for the bottom bar
|
|
312
|
+
function actionItem(hint, label) {
|
|
313
|
+
return '<span class="action-item">' + hint + ' ' + label + '</span>';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// === PARTICLES & SHAKE =======================================================
|
|
317
|
+
// RetroGame.createParticles() manages a pool of visual particles.
|
|
318
|
+
// RetroGame.createShake() provides screen shake that decays over time.
|
|
319
|
+
const particles = RetroGame.createParticles({ maxParticles: 300 });
|
|
320
|
+
const shake = RetroGame.createShake();
|
|
321
|
+
|
|
322
|
+
// === SOUND EFFECTS ===========================================================
|
|
323
|
+
// RetroGame.createSoundEffects() loads effects from an effects.json manifest
|
|
324
|
+
// in this page's files/ directory. Use the Effects Studio to scan this page,
|
|
325
|
+
// generate audio, and Apply — that creates the manifest and audio files.
|
|
326
|
+
// CUSTOMIZE: Adjust volume (0-1). Effects are played with sfx.play('Name').
|
|
327
|
+
const sfx = RetroGame.createSoundEffects({ volume: 1.0 });
|
|
328
|
+
var sfxReady = false;
|
|
329
|
+
sfx.load().then(function() {
|
|
330
|
+
sfxReady = true;
|
|
331
|
+
}).catch(function(e) {
|
|
332
|
+
// No effects.json yet — sound is optional until Effects Studio applies one
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Mute toggle
|
|
336
|
+
var sfxMuted = false;
|
|
337
|
+
document.getElementById("muteBtn").addEventListener("click", function() {
|
|
338
|
+
sfxMuted = !sfxMuted;
|
|
339
|
+
sfx.volume = sfxMuted ? 0 : 1;
|
|
340
|
+
this.innerHTML = sfxMuted ? "\u{1F507}" : "\u{1F50A}";
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// === GAMEPAD DETECTION & CONTROLS SWAP =======================================
|
|
344
|
+
// When a gamepad connects/disconnects, update the side panel controls list
|
|
345
|
+
// and all button prompts throughout the UI.
|
|
346
|
+
const KEYBOARD_CONTROLS = '<li><span class="key-icon">A/\u2190</span><span class="key-desc">Move Left</span></li>' +
|
|
347
|
+
'<li><span class="key-icon">D/\u2192</span><span class="key-desc">Move Right</span></li>' +
|
|
348
|
+
'<li><span class="key-icon">P</span><span class="key-desc">Pause</span></li>';
|
|
349
|
+
|
|
350
|
+
function buildGamepadControls() {
|
|
351
|
+
return '<li><span class="key-icon">L-Stick</span><span class="key-desc">Move</span></li>' +
|
|
352
|
+
'<li><span class="key-icon">D-Pad</span><span class="key-desc">Move</span></li>' +
|
|
353
|
+
'<li><span class="key-icon">' + gpStartName() + '</span><span class="key-desc">Pause</span></li>';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function refreshGamepadUI() {
|
|
357
|
+
gamepadUI.update();
|
|
358
|
+
var listEl = document.getElementById("controlsList");
|
|
359
|
+
var btEl = document.getElementById("btSection");
|
|
360
|
+
if (gamepad.connected) {
|
|
361
|
+
listEl.innerHTML = buildGamepadControls();
|
|
362
|
+
btEl.style.display = "none";
|
|
363
|
+
} else {
|
|
364
|
+
listEl.innerHTML = KEYBOARD_CONTROLS;
|
|
365
|
+
btEl.style.display = "";
|
|
366
|
+
}
|
|
367
|
+
updateAllPrompts();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
gamepad.onConnect = function() { refreshGamepadUI(); };
|
|
371
|
+
gamepad.onDisconnect = function() { refreshGamepadUI(); };
|
|
372
|
+
|
|
373
|
+
// === BOTTOM BAR & PROMPT UPDATES =============================================
|
|
374
|
+
// The bottom bar shows context-sensitive hints (menu navigation vs game controls).
|
|
375
|
+
// updateAllPrompts() refreshes ALL text that changes between keyboard/gamepad.
|
|
376
|
+
function updateBottomBar() {
|
|
377
|
+
var bar = document.getElementById("controlsInfo");
|
|
378
|
+
var startVisible = document.getElementById("startScreen").style.display !== "none";
|
|
379
|
+
var gameOverVisible = document.getElementById("gameOverScreen").style.display === "block";
|
|
380
|
+
var scoreboardVisible = document.getElementById("finalScoreboard").classList.contains("visible");
|
|
381
|
+
|
|
382
|
+
// Menu screens
|
|
383
|
+
if (startVisible) {
|
|
384
|
+
if (gamepad.connected) {
|
|
385
|
+
if (startScreenPhase === "mode") {
|
|
386
|
+
bar.innerHTML =
|
|
387
|
+
actionItem(gpLabel("\u2191\u2193"), "Navigate") +
|
|
388
|
+
actionItem(gpBtn("a"), "Select");
|
|
389
|
+
} else if (startScreenPhase === "count") {
|
|
390
|
+
bar.innerHTML =
|
|
391
|
+
actionItem(gpLabel("\u2190\u2192"), "Players") +
|
|
392
|
+
actionItem(gpBtn("a"), "Start") +
|
|
393
|
+
actionItem(gpBtn("b"), "Back");
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
if (startScreenPhase === "mode") {
|
|
397
|
+
bar.innerHTML =
|
|
398
|
+
actionItem(kbKey("\u2191\u2193"), "Navigate") +
|
|
399
|
+
actionItem(kbKey("ENTER"), "Select");
|
|
400
|
+
} else if (startScreenPhase === "count") {
|
|
401
|
+
bar.innerHTML =
|
|
402
|
+
actionItem(kbKey("\u2190\u2192"), "Players") +
|
|
403
|
+
actionItem(kbKey("ENTER"), "Start") +
|
|
404
|
+
actionItem(kbKey("ESC"), "Back");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// High score entry
|
|
411
|
+
if (hsEntryActive) {
|
|
412
|
+
if (gamepad.connected) {
|
|
413
|
+
bar.innerHTML =
|
|
414
|
+
actionItem(gpLabel("\u2191\u2193"), "Letter") +
|
|
415
|
+
actionItem(gpBtn("a"), "Next") +
|
|
416
|
+
actionItem(gpBtn("b"), "Back");
|
|
417
|
+
} else {
|
|
418
|
+
bar.innerHTML =
|
|
419
|
+
actionItem(kbKey("\u2191\u2193"), "Letter") +
|
|
420
|
+
actionItem(kbKey("\u2190\u2192"), "Slot") +
|
|
421
|
+
actionItem(kbKey("ENTER"), "Confirm");
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Game over / scoreboard
|
|
427
|
+
if (gameOverVisible || scoreboardVisible) {
|
|
428
|
+
if (gamepad.connected) {
|
|
429
|
+
bar.innerHTML = actionItem(gpBtn("a"), "Continue");
|
|
430
|
+
} else {
|
|
431
|
+
bar.innerHTML = actionItem(kbKey("ENTER"), "Continue");
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// In-game controls
|
|
437
|
+
if (gamepad.connected) {
|
|
438
|
+
bar.innerHTML =
|
|
439
|
+
actionItem(gpLabel("L-Stick"), "Move") +
|
|
440
|
+
actionItem(gpLabel(gpStartName()), "Pause");
|
|
441
|
+
} else {
|
|
442
|
+
bar.innerHTML =
|
|
443
|
+
actionItem(kbKey("\u2190\u2192"), "Move") +
|
|
444
|
+
actionItem(kbKey("P"), "Pause");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function updateAllPrompts() {
|
|
449
|
+
// Start screen buttons
|
|
450
|
+
document.getElementById("btn1P").textContent = gamepad.connected ? "1 PLAYER" : "START GAME";
|
|
451
|
+
document.getElementById("btnPassPlay").textContent = "PASS & PLAY";
|
|
452
|
+
updateMenuFocus();
|
|
453
|
+
// Restart button
|
|
454
|
+
var restartBtn = document.getElementById("restartBtn");
|
|
455
|
+
if (gamepad.connected) {
|
|
456
|
+
restartBtn.innerHTML = gpBtn("a") + ' RETRY';
|
|
457
|
+
} else {
|
|
458
|
+
restartBtn.textContent = "TRY AGAIN";
|
|
459
|
+
}
|
|
460
|
+
// HS instructions
|
|
461
|
+
document.getElementById("hsInstructions").innerHTML = btnText(
|
|
462
|
+
"LEFT/RIGHT select slot • UP/DOWN change letter • ENTER confirm",
|
|
463
|
+
gpLabel("\u2191\u2193") + " change letter • " + gpBtn("a") + " next • " + gpBtn("b") + " back"
|
|
464
|
+
);
|
|
465
|
+
// Pause hint
|
|
466
|
+
document.getElementById("pauseHint").innerHTML = btnText(
|
|
467
|
+
"P to resume",
|
|
468
|
+
gpLabel(gpStartName()) + " to resume"
|
|
469
|
+
);
|
|
470
|
+
// Turn ready prompt
|
|
471
|
+
document.getElementById("turnPrompt").innerHTML = btnText(
|
|
472
|
+
"Press SPACE to start",
|
|
473
|
+
"Press " + gpBtn("a") + " to start"
|
|
474
|
+
);
|
|
475
|
+
// Player count confirm
|
|
476
|
+
var pcConfirm = document.getElementById("pcConfirm");
|
|
477
|
+
if (gamepad.connected) {
|
|
478
|
+
pcConfirm.innerHTML = gpBtn("a") + ' START';
|
|
479
|
+
} else {
|
|
480
|
+
pcConfirm.textContent = "START";
|
|
481
|
+
}
|
|
482
|
+
// Scoreboard play again
|
|
483
|
+
var sbBtn = document.getElementById("sbPlayAgain");
|
|
484
|
+
if (gamepad.connected) {
|
|
485
|
+
sbBtn.innerHTML = gpBtn("a") + ' PLAY AGAIN';
|
|
486
|
+
} else {
|
|
487
|
+
sbBtn.textContent = "PLAY AGAIN";
|
|
488
|
+
}
|
|
489
|
+
updateBottomBar();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// === HIGH SCORE SYSTEM =======================================================
|
|
493
|
+
// Stores top 20 scores in synthos.data table storage. Renders into the side leaderboard panel.
|
|
494
|
+
const MAX_LEADERBOARD = 20;
|
|
495
|
+
var _highScores = [{ initials: "ACE", score: 50 }];
|
|
496
|
+
|
|
497
|
+
// Load scores from app table storage on startup
|
|
498
|
+
(async function() {
|
|
499
|
+
try {
|
|
500
|
+
var row = await synthos.data.get('high_scores', 'scores');
|
|
501
|
+
if (row && Array.isArray(row.entries) && row.entries.length > 0) {
|
|
502
|
+
_highScores = row.entries;
|
|
503
|
+
}
|
|
504
|
+
} catch(e) {}
|
|
505
|
+
renderLeaderboard();
|
|
506
|
+
})();
|
|
507
|
+
|
|
508
|
+
function loadHighScores() {
|
|
509
|
+
return _highScores;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function saveHighScores(scores) {
|
|
513
|
+
_highScores = scores;
|
|
514
|
+
synthos.data.save('high_scores', { id: 'scores', entries: scores }).catch(function() {});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function qualifiesForHighScore(s) {
|
|
518
|
+
var scores = loadHighScores();
|
|
519
|
+
return scores.length < MAX_LEADERBOARD || s > scores[scores.length - 1].score;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function insertHighScore(initials, s) {
|
|
523
|
+
var scores = loadHighScores().slice();
|
|
524
|
+
scores.push({ initials: initials, score: s });
|
|
525
|
+
scores.sort(function(a, b) { return b.score - a.score; });
|
|
526
|
+
if (scores.length > MAX_LEADERBOARD) scores.length = MAX_LEADERBOARD;
|
|
527
|
+
saveHighScores(scores);
|
|
528
|
+
renderLeaderboard();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function renderLeaderboard() {
|
|
532
|
+
var scores = loadHighScores();
|
|
533
|
+
var tbody = document.getElementById("leaderboardBody");
|
|
534
|
+
tbody.innerHTML = "";
|
|
535
|
+
scores.forEach(function(entry, i) {
|
|
536
|
+
var tr = document.createElement("tr");
|
|
537
|
+
tr.innerHTML =
|
|
538
|
+
'<td class="rank">' + (i + 1) + '.</td>' +
|
|
539
|
+
'<td class="initials">' + entry.initials + '</td>' +
|
|
540
|
+
'<td class="hs-score">' + entry.score.toLocaleString() + '</td>';
|
|
541
|
+
tbody.appendChild(tr);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
renderLeaderboard();
|
|
546
|
+
|
|
547
|
+
// === BT CONTROLLER INSTRUCTIONS ==============================================
|
|
548
|
+
// Platform-aware Bluetooth pairing instructions in the side controls panel.
|
|
549
|
+
(function() {
|
|
550
|
+
const el = document.getElementById("btSection");
|
|
551
|
+
const isMac = /Mac|iPhone|iPad/.test(navigator.platform) || /Macintosh/.test(navigator.userAgent);
|
|
552
|
+
if (isMac) {
|
|
553
|
+
el.innerHTML =
|
|
554
|
+
'<h3>GAMEPAD</h3>' +
|
|
555
|
+
'<p>1. Put controller in pairing mode</p>' +
|
|
556
|
+
'<p>2. Open <b>System Settings > Bluetooth</b></p>' +
|
|
557
|
+
'<p>3. Find controller in list, click <b>Connect</b></p>' +
|
|
558
|
+
'<p>4. Reload page once paired</p>';
|
|
559
|
+
} else {
|
|
560
|
+
el.innerHTML =
|
|
561
|
+
'<h3>GAMEPAD</h3>' +
|
|
562
|
+
'<p>1. Put controller in pairing mode</p>' +
|
|
563
|
+
'<p>2. Open <b>Settings > Bluetooth & devices</b></p>' +
|
|
564
|
+
'<p>3. Click <b>Add device > Bluetooth</b></p>' +
|
|
565
|
+
'<p>4. Select controller, then reload page</p>';
|
|
566
|
+
}
|
|
567
|
+
})();
|
|
568
|
+
|
|
569
|
+
// === GAME MODE & STATE =======================================================
|
|
570
|
+
// CUSTOMIZE: To add new game modes, extend the gameMode options here.
|
|
571
|
+
// Currently supports "single" (1 player) and "solo_turns" (pass & play).
|
|
572
|
+
let gameMode = "single"; // "single" or "solo_turns"
|
|
573
|
+
let playerCount = 2;
|
|
574
|
+
let currentPlayer = 0;
|
|
575
|
+
let players = []; // { score, lives, alive, initials }
|
|
576
|
+
let turnReady = false;
|
|
577
|
+
let startScreenPhase = "mode"; // "mode" or "count"
|
|
578
|
+
|
|
579
|
+
// --- Core game state ---
|
|
580
|
+
let gameRunning = false, gamePaused = false, score = 0, lives = STARTING_LIVES;
|
|
581
|
+
|
|
582
|
+
// --- Paddle state ---
|
|
583
|
+
let paddle = { x: 0, width: PADDLE_WIDTH, height: PADDLE_HEIGHT, speed: PADDLE_SPEED };
|
|
584
|
+
|
|
585
|
+
// --- Ball state ---
|
|
586
|
+
let ball = { x: 0, y: 0, vx: 0, vy: 0, speed: BALL_INITIAL_SPEED };
|
|
587
|
+
|
|
588
|
+
// --- Stars (background decoration) ---
|
|
589
|
+
let stars = [];
|
|
590
|
+
|
|
591
|
+
// === MULTIPLAYER TURN SYSTEM =================================================
|
|
592
|
+
// Solo Turns: each player gets a fresh game. After all players finish, a
|
|
593
|
+
// scoreboard shows final standings. Highest score wins.
|
|
594
|
+
const TURN_COOLDOWN_MS = 3000;
|
|
595
|
+
let turnReadyAt = 0;
|
|
596
|
+
|
|
597
|
+
function getActivePlayerColor() {
|
|
598
|
+
if (gameMode === "single") return PLAYER_COLORS[0];
|
|
599
|
+
return PLAYER_COLORS[currentPlayer] || PLAYER_COLORS[0];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function initMultiplayerPlayers() {
|
|
603
|
+
players = [];
|
|
604
|
+
for (let i = 0; i < playerCount; i++) {
|
|
605
|
+
players.push({ score: 0, lives: STARTING_LIVES, alive: true, initials: "" });
|
|
606
|
+
}
|
|
607
|
+
currentPlayer = 0;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function showTurnReady() {
|
|
611
|
+
turnReady = true;
|
|
612
|
+
turnReadyAt = Date.now();
|
|
613
|
+
const overlay = document.getElementById("turnReadyOverlay");
|
|
614
|
+
const nameEl = document.getElementById("turnPlayerName");
|
|
615
|
+
const color = getActivePlayerColor();
|
|
616
|
+
nameEl.textContent = "PLAYER " + (currentPlayer + 1);
|
|
617
|
+
nameEl.style.color = color;
|
|
618
|
+
updateAllPrompts();
|
|
619
|
+
overlay.classList.add("visible");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function getTurnCooldownRemaining() {
|
|
623
|
+
return Math.max(0, TURN_COOLDOWN_MS - (Date.now() - turnReadyAt));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function updateTurnPrompt() {
|
|
627
|
+
var remaining = getTurnCooldownRemaining();
|
|
628
|
+
var promptEl = document.getElementById("turnPrompt");
|
|
629
|
+
if (remaining > 0) {
|
|
630
|
+
var secs = Math.ceil(remaining / 1000);
|
|
631
|
+
promptEl.textContent = "Get ready... " + secs;
|
|
632
|
+
} else {
|
|
633
|
+
promptEl.innerHTML = btnText(
|
|
634
|
+
"Press SPACE to start",
|
|
635
|
+
"Press " + gpBtn("a") + " to start"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function hideTurnReady() {
|
|
641
|
+
turnReady = false;
|
|
642
|
+
document.getElementById("turnReadyOverlay").classList.remove("visible");
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function startPlayerTurn() {
|
|
646
|
+
hideTurnReady();
|
|
647
|
+
// Solo turns: each player gets a fresh game
|
|
648
|
+
initGame();
|
|
649
|
+
score = 0;
|
|
650
|
+
lives = STARTING_LIVES;
|
|
651
|
+
gameRunning = true;
|
|
652
|
+
updateUI();
|
|
653
|
+
updateBottomBar();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function saveCurrentPlayerState() {
|
|
657
|
+
if (gameMode === "single") return;
|
|
658
|
+
players[currentPlayer].score = score;
|
|
659
|
+
players[currentPlayer].lives = lives;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function advanceToNextPlayer() {
|
|
663
|
+
saveCurrentPlayerState();
|
|
664
|
+
let next = -1;
|
|
665
|
+
for (let i = 1; i <= playerCount; i++) {
|
|
666
|
+
const idx = (currentPlayer + i) % playerCount;
|
|
667
|
+
if (players[idx].alive) { next = idx; break; }
|
|
668
|
+
}
|
|
669
|
+
if (next === -1) {
|
|
670
|
+
showFinalScoreboard();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
currentPlayer = next;
|
|
674
|
+
gameRunning = false;
|
|
675
|
+
showTurnReady();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function handlePlayerDeath() {
|
|
679
|
+
if (gameMode === "single") {
|
|
680
|
+
gameOver();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
// Solo turns: player lost all lives — they're done
|
|
684
|
+
lives--;
|
|
685
|
+
updateUI();
|
|
686
|
+
if (lives <= 0) {
|
|
687
|
+
saveCurrentPlayerState();
|
|
688
|
+
players[currentPlayer].alive = false;
|
|
689
|
+
gameRunning = false;
|
|
690
|
+
if (qualifiesForHighScore(score)) {
|
|
691
|
+
hsEntryPlayerIndex = currentPlayer;
|
|
692
|
+
showHighScoreEntry();
|
|
693
|
+
} else {
|
|
694
|
+
advanceToNextPlayer();
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
// Respawn ball from center
|
|
698
|
+
resetBall();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// === MENU NAVIGATION =========================================================
|
|
703
|
+
// Two phases: "mode" (Start Game / Pass & Play) and "count" (player count).
|
|
704
|
+
let menuCursor = 0;
|
|
705
|
+
|
|
706
|
+
function getMenuItems() {
|
|
707
|
+
if (startScreenPhase === "mode") return [document.getElementById("btn1P"), document.getElementById("btnPassPlay")];
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function updateMenuFocus() {
|
|
712
|
+
var all = document.querySelectorAll(".mode-btn.focused");
|
|
713
|
+
for (var i = 0; i < all.length; i++) all[i].classList.remove("focused");
|
|
714
|
+
var items = getMenuItems();
|
|
715
|
+
if (items.length > 0 && menuCursor >= 0 && menuCursor < items.length) {
|
|
716
|
+
items[menuCursor].classList.add("focused");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function showStartPhase(phase) {
|
|
721
|
+
startScreenPhase = phase;
|
|
722
|
+
menuCursor = 0;
|
|
723
|
+
var modeSelect = document.getElementById("modeSelect");
|
|
724
|
+
var pcSelect = document.getElementById("playerCountSelect");
|
|
725
|
+
modeSelect.style.display = phase === "mode" ? "flex" : "none";
|
|
726
|
+
pcSelect.style.display = "none";
|
|
727
|
+
pcSelect.classList.remove("visible");
|
|
728
|
+
if (phase === "count") {
|
|
729
|
+
pcSelect.style.display = "flex";
|
|
730
|
+
pcSelect.classList.add("visible");
|
|
731
|
+
}
|
|
732
|
+
updateMenuFocus();
|
|
733
|
+
updateBottomBar();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function resetStartScreen() {
|
|
737
|
+
var startScreen = document.getElementById("startScreen");
|
|
738
|
+
startScreen.style.display = "";
|
|
739
|
+
showStartPhase("mode");
|
|
740
|
+
playerCount = 2;
|
|
741
|
+
document.getElementById("pcNum").textContent = "2";
|
|
742
|
+
document.getElementById("gameOverScreen").style.display = "none";
|
|
743
|
+
hideFinalScoreboard();
|
|
744
|
+
hideTurnReady();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function menuConfirm() {
|
|
748
|
+
if (startScreenPhase === "mode") {
|
|
749
|
+
if (menuCursor === 0) startSinglePlayer();
|
|
750
|
+
else if (menuCursor === 1) showStartPhase("count");
|
|
751
|
+
} else if (startScreenPhase === "count") {
|
|
752
|
+
confirmPlayerCount();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function menuBack() {
|
|
757
|
+
if (startScreenPhase === "count") {
|
|
758
|
+
showStartPhase("mode");
|
|
759
|
+
menuCursor = 1; // return focus to "Pass & Play"
|
|
760
|
+
updateMenuFocus();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function menuMove(delta) {
|
|
765
|
+
var items = getMenuItems();
|
|
766
|
+
if (items.length === 0) return;
|
|
767
|
+
menuCursor = Math.max(0, Math.min(items.length - 1, menuCursor + delta));
|
|
768
|
+
updateMenuFocus();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function startSinglePlayer() {
|
|
772
|
+
document.getElementById("startScreen").style.display = "none";
|
|
773
|
+
gameMode = "single";
|
|
774
|
+
initGame();
|
|
775
|
+
gameRunning = true;
|
|
776
|
+
updateBottomBar();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function adjustPlayerCount(delta) {
|
|
780
|
+
playerCount = Math.max(2, Math.min(8, playerCount + delta));
|
|
781
|
+
document.getElementById("pcNum").textContent = playerCount;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function confirmPlayerCount() {
|
|
785
|
+
document.getElementById("startScreen").style.display = "none";
|
|
786
|
+
gameMode = "solo_turns";
|
|
787
|
+
initMultiplayerPlayers();
|
|
788
|
+
currentPlayer = 0;
|
|
789
|
+
showTurnReady();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// === GAME INIT ===============================================================
|
|
793
|
+
// CUSTOMIZE: This is where the game resets. Add your own game objects here.
|
|
794
|
+
function initStars() {
|
|
795
|
+
stars = [];
|
|
796
|
+
for (let i = 0; i < 100; i++)
|
|
797
|
+
stars.push({
|
|
798
|
+
x: Math.random() * canvas.width,
|
|
799
|
+
y: Math.random() * canvas.height,
|
|
800
|
+
size: 2 * Math.random() + 0.5,
|
|
801
|
+
speed: 0.5 * Math.random() + 0.1,
|
|
802
|
+
brightness: Math.random()
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function resetBall() {
|
|
807
|
+
ball.x = canvas.width / 2;
|
|
808
|
+
ball.y = canvas.height / 2;
|
|
809
|
+
// Launch at a random downward angle
|
|
810
|
+
var angle = Math.PI / 4 + Math.random() * Math.PI / 2; // 45° to 135° (downward)
|
|
811
|
+
ball.speed = BALL_INITIAL_SPEED;
|
|
812
|
+
ball.vx = Math.cos(angle) * ball.speed * (Math.random() > 0.5 ? 1 : -1);
|
|
813
|
+
ball.vy = Math.sin(angle) * ball.speed;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function initGame() {
|
|
817
|
+
// Center the paddle
|
|
818
|
+
paddle.x = (canvas.width - paddle.width) / 2;
|
|
819
|
+
// Reset ball
|
|
820
|
+
resetBall();
|
|
821
|
+
// Reset score and lives
|
|
822
|
+
score = 0;
|
|
823
|
+
lives = STARTING_LIVES;
|
|
824
|
+
// Clear particles
|
|
825
|
+
particles.clear();
|
|
826
|
+
// Create star field
|
|
827
|
+
initStars();
|
|
828
|
+
updateUI();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function formatScore(s) {
|
|
832
|
+
return String(s).padStart(5, "0");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function updateUI() {
|
|
836
|
+
document.getElementById("score").textContent = formatScore(score);
|
|
837
|
+
document.getElementById("lives").textContent = lives;
|
|
838
|
+
// Player indicator for multiplayer
|
|
839
|
+
const piEl = document.getElementById("playerIndicator");
|
|
840
|
+
if (gameMode !== "single") {
|
|
841
|
+
piEl.style.display = "inline";
|
|
842
|
+
piEl.textContent = "P" + (currentPlayer + 1) + " ";
|
|
843
|
+
piEl.style.color = getActivePlayerColor();
|
|
844
|
+
} else {
|
|
845
|
+
piEl.style.display = "none";
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// === FINAL SCOREBOARD ========================================================
|
|
850
|
+
function showFinalScoreboard() {
|
|
851
|
+
gameRunning = false;
|
|
852
|
+
const ranked = players.map(function(p, i) { return { index: i, score: p.score, initials: p.initials }; });
|
|
853
|
+
ranked.sort(function(a, b) { return b.score - a.score; });
|
|
854
|
+
document.getElementById("sbSubtitle").textContent = "SOLO TURNS \u2014 FINAL STANDINGS";
|
|
855
|
+
const tbody = document.getElementById("sbBody");
|
|
856
|
+
tbody.innerHTML = "";
|
|
857
|
+
ranked.forEach(function(r, rank) {
|
|
858
|
+
const tr = document.createElement("tr");
|
|
859
|
+
const color = PLAYER_COLORS[r.index];
|
|
860
|
+
const initStr = r.initials || "";
|
|
861
|
+
tr.innerHTML =
|
|
862
|
+
'<td class="sb-rank">' + (rank + 1) + '.</td>' +
|
|
863
|
+
'<td class="sb-player" style="color:' + color + ';text-shadow:0 0 10px ' + color + '">P' + (r.index + 1) + '</td>' +
|
|
864
|
+
'<td class="sb-initials">' + initStr + '</td>' +
|
|
865
|
+
'<td class="sb-score">' + r.score.toLocaleString() + '</td>';
|
|
866
|
+
tbody.appendChild(tr);
|
|
867
|
+
});
|
|
868
|
+
document.getElementById("finalScoreboard").classList.add("visible");
|
|
869
|
+
updateBottomBar();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function hideFinalScoreboard() {
|
|
873
|
+
document.getElementById("finalScoreboard").classList.remove("visible");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// === UPDATE() ================================================================
|
|
877
|
+
// CUSTOMIZE: This is the main game logic — called every frame.
|
|
878
|
+
// --- Paddle movement, ball physics, collisions, scoring, life loss ---
|
|
879
|
+
function update() {
|
|
880
|
+
// Gamepad pause toggle (edge-triggered, works even when paused)
|
|
881
|
+
if (gpPause() && gameRunning) {
|
|
882
|
+
gamePaused = !gamePaused;
|
|
883
|
+
if (gamePaused) pauseOverlay.classList.add("visible");
|
|
884
|
+
else pauseOverlay.classList.remove("visible");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (!gameRunning || gamePaused) return;
|
|
888
|
+
|
|
889
|
+
// --- Paddle movement ---
|
|
890
|
+
// Keyboard: A/Left and D/Right. Gamepad: left stick or d-pad.
|
|
891
|
+
if (keyboard.isDown("ArrowLeft") || keyboard.isDown("KeyA") || gamepad.left) {
|
|
892
|
+
paddle.x -= paddle.speed;
|
|
893
|
+
}
|
|
894
|
+
if (keyboard.isDown("ArrowRight") || keyboard.isDown("KeyD") || gamepad.right) {
|
|
895
|
+
paddle.x += paddle.speed;
|
|
896
|
+
}
|
|
897
|
+
// Clamp paddle to canvas bounds
|
|
898
|
+
if (paddle.x < 0) paddle.x = 0;
|
|
899
|
+
if (paddle.x + paddle.width > canvas.width) paddle.x = canvas.width - paddle.width;
|
|
900
|
+
|
|
901
|
+
// --- Ball movement ---
|
|
902
|
+
ball.x += ball.vx;
|
|
903
|
+
ball.y += ball.vy;
|
|
904
|
+
|
|
905
|
+
// --- Wall collisions (top, left, right) ---
|
|
906
|
+
// CUSTOMIZE: sfx.play('Wall_Bounce') — add a Wall_Bounce effect in Effects Studio
|
|
907
|
+
if (ball.x - BALL_RADIUS < 0) {
|
|
908
|
+
ball.x = BALL_RADIUS;
|
|
909
|
+
ball.vx = Math.abs(ball.vx);
|
|
910
|
+
if (sfxReady) sfx.play('Wall_Bounce');
|
|
911
|
+
}
|
|
912
|
+
if (ball.x + BALL_RADIUS > canvas.width) {
|
|
913
|
+
ball.x = canvas.width - BALL_RADIUS;
|
|
914
|
+
ball.vx = -Math.abs(ball.vx);
|
|
915
|
+
if (sfxReady) sfx.play('Wall_Bounce');
|
|
916
|
+
}
|
|
917
|
+
if (ball.y - BALL_RADIUS < 0) {
|
|
918
|
+
ball.y = BALL_RADIUS;
|
|
919
|
+
ball.vy = Math.abs(ball.vy);
|
|
920
|
+
if (sfxReady) sfx.play('Wall_Bounce');
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// --- Paddle collision ---
|
|
924
|
+
var paddleTop = canvas.height - PADDLE_Y_OFFSET;
|
|
925
|
+
if (ball.vy > 0 &&
|
|
926
|
+
ball.y + BALL_RADIUS >= paddleTop &&
|
|
927
|
+
ball.y + BALL_RADIUS <= paddleTop + paddle.height + ball.vy &&
|
|
928
|
+
ball.x >= paddle.x - BALL_RADIUS &&
|
|
929
|
+
ball.x <= paddle.x + paddle.width + BALL_RADIUS) {
|
|
930
|
+
// CUSTOMIZE: Ball angle depends on where it hits the paddle
|
|
931
|
+
// -1 at left edge, 0 at center, +1 at right edge
|
|
932
|
+
var hitPos = (ball.x - paddle.x) / paddle.width; // 0 to 1
|
|
933
|
+
var angle = -Math.PI / 2 + (hitPos - 0.5) * (Math.PI * 0.7); // -125° to -55°
|
|
934
|
+
// Speed up slightly on each hit
|
|
935
|
+
ball.speed = Math.min(ball.speed + BALL_SPEED_INCREMENT, BALL_MAX_SPEED);
|
|
936
|
+
ball.vx = Math.cos(angle) * ball.speed;
|
|
937
|
+
ball.vy = Math.sin(angle) * ball.speed;
|
|
938
|
+
ball.y = paddleTop - BALL_RADIUS;
|
|
939
|
+
// Score +1 per hit
|
|
940
|
+
score++;
|
|
941
|
+
updateUI();
|
|
942
|
+
// CUSTOMIZE: sfx.play('Paddle_Hit') — add a Paddle_Hit effect in Effects Studio
|
|
943
|
+
if (sfxReady) sfx.play('Paddle_Hit');
|
|
944
|
+
// CUSTOMIZE: Particles on paddle hit — change color/count for different effects
|
|
945
|
+
particles.emit(ball.x, paddleTop, 8, {
|
|
946
|
+
color: getActivePlayerColor(), speed: 3, size: 2,
|
|
947
|
+
lifetime: 0.5, decay: 0.03, spread: Math.PI,
|
|
948
|
+
direction: -Math.PI / 2
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// --- Ball falls past paddle (lose a life) ---
|
|
953
|
+
if (ball.y - BALL_RADIUS > canvas.height) {
|
|
954
|
+
// CUSTOMIZE: Screen shake, sound, and particles on life loss
|
|
955
|
+
// CUSTOMIZE: sfx.play('Life_Lost') — add a Life_Lost effect in Effects Studio
|
|
956
|
+
if (sfxReady) sfx.play('Life_Lost');
|
|
957
|
+
shake.trigger(12);
|
|
958
|
+
particles.emit(ball.x, canvas.height, 15, {
|
|
959
|
+
color: "#ff0000", speed: 4, size: 3,
|
|
960
|
+
lifetime: 0.8, decay: 0.02, spread: Math.PI,
|
|
961
|
+
direction: -Math.PI / 2
|
|
962
|
+
});
|
|
963
|
+
if (gameMode !== "single") {
|
|
964
|
+
handlePlayerDeath();
|
|
965
|
+
} else {
|
|
966
|
+
lives--;
|
|
967
|
+
updateUI();
|
|
968
|
+
if (lives <= 0) {
|
|
969
|
+
gameOver();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
resetBall();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// --- Update particles and stars ---
|
|
977
|
+
particles.update(1/60);
|
|
978
|
+
stars.forEach(function(s) {
|
|
979
|
+
s.y += s.speed;
|
|
980
|
+
if (s.y > canvas.height) { s.y = 0; s.x = Math.random() * canvas.width; }
|
|
981
|
+
s.brightness = 0.3 + 0.3 * Math.sin(0.003 * Date.now() + s.x);
|
|
982
|
+
});
|
|
983
|
+
shake.update(1/60);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// === DRAW() ==================================================================
|
|
987
|
+
// CUSTOMIZE: This renders everything to the canvas each frame.
|
|
988
|
+
// --- Stars, paddle (neon glow), ball (neon glow), particles ---
|
|
989
|
+
function draw() {
|
|
990
|
+
ctx.save();
|
|
991
|
+
if (shake.isActive) ctx.translate(shake.offsetX, shake.offsetY);
|
|
992
|
+
ctx.fillStyle = "#000";
|
|
993
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
994
|
+
|
|
995
|
+
// --- Stars ---
|
|
996
|
+
stars.forEach(function(s) {
|
|
997
|
+
ctx.fillStyle = "rgba(255, 255, 255, " + s.brightness + ")";
|
|
998
|
+
ctx.beginPath();
|
|
999
|
+
ctx.arc(s.x, s.y, s.size, 0, 2 * Math.PI);
|
|
1000
|
+
ctx.fill();
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// --- Particles ---
|
|
1004
|
+
particles.draw(ctx);
|
|
1005
|
+
|
|
1006
|
+
if (!gameRunning) {
|
|
1007
|
+
ctx.restore();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// --- Paddle (neon glow) ---
|
|
1012
|
+
var paddleColor = getActivePlayerColor();
|
|
1013
|
+
var paddleTop = canvas.height - PADDLE_Y_OFFSET;
|
|
1014
|
+
ctx.shadowColor = paddleColor;
|
|
1015
|
+
ctx.shadowBlur = 20;
|
|
1016
|
+
ctx.fillStyle = paddleColor;
|
|
1017
|
+
// Rounded rectangle paddle
|
|
1018
|
+
var r = paddle.height / 2;
|
|
1019
|
+
ctx.beginPath();
|
|
1020
|
+
ctx.moveTo(paddle.x + r, paddleTop);
|
|
1021
|
+
ctx.lineTo(paddle.x + paddle.width - r, paddleTop);
|
|
1022
|
+
ctx.arc(paddle.x + paddle.width - r, paddleTop + r, r, -Math.PI / 2, Math.PI / 2);
|
|
1023
|
+
ctx.lineTo(paddle.x + r, paddleTop + paddle.height);
|
|
1024
|
+
ctx.arc(paddle.x + r, paddleTop + r, r, Math.PI / 2, 3 * Math.PI / 2);
|
|
1025
|
+
ctx.closePath();
|
|
1026
|
+
ctx.fill();
|
|
1027
|
+
// Inner glow line
|
|
1028
|
+
ctx.strokeStyle = "#ffffff";
|
|
1029
|
+
ctx.lineWidth = 1;
|
|
1030
|
+
ctx.globalAlpha = 0.3;
|
|
1031
|
+
ctx.stroke();
|
|
1032
|
+
ctx.globalAlpha = 1;
|
|
1033
|
+
|
|
1034
|
+
// --- Ball (neon glow) ---
|
|
1035
|
+
ctx.shadowColor = "#ffffff";
|
|
1036
|
+
ctx.shadowBlur = 25;
|
|
1037
|
+
ctx.fillStyle = "#ffffff";
|
|
1038
|
+
ctx.beginPath();
|
|
1039
|
+
ctx.arc(ball.x, ball.y, BALL_RADIUS, 0, 2 * Math.PI);
|
|
1040
|
+
ctx.fill();
|
|
1041
|
+
// Outer glow ring
|
|
1042
|
+
ctx.strokeStyle = paddleColor;
|
|
1043
|
+
ctx.lineWidth = 2;
|
|
1044
|
+
ctx.shadowColor = paddleColor;
|
|
1045
|
+
ctx.shadowBlur = 15;
|
|
1046
|
+
ctx.beginPath();
|
|
1047
|
+
ctx.arc(ball.x, ball.y, BALL_RADIUS + 3, 0, 2 * Math.PI);
|
|
1048
|
+
ctx.stroke();
|
|
1049
|
+
|
|
1050
|
+
ctx.shadowBlur = 0;
|
|
1051
|
+
ctx.restore();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// === GAME OVER & HIGH SCORE ENTRY ============================================
|
|
1055
|
+
let hsEntryActive = false;
|
|
1056
|
+
let hsSlots = [0, 0, 0];
|
|
1057
|
+
let hsActiveSlot = 0;
|
|
1058
|
+
let hsFinalScore = 0;
|
|
1059
|
+
let hsEntryPlayerIndex = -1;
|
|
1060
|
+
|
|
1061
|
+
function gameOver() {
|
|
1062
|
+
gameRunning = false;
|
|
1063
|
+
// CUSTOMIZE: sfx.play('Game_Over') — add a Game_Over effect in Effects Studio
|
|
1064
|
+
if (sfxReady) sfx.play('Game_Over');
|
|
1065
|
+
if (qualifiesForHighScore(score)) {
|
|
1066
|
+
hsEntryPlayerIndex = -1;
|
|
1067
|
+
showHighScoreEntry();
|
|
1068
|
+
} else {
|
|
1069
|
+
showGameOverScreen();
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function showGameOverScreen() {
|
|
1074
|
+
document.getElementById("finalScore").textContent = score.toLocaleString();
|
|
1075
|
+
document.getElementById("gameOverScreen").style.display = "block";
|
|
1076
|
+
updateAllPrompts();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function showHighScoreEntry() {
|
|
1080
|
+
hsFinalScore = score;
|
|
1081
|
+
hsSlots = [0, 0, 0];
|
|
1082
|
+
hsActiveSlot = 0;
|
|
1083
|
+
hsEntryActive = true;
|
|
1084
|
+
document.getElementById("hsScore").textContent = score.toLocaleString();
|
|
1085
|
+
updateSlotDisplay();
|
|
1086
|
+
document.getElementById("highScoreEntry").style.display = "block";
|
|
1087
|
+
updateAllPrompts();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function updateSlotDisplay() {
|
|
1091
|
+
for (let i = 0; i < 3; i++) {
|
|
1092
|
+
const el = document.getElementById("slot" + i);
|
|
1093
|
+
el.textContent = String.fromCharCode(65 + hsSlots[i]);
|
|
1094
|
+
el.className = "initial-slot" + (i === hsActiveSlot ? " active" : "");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function confirmHighScore() {
|
|
1099
|
+
const initials = String.fromCharCode(65 + hsSlots[0]) +
|
|
1100
|
+
String.fromCharCode(65 + hsSlots[1]) +
|
|
1101
|
+
String.fromCharCode(65 + hsSlots[2]);
|
|
1102
|
+
insertHighScore(initials, hsFinalScore);
|
|
1103
|
+
hsEntryActive = false;
|
|
1104
|
+
document.getElementById("highScoreEntry").style.display = "none";
|
|
1105
|
+
if (hsEntryPlayerIndex >= 0 && gameMode !== "single") {
|
|
1106
|
+
players[hsEntryPlayerIndex].initials = initials;
|
|
1107
|
+
advanceToNextPlayer();
|
|
1108
|
+
} else {
|
|
1109
|
+
showGameOverScreen();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// === GAME LOOP ===============================================================
|
|
1114
|
+
// The main loop: polls input, handles turn-ready state, then updates and draws.
|
|
1115
|
+
var _turnStartPressed = false;
|
|
1116
|
+
|
|
1117
|
+
function gameLoop() {
|
|
1118
|
+
keyboard.poll();
|
|
1119
|
+
gamepad.poll();
|
|
1120
|
+
|
|
1121
|
+
// --- Turn ready overlay ---
|
|
1122
|
+
if (turnReady) {
|
|
1123
|
+
updateTurnPrompt();
|
|
1124
|
+
if (getTurnCooldownRemaining() <= 0) {
|
|
1125
|
+
if (gpConfirm() || _turnStartPressed) {
|
|
1126
|
+
_turnStartPressed = false;
|
|
1127
|
+
startPlayerTurn();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
draw();
|
|
1131
|
+
saveDirState();
|
|
1132
|
+
requestAnimationFrame(gameLoop);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// --- Gamepad menu/HS entry controls ---
|
|
1137
|
+
if (hsEntryActive) {
|
|
1138
|
+
if (gpUp()) { hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 1) % 26; updateSlotDisplay(); }
|
|
1139
|
+
if (gpDown()) { hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 25) % 26; updateSlotDisplay(); }
|
|
1140
|
+
if (gpLeft()) { hsActiveSlot = Math.max(0, hsActiveSlot - 1); updateSlotDisplay(); }
|
|
1141
|
+
if (gpRight()) { hsActiveSlot = Math.min(2, hsActiveSlot + 1); updateSlotDisplay(); }
|
|
1142
|
+
if (gpConfirm()) {
|
|
1143
|
+
if (hsActiveSlot < 2) { hsActiveSlot++; updateSlotDisplay(); }
|
|
1144
|
+
else confirmHighScore();
|
|
1145
|
+
}
|
|
1146
|
+
if (gpBack()) {
|
|
1147
|
+
if (hsActiveSlot > 0) { hsActiveSlot--; updateSlotDisplay(); }
|
|
1148
|
+
}
|
|
1149
|
+
} else if (!gameRunning) {
|
|
1150
|
+
// Start screen navigation with gamepad
|
|
1151
|
+
if (document.getElementById("startScreen").style.display !== "none") {
|
|
1152
|
+
if (startScreenPhase === "mode") {
|
|
1153
|
+
if (gpUp()) menuMove(-1);
|
|
1154
|
+
if (gpDown()) menuMove(1);
|
|
1155
|
+
if (gpConfirm()) menuConfirm();
|
|
1156
|
+
} else if (startScreenPhase === "count") {
|
|
1157
|
+
if (gpLeft()) adjustPlayerCount(-1);
|
|
1158
|
+
if (gpRight()) adjustPlayerCount(1);
|
|
1159
|
+
if (gpConfirm()) confirmPlayerCount();
|
|
1160
|
+
if (gpBack()) menuBack();
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Game over screen
|
|
1164
|
+
const gameOverScreen = document.getElementById("gameOverScreen");
|
|
1165
|
+
if (gameOverScreen.style.display === "block") {
|
|
1166
|
+
if (gpConfirm() || gpPause()) {
|
|
1167
|
+
gameOverScreen.style.display = "none";
|
|
1168
|
+
resetStartScreen();
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// Final scoreboard
|
|
1172
|
+
if (document.getElementById("finalScoreboard").classList.contains("visible")) {
|
|
1173
|
+
if (gpConfirm() || gpPause()) {
|
|
1174
|
+
resetStartScreen();
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
update();
|
|
1180
|
+
draw();
|
|
1181
|
+
saveDirState();
|
|
1182
|
+
requestAnimationFrame(gameLoop);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// === KEYBOARD HANDLER ========================================================
|
|
1186
|
+
// One-shot keyboard actions. Continuous movement is handled by keyboard.isDown().
|
|
1187
|
+
document.addEventListener("keydown", function(e) {
|
|
1188
|
+
// Turn ready: SPACE to start
|
|
1189
|
+
if (turnReady) {
|
|
1190
|
+
if (e.code === "Space") {
|
|
1191
|
+
_turnStartPressed = true;
|
|
1192
|
+
e.preventDefault();
|
|
1193
|
+
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// High score entry
|
|
1198
|
+
if (hsEntryActive) {
|
|
1199
|
+
if (e.code === "ArrowLeft") {
|
|
1200
|
+
hsActiveSlot = Math.max(0, hsActiveSlot - 1);
|
|
1201
|
+
updateSlotDisplay();
|
|
1202
|
+
} else if (e.code === "ArrowRight") {
|
|
1203
|
+
hsActiveSlot = Math.min(2, hsActiveSlot + 1);
|
|
1204
|
+
updateSlotDisplay();
|
|
1205
|
+
} else if (e.code === "ArrowUp") {
|
|
1206
|
+
hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 1) % 26;
|
|
1207
|
+
updateSlotDisplay();
|
|
1208
|
+
} else if (e.code === "ArrowDown") {
|
|
1209
|
+
hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 25) % 26;
|
|
1210
|
+
updateSlotDisplay();
|
|
1211
|
+
} else if (e.code === "Enter") {
|
|
1212
|
+
confirmHighScore();
|
|
1213
|
+
}
|
|
1214
|
+
e.preventDefault();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Start screen keyboard navigation
|
|
1219
|
+
if (!gameRunning && document.getElementById("startScreen").style.display !== "none") {
|
|
1220
|
+
if (startScreenPhase === "mode") {
|
|
1221
|
+
if (e.code === "ArrowUp") { menuMove(-1); e.preventDefault(); }
|
|
1222
|
+
if (e.code === "ArrowDown") { menuMove(1); e.preventDefault(); }
|
|
1223
|
+
if (e.code === "Space" || e.code === "Enter") { menuConfirm(); e.preventDefault(); }
|
|
1224
|
+
} else if (startScreenPhase === "count") {
|
|
1225
|
+
if (e.code === "ArrowLeft") { adjustPlayerCount(-1); e.preventDefault(); }
|
|
1226
|
+
if (e.code === "ArrowRight") { adjustPlayerCount(1); e.preventDefault(); }
|
|
1227
|
+
if (e.code === "Space" || e.code === "Enter") { confirmPlayerCount(); e.preventDefault(); }
|
|
1228
|
+
if (e.code === "Escape" || e.code === "Backspace") { menuBack(); e.preventDefault(); }
|
|
1229
|
+
}
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Pause toggle
|
|
1234
|
+
if (e.code === "KeyP" && gameRunning) {
|
|
1235
|
+
gamePaused = !gamePaused;
|
|
1236
|
+
if (gamePaused) pauseOverlay.classList.add("visible");
|
|
1237
|
+
else pauseOverlay.classList.remove("visible");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Game over: enter to go back to start
|
|
1241
|
+
const gameOverScreen = document.getElementById("gameOverScreen");
|
|
1242
|
+
if (!gameRunning && gameOverScreen.style.display === "block" && (e.code === "Space" || e.code === "Enter")) {
|
|
1243
|
+
gameOverScreen.style.display = "none";
|
|
1244
|
+
resetStartScreen();
|
|
1245
|
+
e.preventDefault();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Final scoreboard: enter to go back to start
|
|
1250
|
+
if (!gameRunning && document.getElementById("finalScoreboard").classList.contains("visible") && (e.code === "Space" || e.code === "Enter")) {
|
|
1251
|
+
resetStartScreen();
|
|
1252
|
+
e.preventDefault();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
|
|
1257
|
+
e.preventDefault();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// === CLICK HANDLERS & INIT ===================================================
|
|
1262
|
+
document.getElementById("btn1P").addEventListener("click", startSinglePlayer);
|
|
1263
|
+
document.getElementById("btnPassPlay").addEventListener("click", function() { showStartPhase("count"); });
|
|
1264
|
+
document.getElementById("btn1P").addEventListener("mouseenter", function() { menuCursor = 0; updateMenuFocus(); });
|
|
1265
|
+
document.getElementById("btnPassPlay").addEventListener("mouseenter", function() { menuCursor = 1; updateMenuFocus(); });
|
|
1266
|
+
document.getElementById("pcLeft").addEventListener("click", function() { adjustPlayerCount(-1); });
|
|
1267
|
+
document.getElementById("pcRight").addEventListener("click", function() { adjustPlayerCount(1); });
|
|
1268
|
+
document.getElementById("pcConfirm").addEventListener("click", confirmPlayerCount);
|
|
1269
|
+
document.getElementById("sbPlayAgain").addEventListener("click", function() { resetStartScreen(); });
|
|
1270
|
+
document.getElementById("restartBtn").addEventListener("click", function() {
|
|
1271
|
+
document.getElementById("gameOverScreen").style.display = "none";
|
|
1272
|
+
resetStartScreen();
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Initialize
|
|
1276
|
+
updateAllPrompts();
|
|
1277
|
+
initStars();
|
|
1278
|
+
gameLoop();
|
|
1279
|
+
</script>
|
|
1280
|
+
|
|
1281
|
+
</body></html>
|