synthos 0.8.0 → 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 (359) hide show
  1. package/README.md +1 -1
  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.html → oregon_trail/page.html} +14 -12
  27. package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
  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_page.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} +14 -11
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
  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_page.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.html → us_map/page.html} +193 -192
  39. package/default-pages/{us_map.json → us_map/page.json} +12 -12
  40. package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
  41. package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
  42. package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
  43. package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
  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.v2.css +110 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/nebula-dusk.v2.css +104 -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/index.d.ts +1 -1
  65. package/dist/agents/index.d.ts.map +1 -1
  66. package/dist/agents/index.js +2 -1
  67. package/dist/agents/index.js.map +1 -1
  68. package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
  69. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
  70. package/dist/agents/openclaw/gatewayManager.js +27 -11
  71. package/dist/agents/openclaw/gatewayManager.js.map +1 -1
  72. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
  73. package/dist/agents/openclaw/openclawProvider.js +2 -4
  74. package/dist/agents/openclaw/openclawProvider.js.map +1 -1
  75. package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
  76. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
  77. package/dist/agents/openclaw/sshTunnelManager.js +31 -12
  78. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
  79. package/dist/builders/anthropic.d.ts +31 -0
  80. package/dist/builders/anthropic.d.ts.map +1 -0
  81. package/dist/builders/anthropic.js +227 -0
  82. package/dist/builders/anthropic.js.map +1 -0
  83. package/dist/builders/fireworksai.d.ts +9 -0
  84. package/dist/builders/fireworksai.d.ts.map +1 -0
  85. package/dist/builders/fireworksai.js +57 -0
  86. package/dist/builders/fireworksai.js.map +1 -0
  87. package/dist/builders/index.d.ts +13 -0
  88. package/dist/builders/index.d.ts.map +1 -0
  89. package/dist/builders/index.js +31 -0
  90. package/dist/builders/index.js.map +1 -0
  91. package/dist/builders/openai.d.ts +8 -0
  92. package/dist/builders/openai.d.ts.map +1 -0
  93. package/dist/builders/openai.js +87 -0
  94. package/dist/builders/openai.js.map +1 -0
  95. package/dist/builders/types.d.ts +54 -0
  96. package/dist/builders/types.d.ts.map +1 -0
  97. package/dist/builders/types.js +211 -0
  98. package/dist/builders/types.js.map +1 -0
  99. package/dist/connectors/index.d.ts.map +1 -1
  100. package/dist/connectors/index.js +3 -2
  101. package/dist/connectors/index.js.map +1 -1
  102. package/dist/connectors/registry.d.ts +2 -1
  103. package/dist/connectors/registry.d.ts.map +1 -1
  104. package/dist/connectors/registry.js +31 -8
  105. package/dist/connectors/registry.js.map +1 -1
  106. package/dist/customizer/Customizer.d.ts +57 -0
  107. package/dist/customizer/Customizer.d.ts.map +1 -0
  108. package/dist/customizer/Customizer.js +124 -0
  109. package/dist/customizer/Customizer.js.map +1 -0
  110. package/dist/customizer/index.d.ts.map +1 -0
  111. package/dist/customizer/index.js +9 -0
  112. package/dist/customizer/index.js.map +1 -0
  113. package/dist/files.d.ts +16 -0
  114. package/dist/files.d.ts.map +1 -1
  115. package/dist/files.js +60 -1
  116. package/dist/files.js.map +1 -1
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +1 -0
  119. package/dist/index.js.map +1 -1
  120. package/dist/init.d.ts +10 -6
  121. package/dist/init.d.ts.map +1 -1
  122. package/dist/init.js +96 -113
  123. package/dist/init.js.map +1 -1
  124. package/dist/migrations.d.ts.map +1 -1
  125. package/dist/migrations.js +23 -10
  126. package/dist/migrations.js.map +1 -1
  127. package/dist/models/anthropic.d.ts +4 -2
  128. package/dist/models/anthropic.d.ts.map +1 -1
  129. package/dist/models/anthropic.js +33 -6
  130. package/dist/models/anthropic.js.map +1 -1
  131. package/dist/models/fireworksai.d.ts.map +1 -1
  132. package/dist/models/fireworksai.js +9 -1
  133. package/dist/models/fireworksai.js.map +1 -1
  134. package/dist/models/index.d.ts +1 -1
  135. package/dist/models/index.d.ts.map +1 -1
  136. package/dist/models/index.js +2 -1
  137. package/dist/models/index.js.map +1 -1
  138. package/dist/models/openai.d.ts +1 -1
  139. package/dist/models/openai.d.ts.map +1 -1
  140. package/dist/models/openai.js +24 -3
  141. package/dist/models/openai.js.map +1 -1
  142. package/dist/models/types.d.ts +20 -1
  143. package/dist/models/types.d.ts.map +1 -1
  144. package/dist/models/types.js +6 -1
  145. package/dist/models/types.js.map +1 -1
  146. package/dist/pages.d.ts +30 -7
  147. package/dist/pages.d.ts.map +1 -1
  148. package/dist/pages.js +177 -55
  149. package/dist/pages.js.map +1 -1
  150. package/dist/service/server.d.ts.map +1 -1
  151. package/dist/service/server.js +37 -8
  152. package/dist/service/server.js.map +1 -1
  153. package/dist/service/transformPage.d.ts +47 -20
  154. package/dist/service/transformPage.d.ts.map +1 -1
  155. package/dist/service/transformPage.js +514 -293
  156. package/dist/service/transformPage.js.map +1 -1
  157. package/dist/service/useAgentRoutes.d.ts +2 -1
  158. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  159. package/dist/service/useAgentRoutes.js +5 -2
  160. package/dist/service/useAgentRoutes.js.map +1 -1
  161. package/dist/service/useApiRoutes.d.ts.map +1 -1
  162. package/dist/service/useApiRoutes.js +237 -136
  163. package/dist/service/useApiRoutes.js.map +1 -1
  164. package/dist/service/useConnectorRoutes.js +6 -6
  165. package/dist/service/useConnectorRoutes.js.map +1 -1
  166. package/dist/service/useFileRoutes.d.ts +4 -0
  167. package/dist/service/useFileRoutes.d.ts.map +1 -0
  168. package/dist/service/useFileRoutes.js +122 -0
  169. package/dist/service/useFileRoutes.js.map +1 -0
  170. package/dist/service/usePageRoutes.d.ts.map +1 -1
  171. package/dist/service/usePageRoutes.js +648 -67
  172. package/dist/service/usePageRoutes.js.map +1 -1
  173. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  174. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  175. package/dist/service/useSharedDataRoutes.js +104 -0
  176. package/dist/service/useSharedDataRoutes.js.map +1 -0
  177. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  178. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  179. package/dist/service/useSharedFileRoutes.js +121 -0
  180. package/dist/service/useSharedFileRoutes.js.map +1 -0
  181. package/dist/settings.d.ts +1 -0
  182. package/dist/settings.d.ts.map +1 -1
  183. package/dist/settings.js +1 -0
  184. package/dist/settings.js.map +1 -1
  185. package/dist/synthos-cli.d.ts.map +1 -1
  186. package/dist/synthos-cli.js +4 -3
  187. package/dist/synthos-cli.js.map +1 -1
  188. package/dist/themes.d.ts +1 -0
  189. package/dist/themes.d.ts.map +1 -1
  190. package/dist/themes.js +28 -15
  191. package/dist/themes.js.map +1 -1
  192. package/migration-rules/v1-to-v2.md +193 -0
  193. package/migration-rules/v2-to-v3.md +481 -0
  194. package/package.json +11 -10
  195. package/required-pages/builder/page.html +43 -0
  196. package/required-pages/builder/page.json +10 -0
  197. package/required-pages/{pages.html → pages/page.html} +238 -233
  198. package/required-pages/pages/page.json +10 -0
  199. package/required-pages/{settings.html → settings/page.html} +389 -275
  200. package/required-pages/settings/page.json +10 -0
  201. package/required-pages/synthos_apis/page.html +846 -0
  202. package/required-pages/synthos_apis/page.json +10 -0
  203. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  204. package/required-pages/synthos_scripts/page.json +10 -0
  205. package/src/agents/index.ts +1 -1
  206. package/src/agents/openclaw/gatewayManager.ts +22 -11
  207. package/src/agents/openclaw/openclawProvider.ts +2 -4
  208. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  209. package/src/builders/anthropic.ts +283 -0
  210. package/src/builders/fireworksai.ts +59 -0
  211. package/src/builders/index.ts +33 -0
  212. package/src/builders/openai.ts +89 -0
  213. package/src/builders/types.ts +261 -0
  214. package/src/connectors/index.ts +1 -1
  215. package/src/connectors/registry.ts +28 -8
  216. package/src/customizer/Customizer.ts +151 -0
  217. package/src/customizer/index.ts +5 -0
  218. package/src/files.ts +57 -0
  219. package/src/index.ts +2 -1
  220. package/src/init.ts +137 -123
  221. package/src/migrations.ts +30 -10
  222. package/src/models/anthropic.ts +40 -10
  223. package/src/models/fireworksai.ts +9 -2
  224. package/src/models/index.ts +1 -1
  225. package/src/models/openai.ts +26 -6
  226. package/src/models/types.ts +31 -1
  227. package/src/pages.ts +176 -54
  228. package/src/service/server.ts +36 -9
  229. package/src/service/transformPage.ts +557 -326
  230. package/src/service/useAgentRoutes.ts +7 -2
  231. package/src/service/useApiRoutes.ts +150 -41
  232. package/src/service/useConnectorRoutes.ts +7 -7
  233. package/src/service/useFileRoutes.ts +127 -0
  234. package/src/service/usePageRoutes.ts +720 -73
  235. package/src/service/useSharedDataRoutes.ts +106 -0
  236. package/src/service/useSharedFileRoutes.ts +126 -0
  237. package/src/settings.ts +2 -0
  238. package/src/synthos-cli.ts +4 -3
  239. package/src/themes.ts +25 -14
  240. package/static-files/favicon.svg +12 -0
  241. package/static-files/fluentlm-instructions.llmd +868 -0
  242. package/static-files/fluentlm-instructions.md +1595 -0
  243. package/static-files/fluentlm.css +4844 -0
  244. package/static-files/fluentlm.js +3602 -0
  245. package/static-files/fluentlm.min.css +1 -0
  246. package/static-files/fluentlm.min.js +1 -0
  247. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  248. package/static-files/page.v3.js +1290 -0
  249. package/static-files/recommended-frameworks.llmd +81 -0
  250. package/static-files/recommended-frameworks.md +137 -0
  251. package/static-files/retro-game.js +877 -0
  252. package/static-files/shell.css +797 -0
  253. package/static-files/theme-dark.css +169 -0
  254. package/static-files/theme-light.css +169 -0
  255. package/tests/builders.spec.ts +139 -0
  256. package/tests/pages.spec.ts +8 -8
  257. package/tests/transformPage.spec.ts +299 -360
  258. package/default-pages/application.html +0 -40
  259. package/default-pages/application.json +0 -1
  260. package/default-pages/json_tools.json +0 -1
  261. package/default-pages/my_notes.html +0 -33
  262. package/default-pages/neon_asteroids.html +0 -77
  263. package/default-pages/sidebar_page.json +0 -1
  264. package/default-pages/solar_tutorial.json +0 -1
  265. package/default-pages/two-panel_page.json +0 -1
  266. package/dist/agents/a2a/a2aProvider.d.ts +0 -3
  267. package/dist/agents/discovery.d.ts +0 -30
  268. package/dist/agents/openclaw/openclawProvider.d.ts +0 -3
  269. package/dist/agents/types.d.ts +0 -64
  270. package/dist/connectors/index.d.ts +0 -3
  271. package/dist/connectors/types.d.ts +0 -84
  272. package/dist/index.d.ts +0 -7
  273. package/dist/migrations.d.ts +0 -12
  274. package/dist/models/chainOfThought.d.ts +0 -12
  275. package/dist/models/fireworksai.d.ts +0 -30
  276. package/dist/models/logCompletePrompt.d.ts +0 -3
  277. package/dist/models/providers.d.ts +0 -8
  278. package/dist/models/utils.d.ts +0 -6
  279. package/dist/scripts.d.ts +0 -15
  280. package/dist/service/createCompletePrompt.d.ts +0 -5
  281. package/dist/service/debugLog.d.ts +0 -11
  282. package/dist/service/generateImage.d.ts +0 -32
  283. package/dist/service/index.d.ts +0 -8
  284. package/dist/service/modelInstructions.d.ts +0 -7
  285. package/dist/service/requiresSettings.d.ts +0 -3
  286. package/dist/service/server.d.ts +0 -4
  287. package/dist/service/useApiRoutes.d.ts +0 -4
  288. package/dist/service/useConnectorRoutes.d.ts +0 -4
  289. package/dist/service/useDataRoutes.d.ts +0 -4
  290. package/dist/service/useGatewayRoutes.d.ts +0 -4
  291. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  292. package/dist/service/useGatewayRoutes.js +0 -168
  293. package/dist/service/useGatewayRoutes.js.map +0 -1
  294. package/dist/service/usePageRoutes.d.ts +0 -5
  295. package/dist/synthos-cli.d.ts +0 -2
  296. package/page-scripts/page-v2.js +0 -656
  297. package/required-pages/builder.html +0 -48
  298. package/required-pages/builder.json +0 -1
  299. package/required-pages/pages.json +0 -1
  300. package/required-pages/settings.json +0 -1
  301. package/required-pages/synthos_apis.html +0 -327
  302. package/required-pages/synthos_apis.json +0 -1
  303. package/required-pages/synthos_scripts.json +0 -1
  304. package/src/connectors/airtable/connector.json +0 -27
  305. package/src/connectors/alpha-vantage/connector.json +0 -26
  306. package/src/connectors/brave-search/connector.json +0 -26
  307. package/src/connectors/cloudinary/connector.json +0 -27
  308. package/src/connectors/deepl/connector.json +0 -28
  309. package/src/connectors/elevenlabs/connector.json +0 -30
  310. package/src/connectors/giphy/connector.json +0 -27
  311. package/src/connectors/github/connector.json +0 -29
  312. package/src/connectors/huggingface/connector.json +0 -27
  313. package/src/connectors/imgur/connector.json +0 -29
  314. package/src/connectors/instagram/connector.json +0 -43
  315. package/src/connectors/jira/connector.json +0 -28
  316. package/src/connectors/mapbox/connector.json +0 -26
  317. package/src/connectors/nasa/connector.json +0 -27
  318. package/src/connectors/newsapi/connector.json +0 -27
  319. package/src/connectors/notion/connector.json +0 -28
  320. package/src/connectors/open-exchange-rates/connector.json +0 -27
  321. package/src/connectors/openweathermap/connector.json +0 -26
  322. package/src/connectors/pexels/connector.json +0 -27
  323. package/src/connectors/resend/connector.json +0 -29
  324. package/src/connectors/rss2json/connector.json +0 -27
  325. package/src/connectors/sendgrid/connector.json +0 -27
  326. package/src/connectors/spoonacular/connector.json +0 -28
  327. package/src/connectors/stability-ai/connector.json +0 -27
  328. package/src/connectors/twilio/connector.json +0 -28
  329. package/src/connectors/unsplash/connector.json +0 -27
  330. package/src/connectors/wolfram-alpha/connector.json +0 -26
  331. package/src/connectors/youtube-data/connector.json +0 -30
  332. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  333. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  334. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  335. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  336. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  337. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  338. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  339. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  340. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  341. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
@@ -0,0 +1,877 @@
1
+ /**
2
+ * RetroGame — Shared retro game infrastructure library
3
+ * Provides game loop, input, canvas sizing, particles, and more.
4
+ * Auto-served at /static/retro-game.js
5
+ */
6
+ (function() {
7
+ if (window.RetroGame) return;
8
+
9
+ var RG = {};
10
+
11
+ // -------------------------------------------------------
12
+ // Constants
13
+ // -------------------------------------------------------
14
+ RG.BUTTON = {
15
+ A: 0, B: 1, X: 2, Y: 3,
16
+ LB: 4, RB: 5, LT: 6, RT: 7,
17
+ SELECT: 8, START: 9,
18
+ L_STICK: 10, R_STICK: 11,
19
+ DPAD_UP: 12, DPAD_DOWN: 13, DPAD_LEFT: 14, DPAD_RIGHT: 15
20
+ };
21
+
22
+ // -------------------------------------------------------
23
+ // 1. Game Loop
24
+ // -------------------------------------------------------
25
+ RG.createGameLoop = function(opts) {
26
+ var updateFn = opts.update;
27
+ var drawFn = opts.draw;
28
+ var targetFps = opts.fps || 60;
29
+ var interval = 1000 / targetFps;
30
+ var rafId = null;
31
+ var lastTime = 0;
32
+ var running = false;
33
+ var paused = false;
34
+ var currentDt = 0;
35
+ var measuredFps = 0;
36
+ var frameCount = 0;
37
+ var fpsAccum = 0;
38
+
39
+ function tick(now) {
40
+ rafId = requestAnimationFrame(tick);
41
+ var elapsed = now - lastTime;
42
+ if (elapsed < interval) return;
43
+ lastTime = now - (elapsed % interval);
44
+ var dt = Math.min(elapsed / 1000, 0.1); // clamp to 100ms max
45
+ currentDt = dt;
46
+
47
+ // FPS measurement
48
+ frameCount++;
49
+ fpsAccum += elapsed;
50
+ if (fpsAccum >= 1000) {
51
+ measuredFps = Math.round((frameCount * 1000) / fpsAccum);
52
+ frameCount = 0;
53
+ fpsAccum = 0;
54
+ }
55
+
56
+ if (!paused) {
57
+ updateFn(dt);
58
+ }
59
+ drawFn(dt);
60
+ }
61
+
62
+ var loop = {
63
+ get isPaused() { return paused; },
64
+ get isRunning() { return running; },
65
+ get dt() { return currentDt; },
66
+ get fps() { return measuredFps; },
67
+
68
+ start: function() {
69
+ if (running) return;
70
+ running = true;
71
+ paused = false;
72
+ lastTime = performance.now();
73
+ rafId = requestAnimationFrame(tick);
74
+ },
75
+ stop: function() {
76
+ running = false;
77
+ paused = false;
78
+ if (rafId !== null) {
79
+ cancelAnimationFrame(rafId);
80
+ rafId = null;
81
+ }
82
+ },
83
+ pause: function() {
84
+ paused = true;
85
+ },
86
+ resume: function() {
87
+ if (paused) {
88
+ paused = false;
89
+ lastTime = performance.now();
90
+ }
91
+ },
92
+ toggle: function() {
93
+ if (paused) loop.resume();
94
+ else loop.pause();
95
+ }
96
+ };
97
+
98
+ return loop;
99
+ };
100
+
101
+ // -------------------------------------------------------
102
+ // 2. Responsive Canvas
103
+ // -------------------------------------------------------
104
+ RG.createCanvas = function(canvasEl, opts) {
105
+ var container = opts.container;
106
+ var aspectRatio = opts.aspectRatio != null ? opts.aspectRatio : 4 / 3;
107
+ var debounceMs = opts.debounceMs != null ? opts.debounceMs : 150;
108
+ var onResizeCb = opts.onResize || null;
109
+ var ctx = canvasEl.getContext("2d");
110
+ var timer = 0;
111
+ var destroyed = false;
112
+
113
+ function doResize() {
114
+ if (destroyed) return;
115
+ var cw = container.clientWidth;
116
+ var ch = container.clientHeight;
117
+ if (cw < 1 || ch < 1) return;
118
+ var w, h;
119
+ if (cw / ch > aspectRatio) {
120
+ h = ch;
121
+ w = Math.floor(h * aspectRatio);
122
+ } else {
123
+ w = cw;
124
+ h = Math.floor(w / aspectRatio);
125
+ }
126
+ canvasEl.width = w;
127
+ canvasEl.height = h;
128
+ if (onResizeCb) onResizeCb(w, h);
129
+ }
130
+
131
+ function onWindowResize() {
132
+ doResize();
133
+ clearTimeout(timer);
134
+ timer = setTimeout(doResize, debounceMs);
135
+ }
136
+
137
+ window.addEventListener("resize", onWindowResize);
138
+ doResize();
139
+
140
+ return {
141
+ get width() { return canvasEl.width; },
142
+ get height() { return canvasEl.height; },
143
+ get ctx() { return ctx; },
144
+ resize: doResize,
145
+ set onResize(fn) { onResizeCb = fn; },
146
+ destroy: function() {
147
+ destroyed = true;
148
+ window.removeEventListener("resize", onWindowResize);
149
+ clearTimeout(timer);
150
+ }
151
+ };
152
+ };
153
+
154
+ // -------------------------------------------------------
155
+ // 3. Keyboard Input
156
+ // -------------------------------------------------------
157
+ RG.createKeyboard = function() {
158
+ var current = {}; // currently held
159
+ var previous = {}; // snapshot from last poll
160
+ var destroyed = false;
161
+
162
+ function onKeyDown(e) {
163
+ current[e.code] = true;
164
+ }
165
+ function onKeyUp(e) {
166
+ current[e.code] = false;
167
+ }
168
+
169
+ window.addEventListener("keydown", onKeyDown);
170
+ window.addEventListener("keyup", onKeyUp);
171
+
172
+ return {
173
+ isDown: function(code) {
174
+ return !!current[code];
175
+ },
176
+ wasPressed: function(code) {
177
+ return !!current[code] && !previous[code];
178
+ },
179
+ wasReleased: function(code) {
180
+ return !current[code] && !!previous[code];
181
+ },
182
+ poll: function() {
183
+ // Snapshot current state into previous
184
+ for (var k in previous) delete previous[k];
185
+ for (var k in current) previous[k] = current[k];
186
+ },
187
+ destroy: function() {
188
+ if (destroyed) return;
189
+ destroyed = true;
190
+ window.removeEventListener("keydown", onKeyDown);
191
+ window.removeEventListener("keyup", onKeyUp);
192
+ }
193
+ };
194
+ };
195
+
196
+ // -------------------------------------------------------
197
+ // 4. Gamepad Input
198
+ // -------------------------------------------------------
199
+ RG.createGamepad = function(opts) {
200
+ opts = opts || {};
201
+ var deadzone = opts.deadzone != null ? opts.deadzone : 0.3;
202
+ var padIndex = opts.index != null ? opts.index : -1; // -1 = first connected
203
+ var connected = false;
204
+ var platform = "unknown";
205
+ var current = {};
206
+ var previous = {};
207
+ var lx = 0, ly = 0, rx = 0, ry = 0;
208
+ var dirLeft = false, dirRight = false, dirUp = false, dirDown = false;
209
+ var onConnectCb = null;
210
+ var onDisconnectCb = null;
211
+ var wasConnected = false;
212
+
213
+ function detectPlatform(pad) {
214
+ if (!pad) return "unknown";
215
+ var id = (pad.id || "").toLowerCase();
216
+ if (id.indexOf("playstation") !== -1 || id.indexOf("dualsense") !== -1 ||
217
+ id.indexOf("dualshock") !== -1 || id.indexOf("054c") !== -1 ||
218
+ id.indexOf("sony") !== -1) {
219
+ return "ps";
220
+ }
221
+ if (id.indexOf("xbox") !== -1 || id.indexOf("xinput") !== -1 ||
222
+ id.indexOf("045e") !== -1 || id.indexOf("microsoft") !== -1) {
223
+ return "xbox";
224
+ }
225
+ return "unknown";
226
+ }
227
+
228
+ function applyDeadzone(v) {
229
+ return Math.abs(v) < deadzone ? 0 : v;
230
+ }
231
+
232
+ function getPad() {
233
+ var pads = navigator.getGamepads ? navigator.getGamepads() : [];
234
+ if (padIndex >= 0) {
235
+ var p = pads[padIndex];
236
+ return (p && p.connected) ? p : null;
237
+ }
238
+ for (var i = 0; i < pads.length; i++) {
239
+ if (pads[i] && pads[i].connected) return pads[i];
240
+ }
241
+ return null;
242
+ }
243
+
244
+ return {
245
+ get connected() { return connected; },
246
+ get platform() { return platform; },
247
+ get leftStickX() { return lx; },
248
+ get leftStickY() { return ly; },
249
+ get rightStickX() { return rx; },
250
+ get rightStickY() { return ry; },
251
+ get left() { return dirLeft; },
252
+ get right() { return dirRight; },
253
+ get up() { return dirUp; },
254
+ get down() { return dirDown; },
255
+ set onConnect(fn) { onConnectCb = fn; },
256
+ set onDisconnect(fn) { onDisconnectCb = fn; },
257
+
258
+ isDown: function(btn) {
259
+ return !!current[btn];
260
+ },
261
+ wasPressed: function(btn) {
262
+ return !!current[btn] && !previous[btn];
263
+ },
264
+ wasReleased: function(btn) {
265
+ return !current[btn] && !!previous[btn];
266
+ },
267
+
268
+ poll: function() {
269
+ // Save previous state
270
+ for (var k in current) previous[k] = current[k];
271
+
272
+ var pad = getPad();
273
+ wasConnected = connected;
274
+ connected = !!pad;
275
+
276
+ if (connected && !wasConnected) {
277
+ platform = detectPlatform(pad);
278
+ if (onConnectCb) onConnectCb();
279
+ } else if (!connected && wasConnected) {
280
+ platform = "unknown";
281
+ if (onDisconnectCb) onDisconnectCb();
282
+ }
283
+
284
+ if (!pad) {
285
+ lx = ly = rx = ry = 0;
286
+ dirLeft = dirRight = dirUp = dirDown = false;
287
+ for (var b = 0; b < 17; b++) current[b] = false;
288
+ return;
289
+ }
290
+
291
+ var axes = pad.axes;
292
+ var btns = pad.buttons;
293
+
294
+ lx = applyDeadzone(axes[0] || 0);
295
+ ly = applyDeadzone(axes[1] || 0);
296
+ rx = applyDeadzone(axes[2] || 0);
297
+ ry = applyDeadzone(axes[3] || 0);
298
+
299
+ for (var b = 0; b < btns.length && b < 17; b++) {
300
+ current[b] = btns[b] && btns[b].pressed;
301
+ }
302
+
303
+ var dpadUp = !!current[RG.BUTTON.DPAD_UP];
304
+ var dpadDown = !!current[RG.BUTTON.DPAD_DOWN];
305
+ var dpadLeft = !!current[RG.BUTTON.DPAD_LEFT];
306
+ var dpadRight = !!current[RG.BUTTON.DPAD_RIGHT];
307
+
308
+ dirLeft = lx < 0 || dpadLeft;
309
+ dirRight = lx > 0 || dpadRight;
310
+ dirUp = ly < 0 || dpadUp;
311
+ dirDown = ly > 0 || dpadDown;
312
+ }
313
+ };
314
+ };
315
+
316
+ // -------------------------------------------------------
317
+ // 5. Gamepad UI
318
+ // -------------------------------------------------------
319
+ RG.createGamepadUI = function(gamepadInput, opts) {
320
+ opts = opts || {};
321
+ var statusEl = opts.statusElement || null;
322
+
323
+ function getPlatform() {
324
+ return gamepadInput.platform;
325
+ }
326
+
327
+ var ui = {
328
+ update: function() {
329
+ if (!statusEl) return;
330
+ if (gamepadInput.connected) {
331
+ statusEl.textContent = "GAMEPAD DETECTED";
332
+ statusEl.className = "connected";
333
+ } else {
334
+ statusEl.textContent = "NO GAMEPAD";
335
+ statusEl.className = "";
336
+ }
337
+ },
338
+
339
+ buttonGlyph: function(name) {
340
+ var p = getPlatform();
341
+ if (p === "ps") {
342
+ switch (name) {
343
+ case "a": return '<span class="gp-btn gp-btn-ps-cross">\u2715</span>';
344
+ case "b": return '<span class="gp-btn gp-btn-ps-circle">\u25CB</span>';
345
+ case "x": return '<span class="gp-btn gp-btn-ps-square">\u25A1</span>';
346
+ case "y": return '<span class="gp-btn gp-btn-ps-triangle">\u25B3</span>';
347
+ }
348
+ }
349
+ // Xbox / default
350
+ switch (name) {
351
+ case "a": return '<span class="gp-btn gp-btn-xa">A</span>';
352
+ case "b": return '<span class="gp-btn gp-btn-xb">B</span>';
353
+ case "x": return '<span class="gp-btn gp-btn-xx">X</span>';
354
+ case "y": return '<span class="gp-btn gp-btn-xy">Y</span>';
355
+ }
356
+ return name.toUpperCase();
357
+ },
358
+
359
+ buttonName: function(name) {
360
+ var p = getPlatform();
361
+ if (p === "ps") {
362
+ switch (name) {
363
+ case "a": return "Cross";
364
+ case "b": return "Circle";
365
+ case "x": return "Square";
366
+ case "y": return "Triangle";
367
+ }
368
+ }
369
+ return name.toUpperCase();
370
+ },
371
+
372
+ startName: function() {
373
+ return getPlatform() === "ps" ? "OPTIONS" : "START";
374
+ },
375
+
376
+ prompt: function(kbHtml, gpHtml) {
377
+ return gamepadInput.connected ? gpHtml : kbHtml;
378
+ },
379
+
380
+ get css() { return GAMEPAD_CSS; }
381
+ };
382
+
383
+ return ui;
384
+ };
385
+
386
+ var GAMEPAD_CSS =
387
+ '.gp-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;font-size:13px;font-weight:700;line-height:1;vertical-align:middle;margin:0 2px;font-family:"Segoe UI",sans-serif}' +
388
+ '.gp-btn-xa{background:#3a9a3a;color:#fff;text-shadow:0 0 4px rgba(0,0,0,.5)}' +
389
+ '.gp-btn-xb{background:#c83232;color:#fff;text-shadow:0 0 4px rgba(0,0,0,.5)}' +
390
+ '.gp-btn-xx{background:#2a6ac8;color:#fff;text-shadow:0 0 4px rgba(0,0,0,.5)}' +
391
+ '.gp-btn-xy{background:#c8a82a;color:#fff;text-shadow:0 0 4px rgba(0,0,0,.5)}' +
392
+ '.gp-btn-ps-cross{background:transparent;border:2px solid #6699cc;color:#6699cc;font-size:15px;width:20px;height:20px}' +
393
+ '.gp-btn-ps-circle{background:transparent;border:2px solid #e66a6a;color:#e66a6a;font-size:14px;width:20px;height:20px}' +
394
+ '.gp-btn-ps-square{background:transparent;border:2px solid #d68ad6;color:#d68ad6;font-size:12px;width:20px;height:20px}' +
395
+ '.gp-btn-ps-triangle{background:transparent;border:2px solid #6ad6a0;color:#6ad6a0;font-size:14px;width:20px;height:20px}' +
396
+ '.gp-label{display:inline-block;padding:1px 6px;border:1px solid rgba(0,255,255,.4);border-radius:3px;font-size:11px;color:#0ff;background:rgba(0,255,255,.05);vertical-align:middle;margin:0 2px;font-family:"Segoe UI",sans-serif}';
397
+
398
+ RG.injectCSS = function() {
399
+ if (document.getElementById("retro-game-gp-css")) return;
400
+ var style = document.createElement("style");
401
+ style.id = "retro-game-gp-css";
402
+ style.textContent = GAMEPAD_CSS;
403
+ document.head.appendChild(style);
404
+ };
405
+
406
+ // -------------------------------------------------------
407
+ // 6. Distance
408
+ // -------------------------------------------------------
409
+ RG.distance = function(a, b) {
410
+ var dx = b.x - a.x;
411
+ var dy = b.y - a.y;
412
+ return Math.sqrt(dx * dx + dy * dy);
413
+ };
414
+
415
+ // -------------------------------------------------------
416
+ // 7. Circles Overlap
417
+ // -------------------------------------------------------
418
+ RG.circlesOverlap = function(a, b, rA, rB) {
419
+ var dx = b.x - a.x;
420
+ var dy = b.y - a.y;
421
+ var dist = dx * dx + dy * dy;
422
+ var radii = rA + rB;
423
+ return dist < radii * radii;
424
+ };
425
+
426
+ // -------------------------------------------------------
427
+ // 8. Wrap
428
+ // -------------------------------------------------------
429
+ RG.wrap = function(obj, width, height, margin) {
430
+ margin = margin != null ? margin : 50;
431
+ if (obj.x < -margin) obj.x = width + margin;
432
+ if (obj.x > width + margin) obj.x = -margin;
433
+ if (obj.y < -margin) obj.y = height + margin;
434
+ if (obj.y > height + margin) obj.y = -margin;
435
+ };
436
+
437
+ // -------------------------------------------------------
438
+ // 9. Particle System
439
+ // -------------------------------------------------------
440
+ RG.createParticles = function(opts) {
441
+ opts = opts || {};
442
+ var maxParticles = opts.maxParticles || 500;
443
+ var pool = [];
444
+ var activeCount = 0;
445
+
446
+ // Pre-allocate pool
447
+ for (var i = 0; i < maxParticles; i++) {
448
+ pool.push({ active: false, x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, color: "", size: 0, decay: 0 });
449
+ }
450
+
451
+ function acquire() {
452
+ // Find a dead particle to reuse
453
+ for (var i = 0; i < pool.length; i++) {
454
+ if (!pool[i].active) return pool[i];
455
+ }
456
+ return null; // pool exhausted
457
+ }
458
+
459
+ return {
460
+ get count() { return activeCount; },
461
+
462
+ emit: function(x, y, count, emitOpts) {
463
+ emitOpts = emitOpts || {};
464
+ var color = emitOpts.color || "#ffffff";
465
+ var speed = emitOpts.speed != null ? emitOpts.speed : 5;
466
+ var size = emitOpts.size != null ? emitOpts.size : 3;
467
+ var lifetime = emitOpts.lifetime != null ? emitOpts.lifetime : 1.0;
468
+ var decay = emitOpts.decay != null ? emitOpts.decay : 0;
469
+ var spread = emitOpts.spread != null ? emitOpts.spread : Math.PI * 2;
470
+ var direction = emitOpts.direction != null ? emitOpts.direction : Math.random() * Math.PI * 2;
471
+
472
+ for (var i = 0; i < count; i++) {
473
+ var p = acquire();
474
+ if (!p) break;
475
+ var angle = direction + (Math.random() - 0.5) * spread;
476
+ var spd = speed * (0.4 + Math.random() * 0.6);
477
+ p.active = true;
478
+ p.x = x;
479
+ p.y = y;
480
+ p.vx = Math.cos(angle) * spd;
481
+ p.vy = Math.sin(angle) * spd;
482
+ p.life = lifetime * (0.6 + Math.random() * 0.4);
483
+ p.maxLife = p.life;
484
+ p.color = color;
485
+ p.size = size * (0.3 + Math.random() * 0.7);
486
+ p.decay = decay;
487
+ activeCount++;
488
+ }
489
+ },
490
+
491
+ update: function(dt) {
492
+ activeCount = 0;
493
+ for (var i = 0; i < pool.length; i++) {
494
+ var p = pool[i];
495
+ if (!p.active) continue;
496
+ p.life -= dt;
497
+ if (p.life <= 0) {
498
+ p.active = false;
499
+ continue;
500
+ }
501
+ p.x += p.vx * dt * 60; // normalize to ~60fps feel
502
+ p.y += p.vy * dt * 60;
503
+ if (p.decay) {
504
+ p.vx *= (1 - p.decay * dt * 60);
505
+ p.vy *= (1 - p.decay * dt * 60);
506
+ }
507
+ activeCount++;
508
+ }
509
+ },
510
+
511
+ draw: function(ctx) {
512
+ for (var i = 0; i < pool.length; i++) {
513
+ var p = pool[i];
514
+ if (!p.active) continue;
515
+ var alpha = p.life / p.maxLife;
516
+ ctx.globalAlpha = alpha;
517
+ ctx.fillStyle = p.color;
518
+ ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
519
+ }
520
+ ctx.globalAlpha = 1;
521
+ },
522
+
523
+ clear: function() {
524
+ for (var i = 0; i < pool.length; i++) {
525
+ pool[i].active = false;
526
+ }
527
+ activeCount = 0;
528
+ }
529
+ };
530
+ };
531
+
532
+ // -------------------------------------------------------
533
+ // 10. Screen Shake
534
+ // -------------------------------------------------------
535
+ RG.createShake = function() {
536
+ var intensity = 0;
537
+ var ox = 0;
538
+ var oy = 0;
539
+
540
+ return {
541
+ get offsetX() { return ox; },
542
+ get offsetY() { return oy; },
543
+ get isActive() { return intensity > 0.5; },
544
+
545
+ trigger: function(amount, duration) {
546
+ // duration is unused — shake decays exponentially.
547
+ // Keeping the param for API consistency; intensity drives magnitude.
548
+ intensity = amount;
549
+ },
550
+
551
+ update: function(dt) {
552
+ if (intensity < 0.5) {
553
+ intensity = 0;
554
+ ox = 0;
555
+ oy = 0;
556
+ return;
557
+ }
558
+ ox = (Math.random() - 0.5) * intensity * 2;
559
+ oy = (Math.random() - 0.5) * intensity * 2;
560
+ // Exponential decay: ~0.9 per frame at 60fps
561
+ intensity *= Math.pow(0.9, dt * 60);
562
+ }
563
+ };
564
+ };
565
+
566
+ // -------------------------------------------------------
567
+ // 11. Sound Effects
568
+ // -------------------------------------------------------
569
+ RG.createSoundEffects = function(opts) {
570
+ opts = opts || {};
571
+ var masterVolume = opts.volume != null ? opts.volume : 1;
572
+ var basePath = opts.basePath || null;
573
+ var manifestUrl = opts.manifest || null;
574
+ var loaded = false;
575
+ var manifest = null;
576
+ var audioPool = {}; // name -> Array of Audio objects
577
+ var activeAudios = {}; // name -> Array of currently-playing Audio
578
+ var loopTimers = {}; // name -> timeout id for delay-based loops
579
+ var ambientSources = {}; // name -> { source, gain } for Web Audio ambient loops
580
+ var audioCtx = null;
581
+ var audioBuffers = {}; // name -> AudioBuffer (cached)
582
+ var POOL_SIZE = 4;
583
+
584
+ // Auto-detect basePath from current page URL
585
+ function getBasePath() {
586
+ if (basePath) return basePath;
587
+ // Derive from current page: /api/files/{pageName}
588
+ var path = window.location.pathname.replace(/^\//, '').replace(/\/$/, '');
589
+ return '/api/files/' + encodeURIComponent(path);
590
+ }
591
+
592
+ function getManifestUrl() {
593
+ return manifestUrl || (getBasePath() + '/effects.json');
594
+ }
595
+
596
+ function createAudioClone(name) {
597
+ var entry = manifest.effects.find(function(e) { return e.name === name; });
598
+ if (!entry) return null;
599
+ var audio = new Audio(getBasePath() + '/' + encodeURIComponent(entry.file));
600
+ audio.volume = masterVolume;
601
+ audio.preload = 'auto';
602
+ return audio;
603
+ }
604
+
605
+ function getFromPool(name) {
606
+ var pool = audioPool[name];
607
+ if (!pool || pool.length === 0) return createAudioClone(name);
608
+ // Find one that's not currently playing
609
+ for (var i = 0; i < pool.length; i++) {
610
+ if (pool[i].paused || pool[i].ended) {
611
+ pool[i].currentTime = 0;
612
+ pool[i].volume = masterVolume;
613
+ return pool[i];
614
+ }
615
+ }
616
+ // All busy — create a new clone
617
+ var clone = createAudioClone(name);
618
+ if (clone) pool.push(clone);
619
+ return clone;
620
+ }
621
+
622
+ function preloadAll() {
623
+ if (!manifest || !manifest.effects) return;
624
+ for (var i = 0; i < manifest.effects.length; i++) {
625
+ var name = manifest.effects[i].name;
626
+ audioPool[name] = [];
627
+ activeAudios[name] = [];
628
+ for (var j = 0; j < POOL_SIZE; j++) {
629
+ var a = createAudioClone(name);
630
+ if (a) audioPool[name].push(a);
631
+ }
632
+ }
633
+ }
634
+
635
+ function getAudioCtx() {
636
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
637
+ return audioCtx;
638
+ }
639
+
640
+ function fetchBuffer(name) {
641
+ if (audioBuffers[name]) return Promise.resolve(audioBuffers[name]);
642
+ var entry = getEffectEntry(name);
643
+ if (!entry) return Promise.reject(new Error('Unknown effect: ' + name));
644
+ var url = getBasePath() + '/' + encodeURIComponent(entry.file);
645
+ return new Promise(function(resolve, reject) {
646
+ var xhr = new XMLHttpRequest();
647
+ xhr.open('GET', url, true);
648
+ xhr.responseType = 'arraybuffer';
649
+ xhr.onload = function() {
650
+ if (xhr.status < 200 || xhr.status >= 300) {
651
+ reject(new Error('Failed to fetch audio: HTTP ' + xhr.status));
652
+ return;
653
+ }
654
+ getAudioCtx().decodeAudioData(xhr.response, function(buf) {
655
+ audioBuffers[name] = buf;
656
+ resolve(buf);
657
+ }, reject);
658
+ };
659
+ xhr.onerror = function() { reject(new Error('Network error')); };
660
+ xhr.send();
661
+ });
662
+ }
663
+
664
+ function getEffectEntry(name) {
665
+ if (!manifest || !manifest.effects) return null;
666
+ return manifest.effects.find(function(e) { return e.name === name; }) || null;
667
+ }
668
+
669
+ var sfx = {
670
+ get isLoaded() { return loaded; },
671
+
672
+ get volume() { return masterVolume; },
673
+ set volume(v) {
674
+ masterVolume = Math.max(0, Math.min(1, v));
675
+ // Update all active HTML audios
676
+ for (var name in activeAudios) {
677
+ var list = activeAudios[name];
678
+ for (var i = 0; i < list.length; i++) {
679
+ list[i].volume = masterVolume;
680
+ }
681
+ }
682
+ // Update all ambient gain nodes
683
+ for (var aName in ambientSources) {
684
+ if (ambientSources[aName].gain) {
685
+ ambientSources[aName].gain.gain.value = masterVolume;
686
+ }
687
+ }
688
+ },
689
+
690
+ get effects() {
691
+ if (!manifest || !manifest.effects) return [];
692
+ return manifest.effects.map(function(e) { return e.name; });
693
+ },
694
+
695
+ has: function(name) {
696
+ return !!getEffectEntry(name);
697
+ },
698
+
699
+ load: function() {
700
+ return new Promise(function(resolve, reject) {
701
+ var url = getManifestUrl();
702
+ var xhr = new XMLHttpRequest();
703
+ xhr.open('GET', url, true);
704
+ xhr.onload = function() {
705
+ if (xhr.status < 200 || xhr.status >= 300) {
706
+ reject(new Error('Failed to load effects manifest: HTTP ' + xhr.status));
707
+ return;
708
+ }
709
+ try {
710
+ manifest = JSON.parse(xhr.responseText);
711
+ } catch (e) {
712
+ reject(new Error('Invalid effects manifest JSON'));
713
+ return;
714
+ }
715
+ preloadAll();
716
+ loaded = true;
717
+ resolve();
718
+ };
719
+ xhr.onerror = function() {
720
+ reject(new Error('Network error loading effects manifest'));
721
+ };
722
+ xhr.send();
723
+ });
724
+ },
725
+
726
+ play: function(name) {
727
+ if (!loaded) return;
728
+ var entry = getEffectEntry(name);
729
+ if (!entry) return;
730
+
731
+ var loop = entry.loop || {};
732
+ var isAmbient = loop.ambient || false;
733
+
734
+ if (isAmbient) {
735
+ // Web Audio API — gapless ambient loop
736
+ if (ambientSources[name]) return; // already playing
737
+ fetchBuffer(name).then(function(buffer) {
738
+ var ctx = getAudioCtx();
739
+ if (ctx.state === 'suspended') ctx.resume();
740
+ var source = ctx.createBufferSource();
741
+ source.buffer = buffer;
742
+ source.loop = true;
743
+ var gain = ctx.createGain();
744
+ gain.gain.value = masterVolume;
745
+ source.connect(gain);
746
+ gain.connect(ctx.destination);
747
+ source.start(0);
748
+ ambientSources[name] = { source: source, gain: gain };
749
+ }).catch(function() {});
750
+ return;
751
+ }
752
+
753
+ // Standard HTML Audio path
754
+ var audio = getFromPool(name);
755
+ if (!audio) return;
756
+
757
+ if (!activeAudios[name]) activeAudios[name] = [];
758
+ activeAudios[name].push(audio);
759
+
760
+ var loopEnabled = loop.enabled || false;
761
+ var minDelay = (loop.minDelay || 0) * 1000;
762
+ var maxDelay = (loop.maxDelay || 0) * 1000;
763
+ var seamless = loopEnabled && minDelay === 0 && maxDelay === 0;
764
+
765
+ if (seamless) {
766
+ audio.loop = true;
767
+ } else {
768
+ audio.loop = false;
769
+ }
770
+
771
+ audio.play().catch(function() {});
772
+
773
+ if (loopEnabled && !seamless) {
774
+ audio.onended = function() {
775
+ var idx = activeAudios[name] ? activeAudios[name].indexOf(audio) : -1;
776
+ if (idx >= 0) activeAudios[name].splice(idx, 1);
777
+ if (!loopTimers[name] && loopTimers[name] !== 0) return;
778
+ var delay = minDelay + Math.random() * (maxDelay - minDelay);
779
+ loopTimers[name] = setTimeout(function() {
780
+ sfx.play(name);
781
+ }, delay);
782
+ };
783
+ loopTimers[name] = -1;
784
+ } else if (!loopEnabled) {
785
+ audio.onended = function() {
786
+ var idx = activeAudios[name] ? activeAudios[name].indexOf(audio) : -1;
787
+ if (idx >= 0) activeAudios[name].splice(idx, 1);
788
+ };
789
+ }
790
+ },
791
+
792
+ stop: function(name) {
793
+ // Stop ambient source if present
794
+ if (ambientSources[name]) {
795
+ try { ambientSources[name].source.stop(); } catch (e) {}
796
+ try { ambientSources[name].source.disconnect(); } catch (e) {}
797
+ try { ambientSources[name].gain.disconnect(); } catch (e) {}
798
+ delete ambientSources[name];
799
+ }
800
+ // Clear loop timer
801
+ if (loopTimers[name] != null) {
802
+ if (loopTimers[name] > 0) clearTimeout(loopTimers[name]);
803
+ delete loopTimers[name];
804
+ }
805
+ // Stop all active HTML audios for this effect
806
+ var list = activeAudios[name];
807
+ if (list) {
808
+ for (var i = 0; i < list.length; i++) {
809
+ list[i].loop = false;
810
+ list[i].pause();
811
+ list[i].currentTime = 0;
812
+ list[i].onended = null;
813
+ }
814
+ activeAudios[name] = [];
815
+ }
816
+ },
817
+
818
+ stopAll: function() {
819
+ var name;
820
+ for (name in ambientSources) {
821
+ sfx.stop(name);
822
+ }
823
+ for (name in activeAudios) {
824
+ sfx.stop(name);
825
+ }
826
+ }
827
+ };
828
+
829
+ return sfx;
830
+ };
831
+
832
+ // -------------------------------------------------------
833
+ // 12. Pause Manager
834
+ // -------------------------------------------------------
835
+ RG.createPauseManager = function(overlayEl, opts) {
836
+ opts = opts || {};
837
+ var visibleClass = opts.visibleClass || "visible";
838
+ var gameLoop = opts.gameLoop || null;
839
+ var paused = false;
840
+
841
+ function syncOverlay() {
842
+ if (paused) {
843
+ overlayEl.classList.add(visibleClass);
844
+ } else {
845
+ overlayEl.classList.remove(visibleClass);
846
+ }
847
+ }
848
+
849
+ return {
850
+ get isPaused() { return paused; },
851
+
852
+ toggle: function() {
853
+ if (paused) this.resume();
854
+ else this.pause();
855
+ },
856
+
857
+ pause: function() {
858
+ if (paused) return;
859
+ paused = true;
860
+ if (gameLoop) gameLoop.pause();
861
+ syncOverlay();
862
+ },
863
+
864
+ resume: function() {
865
+ if (!paused) return;
866
+ paused = false;
867
+ if (gameLoop) gameLoop.resume();
868
+ syncOverlay();
869
+ }
870
+ };
871
+ };
872
+
873
+ // -------------------------------------------------------
874
+ // Expose namespace
875
+ // -------------------------------------------------------
876
+ window.RetroGame = RG;
877
+ })();