@guinetik/gcanvas 1.0.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 (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,430 @@
1
+ /**
2
+ * @module FluentGame
3
+ * @description Root builder class for the fluent API, wrapping the Game class
4
+ *
5
+ * This provides a declarative, chainable API for creating GCanvas games
6
+ * with less boilerplate than the traditional class-based approach.
7
+ *
8
+ * @example
9
+ * import { gcanvas } from 'gcanvas';
10
+ *
11
+ * gcanvas({ bg: 'black' })
12
+ * .scene('game')
13
+ * .go({ x: 400, y: 300, name: 'player' })
14
+ * .circle({ radius: 30, fill: 'lime' })
15
+ * .start();
16
+ */
17
+
18
+ import { Game } from "../game/game.js";
19
+ import { Scene } from "../game/objects/scene.js";
20
+ import { FluentScene } from "./fluent-scene.js";
21
+ import { FluentGO } from "./fluent-go.js";
22
+
23
+ /**
24
+ * Main entry point for the fluent API
25
+ * @param {Object} options - Game configuration
26
+ * @returns {FluentGame}
27
+ */
28
+ export function gcanvas(options = {}) {
29
+ return new FluentGame(options);
30
+ }
31
+
32
+ /**
33
+ * FluentGame - Root builder class wrapping Game
34
+ */
35
+ export class FluentGame {
36
+ /** @type {Game} */
37
+ #game;
38
+ /** @type {Map<string, Scene>} */
39
+ #scenes = new Map();
40
+ /** @type {Scene|null} */
41
+ #currentScene = null;
42
+ /** @type {Object} */
43
+ #refs = {};
44
+ /** @type {Object} */
45
+ #state = {};
46
+ /** @type {Array} */
47
+ #plugins = [];
48
+ /** @type {HTMLCanvasElement|null} */
49
+ #canvas = null;
50
+
51
+ /**
52
+ * @param {Object} options - Game configuration
53
+ * @param {HTMLCanvasElement} [options.canvas] - Canvas element to use
54
+ * @param {number} [options.width=800] - Canvas width (if auto-creating)
55
+ * @param {number} [options.height=600] - Canvas height (if auto-creating)
56
+ * @param {string} [options.bg] - Background color
57
+ * @param {boolean} [options.fluid=true] - Enable fluid/responsive sizing
58
+ * @param {HTMLElement} [options.container=document.body] - Container for auto-created canvas
59
+ * @param {number} [options.fps=60] - Target FPS
60
+ * @param {number} [options.pixelRatio=window.devicePixelRatio] - Pixel ratio for HiDPI
61
+ */
62
+ constructor(options = {}) {
63
+ const {
64
+ canvas,
65
+ width = 800,
66
+ height = 600,
67
+ bg,
68
+ fluid, // Default determined below based on canvas presence
69
+ container,
70
+ fps = 60,
71
+ pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1
72
+ } = options;
73
+
74
+ // Create or use provided canvas
75
+ this.#canvas = canvas || this.#createCanvas(width, height, container);
76
+ this.#game = new Game(this.#canvas);
77
+
78
+ // Initialize the game
79
+ this.#game.init();
80
+
81
+ // Apply options
82
+ if (bg) this.#game.backgroundColor = bg;
83
+ // Only enable fluid sizing if explicitly requested, or if we auto-created the canvas
84
+ // When user provides their own canvas, default to respecting its dimensions
85
+ const shouldBeFluid = fluid !== undefined ? fluid : !canvas;
86
+ if (shouldBeFluid) this.#game.enableFluidSize();
87
+ if (fps !== 60) this.#game.targetFPS = fps;
88
+
89
+ // Store pixel ratio for shapes that need it
90
+ this.#game._pixelRatio = pixelRatio;
91
+ }
92
+
93
+ /**
94
+ * Create a canvas element
95
+ * @private
96
+ */
97
+ #createCanvas(width, height, container) {
98
+ const canvas = document.createElement('canvas');
99
+ canvas.width = width;
100
+ canvas.height = height;
101
+ canvas.style.display = 'block';
102
+
103
+ const target = container || document.body;
104
+ target.appendChild(canvas);
105
+
106
+ return canvas;
107
+ }
108
+
109
+ // ─────────────────────────────────────────────────────────
110
+ // SCENE MANAGEMENT
111
+ // ─────────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * Create or switch to a scene
115
+ *
116
+ * Supports multiple signatures:
117
+ * - scene('name') - Create/switch to named scene
118
+ * - scene('name', options) - Create with options
119
+ * - scene('name', CustomSceneClass) - Create using custom class
120
+ * - scene('name', CustomSceneClass, options) - Custom class with options
121
+ * - scene(CustomSceneClass) - Custom class, name derived from class
122
+ * - scene(CustomSceneClass, options) - Custom class with options
123
+ *
124
+ * @param {string|Function} nameOrClass - Scene identifier or custom Scene class
125
+ * @param {Object|Function} [optionsOrClass] - Scene options or custom Scene class
126
+ * @param {Object} [options] - Scene options (when class is second arg)
127
+ * @param {number} [options.zIndex=0] - Scene z-index
128
+ * @param {boolean} [options.active=true] - Whether scene is visible
129
+ * @param {Function} [options.onEnter] - Scene enter callback
130
+ * @param {Function} [options.onExit] - Scene exit callback
131
+ * @returns {FluentScene}
132
+ */
133
+ scene(nameOrClass, optionsOrClass, options) {
134
+ // Parse flexible arguments
135
+ let name, SceneClass, opts;
136
+
137
+ if (typeof nameOrClass === 'function') {
138
+ // scene(CustomClass) or scene(CustomClass, options)
139
+ SceneClass = nameOrClass;
140
+ name = SceneClass.name || 'custom_scene';
141
+ opts = optionsOrClass || {};
142
+ } else if (typeof optionsOrClass === 'function') {
143
+ // scene('name', CustomClass) or scene('name', CustomClass, options)
144
+ name = nameOrClass;
145
+ SceneClass = optionsOrClass;
146
+ opts = options || {};
147
+ } else {
148
+ // scene('name') or scene('name', options)
149
+ name = nameOrClass;
150
+ SceneClass = Scene;
151
+ opts = optionsOrClass || {};
152
+ }
153
+
154
+ const {
155
+ zIndex = 0,
156
+ active = true,
157
+ onEnter,
158
+ onExit,
159
+ ...customOpts
160
+ } = opts;
161
+
162
+ let scene = this.#scenes.get(name);
163
+
164
+ if (!scene) {
165
+ // Instantiate the scene class (custom or default)
166
+ scene = new SceneClass(this.#game, customOpts);
167
+ scene.name = name;
168
+ scene.zIndex = zIndex;
169
+ scene.visible = active;
170
+ scene._onEnter = onEnter;
171
+ scene._onExit = onExit;
172
+ this.#scenes.set(name, scene);
173
+ this.#game.pipeline.add(scene);
174
+ }
175
+
176
+ this.#currentScene = scene;
177
+ return new FluentScene(this, scene, this.#refs, this.#state);
178
+ }
179
+
180
+ /**
181
+ * Switch context to an existing scene (does not create)
182
+ * @param {string} name - Scene name
183
+ * @returns {FluentScene}
184
+ */
185
+ inScene(name) {
186
+ const scene = this.#scenes.get(name);
187
+ if (!scene) {
188
+ throw new Error(`Scene '${name}' does not exist. Use .scene('${name}') to create it.`);
189
+ }
190
+ this.#currentScene = scene;
191
+ return new FluentScene(this, scene, this.#refs, this.#state);
192
+ }
193
+
194
+ /**
195
+ * Shortcut: create GO in current/default scene
196
+ *
197
+ * Supports same signatures as FluentScene.go():
198
+ * - go(options) - Plain GameObject with options
199
+ * - go(CustomClass) - Custom GameObject class
200
+ * - go(CustomClass, options) - Custom class with options
201
+ *
202
+ * @param {Object|Function} [optionsOrClass] - GameObject options or custom class
203
+ * @param {Object} [options] - Options when class is first arg
204
+ * @returns {FluentGO}
205
+ */
206
+ go(optionsOrClass, options) {
207
+ if (!this.#currentScene) {
208
+ this.scene('default');
209
+ }
210
+ return new FluentScene(this, this.#currentScene, this.#refs, this.#state).go(optionsOrClass, options);
211
+ }
212
+
213
+ // ─────────────────────────────────────────────────────────
214
+ // SCENE VISIBILITY & TRANSITIONS
215
+ // ─────────────────────────────────────────────────────────
216
+
217
+ /**
218
+ * Show a scene
219
+ * @param {string} name - Scene name
220
+ * @returns {FluentGame}
221
+ */
222
+ showScene(name) {
223
+ const scene = this.#scenes.get(name);
224
+ if (scene) {
225
+ scene.visible = true;
226
+ scene._onEnter?.(this.#createContext());
227
+ }
228
+ return this;
229
+ }
230
+
231
+ /**
232
+ * Hide a scene
233
+ * @param {string} name - Scene name
234
+ * @returns {FluentGame}
235
+ */
236
+ hideScene(name) {
237
+ const scene = this.#scenes.get(name);
238
+ if (scene) {
239
+ scene._onExit?.(this.#createContext());
240
+ scene.visible = false;
241
+ }
242
+ return this;
243
+ }
244
+
245
+ /**
246
+ * Transition between scenes
247
+ * @param {string} from - Source scene name
248
+ * @param {string} to - Target scene name
249
+ * @param {Object} [options] - Transition options
250
+ * @param {number} [options.fade=0] - Fade duration in seconds
251
+ * @param {Function} [options.onComplete] - Completion callback
252
+ * @returns {FluentGame}
253
+ */
254
+ transition(from, to, options = {}) {
255
+ const { fade = 0, onComplete } = options;
256
+
257
+ // TODO: Implement fade transition using Tweenetik
258
+ this.hideScene(from);
259
+ this.showScene(to);
260
+ onComplete?.();
261
+
262
+ return this;
263
+ }
264
+
265
+ // ─────────────────────────────────────────────────────────
266
+ // STATE MANAGEMENT
267
+ // ─────────────────────────────────────────────────────────
268
+
269
+ /**
270
+ * Set initial state
271
+ * @param {Object} initialState
272
+ * @returns {FluentGame}
273
+ */
274
+ state(initialState) {
275
+ Object.assign(this.#state, initialState);
276
+ return this;
277
+ }
278
+
279
+ /**
280
+ * Get a state value
281
+ * @param {string} key - State key
282
+ * @returns {*}
283
+ */
284
+ getState(key) {
285
+ return this.#state[key];
286
+ }
287
+
288
+ /**
289
+ * Set a state value
290
+ * @param {string} key - State key
291
+ * @param {*} value - State value
292
+ * @returns {FluentGame}
293
+ */
294
+ setState(key, value) {
295
+ this.#state[key] = value;
296
+ return this;
297
+ }
298
+
299
+ // ─────────────────────────────────────────────────────────
300
+ // EVENTS & LIFECYCLE
301
+ // ─────────────────────────────────────────────────────────
302
+
303
+ /**
304
+ * Register event handler
305
+ * @param {string} event - Event name (update, keydown:escape, click, etc.)
306
+ * @param {Function} handler - Handler function receiving context
307
+ * @returns {FluentGame}
308
+ */
309
+ on(event, handler) {
310
+ const ctx = this.#createContext();
311
+
312
+ if (event === 'update') {
313
+ // Hook into the game's update loop
314
+ const originalUpdate = this.#game.update.bind(this.#game);
315
+ this.#game.update = (dt) => {
316
+ originalUpdate(dt);
317
+ handler(dt, ctx);
318
+ };
319
+ } else if (event.startsWith('keydown:')) {
320
+ const key = event.split(':')[1];
321
+ this.#game.events.on('keydown', (e) => {
322
+ if (e.key?.toLowerCase() === key.toLowerCase() ||
323
+ e.code?.toLowerCase() === key.toLowerCase()) {
324
+ handler(ctx, e);
325
+ }
326
+ });
327
+ } else if (event.startsWith('keyup:')) {
328
+ const key = event.split(':')[1];
329
+ this.#game.events.on('keyup', (e) => {
330
+ if (e.key?.toLowerCase() === key.toLowerCase()) {
331
+ handler(ctx, e);
332
+ }
333
+ });
334
+ } else {
335
+ this.#game.events.on(event, (e) => handler(ctx, e));
336
+ }
337
+
338
+ return this;
339
+ }
340
+
341
+ // ─────────────────────────────────────────────────────────
342
+ // PLUGINS & EXTENSIONS
343
+ // ─────────────────────────────────────────────────────────
344
+
345
+ /**
346
+ * Use a plugin or scene builder function
347
+ * @param {Function} plugin - Plugin function receiving FluentGame
348
+ * @returns {FluentGame}
349
+ */
350
+ use(plugin) {
351
+ plugin(this);
352
+ this.#plugins.push(plugin);
353
+ return this;
354
+ }
355
+
356
+ // ─────────────────────────────────────────────────────────
357
+ // LIFECYCLE
358
+ // ─────────────────────────────────────────────────────────
359
+
360
+ /**
361
+ * Start the game loop
362
+ * @returns {FluentGame}
363
+ */
364
+ start() {
365
+ this.#game.start();
366
+ return this;
367
+ }
368
+
369
+ /**
370
+ * Stop the game loop
371
+ * @returns {FluentGame}
372
+ */
373
+ stop() {
374
+ this.#game.stop();
375
+ return this;
376
+ }
377
+
378
+ /**
379
+ * Restart the game
380
+ * @returns {FluentGame}
381
+ */
382
+ restart() {
383
+ this.#game.restart();
384
+ return this;
385
+ }
386
+
387
+ // ─────────────────────────────────────────────────────────
388
+ // ACCESSORS
389
+ // ─────────────────────────────────────────────────────────
390
+
391
+ /** @returns {Game} Underlying Game instance */
392
+ get game() { return this.#game; }
393
+
394
+ /** @returns {Object} Named object references */
395
+ get refs() { return this.#refs; }
396
+
397
+ /** @returns {Map<string, Scene>} All scenes */
398
+ get scenes() { return this.#scenes; }
399
+
400
+ /** @returns {HTMLCanvasElement} Canvas element */
401
+ get canvas() { return this.#canvas; }
402
+
403
+ /** @returns {number} Canvas width */
404
+ get width() { return this.#canvas.width; }
405
+
406
+ /** @returns {number} Canvas height */
407
+ get height() { return this.#canvas.height; }
408
+
409
+ // ─────────────────────────────────────────────────────────
410
+ // INTERNAL
411
+ // ─────────────────────────────────────────────────────────
412
+
413
+ /**
414
+ * Create a context object for handlers
415
+ * @private
416
+ */
417
+ #createContext() {
418
+ return {
419
+ refs: this.#refs,
420
+ state: this.#state,
421
+ scenes: Object.fromEntries(this.#scenes),
422
+ game: this.#game,
423
+ width: this.#canvas.width,
424
+ height: this.#canvas.height,
425
+ showScene: (name) => this.showScene(name),
426
+ hideScene: (name) => this.hideScene(name),
427
+ transition: (from, to, opts) => this.transition(from, to, opts)
428
+ };
429
+ }
430
+ }