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