synthos 0.7.2 → 0.9.0

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 (380) hide show
  1. package/README.md +215 -65
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1822 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/oregon_trail/page.html +323 -0
  27. package/default-pages/oregon_trail/page.json +12 -0
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_builder.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +24 -29
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +4 -4
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_builder.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/us_map/page.html +193 -0
  39. package/default-pages/us_map/page.json +12 -0
  40. package/default-pages/us_map_1850/page.html +326 -0
  41. package/default-pages/us_map_1850/page.json +12 -0
  42. package/default-pages/western_cities_1850/page.html +527 -0
  43. package/default-pages/western_cities_1850/page.json +12 -0
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +134 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +128 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
  65. package/dist/agents/a2a/a2aProvider.js +126 -0
  66. package/dist/agents/a2a/a2aProvider.js.map +1 -0
  67. package/dist/agents/discovery.d.ts.map +1 -0
  68. package/dist/agents/discovery.js +52 -0
  69. package/dist/agents/discovery.js.map +1 -0
  70. package/dist/agents/index.d.ts +7 -0
  71. package/dist/agents/index.d.ts.map +1 -0
  72. package/dist/agents/index.js +20 -0
  73. package/dist/agents/index.js.map +1 -0
  74. package/dist/agents/openclaw/gatewayManager.d.ts +117 -0
  75. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
  76. package/dist/agents/openclaw/gatewayManager.js +486 -0
  77. package/dist/agents/openclaw/gatewayManager.js.map +1 -0
  78. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
  79. package/dist/agents/openclaw/openclawProvider.js +237 -0
  80. package/dist/agents/openclaw/openclawProvider.js.map +1 -0
  81. package/dist/agents/openclaw/sshTunnelManager.d.ts +25 -0
  82. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
  83. package/dist/agents/openclaw/sshTunnelManager.js +359 -0
  84. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
  85. package/dist/agents/types.d.ts.map +1 -0
  86. package/dist/agents/types.js +6 -0
  87. package/dist/agents/types.js.map +1 -0
  88. package/dist/builders/anthropic.d.ts +31 -0
  89. package/dist/builders/anthropic.d.ts.map +1 -0
  90. package/dist/builders/anthropic.js +227 -0
  91. package/dist/builders/anthropic.js.map +1 -0
  92. package/dist/builders/fireworksai.d.ts +9 -0
  93. package/dist/builders/fireworksai.d.ts.map +1 -0
  94. package/dist/builders/fireworksai.js +57 -0
  95. package/dist/builders/fireworksai.js.map +1 -0
  96. package/dist/builders/index.d.ts +13 -0
  97. package/dist/builders/index.d.ts.map +1 -0
  98. package/dist/builders/index.js +31 -0
  99. package/dist/builders/index.js.map +1 -0
  100. package/dist/builders/openai.d.ts +8 -0
  101. package/dist/builders/openai.d.ts.map +1 -0
  102. package/dist/builders/openai.js +87 -0
  103. package/dist/builders/openai.js.map +1 -0
  104. package/dist/builders/types.d.ts +54 -0
  105. package/dist/builders/types.d.ts.map +1 -0
  106. package/dist/builders/types.js +211 -0
  107. package/dist/builders/types.js.map +1 -0
  108. package/dist/connectors/index.d.ts.map +1 -1
  109. package/dist/connectors/index.js +3 -2
  110. package/dist/connectors/index.js.map +1 -1
  111. package/dist/connectors/registry.d.ts +2 -1
  112. package/dist/connectors/registry.d.ts.map +1 -1
  113. package/dist/connectors/registry.js +65 -96
  114. package/dist/connectors/registry.js.map +1 -1
  115. package/dist/connectors/types.d.ts.map +1 -1
  116. package/dist/customizer/Customizer.d.ts +57 -0
  117. package/dist/customizer/Customizer.d.ts.map +1 -0
  118. package/dist/customizer/Customizer.js +124 -0
  119. package/dist/customizer/Customizer.js.map +1 -0
  120. package/dist/customizer/index.d.ts.map +1 -0
  121. package/dist/customizer/index.js +9 -0
  122. package/dist/customizer/index.js.map +1 -0
  123. package/dist/files.d.ts +17 -0
  124. package/dist/files.d.ts.map +1 -1
  125. package/dist/files.js +75 -1
  126. package/dist/files.js.map +1 -1
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +1 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/init.d.ts +10 -6
  131. package/dist/init.d.ts.map +1 -1
  132. package/dist/init.js +97 -86
  133. package/dist/init.js.map +1 -1
  134. package/dist/migrations.d.ts.map +1 -1
  135. package/dist/migrations.js +142 -145
  136. package/dist/migrations.js.map +1 -1
  137. package/dist/models/anthropic.d.ts +24 -0
  138. package/dist/models/anthropic.d.ts.map +1 -0
  139. package/dist/models/anthropic.js +103 -0
  140. package/dist/models/anthropic.js.map +1 -0
  141. package/dist/models/chainOfThought.d.ts.map +1 -0
  142. package/dist/models/chainOfThought.js +45 -0
  143. package/dist/models/chainOfThought.js.map +1 -0
  144. package/dist/models/fireworksai.d.ts.map +1 -0
  145. package/dist/models/fireworksai.js +141 -0
  146. package/dist/models/fireworksai.js.map +1 -0
  147. package/dist/models/index.d.ts +7 -1
  148. package/dist/models/index.d.ts.map +1 -1
  149. package/dist/models/index.js +20 -1
  150. package/dist/models/index.js.map +1 -1
  151. package/dist/models/logCompletePrompt.d.ts.map +1 -0
  152. package/dist/models/logCompletePrompt.js +23 -0
  153. package/dist/models/logCompletePrompt.js.map +1 -0
  154. package/dist/models/openai.d.ts +24 -0
  155. package/dist/models/openai.d.ts.map +1 -0
  156. package/dist/models/openai.js +101 -0
  157. package/dist/models/openai.js.map +1 -0
  158. package/dist/models/providers.d.ts.map +1 -1
  159. package/dist/models/providers.js +12 -4
  160. package/dist/models/providers.js.map +1 -1
  161. package/dist/models/types.d.ts +53 -2
  162. package/dist/models/types.d.ts.map +1 -1
  163. package/dist/models/types.js +21 -0
  164. package/dist/models/types.js.map +1 -1
  165. package/dist/models/utils.d.ts.map +1 -0
  166. package/dist/models/utils.js +21 -0
  167. package/dist/models/utils.js.map +1 -0
  168. package/dist/pages.d.ts +30 -7
  169. package/dist/pages.d.ts.map +1 -1
  170. package/dist/pages.js +177 -55
  171. package/dist/pages.js.map +1 -1
  172. package/dist/scripts.d.ts.map +1 -1
  173. package/dist/scripts.js +4 -3
  174. package/dist/scripts.js.map +1 -1
  175. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  176. package/dist/service/createCompletePrompt.js +9 -6
  177. package/dist/service/createCompletePrompt.js.map +1 -1
  178. package/dist/service/generateImage.d.ts.map +1 -1
  179. package/dist/service/generateImage.js +3 -3
  180. package/dist/service/generateImage.js.map +1 -1
  181. package/dist/service/server.d.ts.map +1 -1
  182. package/dist/service/server.js +39 -7
  183. package/dist/service/server.js.map +1 -1
  184. package/dist/service/transformPage.d.ts +47 -18
  185. package/dist/service/transformPage.d.ts.map +1 -1
  186. package/dist/service/transformPage.js +559 -270
  187. package/dist/service/transformPage.js.map +1 -1
  188. package/dist/service/useAgentRoutes.d.ts +5 -0
  189. package/dist/service/useAgentRoutes.d.ts.map +1 -0
  190. package/dist/service/useAgentRoutes.js +392 -0
  191. package/dist/service/useAgentRoutes.js.map +1 -0
  192. package/dist/service/useApiRoutes.d.ts.map +1 -1
  193. package/dist/service/useApiRoutes.js +380 -138
  194. package/dist/service/useApiRoutes.js.map +1 -1
  195. package/dist/service/useConnectorRoutes.d.ts.map +1 -1
  196. package/dist/service/useConnectorRoutes.js +20 -9
  197. package/dist/service/useConnectorRoutes.js.map +1 -1
  198. package/dist/service/useFileRoutes.d.ts +4 -0
  199. package/dist/service/useFileRoutes.d.ts.map +1 -0
  200. package/dist/service/useFileRoutes.js +122 -0
  201. package/dist/service/useFileRoutes.js.map +1 -0
  202. package/dist/service/usePageRoutes.d.ts.map +1 -1
  203. package/dist/service/usePageRoutes.js +660 -68
  204. package/dist/service/usePageRoutes.js.map +1 -1
  205. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  206. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  207. package/dist/service/useSharedDataRoutes.js +104 -0
  208. package/dist/service/useSharedDataRoutes.js.map +1 -0
  209. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  210. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  211. package/dist/service/useSharedFileRoutes.js +121 -0
  212. package/dist/service/useSharedFileRoutes.js.map +1 -0
  213. package/dist/settings.d.ts +3 -1
  214. package/dist/settings.d.ts.map +1 -1
  215. package/dist/settings.js +5 -8
  216. package/dist/settings.js.map +1 -1
  217. package/dist/synthos-cli.d.ts.map +1 -1
  218. package/dist/synthos-cli.js +4 -3
  219. package/dist/synthos-cli.js.map +1 -1
  220. package/dist/themes.d.ts +15 -0
  221. package/dist/themes.d.ts.map +1 -1
  222. package/dist/themes.js +106 -20
  223. package/dist/themes.js.map +1 -1
  224. package/migration-rules/v1-to-v2.md +193 -0
  225. package/migration-rules/v2-to-v3.md +481 -0
  226. package/package.json +15 -11
  227. package/required-pages/builder/page.html +43 -0
  228. package/required-pages/builder/page.json +10 -0
  229. package/required-pages/pages/page.html +924 -0
  230. package/required-pages/pages/page.json +10 -0
  231. package/required-pages/settings/page.html +1753 -0
  232. package/required-pages/settings/page.json +10 -0
  233. package/required-pages/synthos_apis/page.html +846 -0
  234. package/required-pages/synthos_apis/page.json +10 -0
  235. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  236. package/required-pages/synthos_scripts/page.json +10 -0
  237. package/service-connectors/airtable/connector.json +27 -0
  238. package/service-connectors/alpha-vantage/connector.json +26 -0
  239. package/service-connectors/brave-search/connector.json +26 -0
  240. package/service-connectors/cloudinary/connector.json +27 -0
  241. package/service-connectors/deepl/connector.json +28 -0
  242. package/service-connectors/elevenlabs/connector.json +30 -0
  243. package/service-connectors/giphy/connector.json +27 -0
  244. package/service-connectors/github/connector.json +29 -0
  245. package/service-connectors/huggingface/connector.json +27 -0
  246. package/service-connectors/imgur/connector.json +29 -0
  247. package/service-connectors/instagram/connector.json +43 -0
  248. package/service-connectors/jira/connector.json +28 -0
  249. package/service-connectors/mapbox/connector.json +26 -0
  250. package/service-connectors/nasa/connector.json +27 -0
  251. package/service-connectors/newsapi/connector.json +27 -0
  252. package/service-connectors/notion/connector.json +28 -0
  253. package/service-connectors/open-exchange-rates/connector.json +27 -0
  254. package/service-connectors/openweathermap/connector.json +26 -0
  255. package/service-connectors/pexels/connector.json +27 -0
  256. package/service-connectors/resend/connector.json +29 -0
  257. package/service-connectors/rss2json/connector.json +27 -0
  258. package/service-connectors/sendgrid/connector.json +27 -0
  259. package/service-connectors/spoonacular/connector.json +28 -0
  260. package/service-connectors/stability-ai/connector.json +27 -0
  261. package/service-connectors/twilio/connector.json +28 -0
  262. package/service-connectors/unsplash/connector.json +27 -0
  263. package/service-connectors/wolfram-alpha/connector.json +26 -0
  264. package/service-connectors/youtube-data/connector.json +30 -0
  265. package/src/agents/a2a/a2aProvider.ts +110 -0
  266. package/src/agents/discovery.ts +74 -0
  267. package/src/agents/index.ts +6 -0
  268. package/src/agents/openclaw/gatewayManager.ts +570 -0
  269. package/src/agents/openclaw/openclawProvider.ts +259 -0
  270. package/src/agents/openclaw/sshTunnelManager.ts +393 -0
  271. package/src/agents/types.ts +82 -0
  272. package/src/builders/anthropic.ts +283 -0
  273. package/src/builders/fireworksai.ts +59 -0
  274. package/src/builders/index.ts +33 -0
  275. package/src/builders/openai.ts +89 -0
  276. package/src/builders/types.ts +261 -0
  277. package/src/connectors/index.ts +3 -1
  278. package/src/connectors/registry.ts +40 -96
  279. package/src/connectors/types.ts +25 -0
  280. package/src/customizer/Customizer.ts +151 -0
  281. package/src/customizer/index.ts +5 -0
  282. package/src/files.ts +71 -0
  283. package/src/index.ts +2 -1
  284. package/src/init.ts +138 -97
  285. package/src/migrations.ts +148 -145
  286. package/src/models/anthropic.ts +119 -0
  287. package/src/models/chainOfThought.ts +56 -0
  288. package/src/models/fireworksai.ts +143 -0
  289. package/src/models/index.ts +7 -1
  290. package/src/models/logCompletePrompt.ts +25 -0
  291. package/src/models/openai.ts +110 -0
  292. package/src/models/providers.ts +12 -3
  293. package/src/models/types.ts +97 -2
  294. package/src/models/utils.ts +16 -0
  295. package/src/pages.ts +176 -54
  296. package/src/scripts.ts +2 -2
  297. package/src/service/createCompletePrompt.ts +3 -1
  298. package/src/service/generateImage.ts +2 -2
  299. package/src/service/server.ts +39 -8
  300. package/src/service/transformPage.ts +605 -301
  301. package/src/service/useAgentRoutes.ts +428 -0
  302. package/src/service/useApiRoutes.ts +309 -45
  303. package/src/service/useConnectorRoutes.ts +21 -10
  304. package/src/service/useFileRoutes.ts +127 -0
  305. package/src/service/usePageRoutes.ts +736 -75
  306. package/src/service/useSharedDataRoutes.ts +106 -0
  307. package/src/service/useSharedFileRoutes.ts +126 -0
  308. package/src/settings.ts +8 -10
  309. package/src/synthos-cli.ts +4 -3
  310. package/src/themes.ts +103 -20
  311. package/static-files/favicon.svg +12 -0
  312. package/static-files/fluentlm-instructions.llmd +868 -0
  313. package/static-files/fluentlm-instructions.md +1595 -0
  314. package/static-files/fluentlm.css +4844 -0
  315. package/static-files/fluentlm.js +3602 -0
  316. package/static-files/fluentlm.min.css +1 -0
  317. package/static-files/fluentlm.min.js +1 -0
  318. package/static-files/helpers.v3.js +304 -0
  319. package/static-files/page.v3.js +1290 -0
  320. package/static-files/recommended-frameworks.llmd +81 -0
  321. package/static-files/recommended-frameworks.md +137 -0
  322. package/static-files/retro-game.js +877 -0
  323. package/static-files/shell.css +797 -0
  324. package/static-files/theme-dark.css +169 -0
  325. package/static-files/theme-light.css +169 -0
  326. package/tests/anthropic.spec.ts +84 -0
  327. package/tests/builders.spec.ts +139 -0
  328. package/tests/chainOfThought.spec.ts +108 -0
  329. package/tests/ensureScripts.spec.ts +82 -0
  330. package/tests/files.spec.ts +233 -0
  331. package/tests/fireworksai.spec.ts +92 -0
  332. package/tests/logCompletePrompt.spec.ts +74 -0
  333. package/tests/migrations.spec.ts +79 -1
  334. package/tests/openai.spec.ts +71 -0
  335. package/tests/pages.spec.ts +226 -1
  336. package/tests/providers.spec.ts +144 -0
  337. package/tests/scripts.spec.ts +209 -0
  338. package/tests/transformPage.spec.ts +456 -0
  339. package/tests/types.spec.ts +23 -0
  340. package/default-pages/app_builder.html +0 -40
  341. package/default-pages/app_builder.json +0 -1
  342. package/default-pages/json_tools.json +0 -1
  343. package/default-pages/my_notes.html +0 -33
  344. package/default-pages/neon_asteroids.html +0 -77
  345. package/default-pages/sidebar_builder.json +0 -1
  346. package/default-pages/solar_tutorial.json +0 -1
  347. package/default-pages/two-panel_builder.json +0 -1
  348. package/dist/connectors/index.d.ts +0 -3
  349. package/dist/connectors/types.d.ts +0 -61
  350. package/dist/index.d.ts +0 -7
  351. package/dist/migrations.d.ts +0 -11
  352. package/dist/models/providers.d.ts +0 -7
  353. package/dist/scripts.d.ts +0 -14
  354. package/dist/service/createCompletePrompt.d.ts +0 -5
  355. package/dist/service/debugLog.d.ts +0 -11
  356. package/dist/service/generateImage.d.ts +0 -32
  357. package/dist/service/index.d.ts +0 -8
  358. package/dist/service/modelInstructions.d.ts +0 -7
  359. package/dist/service/requiresSettings.d.ts +0 -3
  360. package/dist/service/server.d.ts +0 -4
  361. package/dist/service/useApiRoutes.d.ts +0 -4
  362. package/dist/service/useConnectorRoutes.d.ts +0 -4
  363. package/dist/service/useDataRoutes.d.ts +0 -4
  364. package/dist/service/usePageRoutes.d.ts +0 -5
  365. package/dist/synthos-cli.d.ts +0 -2
  366. package/images/home.png +0 -0
  367. package/images/page-management.png +0 -0
  368. package/images/settings.png +0 -0
  369. package/images/synthos-square.png +0 -0
  370. package/page-scripts/helpers-v2.js +0 -121
  371. package/page-scripts/page-v2.js +0 -615
  372. package/required-pages/builder.html +0 -74
  373. package/required-pages/builder.json +0 -1
  374. package/required-pages/pages.html +0 -196
  375. package/required-pages/pages.json +0 -1
  376. package/required-pages/settings.html +0 -841
  377. package/required-pages/settings.json +0 -1
  378. package/required-pages/synthos_apis.html +0 -272
  379. package/required-pages/synthos_apis.json +0 -1
  380. package/required-pages/synthos_scripts.json +0 -1
@@ -0,0 +1,1822 @@
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
+ .level-display,.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
+ /* Sub-mode selection */
39
+ .submode-select{display:none;flex-direction:column;align-items:center;gap:12px;margin-top:16px}
40
+ .submode-select.visible{display:flex}
41
+ .submode-title{font-family:Orbitron,'Segoe UI',sans-serif;font-size:14px;color:rgba(0,255,255,.7);letter-spacing:2px;margin-bottom:4px}
42
+ .submode-btn{padding:10px 30px;font-size:14px;min-width:220px;font-family:Orbitron,'Segoe UI',sans-serif;background:0 0;border:2px solid #0ff;color:#0ff;cursor:pointer;text-transform:uppercase;letter-spacing:2px;transition:.3s;box-shadow:0 0 15px rgba(0,255,255,.2),inset 0 0 15px rgba(0,255,255,.08)}
43
+ .submode-btn:hover{background:rgba(0,255,255,.2);box-shadow:0 0 25px rgba(0,255,255,.5);transform:scale(1.03)}
44
+ .submode-desc{font-size:11px;color:rgba(0,255,255,.45);font-family:'Segoe UI',sans-serif;max-width:280px;text-align:center;margin-top:-4px}
45
+
46
+ /* Side panels */
47
+ .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}
48
+ .side-panel.hidden{display:none}
49
+ .side-panel h2{font-size:16px;letter-spacing:3px;margin:0 0 16px 0;text-align:center;font-weight:700}
50
+ #side-leaderboard{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px rgba(255,0,255,.4)}
51
+ #side-leaderboard h2{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px #f0f}
52
+ #side-controls{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px rgba(0,255,255,.4)}
53
+ #side-controls h2{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px #0ff}
54
+ .leaderboard-table{width:100%;border-collapse:collapse;font-family:'Courier New',monospace;font-size:13px}
55
+ .leaderboard-table td{padding:4px 0;white-space:nowrap}
56
+ .leaderboard-table .rank{width:24px;color:rgba(255,0,255,.6);text-align:right;padding-right:6px}
57
+ .leaderboard-table .initials{color:#f0f;letter-spacing:2px}
58
+ .leaderboard-table .hs-score{text-align:right;color:rgba(255,0,255,.7);position:relative;cursor:default}
59
+ .leaderboard-table .hs-score.overflow{animation:scoreFlash 0.8s ease-in-out infinite}
60
+ .leaderboard-table .hs-score[title]:hover::after{content:attr(title);position:absolute;right:0;top:-22px;background:#1a0028;color:#f0f;font-size:11px;padding:2px 6px;border:1px solid rgba(255,0,255,.4);border-radius:3px;white-space:nowrap;z-index:5;pointer-events:none}
61
+ @keyframes scoreFlash{0%,100%{opacity:1}50%{opacity:.4}}
62
+ .score-display .score-overflow{animation:hudScoreFlash 0.8s ease-in-out infinite}
63
+ @keyframes hudScoreFlash{0%,100%{color:#0ff;text-shadow:0 0 10px #0ff,0 0 20px #0ff}50%{color:#f0f;text-shadow:0 0 10px #f0f,0 0 20px #f0f}}
64
+ .controls-list{list-style:none;padding:0;margin:0;font-size:12px}
65
+ .controls-list li{padding:6px 0;display:flex;gap:8px;align-items:center;color:#0ff}
66
+ .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}
67
+ .controls-list .key-desc{color:rgba(0,255,255,.7);font-family:'Segoe UI',sans-serif}
68
+ .bt-section{margin-top:auto;padding-top:16px;border-top:1px solid rgba(0,255,255,.15)}
69
+ .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}
70
+ .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}
71
+ .side-panel{overflow-y:auto}
72
+ #side-leaderboard{padding-top:20px}
73
+ #side-controls{padding-top:20px}
74
+
75
+ /* Gamepad status indicator */
76
+ #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}
77
+ #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}
78
+ @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)}}
79
+
80
+ /* Pause overlay */
81
+ #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}
82
+ #pauseOverlay.visible{display:flex}
83
+ #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}
84
+ #pauseOverlay .pause-hint{font-family:'Segoe UI',sans-serif;font-size:14px;color:rgba(0,255,255,.5);letter-spacing:1px}
85
+
86
+ /* High score entry */
87
+ #highScoreEntry{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;z-index:30;display:none}
88
+ #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}
89
+ #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}
90
+ .initial-slots{display:flex;justify-content:center;gap:12px;margin-bottom:20px}
91
+ .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)}
92
+ .initial-slot.active{border-color:#0ff;color:#0ff;text-shadow:0 0 10px #0ff;box-shadow:0 0 15px rgba(0,255,255,.5)}
93
+ .hs-instructions{font-size:12px;color:rgba(0,255,255,.6);font-family:'Segoe UI',sans-serif;margin-bottom:16px}
94
+
95
+ /* Keyboard key hint (for bottom bar) */
96
+ .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}
97
+ /* Focused menu item */
98
+ .mode-btn.focused,.submode-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)}
99
+
100
+ /* Turn ready overlay */
101
+ #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)}
102
+ #turnReadyOverlay.visible{display:flex}
103
+ #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}
104
+ #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}
105
+ #turnReadyOverlay .turn-prompt{font-family:'Segoe UI',sans-serif;font-size:16px;color:rgba(0,255,255,.6);letter-spacing:1px}
106
+
107
+ /* Final scoreboard overlay */
108
+ #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)}
109
+ #finalScoreboard.visible{display:flex}
110
+ #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}
111
+ #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}
112
+ .sb-table{border-collapse:collapse;font-family:Orbitron,'Segoe UI',sans-serif;font-size:18px}
113
+ .sb-table td{padding:8px 16px}
114
+ .sb-table .sb-rank{color:rgba(255,255,255,.5);text-align:right;font-size:14px}
115
+ .sb-table .sb-player{letter-spacing:3px}
116
+ .sb-table .sb-initials{font-size:14px;color:rgba(255,255,255,.5);letter-spacing:2px}
117
+ .sb-table .sb-score{text-align:right;color:#fff;letter-spacing:2px}
118
+ .sb-play-again{margin-top:16px}
119
+ </style>
120
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&amp;display=swap" rel="stylesheet">
121
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
122
+ </head>
123
+
124
+ <body>
125
+ <div class="shell-toolbar" data-locked="true">
126
+ <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"></path><circle cx="8.5" cy="11.5" r="1" fill="currentColor"></circle><circle cx="12" cy="11.5" r="1" fill="currentColor"></circle><circle cx="15.5" cy="11.5" r="1" fill="currentColor"></circle></svg></button>
127
+ <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"></rect><path d="M6 7.5h5M6 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><rect x="18" y="3" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"></rect><path d="M21 7.5h5M21 10h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><rect x="3" y="18" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"></rect><path d="M6 22.5h5M6 25h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><rect x="18" y="18" width="11" height="12" rx="1.5" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"></rect><path d="M21 22.5h5M21 25h3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg></button>
128
+ <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><path d="M17 21v-8H7v8" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"></path><path d="M7 3v5h8" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"></path></svg></button>
129
+ <div class="shell-toolbar-spacer" data-locked="true"></div>
130
+ <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><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"></path></svg></button>
131
+ </div>
132
+ <div class="chat-panel" data-locked="true">
133
+ <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">×</button></div>
134
+ <div class="chat-messages" id="chatMessages" data-locked="true">
135
+ <div class="chat-message">
136
+ <p><strong>SynthOS:</strong> <b>NEON ASTEROIDS</b> &mdash; Destroy asteroids, dodge saucers, collect power-ups. Controls: <b>WASD/Arrows</b> to move, <b>Space</b> to fire, <b>H</b> for hyperspace, <b>P</b> to pause. Gamepad supported. Up to 4 players in Pass &amp; Play mode.</p>
137
+ <p>Want to build your own retro game? Start from the <a href="/retro_game_starter">Retro Game Starter</a> — it gives you a ready-made canvas, game loop, and input handling so you can jump straight into designing your game.</p>
138
+ </div>
139
+ <div class="chat-message"><p><strong>User:</strong> Integrate sound effects from the uploaded effects.json manifest into the game — laser fire, explosions, thrust, hyperspace, power-ups, saucer alerts, menu navigation, and ambient space sounds.</p></div><div class="chat-message"><p><strong>SynthOS:</strong> Sound effects integrated! Here's what I added:
140
+ <br>• <b>Laser Fire</b> — plays each time you shoot
141
+ <br>• <b>Asteroid Explosion</b> — plays when asteroids are destroyed
142
+ <br>• <b>Ship Thrust</b> — loops while you're thrusting, stops when you release
143
+ <br>• <b>Hyperspace Jump</b> — plays on teleport
144
+ <br>• <b>Power-Up Collect</b> — plays when you pick up any power-up
145
+ <br>• <b>Saucer Alert</b> — plays when an enemy saucer spawns
146
+ <br>• <b>Menu Navigate</b> — plays when moving through menu options
147
+ <br>• <b>Ambient Space</b> — three ambient tracks randomly cycle as background atmosphere during gameplay
148
+ <br><br>All sounds load asynchronously on page load. The ambient soundscape starts when gameplay begins and stops on game over or return to menu. Close the chat panel and start a mission to hear them!</p></div><div class="chat-message"><p><strong>User:</strong> the sound effects aren't playing</p></div><div class="chat-message"><p><strong>SynthOS:</strong> Let me check the sound system. I've added error handling to diagnose the issue. Open your browser's console (F12) and look for messages about sound loading.<br><br>Common issues:<br>• <b>Missing manifest</b> — The effects.json file needs to be uploaded to this page's file storage<br>• <b>Missing audio files</b> — Each sound referenced in effects.json must also be uploaded<br>• <b>Browser autoplay policy</b> — Some browsers block audio until user interaction<br><br>To fix:<br>1. Make sure effects.json is uploaded via the Files API<br>2. Upload all referenced .mp3/.ogg files<br>3. Check the console for specific error messages<br><br>The sound system will now log detailed loading info to help debug!</p></div><div class="chat-message"><p><strong>User:</strong> Fix the JavaScript errors — SyntaxError: Unexpected end of input and TypeError: Cannot read properties of null.</p></div><div class="chat-message"><p><strong>SynthOS:</strong> Found and fixed the bug! The <code>stopAmbient()</code> function was missing its closing brace <code>}</code>, which caused a SyntaxError that prevented the entire game script from parsing. This cascaded into the null reference error in <code>gameLoop()</code>. The game should now load and run correctly.</p></div><div class="chat-message"><p><strong>User:</strong> Fix the JavaScript error: SyntaxError: Identifier 'sfx' has already been declared</p></div><div class="chat-message"><p><strong>SynthOS:</strong> Fixed! The issue was that the inline game script used <code>const</code> and <code>let</code> for top-level variable declarations. Since <code>retro-game.js</code> (the library) likely declares some of the same identifiers, using <code>const</code>/<code>let</code> causes a "already been declared" error because they don't allow redeclaration in the same scope. I've changed all top-level <code>const</code> and <code>let</code> declarations to <code>var</code>, which allows redeclaration without errors. The game should now load and run correctly.</p></div><div class="save-line" data-locked="true"><span class="save-line-label">Saved</span></div></div>
149
+
150
+ <form action="/" method="POST" id="chatForm" data-locked="true">
151
+ <textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..." data-locked="true"></textarea>
152
+ </form>
153
+ </div>
154
+ <div class="viewer-panel full-viewer" id="viewerPanel" style="background:#000;">
155
+ <div id="game-wrapper">
156
+ <div id="side-leaderboard" class="side-panel hidden">
157
+ <h2>HIGH SCORES</h2>
158
+ <table class="leaderboard-table">
159
+ <tbody id="leaderboardBody"></tbody>
160
+ </table>
161
+ </div>
162
+ <div id="game-container">
163
+ <canvas id="gameCanvas"></canvas>
164
+ <div class="game-ui">
165
+ <div class="score-display"><span id="playerIndicator" class="player-indicator" style="display:none"></span>SCORE: <span id="score">00000</span></div>
166
+ <div class="level-display">LEVEL: <span id="level">1</span></div>
167
+ <div class="lives-display">LIVES: <span id="lives">3</span></div>
168
+ <button class="mute-btn" id="muteBtn" title="Toggle sound">&#128266;</button>
169
+ </div>
170
+ <div id="pauseOverlay"><span class="pause-title">PAUSED</span><span class="pause-hint" id="pauseHint"></span></div>
171
+ <div id="turnReadyOverlay">
172
+ <div class="turn-player" id="turnPlayerName">PLAYER 1</div>
173
+ <div class="turn-subtitle">YOUR TURN</div>
174
+ <div class="turn-prompt" id="turnPrompt">Press SPACE to start</div>
175
+ </div>
176
+ <div id="finalScoreboard">
177
+ <div class="sb-title">GAME OVER</div>
178
+ <div class="sb-subtitle" id="sbSubtitle">FINAL STANDINGS</div>
179
+ <table class="sb-table"><tbody id="sbBody"></tbody></table>
180
+ <button class="restart-btn sb-play-again" id="sbPlayAgain">PLAY AGAIN</button>
181
+ </div>
182
+ <div class="start-screen" id="startScreen">
183
+ <h1>NEON<br>ASTEROIDS</h1>
184
+ <p>Navigate the cosmic void. Destroy all asteroids. Beware the saucers!</p>
185
+ <div class="mode-select" id="modeSelect">
186
+ <button class="mode-btn" id="btn1P">START MISSION</button>
187
+ <button class="mode-btn" id="btnPassPlay">PASS &amp; PLAY</button>
188
+ </div>
189
+ <div class="submode-select" id="submodeSelect">
190
+ <div class="submode-title">CHOOSE MODE</div>
191
+ <button class="submode-btn" id="btnSoloTurns">SOLO TURNS</button>
192
+ <div class="submode-desc">Each player gets a fresh game. Highest total score wins.</div>
193
+ <button class="submode-btn" id="btnHotSeat">HOT SEAT</button>
194
+ <div class="submode-desc">Shared asteroid field. Rotate on each death. Last one standing!</div>
195
+ </div>
196
+ <div class="player-count-select" id="playerCountSelect">
197
+ <div class="player-count-label">PLAYERS</div>
198
+ <div class="player-count-row">
199
+ <span class="player-count-arrow" id="pcLeft">←</span>
200
+ <span class="player-count-num" id="pcNum">2</span>
201
+ <span class="player-count-arrow" id="pcRight">→</span>
202
+ </div>
203
+ <button class="mode-btn player-count-confirm" id="pcConfirm">START</button>
204
+ </div>
205
+ </div>
206
+ <div class="game-over-screen" id="gameOverScreen" style="display: none;">
207
+ <h1>GAME OVER</h1>
208
+ <p>Final Score: <span id="finalScore">0</span></p>
209
+ <button class="restart-btn" id="restartBtn">TRY AGAIN</button>
210
+ </div>
211
+ <div id="highScoreEntry">
212
+ <h2>NEW HIGH SCORE!</h2>
213
+ <div class="hs-final-score">SCORE: <span id="hsScore">0</span></div>
214
+ <div class="initial-slots">
215
+ <div class="initial-slot active" id="slot0">A</div>
216
+ <div class="initial-slot" id="slot1">A</div>
217
+ <div class="initial-slot" id="slot2">A</div>
218
+ </div>
219
+ <div class="hs-instructions" id="hsInstructions">LEFT/RIGHT select slot • UP/DOWN change letter • ENTER confirm</div>
220
+ </div>
221
+ <div class="controls-info" id="controlsInfo">WASD / ARROWS to move • SPACE to fire • H hyperspace • P pause</div>
222
+ </div>
223
+ <div id="side-controls" class="side-panel hidden">
224
+ <div id="gamepadStatus">NO GAMEPAD</div>
225
+ <h2>CONTROLS</h2>
226
+ <ul class="controls-list" id="controlsList">
227
+ <li><span class="key-icon">W/↑</span><span class="key-desc">Thrust</span></li>
228
+ <li><span class="key-icon">A/←</span><span class="key-desc">Rotate Left</span></li>
229
+ <li><span class="key-icon">D/→</span><span class="key-desc">Rotate Right</span></li>
230
+ <li><span class="key-icon">SPACE</span><span class="key-desc">Fire</span></li>
231
+ <li><span class="key-icon">H</span><span class="key-desc">Hyperspace</span></li>
232
+ <li><span class="key-icon">P</span><span class="key-desc">Pause</span></li>
233
+ </ul>
234
+ <h2 style="margin-top:24px">POWER-UPS</h2>
235
+ <ul class="controls-list">
236
+ <li><span class="key-icon" style="color:#0ff;border-color:rgba(0,255,255,.4)">S</span><span class="key-desc">Shield</span></li>
237
+ <li><span class="key-icon" style="color:#f00;border-color:rgba(255,0,0,.4)">R</span><span class="key-desc">Rapid Fire</span></li>
238
+ <li><span class="key-icon" style="color:#ff0;border-color:rgba(255,255,0,.4)">M</span><span class="key-desc">Multi-Shot</span></li>
239
+ <li><span class="key-icon" style="color:#0f0;border-color:rgba(0,255,0,.4)">+</span><span class="key-desc">Extra Life</span></li>
240
+ </ul>
241
+ <div class="bt-section" id="btSection"></div>
242
+ </div>
243
+ </div>
244
+ <div id="loadingOverlay" class="loading-overlay">
245
+ <div class="spinner"></div>
246
+ </div>
247
+ </div>
248
+ <div id="instructions" style="display: none;" data-locked="true"></div>
249
+ <div id="thoughts" style="display: none;" data-locked="true">User reports a SyntaxError: "Identifier 'sfx' has already been declared". This means there are two script blocks both declaring `const sfx = ...`. Looking at the page, I see:
250
+ 1. The retro-game.js library is loaded via `<script src="/static/retro-game.js">` (node 211)
251
+ 2. The inline script (node 212) declares `const sfx = RetroGame.createSoundEffects();`
252
+
253
+ The retro-game.js library likely also declares or exports an `sfx` variable, OR there's a duplicate declaration somewhere. Since the error is at position 1:27863 which is a very high column number, it's likely in the inline script block.
254
+
255
+ Actually, looking more carefully - the issue is that `retro-game.js` (the library) probably injects or declares `sfx` as well. Or perhaps the page was saved with duplicate script blocks.
256
+
257
+ The fix: Change `const sfx` to `var sfx` so it can be redeclared, or wrap the sound effects code to avoid the conflict. Actually, the better approach is to check if there's a duplicate. Since I can only see one inline script block (node 212), the conflict must be with something in retro-game.js.
258
+
259
+ Let me change `const sfx` to use a different variable name or use `var` instead, and also change all other `const` declarations at the top level to `var` to avoid similar issues if the script somehow gets evaluated twice. Actually, the simplest fix is to just use `var` for the sfx declaration since `var` allows redeclaration in the same scope.</script></div>
260
+ <script src="/static/retro-game.js"></script>
261
+ <script id="asteroids-game">
262
+ RetroGame.injectCSS();
263
+
264
+ // --- Sound Effects ---
265
+ var sfx = RetroGame.createSoundEffects();
266
+ var sfxReady = false;
267
+ sfx.load().then(function() {
268
+ console.log('✓ Sound effects loaded successfully');
269
+ sfxReady = true;
270
+ }).catch(function(e) {
271
+ console.error('✗ Sound effects failed to load:', e);
272
+ console.error('Make sure effects.json exists in page file storage');
273
+ console.error('Upload it via: synthos.files.upload("effects.json", blob)');
274
+ });
275
+
276
+ // Mute toggle
277
+ var sfxMuted = false;
278
+ document.getElementById("muteBtn").addEventListener("click", function() {
279
+ sfxMuted = !sfxMuted;
280
+ sfx.volume = sfxMuted ? 0 : 1;
281
+ this.innerHTML = sfxMuted ? "\u{1F507}" : "\u{1F50A}";
282
+ });
283
+
284
+ var ambientPlaying = false;
285
+ var ambientTimeout = null;
286
+ var AMBIENT_TRACKS = ['Ambient_Space', 'Ambient_Space2', 'Ambient_Space3'];
287
+ var currentAmbientIndex = -1;
288
+
289
+ function startAmbient() {
290
+ if (ambientPlaying) return;
291
+ ambientPlaying = true;
292
+ playNextAmbient();
293
+ }
294
+
295
+ function playNextAmbient() {
296
+ if (!ambientPlaying || !sfxReady) return;
297
+ // Pick a random track different from the last one
298
+ let idx;
299
+ do { idx = Math.floor(Math.random() * AMBIENT_TRACKS.length); } while (idx === currentAmbientIndex && AMBIENT_TRACKS.length > 1);
300
+ currentAmbientIndex = idx;
301
+ sfx.play(AMBIENT_TRACKS[idx]);
302
+ }
303
+
304
+ function stopAmbient() {
305
+ ambientPlaying = false;
306
+ if (ambientTimeout) { clearTimeout(ambientTimeout); ambientTimeout = null; }
307
+ if (sfxReady) AMBIENT_TRACKS.forEach(function(t) { sfx.stop(t); });
308
+ currentAmbientIndex = -1;
309
+ }
310
+
311
+ var canvas = document.getElementById("gameCanvas");
312
+ var ctx = canvas.getContext("2d");
313
+ var viewerPanel = document.getElementById("viewerPanel");
314
+ var gameContainer = document.getElementById("game-container");
315
+ var sideLeaderboard = document.getElementById("side-leaderboard");
316
+ var sideControls = document.getElementById("side-controls");
317
+ var pauseOverlay = document.getElementById("pauseOverlay");
318
+ var ASPECT = 4 / 3;
319
+ var MAX_SCORE = 99999;
320
+
321
+
322
+ // Player colors: P1=cyan, P2=magenta, P3=yellow, P4=green
323
+ var PLAYER_COLORS = ["#00ffff", "#ff00ff", "#ffff00", "#00ff00", "#ff6600", "#ff3388", "#33ccff", "#ccff33"];
324
+
325
+ var resizeTimer = 0;
326
+
327
+ function resizeCanvas() {
328
+ const vw = viewerPanel.clientWidth;
329
+ const vh = viewerPanel.clientHeight;
330
+ if (vw < 1 || vh < 1) return;
331
+ const panelRatio = vw / vh;
332
+ let w, h;
333
+ if (panelRatio > ASPECT) {
334
+ h = vh;
335
+ w = Math.floor(h * ASPECT);
336
+ } else {
337
+ w = vw;
338
+ h = Math.floor(w / ASPECT);
339
+ }
340
+ gameContainer.style.width = w + "px";
341
+ gameContainer.style.height = h + "px";
342
+ canvas.width = w;
343
+ canvas.height = h;
344
+ const extraSpace = vw - w;
345
+ if (extraSpace > 400) {
346
+ sideLeaderboard.classList.remove("hidden");
347
+ sideControls.classList.remove("hidden");
348
+ } else {
349
+ sideLeaderboard.classList.add("hidden");
350
+ sideControls.classList.add("hidden");
351
+ }
352
+ }
353
+
354
+ function debouncedResize() {
355
+ clearTimeout(resizeTimer);
356
+ resizeCanvas();
357
+ resizeTimer = setTimeout(resizeCanvas, 300);
358
+ }
359
+
360
+ resizeCanvas();
361
+ window.addEventListener("resize", resizeCanvas);
362
+ new MutationObserver(debouncedResize).observe(document.body, { attributes: true, attributeFilter: ["class"] });
363
+
364
+ // --- Gamepad input (library) ---
365
+ var gamepad = RetroGame.createGamepad({ deadzone: 0.3 });
366
+ var B = RetroGame.BUTTON;
367
+ var gamepadUI = RetroGame.createGamepadUI(gamepad, { statusElement: document.getElementById("gamepadStatus") });
368
+
369
+ // Game-specific gamepad action mappings
370
+ function gpFire() { return gamepad.isDown(B.A) || gamepad.isDown(B.X) || gamepad.isDown(B.RB) || gamepad.isDown(B.RT); }
371
+ function gpHyper() { return gamepad.wasPressed(B.B) || gamepad.wasPressed(B.LB); }
372
+ function gpPause() { return gamepad.wasPressed(B.START); }
373
+ function gpConfirm() { return gamepad.wasPressed(B.A); }
374
+ function gpBack() { return gamepad.wasPressed(B.B); }
375
+
376
+ // Edge-triggered directional helpers (stick + dpad)
377
+ // Track previous directional state for stick-based edge detection
378
+ var _prevDir = { up: false, down: false, left: false, right: false };
379
+ function gpUp() { var r = gamepad.up && !_prevDir.up; return r; }
380
+ function gpDown() { var r = gamepad.down && !_prevDir.down; return r; }
381
+ function gpLeft() { var r = gamepad.left && !_prevDir.left; return r; }
382
+ function gpRight() { var r = gamepad.right && !_prevDir.right; return r; }
383
+ function saveDirState() { _prevDir.up = gamepad.up; _prevDir.down = gamepad.down; _prevDir.left = gamepad.left; _prevDir.right = gamepad.right; }
384
+
385
+ // Delegate to library UI
386
+ function gpBtn(name) { return gamepadUI.buttonGlyph(name); }
387
+ function gpBtnName(name) { return gamepadUI.buttonName(name); }
388
+ function gpStartName() { return gamepadUI.startName(); }
389
+
390
+ // Returns styled HTML for a non-face button label (game-specific, not in library)
391
+ function gpLabel(text) {
392
+ return '<span class="gp-label">' + text + '</span>';
393
+ }
394
+
395
+ // --- Gamepad-aware text helper ---
396
+ function btnText(kbLabel, gpLabelText) {
397
+ return gamepad.connected ? gpLabelText : kbLabel;
398
+ }
399
+
400
+ // Keyboard key styled span
401
+ function kbKey(text) {
402
+ return '<span class="kb-key">' + text + '</span>';
403
+ }
404
+
405
+ // Build an action item for the bottom bar
406
+ function actionItem(hint, label) {
407
+ return '<span class="action-item">' + hint + ' ' + label + '</span>';
408
+ }
409
+
410
+ // --- Unified bottom bar: menu nav OR game controls ---
411
+ function updateBottomBar() {
412
+ var bar = document.getElementById("controlsInfo");
413
+ var startVisible = document.getElementById("startScreen").style.display !== "none";
414
+ var gameOverVisible = document.getElementById("gameOverScreen").style.display === "block";
415
+ var scoreboardVisible = document.getElementById("finalScoreboard").classList.contains("visible");
416
+
417
+ // Menu screens — show navigation hints
418
+ if (startVisible) {
419
+ if (gamepad.connected) {
420
+ if (startScreenPhase === "mode") {
421
+ bar.innerHTML =
422
+ actionItem(gpLabel("\u2191\u2193"), "Navigate") +
423
+ actionItem(gpBtn("a"), "Select");
424
+ } else if (startScreenPhase === "submode") {
425
+ bar.innerHTML =
426
+ actionItem(gpLabel("\u2191\u2193"), "Navigate") +
427
+ actionItem(gpBtn("a"), "Select") +
428
+ actionItem(gpBtn("b"), "Back");
429
+ } else if (startScreenPhase === "count") {
430
+ bar.innerHTML =
431
+ actionItem(gpLabel("\u2190\u2192"), "Players") +
432
+ actionItem(gpBtn("a"), "Start") +
433
+ actionItem(gpBtn("b"), "Back");
434
+ }
435
+ } else {
436
+ if (startScreenPhase === "mode") {
437
+ bar.innerHTML =
438
+ actionItem(kbKey("\u2191\u2193"), "Navigate") +
439
+ actionItem(kbKey("ENTER"), "Select");
440
+ } else if (startScreenPhase === "submode") {
441
+ bar.innerHTML =
442
+ actionItem(kbKey("\u2191\u2193"), "Navigate") +
443
+ actionItem(kbKey("ENTER"), "Select") +
444
+ actionItem(kbKey("ESC"), "Back");
445
+ } else if (startScreenPhase === "count") {
446
+ bar.innerHTML =
447
+ actionItem(kbKey("\u2190\u2192"), "Players") +
448
+ actionItem(kbKey("ENTER"), "Start") +
449
+ actionItem(kbKey("ESC"), "Back");
450
+ }
451
+ }
452
+ return;
453
+ }
454
+
455
+ // High score entry
456
+ if (hsEntryActive) {
457
+ if (gamepad.connected) {
458
+ bar.innerHTML =
459
+ actionItem(gpLabel("\u2191\u2193"), "Letter") +
460
+ actionItem(gpBtn("a"), "Next") +
461
+ actionItem(gpBtn("b"), "Back");
462
+ } else {
463
+ bar.innerHTML =
464
+ actionItem(kbKey("\u2191\u2193"), "Letter") +
465
+ actionItem(kbKey("\u2190\u2192"), "Slot") +
466
+ actionItem(kbKey("ENTER"), "Confirm");
467
+ }
468
+ return;
469
+ }
470
+
471
+ // Game over / scoreboard
472
+ if (gameOverVisible || scoreboardVisible) {
473
+ if (gamepad.connected) {
474
+ bar.innerHTML = actionItem(gpBtn("a"), "Continue");
475
+ } else {
476
+ bar.innerHTML = actionItem(kbKey("ENTER"), "Continue");
477
+ }
478
+ return;
479
+ }
480
+
481
+ // In-game controls
482
+ if (gamepad.connected) {
483
+ bar.innerHTML =
484
+ actionItem(gpLabel("L-Stick"), "Move") +
485
+ actionItem(gpBtn("a"), "Fire") +
486
+ actionItem(gpBtn("b"), "Hyperspace") +
487
+ actionItem(gpLabel(gpStartName()), "Pause");
488
+ } else {
489
+ bar.innerHTML =
490
+ actionItem(kbKey("WASD"), "Move") +
491
+ actionItem(kbKey("SPACE"), "Fire") +
492
+ actionItem(kbKey("H"), "Hyperspace") +
493
+ actionItem(kbKey("P"), "Pause");
494
+ }
495
+ }
496
+
497
+ function updateAllPrompts() {
498
+ // Start screen buttons
499
+ document.getElementById("btn1P").textContent = gamepad.connected ? "1 PLAYER" : "START MISSION";
500
+ document.getElementById("btnPassPlay").textContent = "PASS & PLAY";
501
+ document.getElementById("btnSoloTurns").textContent = "SOLO TURNS";
502
+ document.getElementById("btnHotSeat").textContent = "HOT SEAT";
503
+ // Focus highlight
504
+ updateMenuFocus();
505
+ // Restart button
506
+ var restartBtn = document.getElementById("restartBtn");
507
+ if (gamepad.connected) {
508
+ restartBtn.innerHTML = gpBtn("a") + ' RETRY';
509
+ } else {
510
+ restartBtn.textContent = "TRY AGAIN";
511
+ }
512
+ // HS instructions
513
+ document.getElementById("hsInstructions").innerHTML = btnText(
514
+ "LEFT/RIGHT select slot &bull; UP/DOWN change letter &bull; ENTER confirm",
515
+ gpLabel("\u2191\u2193") + " change letter &bull; " + gpBtn("a") + " next &bull; " + gpBtn("b") + " back"
516
+ );
517
+ // Pause hint
518
+ document.getElementById("pauseHint").innerHTML = btnText(
519
+ "P to resume",
520
+ gpLabel(gpStartName()) + " to resume"
521
+ );
522
+ // Turn ready prompt
523
+ document.getElementById("turnPrompt").innerHTML = btnText(
524
+ "Press SPACE to start",
525
+ "Press " + gpBtn("a") + " to start"
526
+ );
527
+ // Player count confirm
528
+ var pcConfirm = document.getElementById("pcConfirm");
529
+ if (gamepad.connected) {
530
+ pcConfirm.innerHTML = gpBtn("a") + ' START';
531
+ } else {
532
+ pcConfirm.textContent = "START";
533
+ }
534
+ // Scoreboard play again
535
+ var sbBtn = document.getElementById("sbPlayAgain");
536
+ if (gamepad.connected) {
537
+ sbBtn.innerHTML = gpBtn("a") + ' PLAY AGAIN';
538
+ } else {
539
+ sbBtn.textContent = "PLAY AGAIN";
540
+ }
541
+ // Bottom bar
542
+ updateBottomBar();
543
+ }
544
+
545
+ // --- High Score System (synthos.data, top 20) ---
546
+ var MAX_LEADERBOARD = 20;
547
+ var _highScores = [{ initials: "ACE", score: 10000 }];
548
+
549
+ // Load scores from app table storage on startup
550
+ (async function() {
551
+ try {
552
+ var row = await synthos.data.get('high_scores', 'scores');
553
+ if (row && Array.isArray(row.entries) && row.entries.length > 0) {
554
+ _highScores = row.entries;
555
+ }
556
+ } catch(e) {}
557
+ renderLeaderboard();
558
+ })();
559
+
560
+ function loadHighScores() {
561
+ return _highScores;
562
+ }
563
+
564
+ function saveHighScores(scores) {
565
+ _highScores = scores;
566
+ synthos.data.save('high_scores', { id: 'scores', entries: scores }).catch(function() {});
567
+ }
568
+
569
+ function qualifiesForHighScore(s) {
570
+ var scores = loadHighScores();
571
+ return scores.length < MAX_LEADERBOARD || s > scores[scores.length - 1].score;
572
+ }
573
+
574
+ function insertHighScore(initials, s) {
575
+ var scores = loadHighScores().slice();
576
+ scores.push({ initials: initials, score: s });
577
+ scores.sort(function(a, b) { return b.score - a.score; });
578
+ if (scores.length > MAX_LEADERBOARD) scores.length = MAX_LEADERBOARD;
579
+ saveHighScores(scores);
580
+ renderLeaderboard();
581
+ }
582
+
583
+ function formatLeaderboardScore(s) {
584
+ if (s > MAX_SCORE) {
585
+ return { text: "99,999+", overflow: true };
586
+ }
587
+ return { text: s.toLocaleString(), overflow: false };
588
+ }
589
+
590
+ function renderLeaderboard() {
591
+ var scores = loadHighScores();
592
+ var tbody = document.getElementById("leaderboardBody");
593
+ tbody.innerHTML = "";
594
+ scores.forEach(function(entry, i) {
595
+ var tr = document.createElement("tr");
596
+ var fmt = formatLeaderboardScore(entry.score);
597
+ var overflowClass = fmt.overflow ? " overflow" : "";
598
+ var titleAttr = fmt.overflow ? ' title="' + entry.score.toLocaleString() + '"' : "";
599
+ tr.innerHTML =
600
+ '<td class="rank">' + (i + 1) + '.</td>' +
601
+ '<td class="initials">' + entry.initials + '</td>' +
602
+ '<td class="hs-score' + overflowClass + '"' + titleAttr + '>' + fmt.text + '</td>';
603
+ tbody.appendChild(tr);
604
+ });
605
+ }
606
+
607
+ renderLeaderboard();
608
+
609
+ // --- BT Controller Instructions (platform-aware) ---
610
+ (function() {
611
+ const el = document.getElementById("btSection");
612
+ const isMac = /Mac|iPhone|iPad/.test(navigator.platform) || /Macintosh/.test(navigator.userAgent);
613
+ if (isMac) {
614
+ el.innerHTML =
615
+ '<h3>GAMEPAD</h3>' +
616
+ '<p>1. Put controller in pairing mode</p>' +
617
+ '<p>2. Open <b>System Settings &gt; Bluetooth</b></p>' +
618
+ '<p>3. Find controller in list, click <b>Connect</b></p>' +
619
+ '<p>4. Reload page once paired</p>';
620
+ } else {
621
+ el.innerHTML =
622
+ '<h3>GAMEPAD</h3>' +
623
+ '<p>1. Put controller in pairing mode</p>' +
624
+ '<p>2. Open <b>Settings &gt; Bluetooth &amp; devices</b></p>' +
625
+ '<p>3. Click <b>Add device &gt; Bluetooth</b></p>' +
626
+ '<p>4. Select controller, then reload page</p>';
627
+ }
628
+ })();
629
+
630
+ // --- Gamepad Detection & Controls Swap ---
631
+ var KEYBOARD_CONTROLS = '<li><span class="key-icon">W/\u2191</span><span class="key-desc">Thrust</span></li>' +
632
+ '<li><span class="key-icon">A/\u2190</span><span class="key-desc">Rotate Left</span></li>' +
633
+ '<li><span class="key-icon">D/\u2192</span><span class="key-desc">Rotate Right</span></li>' +
634
+ '<li><span class="key-icon">SPACE</span><span class="key-desc">Fire</span></li>' +
635
+ '<li><span class="key-icon">H</span><span class="key-desc">Hyperspace</span></li>' +
636
+ '<li><span class="key-icon">P</span><span class="key-desc">Pause</span></li>';
637
+
638
+ function buildGamepadControls() {
639
+ return '<li><span class="key-icon">L-Stick</span><span class="key-desc">Thrust / Rotate</span></li>' +
640
+ '<li><span class="key-icon">D-Pad</span><span class="key-desc">Thrust / Rotate</span></li>' +
641
+ '<li><span class="key-icon">' + gpBtnName("a") + ' / RB</span><span class="key-desc">Fire</span></li>' +
642
+ '<li><span class="key-icon">' + gpBtnName("b") + ' / LB</span><span class="key-desc">Hyperspace</span></li>' +
643
+ '<li><span class="key-icon">' + gpStartName() + '</span><span class="key-desc">Pause</span></li>';
644
+ }
645
+
646
+ function refreshGamepadUI() {
647
+ gamepadUI.update();
648
+ var listEl = document.getElementById("controlsList");
649
+ var btEl = document.getElementById("btSection");
650
+ if (gamepad.connected) {
651
+ listEl.innerHTML = buildGamepadControls();
652
+ btEl.style.display = "none";
653
+ } else {
654
+ listEl.innerHTML = KEYBOARD_CONTROLS;
655
+ btEl.style.display = "";
656
+ }
657
+ updateAllPrompts();
658
+ }
659
+
660
+ gamepad.onConnect = function() { refreshGamepadUI(); };
661
+ gamepad.onDisconnect = function() { refreshGamepadUI(); };
662
+
663
+ // --- Game Mode State ---
664
+ var gameMode = "single"; // "single", "solo_turns", "hot_seat"
665
+ var playerCount = 2;
666
+ var currentPlayer = 0;
667
+ var players = []; // {score, lives, level, alive, initials}
668
+ var turnReady = false;
669
+ var startScreenPhase = "mode"; // "mode", "submode", "count"
670
+
671
+ // --- Game State ---
672
+ var gameRunning = false, gamePaused = false, score = 0, lives = 3, level = 1;
673
+ var shake = RetroGame.createShake();
674
+ var hyperspaceCooldown = 0;
675
+ var hyperspaceInvincible = 0;
676
+
677
+ var ship = {
678
+ x: 0, y: 0, vx: 0, vy: 0, angle: -Math.PI / 2,
679
+ radius: 15, thrust: 0, rotationSpeed: 0,
680
+ invincible: 0, shield: 0, rapidFire: 0, multiShot: 0
681
+ };
682
+
683
+ var bullets = [], asteroids = [], powerUps = [], stars = [];
684
+ var saucers = [], saucerBullets = [], saucerSpawnTimer = 0;
685
+ var keyboard = RetroGame.createKeyboard();
686
+ var particles = RetroGame.createParticles({ maxParticles: 500 });
687
+
688
+ // --- High Score Entry State ---
689
+ var hsEntryActive = false;
690
+ var hsSlots = [0, 0, 0];
691
+ var hsActiveSlot = 0;
692
+ var hsFinalScore = 0;
693
+ var hsEntryPlayerIndex = -1; // track which player is entering HS in multiplayer
694
+
695
+ function getActivePlayerColor() {
696
+ if (gameMode === "single") return PLAYER_COLORS[0];
697
+ return PLAYER_COLORS[currentPlayer] || PLAYER_COLORS[0];
698
+ }
699
+
700
+ function initStars() {
701
+ stars = [];
702
+ for (let i = 0; i < 150; i++)
703
+ stars.push({
704
+ x: Math.random() * canvas.width,
705
+ y: Math.random() * canvas.height,
706
+ size: 2 * Math.random() + 0.5,
707
+ speed: 0.5 * Math.random() + 0.1,
708
+ brightness: Math.random()
709
+ });
710
+ }
711
+
712
+ function initGame() {
713
+ ship.x = canvas.width / 2;
714
+ ship.y = canvas.height / 2;
715
+ ship.vx = 0; ship.vy = 0;
716
+ ship.angle = -Math.PI / 2;
717
+ ship.invincible = 180;
718
+ ship.shield = 0; ship.rapidFire = 0; ship.multiShot = 0;
719
+ bullets = []; asteroids = []; powerUps = [];
720
+ particles.clear();
721
+ saucers = []; saucerBullets = []; saucerSpawnTimer = 0;
722
+ hyperspaceCooldown = 0; hyperspaceInvincible = 0;
723
+ score = 0; lives = 3; level = 1;
724
+ initStars();
725
+ spawnAsteroids(4);
726
+ updateUI();
727
+ }
728
+
729
+ // Init game for hot seat (preserves asteroids/level across player swaps)
730
+ function initHotSeatRespawn() {
731
+ ship.x = canvas.width / 2;
732
+ ship.y = canvas.height / 2;
733
+ ship.vx = 0; ship.vy = 0;
734
+ ship.angle = -Math.PI / 2;
735
+ ship.invincible = 180;
736
+ ship.shield = 0; ship.rapidFire = 0; ship.multiShot = 0;
737
+ bullets = [];
738
+ saucerBullets = [];
739
+ hyperspaceCooldown = 0; hyperspaceInvincible = 0;
740
+ shootCooldown = 0;
741
+ }
742
+
743
+ function initMultiplayerPlayers() {
744
+ players = [];
745
+ for (let i = 0; i < playerCount; i++) {
746
+ players.push({ score: 0, lives: 3, level: 1, alive: true, initials: "" });
747
+ }
748
+ currentPlayer = 0;
749
+ }
750
+
751
+ function formatScore(s) {
752
+ return String(s).padStart(5, "0");
753
+ }
754
+
755
+ function addScore(points) {
756
+ score += points;
757
+ }
758
+
759
+ function getLevelSpeedMultiplier() {
760
+ return 1 + 0.15 * (level - 1);
761
+ }
762
+
763
+ function spawnAsteroids(count) {
764
+ for (let i = 0; i < count; i++) {
765
+ let x, y;
766
+ do {
767
+ x = Math.random() * canvas.width;
768
+ y = Math.random() * canvas.height;
769
+ } while (distance(x, y, ship.x, ship.y) < 150);
770
+ asteroids.push(createAsteroid(x, y, 3));
771
+ }
772
+ }
773
+
774
+ function createAsteroid(x, y, size) {
775
+ const speed = (0.5 * (4 - size) + 1.5 * Math.random()) * getLevelSpeedMultiplier();
776
+ const angle = Math.random() * Math.PI * 2;
777
+ const radii = [];
778
+ const points = 8 + Math.floor(5 * Math.random());
779
+ const baseRadius = 15 * size + 10;
780
+ for (let i = 0; i < points; i++)
781
+ radii.push(baseRadius * (0.7 + 0.6 * Math.random()));
782
+ return {
783
+ x, y,
784
+ vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
785
+ size, radii, points, rotation: 0,
786
+ rotationSpeed: 0.03 * (Math.random() - 0.5),
787
+ color: size === 3 ? "#ff00ff" : size === 2 ? "#ffff00" : "#00ffff"
788
+ };
789
+ }
790
+
791
+ function createSaucer() {
792
+ const isSmall = Math.random() > 0.5;
793
+ const fromLeft = Math.random() > 0.5;
794
+ const y = Math.random() * (canvas.height - 100) + 50;
795
+ return {
796
+ x: fromLeft ? -30 : canvas.width + 30, y,
797
+ vx: (fromLeft ? 1 : -1) * (isSmall ? 3 : 2) * getLevelSpeedMultiplier(),
798
+ vy: 0, isSmall,
799
+ radius: isSmall ? 15 : 25,
800
+ shootTimer: 60 + 60 * Math.random(),
801
+ directionChangeTimer: 60 + 120 * Math.random(),
802
+ points: isSmall ? 1000 : 500
803
+ };
804
+ }
805
+
806
+ function distance(x1, y1, x2, y2) {
807
+ return RetroGame.distance({x: x1, y: y1}, {x: x2, y: y2});
808
+ }
809
+
810
+ function wrap(obj) {
811
+ RetroGame.wrap(obj, canvas.width, canvas.height, 50);
812
+ }
813
+
814
+ function createExplosion(x, y, color, count) {
815
+ particles.emit(x, y, count || 20, {
816
+ color: color, speed: 5, size: 3,
817
+ lifetime: 1.2, decay: 0.02, spread: Math.PI * 2
818
+ });
819
+ shake.trigger(10);
820
+ }
821
+
822
+ // --- Hyperspace ---
823
+ function hyperspace() {
824
+ if (hyperspaceCooldown > 0) return;
825
+ if (sfxReady) sfx.play('Hyperspace_Jump');
826
+ createExplosion(ship.x, ship.y, "#ffffff", 12);
827
+ ship.x = Math.random() * (canvas.width - 100) + 50;
828
+ ship.y = Math.random() * (canvas.height - 100) + 50;
829
+ ship.vx = 0; ship.vy = 0;
830
+ createExplosion(ship.x, ship.y, "#00ffff", 12);
831
+ hyperspaceCooldown = 180;
832
+ hyperspaceInvincible = 60;
833
+ }
834
+
835
+ var shootCooldown = 0;
836
+
837
+ function shoot() {
838
+ if (shootCooldown > 0) return;
839
+ const cooldown = ship.rapidFire > 0 ? 5 : 15;
840
+ shootCooldown = cooldown;
841
+ if (sfxReady) sfx.play('Laser_Fire');
842
+ const offsets = ship.multiShot > 0 ? [-0.2, 0, 0.2] : [0];
843
+ offsets.forEach(function(offset) {
844
+ const angle = ship.angle + offset;
845
+ bullets.push({
846
+ x: ship.x + 20 * Math.cos(angle),
847
+ y: ship.y + 20 * Math.sin(angle),
848
+ vx: 10 * Math.cos(angle) + 0.5 * ship.vx,
849
+ vy: 10 * Math.sin(angle) + 0.5 * ship.vy,
850
+ life: 60
851
+ });
852
+ });
853
+ }
854
+
855
+ function saucerShoot(saucer) {
856
+ let angle = Math.atan2(ship.y - saucer.y, ship.x - saucer.x);
857
+ angle += (saucer.isSmall ? 0.2 : 0.5) * (Math.random() - 0.5);
858
+ saucerBullets.push({
859
+ x: saucer.x, y: saucer.y,
860
+ vx: 6 * Math.cos(angle), vy: 6 * Math.sin(angle),
861
+ life: 90
862
+ });
863
+ }
864
+
865
+ function spawnPowerUp(x, y) {
866
+ if (Math.random() > 0.15) return;
867
+ let type;
868
+ const rand = Math.random();
869
+ type = rand < 0.05 ? "life" : rand < 0.38 ? "shield" : rand < 0.71 ? "rapid" : "multi";
870
+ powerUps.push({ x, y, type, life: 600, pulse: 0 });
871
+ }
872
+
873
+ function updateUI() {
874
+ const scoreEl = document.getElementById("score");
875
+ if (score > MAX_SCORE) {
876
+ scoreEl.textContent = "99,999+";
877
+ scoreEl.className = "score-overflow";
878
+ scoreEl.title = score.toLocaleString();
879
+ } else {
880
+ scoreEl.textContent = formatScore(score);
881
+ scoreEl.className = "";
882
+ scoreEl.title = "";
883
+ }
884
+ document.getElementById("level").textContent = level;
885
+ document.getElementById("lives").textContent = lives;
886
+
887
+ // Player indicator for multiplayer
888
+ const piEl = document.getElementById("playerIndicator");
889
+ if (gameMode !== "single") {
890
+ piEl.style.display = "inline";
891
+ piEl.textContent = "P" + (currentPlayer + 1) + " ";
892
+ piEl.style.color = getActivePlayerColor();
893
+ } else {
894
+ piEl.style.display = "none";
895
+ }
896
+ }
897
+
898
+ function isShipInvincible() {
899
+ return ship.invincible > 0 || ship.shield > 0 || hyperspaceInvincible > 0;
900
+ }
901
+
902
+ // Gamepad polling is now handled by gamepad.poll() in the game loop
903
+ // Game-specific action functions (gpFire, gpHyper, etc.) are defined above
904
+
905
+ // --- Multiplayer Turn Management ---
906
+ var TURN_COOLDOWN_MS = 3000; // 3 second cooldown between players
907
+ var turnReadyAt = 0;
908
+
909
+ function showTurnReady() {
910
+ turnReady = true;
911
+ turnReadyAt = Date.now();
912
+ const overlay = document.getElementById("turnReadyOverlay");
913
+ const nameEl = document.getElementById("turnPlayerName");
914
+ const color = getActivePlayerColor();
915
+ nameEl.textContent = "PLAYER " + (currentPlayer + 1);
916
+ nameEl.style.color = color;
917
+ updateAllPrompts();
918
+ overlay.classList.add("visible");
919
+ }
920
+
921
+ function getTurnCooldownRemaining() {
922
+ return Math.max(0, TURN_COOLDOWN_MS - (Date.now() - turnReadyAt));
923
+ }
924
+
925
+ function updateTurnPrompt() {
926
+ var remaining = getTurnCooldownRemaining();
927
+ var promptEl = document.getElementById("turnPrompt");
928
+ if (remaining > 0) {
929
+ var secs = Math.ceil(remaining / 1000);
930
+ promptEl.textContent = "Get ready... " + secs;
931
+ } else {
932
+ promptEl.innerHTML = btnText(
933
+ "Press SPACE to start",
934
+ "Press " + gpBtn("a") + " to start"
935
+ );
936
+ }
937
+ }
938
+
939
+ function hideTurnReady() {
940
+ turnReady = false;
941
+ document.getElementById("turnReadyOverlay").classList.remove("visible");
942
+ }
943
+
944
+ function startPlayerTurn() {
945
+ hideTurnReady();
946
+ if (gameMode === "solo_turns") {
947
+ // Fresh game for each player
948
+ initGame();
949
+ score = 0;
950
+ lives = 3;
951
+ level = 1;
952
+ } else if (gameMode === "hot_seat") {
953
+ // Load this player's state into shared field
954
+ const p = players[currentPlayer];
955
+ score = p.score;
956
+ lives = p.lives;
957
+ initHotSeatRespawn();
958
+ }
959
+ gameRunning = true;
960
+ startAmbient();
961
+ updateUI();
962
+ updateBottomBar();
963
+ }
964
+
965
+ function saveCurrentPlayerState() {
966
+ if (gameMode === "single") return;
967
+ players[currentPlayer].score = score;
968
+ players[currentPlayer].lives = lives;
969
+ if (gameMode === "solo_turns") {
970
+ players[currentPlayer].level = level;
971
+ }
972
+ }
973
+
974
+ function advanceToNextPlayer() {
975
+ saveCurrentPlayerState();
976
+
977
+ // Find next alive player
978
+ let next = -1;
979
+ for (let i = 1; i <= playerCount; i++) {
980
+ const idx = (currentPlayer + i) % playerCount;
981
+ if (players[idx].alive) { next = idx; break; }
982
+ }
983
+
984
+ if (next === -1) {
985
+ // All players eliminated
986
+ showFinalScoreboard();
987
+ return;
988
+ }
989
+
990
+ currentPlayer = next;
991
+ gameRunning = false;
992
+ showTurnReady();
993
+ }
994
+
995
+ function handlePlayerDeath() {
996
+ if (gameMode === "single") {
997
+ gameOver();
998
+ return;
999
+ }
1000
+ sfx.stop('Ship_Thrust');
1001
+
1002
+ if (gameMode === "solo_turns") {
1003
+ // Player lost all lives — they're done
1004
+ lives--;
1005
+ updateUI();
1006
+ if (lives <= 0) {
1007
+ saveCurrentPlayerState();
1008
+ players[currentPlayer].alive = false;
1009
+ gameRunning = false;
1010
+ // Check HS for this player
1011
+ if (qualifiesForHighScore(score)) {
1012
+ hsEntryPlayerIndex = currentPlayer;
1013
+ showHighScoreEntry();
1014
+ } else {
1015
+ advanceToNextPlayer();
1016
+ }
1017
+ } else {
1018
+ // Respawn
1019
+ ship.x = canvas.width / 2; ship.y = canvas.height / 2;
1020
+ ship.vx = 0; ship.vy = 0; ship.invincible = 180;
1021
+ }
1022
+ } else if (gameMode === "hot_seat") {
1023
+ // Lost one life — rotate to next player
1024
+ lives--;
1025
+ updateUI();
1026
+ saveCurrentPlayerState();
1027
+ if (lives <= 0) {
1028
+ players[currentPlayer].alive = false;
1029
+ // Check HS for this player
1030
+ if (qualifiesForHighScore(score)) {
1031
+ hsEntryPlayerIndex = currentPlayer;
1032
+ gameRunning = false;
1033
+ showHighScoreEntry();
1034
+ return;
1035
+ }
1036
+ }
1037
+ gameRunning = false;
1038
+ advanceToNextPlayer();
1039
+ }
1040
+ }
1041
+
1042
+ function showFinalScoreboard() {
1043
+ gameRunning = false;
1044
+ stopAmbient();
1045
+ sfx.stop('Ship_Thrust');
1046
+ // Sort players by score descending
1047
+ const ranked = players.map(function(p, i) { return { index: i, score: p.score, initials: p.initials }; });
1048
+ ranked.sort(function(a, b) { return b.score - a.score; });
1049
+
1050
+ const modeLabel = gameMode === "solo_turns" ? "SOLO TURNS" : "HOT SEAT";
1051
+ document.getElementById("sbSubtitle").textContent = modeLabel + " \u2014 FINAL STANDINGS";
1052
+
1053
+ const tbody = document.getElementById("sbBody");
1054
+ tbody.innerHTML = "";
1055
+ ranked.forEach(function(r, rank) {
1056
+ const tr = document.createElement("tr");
1057
+ const color = PLAYER_COLORS[r.index];
1058
+ const initStr = r.initials || "";
1059
+ tr.innerHTML =
1060
+ '<td class="sb-rank">' + (rank + 1) + '.</td>' +
1061
+ '<td class="sb-player" style="color:' + color + ';text-shadow:0 0 10px ' + color + '">P' + (r.index + 1) + '</td>' +
1062
+ '<td class="sb-initials">' + initStr + '</td>' +
1063
+ '<td class="sb-score">' + r.score.toLocaleString() + '</td>';
1064
+ tbody.appendChild(tr);
1065
+ });
1066
+
1067
+ document.getElementById("finalScoreboard").classList.add("visible");
1068
+ updateBottomBar();
1069
+ }
1070
+
1071
+ function hideFinalScoreboard() {
1072
+ document.getElementById("finalScoreboard").classList.remove("visible");
1073
+ }
1074
+
1075
+ // --- Start Screen UI Logic ---
1076
+ var menuCursor = 0; // index of focused item in current phase
1077
+
1078
+ // Items per phase: mode=[1P, PassPlay], submode=[Solo, HotSeat], count=N/A (uses arrows)
1079
+ function getMenuItems() {
1080
+ if (startScreenPhase === "mode") return [document.getElementById("btn1P"), document.getElementById("btnPassPlay")];
1081
+ if (startScreenPhase === "submode") return [document.getElementById("btnSoloTurns"), document.getElementById("btnHotSeat")];
1082
+ return [];
1083
+ }
1084
+
1085
+ function updateMenuFocus() {
1086
+ // Clear all focused classes
1087
+ var all = document.querySelectorAll(".mode-btn.focused, .submode-btn.focused");
1088
+ for (var i = 0; i < all.length; i++) all[i].classList.remove("focused");
1089
+ // Apply to current
1090
+ var items = getMenuItems();
1091
+ if (items.length > 0 && menuCursor >= 0 && menuCursor < items.length) {
1092
+ items[menuCursor].classList.add("focused");
1093
+ }
1094
+ }
1095
+
1096
+ function showStartPhase(phase) {
1097
+ startScreenPhase = phase;
1098
+ menuCursor = 0;
1099
+ var modeSelect = document.getElementById("modeSelect");
1100
+ var submodeSelect = document.getElementById("submodeSelect");
1101
+ var pcSelect = document.getElementById("playerCountSelect");
1102
+ modeSelect.style.display = phase === "mode" ? "flex" : "none";
1103
+ submodeSelect.style.display = phase === "submode" ? "flex" : "none";
1104
+ pcSelect.style.display = "none";
1105
+ pcSelect.classList.remove("visible");
1106
+ if (phase === "count") {
1107
+ pcSelect.style.display = "flex";
1108
+ pcSelect.classList.add("visible");
1109
+ }
1110
+ updateMenuFocus();
1111
+ updateBottomBar();
1112
+ }
1113
+
1114
+ function resetStartScreen() {
1115
+ var startScreen = document.getElementById("startScreen");
1116
+ startScreen.style.display = "";
1117
+ showStartPhase("mode");
1118
+ playerCount = 2;
1119
+ document.getElementById("pcNum").textContent = "2";
1120
+ document.getElementById("gameOverScreen").style.display = "none";
1121
+ hideFinalScoreboard();
1122
+ hideTurnReady();
1123
+ stopAmbient();
1124
+ sfx.stop('Ship_Thrust');
1125
+ }
1126
+
1127
+ function menuConfirm() {
1128
+ if (startScreenPhase === "mode") {
1129
+ if (menuCursor === 0) startSinglePlayer();
1130
+ else if (menuCursor === 1) showStartPhase("submode");
1131
+ } else if (startScreenPhase === "submode") {
1132
+ if (menuCursor === 0) selectSubmode("solo_turns");
1133
+ else if (menuCursor === 1) selectSubmode("hot_seat");
1134
+ } else if (startScreenPhase === "count") {
1135
+ confirmPlayerCount();
1136
+ }
1137
+ }
1138
+
1139
+ function menuBack() {
1140
+ if (startScreenPhase === "submode") {
1141
+ showStartPhase("mode");
1142
+ menuCursor = 1; // return focus to "Pass & Play"
1143
+ updateMenuFocus();
1144
+ } else if (startScreenPhase === "count") {
1145
+ showStartPhase("submode");
1146
+ }
1147
+ }
1148
+
1149
+ function menuMove(delta) {
1150
+ var items = getMenuItems();
1151
+ if (items.length === 0) return;
1152
+ var prev = menuCursor;
1153
+ menuCursor = Math.max(0, Math.min(items.length - 1, menuCursor + delta));
1154
+ if (menuCursor !== prev && sfxReady) sfx.play('Menu_Navigate');
1155
+ updateMenuFocus();
1156
+ }
1157
+
1158
+ function startSinglePlayer() {
1159
+ document.getElementById("startScreen").style.display = "none";
1160
+ gameMode = "single";
1161
+ initGame();
1162
+ gameRunning = true;
1163
+ startAmbient();
1164
+ updateBottomBar();
1165
+ }
1166
+
1167
+ function selectSubmode(mode) {
1168
+ gameMode = mode;
1169
+ showStartPhase("count");
1170
+ }
1171
+
1172
+ function adjustPlayerCount(delta) {
1173
+ playerCount = Math.max(2, Math.min(8, playerCount + delta));
1174
+ document.getElementById("pcNum").textContent = playerCount;
1175
+ }
1176
+
1177
+ function confirmPlayerCount() {
1178
+ document.getElementById("startScreen").style.display = "none";
1179
+ initMultiplayerPlayers();
1180
+ currentPlayer = 0;
1181
+
1182
+ if (gameMode === "hot_seat") {
1183
+ initGame();
1184
+ gameRunning = false;
1185
+ }
1186
+
1187
+ showTurnReady();
1188
+ }
1189
+
1190
+ // Start screen button events
1191
+ document.getElementById("btn1P").addEventListener("click", startSinglePlayer);
1192
+ document.getElementById("btnPassPlay").addEventListener("click", function() { showStartPhase("submode"); });
1193
+ document.getElementById("btnSoloTurns").addEventListener("click", function() { selectSubmode("solo_turns"); });
1194
+ document.getElementById("btnHotSeat").addEventListener("click", function() { selectSubmode("hot_seat"); });
1195
+ // Mouse hover syncs focus cursor
1196
+ document.getElementById("btn1P").addEventListener("mouseenter", function() { if (menuCursor !== 0 && sfxReady) sfx.play('Menu_Navigate'); menuCursor = 0; updateMenuFocus(); });
1197
+ document.getElementById("btnPassPlay").addEventListener("mouseenter", function() { if (menuCursor !== 1 && sfxReady) sfx.play('Menu_Navigate'); menuCursor = 1; updateMenuFocus(); });
1198
+ document.getElementById("btnSoloTurns").addEventListener("mouseenter", function() { if (menuCursor !== 0 && sfxReady) sfx.play('Menu_Navigate'); menuCursor = 0; updateMenuFocus(); });
1199
+ document.getElementById("btnHotSeat").addEventListener("mouseenter", function() { if (menuCursor !== 1 && sfxReady) sfx.play('Menu_Navigate'); menuCursor = 1; updateMenuFocus(); });
1200
+ document.getElementById("pcLeft").addEventListener("click", function() { adjustPlayerCount(-1); });
1201
+ document.getElementById("pcRight").addEventListener("click", function() { adjustPlayerCount(1); });
1202
+ document.getElementById("pcConfirm").addEventListener("click", confirmPlayerCount);
1203
+ document.getElementById("sbPlayAgain").addEventListener("click", function() { resetStartScreen(); });
1204
+
1205
+ function update() {
1206
+ // keyboard.poll() and gamepad.poll() are called in gameLoop() before this
1207
+
1208
+ // Gamepad pause toggle (edge-triggered, works even when paused)
1209
+ if (gpPause() && gameRunning) {
1210
+ gamePaused = !gamePaused;
1211
+ if (gamePaused) pauseOverlay.classList.add("visible");
1212
+ else pauseOverlay.classList.remove("visible");
1213
+ }
1214
+
1215
+ if (!gameRunning || gamePaused) return;
1216
+
1217
+ // Gamepad hyperspace (edge-triggered)
1218
+ if (gpHyper()) hyperspace();
1219
+
1220
+ // Ship controls (keyboard OR gamepad)
1221
+ if (keyboard.isDown("ArrowLeft") || keyboard.isDown("KeyA") || gamepad.left) ship.angle -= 0.08;
1222
+ if (keyboard.isDown("ArrowRight") || keyboard.isDown("KeyD") || gamepad.right) ship.angle += 0.08;
1223
+ if (keyboard.isDown("ArrowUp") || keyboard.isDown("KeyW") || gamepad.up) {
1224
+ ship.vx += 0.15 * Math.cos(ship.angle);
1225
+ ship.vy += 0.15 * Math.sin(ship.angle);
1226
+ ship.thrust = 1;
1227
+ if (sfxReady) sfx.play('Ship_Thrust');
1228
+ } else {
1229
+ if (ship.thrust > 0.1 && sfxReady) sfx.stop('Ship_Thrust');
1230
+ ship.thrust *= 0.95;
1231
+ }
1232
+ if (keyboard.isDown("Space") || gpFire()) shoot();
1233
+
1234
+ ship.vx *= 0.99;
1235
+ ship.vy *= 0.99;
1236
+ const speed = Math.sqrt(ship.vx ** 2 + ship.vy ** 2);
1237
+ if (speed > 8) {
1238
+ ship.vx = ship.vx / speed * 8;
1239
+ ship.vy = ship.vy / speed * 8;
1240
+ }
1241
+ ship.x += ship.vx;
1242
+ ship.y += ship.vy;
1243
+ wrap(ship);
1244
+
1245
+ if (ship.invincible > 0) ship.invincible--;
1246
+ if (ship.shield > 0) ship.shield--;
1247
+ if (ship.rapidFire > 0) ship.rapidFire--;
1248
+ if (ship.multiShot > 0) ship.multiShot--;
1249
+ if (shootCooldown > 0) shootCooldown--;
1250
+ if (hyperspaceCooldown > 0) hyperspaceCooldown--;
1251
+ if (hyperspaceInvincible > 0) hyperspaceInvincible--;
1252
+
1253
+ // Saucer spawning
1254
+ if (level >= 2) {
1255
+ saucerSpawnTimer++;
1256
+ const spawnInterval = Math.max(300, 600 - 30 * level);
1257
+ if (saucerSpawnTimer >= spawnInterval && saucers.length < 2) {
1258
+ saucers.push(createSaucer());
1259
+ if (sfxReady) sfx.play('Saucer_Alert');
1260
+ saucerSpawnTimer = 0;
1261
+ }
1262
+ }
1263
+
1264
+ // Update saucers
1265
+ saucers = saucers.filter(function(s) {
1266
+ s.x += s.vx; s.y += s.vy;
1267
+ s.directionChangeTimer--;
1268
+ if (s.directionChangeTimer <= 0) {
1269
+ s.vy = 2 * (Math.random() - 0.5);
1270
+ s.directionChangeTimer = 60 + 120 * Math.random();
1271
+ }
1272
+ if (s.y < 50) s.vy = Math.abs(s.vy);
1273
+ if (s.y > canvas.height - 50) s.vy = -Math.abs(s.vy);
1274
+ s.shootTimer--;
1275
+ if (s.shootTimer <= 0) {
1276
+ saucerShoot(s);
1277
+ s.shootTimer = s.isSmall ? 40 + 40 * Math.random() : 60 + 60 * Math.random();
1278
+ }
1279
+ return s.x > -50 && s.x < canvas.width + 50;
1280
+ });
1281
+
1282
+ // Update saucer bullets
1283
+ saucerBullets = saucerBullets.filter(function(b) {
1284
+ b.x += b.vx; b.y += b.vy; b.life--;
1285
+ return b.life > 0 && b.x > -10 && b.x < canvas.width + 10 && b.y > -10 && b.y < canvas.height + 10;
1286
+ });
1287
+
1288
+ // Update bullets
1289
+ bullets = bullets.filter(function(b) {
1290
+ b.x += b.vx; b.y += b.vy; b.life--;
1291
+ wrap(b);
1292
+ return b.life > 0;
1293
+ });
1294
+
1295
+ // Update asteroids
1296
+ asteroids.forEach(function(a) {
1297
+ a.x += a.vx; a.y += a.vy;
1298
+ a.rotation += a.rotationSpeed;
1299
+ wrap(a);
1300
+ });
1301
+
1302
+ // Update particles
1303
+ particles.update(1/60);
1304
+
1305
+ // Update power-ups
1306
+ powerUps = powerUps.filter(function(p) {
1307
+ p.life--; p.pulse += 0.1;
1308
+ return p.life > 0;
1309
+ });
1310
+
1311
+ // Update stars
1312
+ stars.forEach(function(s) {
1313
+ s.y += s.speed;
1314
+ if (s.y > canvas.height) { s.y = 0; s.x = Math.random() * canvas.width; }
1315
+ s.brightness = 0.3 + 0.3 * Math.sin(0.003 * Date.now() + s.x);
1316
+ });
1317
+
1318
+ // Bullet-asteroid collisions
1319
+ bullets.forEach(function(b, bi) {
1320
+ asteroids.forEach(function(a, ai) {
1321
+ const avgRadius = a.radii.reduce(function(sum, r) { return sum + r; }, 0) / a.radii.length;
1322
+ if (distance(b.x, b.y, a.x, a.y) < avgRadius) {
1323
+ bullets.splice(bi, 1);
1324
+ if (sfxReady) sfx.play('Asteroid_Explosion');
1325
+ createExplosion(a.x, a.y, a.color, 15);
1326
+ if (a.size > 1) {
1327
+ for (let i = 0; i < 2; i++) asteroids.push(createAsteroid(a.x, a.y, a.size - 1));
1328
+ }
1329
+ spawnPowerUp(a.x, a.y);
1330
+ asteroids.splice(ai, 1);
1331
+ addScore(100 * (4 - a.size));
1332
+ updateUI();
1333
+ }
1334
+ });
1335
+ });
1336
+
1337
+ // Bullet-saucer collisions
1338
+ bullets.forEach(function(b, bi) {
1339
+ saucers.forEach(function(s, si) {
1340
+ if (distance(b.x, b.y, s.x, s.y) < s.radius) {
1341
+ bullets.splice(bi, 1);
1342
+ createExplosion(s.x, s.y, "#ff6600", 25);
1343
+ addScore(s.points);
1344
+ saucers.splice(si, 1);
1345
+ updateUI();
1346
+ }
1347
+ });
1348
+ });
1349
+
1350
+ // Ship-asteroid collisions
1351
+ if (!isShipInvincible()) {
1352
+ asteroids.forEach(function(a, ai) {
1353
+ const avgRadius = a.radii.reduce(function(sum, r) { return sum + r; }, 0) / a.radii.length;
1354
+ if (distance(ship.x, ship.y, a.x, a.y) < avgRadius + ship.radius) {
1355
+ createExplosion(ship.x, ship.y, getActivePlayerColor(), 30);
1356
+ if (gameMode !== "single") {
1357
+ handlePlayerDeath();
1358
+ } else {
1359
+ lives--;
1360
+ updateUI();
1361
+ if (lives <= 0) { gameOver(); return; }
1362
+ ship.x = canvas.width / 2; ship.y = canvas.height / 2;
1363
+ ship.vx = 0; ship.vy = 0; ship.invincible = 180;
1364
+ }
1365
+ }
1366
+ });
1367
+ }
1368
+
1369
+ // Ship-saucer collisions
1370
+ if (!isShipInvincible()) {
1371
+ saucers.forEach(function(s, si) {
1372
+ if (distance(ship.x, ship.y, s.x, s.y) < s.radius + ship.radius) {
1373
+ createExplosion(ship.x, ship.y, getActivePlayerColor(), 30);
1374
+ createExplosion(s.x, s.y, "#ff6600", 25);
1375
+ saucers.splice(si, 1);
1376
+ if (gameMode !== "single") {
1377
+ handlePlayerDeath();
1378
+ } else {
1379
+ lives--;
1380
+ updateUI();
1381
+ if (lives <= 0) { gameOver(); return; }
1382
+ ship.x = canvas.width / 2; ship.y = canvas.height / 2;
1383
+ ship.vx = 0; ship.vy = 0; ship.invincible = 180;
1384
+ }
1385
+ }
1386
+ });
1387
+ }
1388
+
1389
+ // Ship-saucerBullet collisions
1390
+ if (!isShipInvincible()) {
1391
+ saucerBullets.forEach(function(b, bi) {
1392
+ if (distance(ship.x, ship.y, b.x, b.y) < ship.radius + 5) {
1393
+ createExplosion(ship.x, ship.y, getActivePlayerColor(), 30);
1394
+ saucerBullets.splice(bi, 1);
1395
+ if (gameMode !== "single") {
1396
+ handlePlayerDeath();
1397
+ } else {
1398
+ lives--;
1399
+ updateUI();
1400
+ if (lives <= 0) { gameOver(); return; }
1401
+ ship.x = canvas.width / 2; ship.y = canvas.height / 2;
1402
+ ship.vx = 0; ship.vy = 0; ship.invincible = 180;
1403
+ }
1404
+ }
1405
+ });
1406
+ }
1407
+
1408
+ // Power-up pickups
1409
+ powerUps.forEach(function(p, pi) {
1410
+ if (distance(ship.x, ship.y, p.x, p.y) < 30) {
1411
+ powerUps.splice(pi, 1);
1412
+ if (sfxReady) sfx.play('Power_Up_Collect');
1413
+ createExplosion(p.x, p.y, "#00ff00", 10);
1414
+ switch (p.type) {
1415
+ case "shield": ship.shield = 600; break;
1416
+ case "rapid": ship.rapidFire = 600; break;
1417
+ case "multi": ship.multiShot = 600; break;
1418
+ case "life": lives = Math.min(lives + 1, 5); updateUI(); break;
1419
+ }
1420
+ }
1421
+ });
1422
+
1423
+ // Next level
1424
+ if (asteroids.length === 0) {
1425
+ level++;
1426
+ updateUI();
1427
+ spawnAsteroids(3 + level);
1428
+ saucerSpawnTimer = 0;
1429
+ }
1430
+
1431
+ shake.update(1/60);
1432
+ }
1433
+
1434
+ function draw() {
1435
+ ctx.save();
1436
+ if (shake.isActive) ctx.translate(shake.offsetX, shake.offsetY);
1437
+ ctx.fillStyle = "#000";
1438
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1439
+
1440
+ // Stars
1441
+ stars.forEach(function(s) {
1442
+ ctx.fillStyle = "rgba(255, 255, 255, " + s.brightness + ")";
1443
+ ctx.beginPath();
1444
+ ctx.arc(s.x, s.y, s.size, 0, 2 * Math.PI);
1445
+ ctx.fill();
1446
+ });
1447
+
1448
+ // Particles
1449
+ particles.draw(ctx);
1450
+
1451
+ // Power-ups
1452
+ powerUps.forEach(function(p) {
1453
+ const pulse = 5 * Math.sin(p.pulse);
1454
+ ctx.strokeStyle = p.type === "shield" ? "#00ffff" : p.type === "rapid" ? "#ff0000" : p.type === "multi" ? "#ffff00" : "#00ff00";
1455
+ ctx.lineWidth = 2;
1456
+ ctx.shadowColor = ctx.strokeStyle;
1457
+ ctx.shadowBlur = 15;
1458
+ ctx.beginPath();
1459
+ ctx.arc(p.x, p.y, 15 + pulse, 0, 2 * Math.PI);
1460
+ ctx.stroke();
1461
+ ctx.fillStyle = ctx.strokeStyle;
1462
+ ctx.font = "12px Arial";
1463
+ ctx.textAlign = "center";
1464
+ ctx.textBaseline = "middle";
1465
+ const icon = p.type === "shield" ? "S" : p.type === "rapid" ? "R" : p.type === "multi" ? "M" : "+";
1466
+ ctx.fillText(icon, p.x, p.y);
1467
+ ctx.shadowBlur = 0;
1468
+ });
1469
+
1470
+ // Asteroids
1471
+ asteroids.forEach(function(a) {
1472
+ ctx.save();
1473
+ ctx.translate(a.x, a.y);
1474
+ ctx.rotate(a.rotation);
1475
+ ctx.strokeStyle = a.color;
1476
+ ctx.lineWidth = 2;
1477
+ ctx.shadowColor = a.color;
1478
+ ctx.shadowBlur = 20;
1479
+ ctx.beginPath();
1480
+ for (let i = 0; i < a.points; i++) {
1481
+ const angle = i / a.points * Math.PI * 2;
1482
+ const r = a.radii[i];
1483
+ const x = Math.cos(angle) * r;
1484
+ const y = Math.sin(angle) * r;
1485
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
1486
+ }
1487
+ ctx.closePath();
1488
+ ctx.stroke();
1489
+ ctx.restore();
1490
+ });
1491
+
1492
+ // Saucers
1493
+ saucers.forEach(function(s) {
1494
+ ctx.save();
1495
+ ctx.translate(s.x, s.y);
1496
+ ctx.strokeStyle = "#ff6600";
1497
+ ctx.lineWidth = 2;
1498
+ ctx.shadowColor = "#ff6600";
1499
+ ctx.shadowBlur = 20;
1500
+ const r = s.radius;
1501
+ ctx.beginPath(); ctx.ellipse(0, -0.2 * r, 0.4 * r, 0.3 * r, 0, Math.PI, 0); ctx.stroke();
1502
+ ctx.beginPath(); ctx.ellipse(0, 0, r, 0.35 * r, 0, 0, 2 * Math.PI); ctx.stroke();
1503
+ ctx.beginPath(); ctx.ellipse(0, 0.15 * r, 0.5 * r, 0.2 * r, 0, 0, Math.PI); ctx.stroke();
1504
+ ctx.fillStyle = "#ff6600";
1505
+ for (let i = 0; i < 3; i++) {
1506
+ const lx = (i - 1) * r * 0.5;
1507
+ ctx.beginPath(); ctx.arc(lx, 0, 3, 0, 2 * Math.PI); ctx.fill();
1508
+ }
1509
+ ctx.restore();
1510
+ });
1511
+
1512
+ // Saucer bullets
1513
+ saucerBullets.forEach(function(b) {
1514
+ ctx.fillStyle = "#ff6600";
1515
+ ctx.shadowColor = "#ff6600";
1516
+ ctx.shadowBlur = 15;
1517
+ ctx.beginPath();
1518
+ ctx.arc(b.x, b.y, 4, 0, 2 * Math.PI);
1519
+ ctx.fill();
1520
+ });
1521
+
1522
+ // Player bullets — use active player color
1523
+ const bulletColor = getActivePlayerColor();
1524
+ bullets.forEach(function(b) {
1525
+ ctx.fillStyle = bulletColor;
1526
+ ctx.shadowColor = bulletColor;
1527
+ ctx.shadowBlur = 15;
1528
+ ctx.beginPath();
1529
+ ctx.arc(b.x, b.y, 3, 0, 2 * Math.PI);
1530
+ ctx.fill();
1531
+ ctx.strokeStyle = bulletColor.replace(")", ", 0.5)").replace("rgb", "rgba").replace("#", "");
1532
+ // Use hex alpha approach for trail
1533
+ ctx.globalAlpha = 0.5;
1534
+ ctx.strokeStyle = bulletColor;
1535
+ ctx.lineWidth = 2;
1536
+ ctx.beginPath();
1537
+ ctx.moveTo(b.x, b.y);
1538
+ ctx.lineTo(b.x - 2 * b.vx, b.y - 2 * b.vy);
1539
+ ctx.stroke();
1540
+ ctx.globalAlpha = 1;
1541
+ });
1542
+
1543
+ // Ship — use active player color
1544
+ const shipColor = getActivePlayerColor();
1545
+ if (gameRunning && (ship.invincible <= 0 || Math.floor(ship.invincible / 5) % 2 === 0)) {
1546
+ ctx.save();
1547
+ ctx.translate(ship.x, ship.y);
1548
+ ctx.rotate(ship.angle);
1549
+ if (ship.shield > 0) {
1550
+ ctx.strokeStyle = shipColor.replace("ff", "80"); // semi-transparent
1551
+ ctx.globalAlpha = 0.5;
1552
+ ctx.strokeStyle = shipColor;
1553
+ ctx.lineWidth = 2;
1554
+ ctx.shadowColor = shipColor;
1555
+ ctx.shadowBlur = 20;
1556
+ ctx.beginPath();
1557
+ ctx.arc(0, 0, 25, 0, 2 * Math.PI);
1558
+ ctx.stroke();
1559
+ ctx.globalAlpha = 1;
1560
+ }
1561
+ if (hyperspaceInvincible > 0) {
1562
+ ctx.strokeStyle = "rgba(255, 255, 255, " + (0.3 + 0.2 * Math.sin(hyperspaceInvincible * 0.3)) + ")";
1563
+ ctx.lineWidth = 1;
1564
+ ctx.shadowColor = "#ffffff";
1565
+ ctx.shadowBlur = 25;
1566
+ ctx.beginPath();
1567
+ ctx.arc(0, 0, 22, 0, 2 * Math.PI);
1568
+ ctx.stroke();
1569
+ }
1570
+ ctx.strokeStyle = shipColor;
1571
+ ctx.lineWidth = 2;
1572
+ ctx.shadowColor = shipColor;
1573
+ ctx.shadowBlur = 15;
1574
+ ctx.beginPath();
1575
+ ctx.moveTo(20, 0);
1576
+ ctx.lineTo(-15, -12);
1577
+ ctx.lineTo(-8, 0);
1578
+ ctx.lineTo(-15, 12);
1579
+ ctx.closePath();
1580
+ ctx.stroke();
1581
+ if (ship.thrust > 0.1) {
1582
+ ctx.strokeStyle = "#ff6600";
1583
+ ctx.shadowColor = "#ff6600";
1584
+ ctx.beginPath();
1585
+ ctx.moveTo(-8, -5);
1586
+ ctx.lineTo(-20 - 10 * Math.random() * ship.thrust, 0);
1587
+ ctx.lineTo(-8, 5);
1588
+ ctx.stroke();
1589
+ }
1590
+ ctx.restore();
1591
+ }
1592
+
1593
+ ctx.shadowBlur = 0;
1594
+ ctx.restore();
1595
+ }
1596
+
1597
+ // --- Game Over & High Score Entry ---
1598
+ function gameOver() {
1599
+ gameRunning = false;
1600
+ stopAmbient();
1601
+ sfx.stop('Ship_Thrust');
1602
+ if (qualifiesForHighScore(score)) {
1603
+ hsEntryPlayerIndex = -1;
1604
+ showHighScoreEntry();
1605
+ } else {
1606
+ showGameOverScreen();
1607
+ }
1608
+ }
1609
+
1610
+ function showGameOverScreen() {
1611
+ document.getElementById("finalScore").textContent = score.toLocaleString();
1612
+ document.getElementById("gameOverScreen").style.display = "block";
1613
+ updateAllPrompts();
1614
+ }
1615
+
1616
+ function showHighScoreEntry() {
1617
+ hsFinalScore = score;
1618
+ hsSlots = [0, 0, 0];
1619
+ hsActiveSlot = 0;
1620
+ hsEntryActive = true;
1621
+ document.getElementById("hsScore").textContent = score.toLocaleString();
1622
+ updateSlotDisplay();
1623
+ document.getElementById("highScoreEntry").style.display = "block";
1624
+ updateAllPrompts();
1625
+ }
1626
+
1627
+ function updateSlotDisplay() {
1628
+ for (let i = 0; i < 3; i++) {
1629
+ const el = document.getElementById("slot" + i);
1630
+ el.textContent = String.fromCharCode(65 + hsSlots[i]);
1631
+ el.className = "initial-slot" + (i === hsActiveSlot ? " active" : "");
1632
+ }
1633
+ }
1634
+
1635
+ function confirmHighScore() {
1636
+ const initials = String.fromCharCode(65 + hsSlots[0]) +
1637
+ String.fromCharCode(65 + hsSlots[1]) +
1638
+ String.fromCharCode(65 + hsSlots[2]);
1639
+ insertHighScore(initials, hsFinalScore);
1640
+ hsEntryActive = false;
1641
+ document.getElementById("highScoreEntry").style.display = "none";
1642
+
1643
+ // In multiplayer, store initials and advance
1644
+ if (hsEntryPlayerIndex >= 0 && gameMode !== "single") {
1645
+ players[hsEntryPlayerIndex].initials = initials;
1646
+ advanceToNextPlayer();
1647
+ } else {
1648
+ showGameOverScreen();
1649
+ }
1650
+ }
1651
+
1652
+ var _turnStartPressed = false;
1653
+
1654
+ function gameLoop() {
1655
+ keyboard.poll();
1656
+ gamepad.poll();
1657
+
1658
+ // --- Turn ready overlay: cooldown then wait for player input ---
1659
+ if (turnReady) {
1660
+ updateTurnPrompt();
1661
+ if (getTurnCooldownRemaining() <= 0) {
1662
+ if (gpConfirm() || _turnStartPressed) {
1663
+ _turnStartPressed = false;
1664
+ startPlayerTurn();
1665
+ }
1666
+ }
1667
+ // Still draw background
1668
+ draw();
1669
+ saveDirState();
1670
+ requestAnimationFrame(gameLoop);
1671
+ return;
1672
+ }
1673
+
1674
+ // Gamepad menu/HS entry controls (when game not running)
1675
+ if (hsEntryActive) {
1676
+ if (gpUp()) { hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 1) % 26; updateSlotDisplay(); }
1677
+ if (gpDown()) { hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 25) % 26; updateSlotDisplay(); }
1678
+ if (gpLeft()) { hsActiveSlot = Math.max(0, hsActiveSlot - 1); updateSlotDisplay(); }
1679
+ if (gpRight()) { hsActiveSlot = Math.min(2, hsActiveSlot + 1); updateSlotDisplay(); }
1680
+ // A = accept letter & advance, confirm on last slot
1681
+ if (gpConfirm()) {
1682
+ if (hsActiveSlot < 2) { hsActiveSlot++; updateSlotDisplay(); }
1683
+ else confirmHighScore();
1684
+ }
1685
+ // B = go back one slot
1686
+ if (gpBack()) {
1687
+ if (hsActiveSlot > 0) { hsActiveSlot--; updateSlotDisplay(); }
1688
+ }
1689
+ } else if (!gameRunning) {
1690
+ // Start screen navigation with gamepad (d-pad + A/B)
1691
+ if (document.getElementById("startScreen").style.display !== "none") {
1692
+ if (startScreenPhase === "mode" || startScreenPhase === "submode") {
1693
+ if (gpUp()) menuMove(-1);
1694
+ if (gpDown()) menuMove(1);
1695
+ if (gpConfirm()) menuConfirm();
1696
+ if (gpBack()) menuBack(); // B = back
1697
+ } else if (startScreenPhase === "count") {
1698
+ if (gpLeft()) adjustPlayerCount(-1);
1699
+ if (gpRight()) adjustPlayerCount(1);
1700
+ if (gpConfirm()) confirmPlayerCount();
1701
+ if (gpBack()) menuBack(); // B = back
1702
+ }
1703
+ }
1704
+ // Game over screen — restart
1705
+ const gameOverScreen = document.getElementById("gameOverScreen");
1706
+ if (gameOverScreen.style.display === "block") {
1707
+ if (gpConfirm() || gpPause()) {
1708
+ gameOverScreen.style.display = "none";
1709
+ resetStartScreen();
1710
+ }
1711
+ }
1712
+ // Final scoreboard — play again
1713
+ if (document.getElementById("finalScoreboard").classList.contains("visible")) {
1714
+ if (gpConfirm() || gpPause()) {
1715
+ resetStartScreen();
1716
+ }
1717
+ }
1718
+ }
1719
+
1720
+ update();
1721
+ draw();
1722
+ saveDirState();
1723
+ requestAnimationFrame(gameLoop);
1724
+ }
1725
+
1726
+ // --- Input Handling (one-shot keyboard actions) ---
1727
+ // Continuous key state is handled by keyboard.isDown() via RetroGame.createKeyboard()
1728
+ document.addEventListener("keydown", function(e) {
1729
+ // Turn ready: SPACE to start
1730
+ if (turnReady) {
1731
+ if (e.code === "Space") {
1732
+ _turnStartPressed = true;
1733
+ e.preventDefault();
1734
+ }
1735
+ return;
1736
+ }
1737
+
1738
+ if (hsEntryActive) {
1739
+ if (e.code === "ArrowLeft") {
1740
+ hsActiveSlot = Math.max(0, hsActiveSlot - 1);
1741
+ updateSlotDisplay();
1742
+ } else if (e.code === "ArrowRight") {
1743
+ hsActiveSlot = Math.min(2, hsActiveSlot + 1);
1744
+ updateSlotDisplay();
1745
+ } else if (e.code === "ArrowUp") {
1746
+ hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 1) % 26;
1747
+ updateSlotDisplay();
1748
+ } else if (e.code === "ArrowDown") {
1749
+ hsSlots[hsActiveSlot] = (hsSlots[hsActiveSlot] + 25) % 26;
1750
+ updateSlotDisplay();
1751
+ } else if (e.code === "Enter") {
1752
+ confirmHighScore();
1753
+ }
1754
+ e.preventDefault();
1755
+ return;
1756
+ }
1757
+
1758
+ // Start screen keyboard navigation
1759
+ if (!gameRunning && document.getElementById("startScreen").style.display !== "none") {
1760
+ if (startScreenPhase === "mode" || startScreenPhase === "submode") {
1761
+ if (e.code === "ArrowUp") { menuMove(-1); e.preventDefault(); }
1762
+ if (e.code === "ArrowDown") { menuMove(1); e.preventDefault(); }
1763
+ if (e.code === "Space" || e.code === "Enter") { menuConfirm(); e.preventDefault(); }
1764
+ if (e.code === "Escape" || e.code === "Backspace") { menuBack(); e.preventDefault(); }
1765
+ } else if (startScreenPhase === "count") {
1766
+ if (e.code === "ArrowLeft") { adjustPlayerCount(-1); e.preventDefault(); }
1767
+ if (e.code === "ArrowRight") { adjustPlayerCount(1); e.preventDefault(); }
1768
+ if (e.code === "Space" || e.code === "Enter") { confirmPlayerCount(); e.preventDefault(); }
1769
+ if (e.code === "Escape" || e.code === "Backspace") { menuBack(); e.preventDefault(); }
1770
+ }
1771
+ return;
1772
+ }
1773
+
1774
+ if (e.code === "KeyP" && gameRunning) {
1775
+ gamePaused = !gamePaused;
1776
+ if (gamePaused) {
1777
+ pauseOverlay.classList.add("visible");
1778
+ } else {
1779
+ pauseOverlay.classList.remove("visible");
1780
+ }
1781
+ }
1782
+
1783
+ if (e.code === "KeyH" && gameRunning && !gamePaused) {
1784
+ hyperspace();
1785
+ }
1786
+
1787
+ // Game over: space or enter to go back to start
1788
+ const gameOverScreen = document.getElementById("gameOverScreen");
1789
+ if (!gameRunning && gameOverScreen.style.display === "block" && (e.code === "Space" || e.code === "Enter")) {
1790
+ gameOverScreen.style.display = "none";
1791
+ resetStartScreen();
1792
+ e.preventDefault();
1793
+ return;
1794
+ }
1795
+
1796
+ // Final scoreboard: space or enter to go back to start
1797
+ if (!gameRunning && document.getElementById("finalScoreboard").classList.contains("visible") && (e.code === "Space" || e.code === "Enter")) {
1798
+ resetStartScreen();
1799
+ e.preventDefault();
1800
+ return;
1801
+ }
1802
+
1803
+ if (["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)) {
1804
+ e.preventDefault();
1805
+ }
1806
+ });
1807
+
1808
+ document.getElementById("restartBtn").addEventListener("click", function() {
1809
+ document.getElementById("gameOverScreen").style.display = "none";
1810
+ resetStartScreen();
1811
+ });
1812
+
1813
+ // Initialize
1814
+ updateAllPrompts();
1815
+ initStars();
1816
+ gameLoop();
1817
+ </script>
1818
+
1819
+
1820
+
1821
+
1822
+ <script id="page-helpers" src="/api/page-helpers.js?v=3" data-locked="true"></script><script id="page-script" src="/api/page-script.js?v=3" data-locked="true"></script></body></html>