quantum-framework 0.9.0__py3-none-any.whl

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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2006 @@
1
+ """
2
+ Game Engine 2D - HTML/JS Templates and JS Builder
3
+
4
+ Provides:
5
+ - JsBuilder: Structured JS code emitter with proper indentation and escaping
6
+ - HTML shell template
7
+ - Runtime JS subsystems (animation, particles, tweens, audio, etc.)
8
+ """
9
+
10
+ import json
11
+ import re
12
+ from html import escape as html_escape
13
+
14
+
15
+ PIXI_CDN = "https://cdn.jsdelivr.net/npm/pixi.js@8/dist/pixi.min.js"
16
+ MATTER_CDN = "https://cdn.jsdelivr.net/npm/matter-js@0.20/build/matter.min.js"
17
+
18
+
19
+ # ==========================================================================
20
+ # JsBuilder - Structured JS code emitter
21
+ # ==========================================================================
22
+
23
+ class JsBuilder:
24
+ """Builds syntactically valid JavaScript code with proper indentation.
25
+
26
+ Every append method returns self for chaining.
27
+ """
28
+
29
+ def __init__(self, indent_size: int = 2):
30
+ self._lines: list[str] = []
31
+ self._indent: int = 0
32
+ self._indent_size: int = indent_size
33
+
34
+ # -- core --
35
+
36
+ @property
37
+ def _pad(self) -> str:
38
+ return ' ' * (self._indent * self._indent_size)
39
+
40
+ def line(self, code: str = '') -> 'JsBuilder':
41
+ """Emit a single line at current indent."""
42
+ self._lines.append(f"{self._pad}{code}" if code else '')
43
+ return self
44
+
45
+ def raw(self, code: str) -> 'JsBuilder':
46
+ """Emit raw code without indentation changes (for pre-formatted blocks)."""
47
+ for ln in code.split('\n'):
48
+ self._lines.append(f"{self._pad}{ln}")
49
+ return self
50
+
51
+ def blank(self) -> 'JsBuilder':
52
+ self._lines.append('')
53
+ return self
54
+
55
+ def comment(self, text: str) -> 'JsBuilder':
56
+ return self.line(f"// {text}")
57
+
58
+ def section(self, title: str) -> 'JsBuilder':
59
+ return self.blank().line(f"// === {title} ===")
60
+
61
+ def indent(self) -> 'JsBuilder':
62
+ self._indent += 1
63
+ return self
64
+
65
+ def dedent(self) -> 'JsBuilder':
66
+ self._indent = max(0, self._indent - 1)
67
+ return self
68
+
69
+ # -- variables --
70
+
71
+ def const(self, name: str, value: str) -> 'JsBuilder':
72
+ return self.line(f"const {name} = {value};")
73
+
74
+ def let(self, name: str, value: str) -> 'JsBuilder':
75
+ return self.line(f"let {name} = {value};")
76
+
77
+ def assign(self, target: str, value: str) -> 'JsBuilder':
78
+ return self.line(f"{target} = {value};")
79
+
80
+ # -- blocks --
81
+
82
+ def block_open(self, header: str) -> 'JsBuilder':
83
+ """Open a block: `header {`"""
84
+ self.line(f"{header} {{")
85
+ self._indent += 1
86
+ return self
87
+
88
+ def block_close(self, suffix: str = '') -> 'JsBuilder':
89
+ self._indent = max(0, self._indent - 1)
90
+ self.line(f"}}{suffix}")
91
+ return self
92
+
93
+ def func(self, name: str, params: str = '') -> 'JsBuilder':
94
+ return self.block_open(f"function {name}({params})")
95
+
96
+ def method(self, name: str, params: str = '') -> 'JsBuilder':
97
+ return self.block_open(f"{name}({params})")
98
+
99
+ def if_block(self, condition: str) -> 'JsBuilder':
100
+ return self.block_open(f"if ({condition})")
101
+
102
+ def else_if_block(self, condition: str) -> 'JsBuilder':
103
+ self._indent = max(0, self._indent - 1)
104
+ self.line(f"}} else if ({condition}) {{")
105
+ self._indent += 1
106
+ return self
107
+
108
+ def else_block(self) -> 'JsBuilder':
109
+ self._indent = max(0, self._indent - 1)
110
+ self.line("} else {")
111
+ self._indent += 1
112
+ return self
113
+
114
+ def for_of(self, item: str, iterable: str) -> 'JsBuilder':
115
+ return self.block_open(f"for (const {item} of {iterable})")
116
+
117
+ def for_entries(self, key: str, val: str, obj: str) -> 'JsBuilder':
118
+ return self.block_open(f"for (const [{key}, {val}] of Object.entries({obj}))")
119
+
120
+ def for_range(self, var: str, start: str, end: str, step: str = '1') -> 'JsBuilder':
121
+ return self.block_open(f"for (let {var} = {start}; {var} <= {end}; {var} += {step})")
122
+
123
+ # -- classes --
124
+
125
+ def class_open(self, name: str) -> 'JsBuilder':
126
+ return self.block_open(f"class {name}")
127
+
128
+ def class_close(self) -> 'JsBuilder':
129
+ return self.block_close()
130
+
131
+ # -- statements --
132
+
133
+ def call(self, fn: str, *args: str) -> 'JsBuilder':
134
+ return self.line(f"{fn}({', '.join(args)});")
135
+
136
+ def ret(self, value: str = '') -> 'JsBuilder':
137
+ return self.line(f"return {value};") if value else self.line("return;")
138
+
139
+ # -- output --
140
+
141
+ def build(self) -> str:
142
+ return '\n'.join(self._lines)
143
+
144
+ def __str__(self) -> str:
145
+ return self.build()
146
+
147
+
148
+ # ==========================================================================
149
+ # Safe value helpers
150
+ # ==========================================================================
151
+
152
+ def js_string(val: str) -> str:
153
+ """Safely encode a string as a JS string literal using JSON encoding."""
154
+ return json.dumps(val)
155
+
156
+
157
+ def js_id(name: str) -> str:
158
+ """Sanitize a string to be a valid JS identifier."""
159
+ s = re.sub(r'[^a-zA-Z0-9_$]', '_', name)
160
+ if s and s[0].isdigit():
161
+ s = '_' + s
162
+ return s or '_unnamed'
163
+
164
+
165
+ def js_number(val) -> str:
166
+ """Safely format a number for JS."""
167
+ if isinstance(val, bool):
168
+ return 'true' if val else 'false'
169
+ if isinstance(val, (int, float)):
170
+ if isinstance(val, float) and val == int(val):
171
+ return str(int(val))
172
+ return str(val)
173
+ try:
174
+ f = float(val)
175
+ if f == int(f):
176
+ return str(int(f))
177
+ return str(f)
178
+ except (ValueError, TypeError):
179
+ return '0'
180
+
181
+
182
+ def js_bool(val) -> str:
183
+ if isinstance(val, bool):
184
+ return 'true' if val else 'false'
185
+ if isinstance(val, str):
186
+ return 'true' if val.lower() in ('true', '1', 'yes') else 'false'
187
+ return 'true' if val else 'false'
188
+
189
+
190
+ def is_number(val: str) -> bool:
191
+ try:
192
+ float(val)
193
+ return True
194
+ except (ValueError, TypeError):
195
+ return False
196
+
197
+
198
+ def js_safe_value(val: str, val_type: str = 'string') -> str:
199
+ """Convert a Quantum value to a safe JS literal based on declared type."""
200
+ if val_type == 'integer':
201
+ return val if val.lstrip('-').isdigit() else '0'
202
+ elif val_type == 'boolean':
203
+ return js_bool(val)
204
+ elif val_type in ('float', 'number', 'decimal'):
205
+ try:
206
+ float(val)
207
+ return val
208
+ except ValueError:
209
+ return '0'
210
+ elif val_type == 'array':
211
+ # Return array literals as-is (e.g., "[]", "[1,2,3]")
212
+ if val.startswith('[') and val.endswith(']'):
213
+ return val
214
+ return '[]'
215
+ elif val_type == 'object':
216
+ # Return object literals as-is (e.g., "{}", "{a:1}")
217
+ if val.startswith('{') and val.endswith('}'):
218
+ return val
219
+ return '{}'
220
+ else:
221
+ if val in ('true', 'false'):
222
+ return val
223
+ if is_number(val):
224
+ return val
225
+ return json.dumps(val)
226
+
227
+
228
+ def sanitize_hud_text(text: str) -> str:
229
+ """Escape HTML in HUD text content, preserving {binding} expressions."""
230
+ # Split on {bindings}, escape the non-binding parts
231
+ parts = re.split(r'(\{[^}]+\})', text)
232
+ result = []
233
+ for part in parts:
234
+ if part.startswith('{') and part.endswith('}'):
235
+ result.append(part) # keep binding as-is
236
+ else:
237
+ result.append(html_escape(part))
238
+ return ''.join(result)
239
+
240
+
241
+ def compile_binding_to_js(text: str) -> str:
242
+ """Convert HUD text with {bindings} to a safe JS template expression.
243
+
244
+ Instead of innerHTML with template literals (XSS risk), generates a
245
+ function that builds text content safely using only whitelisted variables.
246
+
247
+ Input: "Score: {score}"
248
+ Output: "'Score: ' + String(score)"
249
+ """
250
+ parts = re.split(r'(\{[^}]+\})', text)
251
+ js_parts = []
252
+ for part in parts:
253
+ if part.startswith('{') and part.endswith('}'):
254
+ expr = part[1:-1].strip()
255
+ # Whitelist: only allow identifiers, basic math, dots, arrays, ternary
256
+ if re.match(r'^[a-zA-Z_$0-9][a-zA-Z0-9_$.\s+\-*/()%<>=!&|,\[\]?:\'"]*$', expr):
257
+ js_parts.append(f"String({expr})")
258
+ else:
259
+ js_parts.append(json.dumps(part)) # escape the raw binding
260
+ elif part:
261
+ js_parts.append(json.dumps(part))
262
+ return ' + '.join(js_parts) if js_parts else '""'
263
+
264
+
265
+ # ==========================================================================
266
+ # HTML Template
267
+ # ==========================================================================
268
+
269
+ HTML_TEMPLATE = """\
270
+ <!DOCTYPE html>
271
+ <html>
272
+ <head>
273
+ <meta charset="utf-8" />
274
+ <title>{title}</title>
275
+ <style>
276
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
277
+ body {{ overflow: hidden; background: #111; display: flex; justify-content: center; align-items: center; width: 100vw; height: 100vh; }}
278
+ canvas {{ display: block; image-rendering: pixelated; image-rendering: crisp-edges; transform: scale(3); transform-origin: center center; }}
279
+ .qg-hud {{ position: absolute; pointer-events: none; padding: 8px; font-family: sans-serif; z-index: 10; }}
280
+ .qg-hud-top-left {{ top: 0; left: 0; }}
281
+ .qg-hud-top-right {{ top: 0; right: 0; }}
282
+ .qg-hud-top-center {{ top: 0; left: 50%; transform: translateX(-50%); }}
283
+ .qg-hud-bottom-left {{ bottom: 0; left: 0; }}
284
+ .qg-hud-bottom-right {{ bottom: 0; right: 0; }}
285
+ .qg-hud-bottom-center {{ bottom: 0; left: 50%; transform: translateX(-50%); }}
286
+ .qg-hud-center {{ top: 50%; left: 50%; transform: translate(-50%, -50%); }}
287
+
288
+ /* Source Code Viewer */
289
+ #qg-source-btn {{
290
+ position: fixed;
291
+ bottom: 16px;
292
+ right: 16px;
293
+ z-index: 1000;
294
+ background: rgba(255, 255, 255, 0.1);
295
+ color: rgba(255, 255, 255, 0.5);
296
+ border: 1px solid rgba(255, 255, 255, 0.15);
297
+ padding: 8px 16px;
298
+ border-radius: 6px;
299
+ cursor: pointer;
300
+ font-family: monospace;
301
+ font-size: 12px;
302
+ transition: all 0.2s;
303
+ }}
304
+ #qg-source-btn:hover {{
305
+ background: rgba(255, 255, 255, 0.2);
306
+ color: rgba(255, 255, 255, 0.9);
307
+ }}
308
+ #qg-source-modal {{
309
+ display: none;
310
+ position: fixed;
311
+ top: 0;
312
+ left: 0;
313
+ width: 100%;
314
+ height: 100%;
315
+ background: rgba(0, 0, 0, 0.92);
316
+ z-index: 999;
317
+ overflow: auto;
318
+ }}
319
+ #qg-source-modal.visible {{
320
+ display: flex;
321
+ flex-direction: column;
322
+ }}
323
+ #qg-source-close {{
324
+ position: fixed;
325
+ top: 16px;
326
+ right: 16px;
327
+ background: rgba(255, 255, 255, 0.1);
328
+ color: #fff;
329
+ border: 1px solid rgba(255, 255, 255, 0.2);
330
+ padding: 8px 16px;
331
+ border-radius: 6px;
332
+ cursor: pointer;
333
+ font-family: monospace;
334
+ font-size: 13px;
335
+ z-index: 1001;
336
+ }}
337
+ #qg-source-close:hover {{
338
+ background: rgba(255, 255, 255, 0.2);
339
+ }}
340
+ #qg-source-content {{
341
+ flex: 1;
342
+ overflow: auto;
343
+ padding: 48px 24px 24px;
344
+ }}
345
+ #qg-source-content pre {{
346
+ background: #1e1e2e;
347
+ color: #cdd6f4;
348
+ padding: 24px;
349
+ border-radius: 12px;
350
+ font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', monospace;
351
+ font-size: 14px;
352
+ line-height: 1.7;
353
+ overflow-x: auto;
354
+ white-space: pre;
355
+ tab-size: 2;
356
+ border: 1px solid #313244;
357
+ max-width: 900px;
358
+ margin: 0 auto;
359
+ }}
360
+ </style>
361
+ </head>
362
+ <body>
363
+ {hud_html}
364
+ <button id="qg-source-btn" onclick="toggleSource()">source</button>
365
+ <div id="qg-source-modal">
366
+ <button id="qg-source-close" onclick="toggleSource()">ESC</button>
367
+ <div id="qg-source-content">
368
+ <pre id="qg-source-code">{source_code}</pre>
369
+ </div>
370
+ </div>
371
+ <script src="{pixi_cdn}"></script>
372
+ <script src="{matter_cdn}"></script>
373
+ <script>
374
+ function toggleSource() {{
375
+ var modal = document.getElementById('qg-source-modal');
376
+ modal.classList.toggle('visible');
377
+ }}
378
+ document.addEventListener('keydown', function(e) {{
379
+ if (e.key === 'Escape') {{
380
+ document.getElementById('qg-source-modal').classList.remove('visible');
381
+ }}
382
+ }});
383
+ {game_js}
384
+ </script>
385
+ </body>
386
+ </html>
387
+ """
388
+
389
+
390
+ # ==========================================================================
391
+ # Runtime JS subsystems (emitted as pre-validated blocks)
392
+ # ==========================================================================
393
+
394
+ def emit_physics_sync(js: JsBuilder):
395
+ """Emit the syncPhysics function to sync sprite positions with physics bodies."""
396
+ js.func('syncPhysics')
397
+ js.for_entries('id', 'info', '_sprites')
398
+ js.if_block('info.body && !info.body.isStatic')
399
+ js.assign('info.sprite.x', 'info.body.position.x')
400
+ js.assign('info.sprite.y', 'info.body.position.y')
401
+ js.assign('info.sprite.rotation', 'info.body.angle')
402
+ js.block_close() # if
403
+ js.block_close() # for
404
+ js.block_close() # function
405
+
406
+
407
+ def emit_tilemap_collision(js: JsBuilder):
408
+ """Emit AABB-based tilemap collision resolution.
409
+
410
+ This replaces per-tile physics bodies with a direct grid lookup,
411
+ which avoids edge-catching between adjacent tiles and naturally
412
+ supports walking up stair-step terrain.
413
+ """
414
+ js.comment("Tilemap Collision System")
415
+ js.const('_collisionGrids', '[]')
416
+ js.blank()
417
+
418
+ # The core resolution function
419
+ js.raw("""function _resolveTilemapCollisions() {
420
+ for (const cg of _collisionGrids) {
421
+ const grid = cg.grid, tw = cg.tw, th = cg.th;
422
+ for (const [id, info] of Object.entries(_sprites)) {
423
+ if (!info.body || info.body.isStatic) continue;
424
+ const body = info.body;
425
+ const bw = body.bounds.max.x - body.bounds.min.x;
426
+ const bh = body.bounds.max.y - body.bounds.min.y;
427
+ const hw = bw / 2, hh = bh / 2;
428
+ let bx = body.position.x, by = body.position.y;
429
+
430
+ // Vertical resolution first (landing/ceiling)
431
+ const vy = body.velocity.y;
432
+ const minCol = Math.floor((bx - hw + 2) / tw);
433
+ const maxCol = Math.floor((bx + hw - 2) / tw);
434
+
435
+ if (vy >= 0) {
436
+ // Falling or on ground — check below
437
+ const footRow = Math.floor((by + hh) / th);
438
+ if (footRow >= 0 && footRow < grid.length) {
439
+ for (let c = minCol; c <= maxCol; c++) {
440
+ if (c >= 0 && c < (grid[footRow] || []).length && grid[footRow][c]) {
441
+ const tileTop = footRow * th;
442
+ if (by + hh > tileTop && by + hh < tileTop + th) {
443
+ by = tileTop - hh;
444
+ Matter.Body.setPosition(body, { x: bx, y: by });
445
+ Matter.Body.setVelocity(body, { x: body.velocity.x, y: 0 });
446
+ body._onGround = true;
447
+ break;
448
+ }
449
+ }
450
+ }
451
+ }
452
+ } else {
453
+ // Rising — check above
454
+ const headRow = Math.floor((by - hh) / th);
455
+ if (headRow >= 0 && headRow < grid.length) {
456
+ for (let c = minCol; c <= maxCol; c++) {
457
+ if (c >= 0 && c < (grid[headRow] || []).length && grid[headRow][c]) {
458
+ const tileBottom = (headRow + 1) * th;
459
+ if (by - hh < tileBottom && by - hh > tileBottom - th) {
460
+ by = tileBottom + hh;
461
+ Matter.Body.setPosition(body, { x: bx, y: by });
462
+ Matter.Body.setVelocity(body, { x: body.velocity.x, y: 0 });
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ // Horizontal resolution (walls) with step-up support
471
+ const vx = body.velocity.x;
472
+ if (Math.abs(vx) > 0.1) {
473
+ // Check the column the body is moving into
474
+ const frontX = vx > 0 ? bx + hw : bx - hw;
475
+ const frontCol = Math.floor(frontX / tw);
476
+ const topRow = Math.floor((by - hh + 2) / th);
477
+ const botRow = Math.floor((by + hh - 2) / th);
478
+
479
+ let blocked = false;
480
+ for (let r = topRow; r <= botRow; r++) {
481
+ if (r >= 0 && r < grid.length && frontCol >= 0 && frontCol < (grid[r] || []).length && grid[r][frontCol]) {
482
+ blocked = true;
483
+ break;
484
+ }
485
+ }
486
+
487
+ if (blocked) {
488
+ // Check for step-up: is the tile just at foot level?
489
+ // If only the bottom row is blocked and the row above is clear, step up
490
+ const stepRow = botRow;
491
+ const aboveRow = stepRow - 1;
492
+ const onlyFootBlocked = (
493
+ stepRow >= 0 && stepRow < grid.length && frontCol >= 0 &&
494
+ grid[stepRow] && grid[stepRow][frontCol] &&
495
+ (aboveRow < 0 || !grid[aboveRow] || !grid[aboveRow][frontCol])
496
+ );
497
+
498
+ if (onlyFootBlocked) {
499
+ // Step up: move body to top of the blocking tile
500
+ const stepTop = stepRow * th;
501
+ const newY = stepTop - hh;
502
+ if (by - newY <= th) { // Only step up one tile height max
503
+ Matter.Body.setPosition(body, { x: bx, y: newY });
504
+ by = newY;
505
+ }
506
+ } else {
507
+ // Full wall — block horizontal movement
508
+ if (vx > 0) {
509
+ Matter.Body.setPosition(body, { x: frontCol * tw - hw, y: by });
510
+ } else {
511
+ Matter.Body.setPosition(body, { x: (frontCol + 1) * tw + hw, y: by });
512
+ }
513
+ Matter.Body.setVelocity(body, { x: 0, y: body.velocity.y });
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }""")
520
+ js.blank()
521
+
522
+
523
+ def emit_input_system(js: JsBuilder):
524
+ """Emit keyboard input tracking."""
525
+ js.const('_keys', '{}')
526
+ js.const('_justPressed', '{}')
527
+ js.line("window.addEventListener('keydown', (e) => {")
528
+ js.indent()
529
+ js.line("if (!_keys[e.key]) _justPressed[e.key] = true;")
530
+ js.assign('_keys[e.key]', 'true')
531
+ js.dedent()
532
+ js.line("});")
533
+ js.line("window.addEventListener('keyup', (e) => {")
534
+ js.indent()
535
+ js.assign('_keys[e.key]', 'false')
536
+ js.dedent()
537
+ js.line("});")
538
+ js.func('_clearJustPressed')
539
+ js.line("for (const k in _justPressed) delete _justPressed[k];")
540
+ js.block_close()
541
+
542
+
543
+ def emit_animation_system(js: JsBuilder):
544
+ """Emit the spritesheet animation runtime.
545
+
546
+ Manages frame-based animations on sprites with spritesheets.
547
+ Supports named animations, auto-play, speed, loop, and control-based switching.
548
+ """
549
+ js.comment("Animation System")
550
+ js.const('_animatedSprites', '{}')
551
+ js.blank()
552
+
553
+ # parseFrames: "0-3" => [0,1,2,3], "0,2,4" => [0,2,4]
554
+ js.func('_parseFrames', 'spec')
555
+ js.if_block("spec.includes('-')")
556
+ js.const('parts', "spec.split('-').map(Number)")
557
+ js.const('result', '[]')
558
+ js.for_range('i', 'parts[0]', 'parts[1]')
559
+ js.line('result.push(i);')
560
+ js.block_close() # for
561
+ js.ret('result')
562
+ js.block_close() # if
563
+ js.ret("spec.split(',').map(s => parseInt(s.trim(), 10))")
564
+ js.block_close() # function
565
+ js.blank()
566
+
567
+ # registerAnimation
568
+ js.func('_registerAnimation', 'spriteId, texture, frameW, frameH, anims')
569
+ js.if_block('!texture || !frameW || !frameH')
570
+ js.ret()
571
+ js.block_close()
572
+ js.comment('PIXI v8: get texture source and dimensions')
573
+ js.const('baseTexture', 'texture.source || texture.baseTexture || texture')
574
+ js.const('texW', 'texture.width || baseTexture.width || (texture.frame && texture.frame.width) || 0')
575
+ js.const('texH', 'texture.height || baseTexture.height || (texture.frame && texture.frame.height) || 0')
576
+ js.if_block('!texW || !texH')
577
+ js.line("console.warn('Animation failed for', spriteId, '- texture dimensions not found');")
578
+ js.ret()
579
+ js.block_close()
580
+ js.const('cols', 'Math.floor(texW / frameW)')
581
+ js.const('rows', 'Math.floor(texH / frameH)')
582
+ js.const('totalFrames', 'cols * rows')
583
+ js.blank()
584
+ js.comment('Build frame textures')
585
+ js.const('frameTextures', '[]')
586
+ js.for_range('i', '0', 'totalFrames - 1')
587
+ js.const('fx', 'i % cols')
588
+ js.const('fy', 'Math.floor(i / cols)')
589
+ js.const('rect', 'new PIXI.Rectangle(fx * frameW, fy * frameH, frameW, frameH)')
590
+ js.const('frameTex', 'new PIXI.Texture({ source: baseTexture, frame: rect })')
591
+ js.line('frameTextures.push(frameTex);')
592
+ js.block_close() # for
593
+ js.blank()
594
+ js.comment('Parse named animations')
595
+ js.const('animDefs', '{}')
596
+ js.let('defaultAnim', 'null')
597
+ js.for_of('[name, cfg]', 'Object.entries(anims)')
598
+ js.const('frames', '_parseFrames(cfg.frames)')
599
+ js.const('textures', 'frames.filter(f => f < frameTextures.length).map(f => frameTextures[f])')
600
+ js.if_block('textures.length > 0')
601
+ js.line("animDefs[name] = { textures: textures, speed: cfg.speed || 0.1, loop: cfg.loop !== false };")
602
+ js.if_block('cfg.autoPlay')
603
+ js.assign('defaultAnim', 'name')
604
+ js.block_close()
605
+ js.block_close()
606
+ js.block_close() # for
607
+ js.blank()
608
+ js.if_block('Object.keys(animDefs).length === 0')
609
+ js.ret()
610
+ js.block_close()
611
+ js.blank()
612
+ js.if_block('!defaultAnim')
613
+ js.assign('defaultAnim', 'Object.keys(animDefs)[0]')
614
+ js.block_close()
615
+ js.blank()
616
+ js.comment('Create animated sprite')
617
+ js.const('firstAnim', 'animDefs[defaultAnim]')
618
+ js.const('animSprite', 'new PIXI.AnimatedSprite(firstAnim.textures)')
619
+ js.assign('animSprite.animationSpeed', 'firstAnim.speed')
620
+ js.if_block('firstAnim.loop !== false')
621
+ js.assign('animSprite.loop', 'true')
622
+ js.block_close()
623
+ js.line('animSprite.play();')
624
+ js.blank()
625
+ js.comment('Copy transform from static sprite')
626
+ js.const('info', '_sprites[spriteId]')
627
+ js.if_block('info')
628
+ js.const('oldSprite', 'info.sprite')
629
+ js.assign('animSprite.x', 'oldSprite.x')
630
+ js.assign('animSprite.y', 'oldSprite.y')
631
+ js.assign('animSprite.anchor.x', 'oldSprite.anchor ? oldSprite.anchor.x : 0.5')
632
+ js.assign('animSprite.anchor.y', 'oldSprite.anchor ? oldSprite.anchor.y : 0.5')
633
+ # Don't copy width/height - let AnimatedSprite use frame texture's natural size
634
+ js.assign('animSprite.alpha', 'oldSprite.alpha')
635
+ js.assign('animSprite.visible', 'oldSprite.visible')
636
+ js.blank()
637
+ js.comment('Replace in container')
638
+ js.const('parent', 'oldSprite.parent')
639
+ js.if_block('parent')
640
+ js.const('idx', 'parent.getChildIndex(oldSprite)')
641
+ js.line('parent.removeChild(oldSprite);')
642
+ js.line('parent.addChildAt(animSprite, idx);')
643
+ js.block_close()
644
+ js.assign('info.sprite', 'animSprite')
645
+ js.line("_animatedSprites[spriteId] = { anims: animDefs, current: defaultAnim, sprite: animSprite };")
646
+ js.block_close()
647
+ js.block_close() # function
648
+ js.blank()
649
+
650
+ # switchAnimation
651
+ js.func('_switchAnimation', 'spriteId, animName')
652
+ js.const('entry', '_animatedSprites[spriteId]')
653
+ js.if_block('!entry || entry.current === animName || !entry.anims[animName]')
654
+ js.ret()
655
+ js.block_close()
656
+ js.const('anim', 'entry.anims[animName]')
657
+ js.assign('entry.sprite.textures', 'anim.textures')
658
+ js.assign('entry.sprite.animationSpeed', 'anim.speed')
659
+ js.assign('entry.sprite.loop', 'anim.loop !== false')
660
+ js.line('entry.sprite.gotoAndPlay(0);')
661
+ js.assign('entry.current', 'animName')
662
+ js.block_close() # function
663
+ js.blank()
664
+
665
+ # updateAnimatedSprites - manually tick all animated sprites (PIXI v8 fix)
666
+ js.func('_updateAnimatedSprites', 'ticker')
667
+ js.for_entries('spriteId', 'entry', '_animatedSprites')
668
+ js.if_block('entry.sprite && entry.sprite.playing')
669
+ js.comment('PIXI v8: manually update animation using deltaMS')
670
+ js.line('entry.sprite.update(ticker);')
671
+ js.block_close()
672
+ js.block_close()
673
+ js.block_close() # function
674
+ js.blank()
675
+
676
+ # updateControlAnimations - auto-switch based on input
677
+ js.func('_updateControlAnimations')
678
+ js.for_entries('spriteId', 'ctrl', '_controlledSprites')
679
+ js.const('entry', '_animatedSprites[spriteId]')
680
+ js.if_block('!entry')
681
+ js.line('continue;')
682
+ js.block_close()
683
+ js.const('info', '_sprites[spriteId]')
684
+ js.if_block('!info || !info.body')
685
+ js.line('continue;')
686
+ js.block_close()
687
+ js.const('vx', 'Math.abs(info.body.velocity.x)')
688
+ js.const('vy', 'info.body.velocity.y')
689
+ js.blank()
690
+ js.comment('Priority: jump > walk > idle')
691
+ js.if_block("Math.abs(vy) > 5 && entry.anims['jump']")
692
+ js.line("_switchAnimation(spriteId, 'jump');")
693
+ js.else_if_block("vx > 0.1 && _keys[ctrl.right] && entry.anims['walk-right']")
694
+ js.line("_switchAnimation(spriteId, 'walk-right');")
695
+ js.else_if_block("vx > 0.1 && _keys[ctrl.left] && entry.anims['walk-left']")
696
+ js.line("_switchAnimation(spriteId, 'walk-left');")
697
+ js.else_if_block("vx > 0.1 && (entry.anims['walk'] || entry.anims['walk-right'])")
698
+ js.line("_switchAnimation(spriteId, entry.anims['walk'] ? 'walk' : 'walk-right');")
699
+ js.else_if_block("entry.anims['idle']")
700
+ js.line("_switchAnimation(spriteId, 'idle');")
701
+ js.block_close() # if chain
702
+ js.block_close() # for
703
+ js.block_close() # function
704
+
705
+
706
+ def emit_easing_functions(js: JsBuilder):
707
+ """Emit a comprehensive set of easing functions."""
708
+ js.comment("Easing Functions")
709
+ js.line('const _easing = {')
710
+ js.indent()
711
+ js.line("linear: t => t,")
712
+ js.line("easeIn: t => t * t,")
713
+ js.line("easeOut: t => t * (2 - t),")
714
+ js.line("easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,")
715
+ js.line("easeInCubic: t => t * t * t,")
716
+ js.line("easeOutCubic: t => (--t) * t * t + 1,")
717
+ js.line("easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,")
718
+ js.line("easeInQuart: t => t * t * t * t,")
719
+ js.line("easeOutQuart: t => 1 - (--t) * t * t * t,")
720
+ js.line("easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,")
721
+ js.line("easeOutBounce: t => {")
722
+ js.indent()
723
+ js.line("if (t < 1/2.75) return 7.5625 * t * t;")
724
+ js.line("if (t < 2/2.75) return 7.5625 * (t -= 1.5/2.75) * t + 0.75;")
725
+ js.line("if (t < 2.5/2.75) return 7.5625 * (t -= 2.25/2.75) * t + 0.9375;")
726
+ js.line("return 7.5625 * (t -= 2.625/2.75) * t + 0.984375;")
727
+ js.dedent()
728
+ js.line("},")
729
+ js.line("easeInBounce: t => 1 - _easing.easeOutBounce(1 - t),")
730
+ js.line("easeOutElastic: t => {")
731
+ js.indent()
732
+ js.line("if (t === 0 || t === 1) return t;")
733
+ js.line("return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1;")
734
+ js.dedent()
735
+ js.line("},")
736
+ js.line("easeInElastic: t => {")
737
+ js.indent()
738
+ js.line("if (t === 0 || t === 1) return t;")
739
+ js.line("return -Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.075) * (2 * Math.PI) / 0.3);")
740
+ js.dedent()
741
+ js.line("},")
742
+ js.dedent()
743
+ js.line("};")
744
+
745
+
746
+ def emit_tween_system(js: JsBuilder):
747
+ """Emit the tween runtime with real easing."""
748
+ emit_easing_functions(js)
749
+ js.blank()
750
+ js.const('_tweens', '{}')
751
+ js.let('_tweenIdCounter', '0')
752
+ js.blank()
753
+
754
+ js.func('_createTween', 'config')
755
+ js.const('id', "config.id || ('tween_' + _tweenIdCounter++)")
756
+ js.line("_tweens[id] = {")
757
+ js.indent()
758
+ js.line("target: config.target,")
759
+ js.line("prop: config.prop,")
760
+ js.line("from: null,")
761
+ js.line("to: config.to,")
762
+ js.line("duration: config.duration || 1,")
763
+ js.line("easing: config.easing || 'linear',")
764
+ js.line("loop: !!config.loop,")
765
+ js.line("yoyo: !!config.yoyo,")
766
+ js.line("delay: config.delay || 0,")
767
+ js.line("elapsed: 0,")
768
+ js.line("delayElapsed: 0,")
769
+ js.line("active: config.active !== false,")
770
+ js.line("started: false,")
771
+ js.dedent()
772
+ js.line("};")
773
+ js.ret('id')
774
+ js.block_close()
775
+ js.blank()
776
+
777
+ js.func('_updateTweens', 'dt')
778
+ js.for_entries('tid', 'tw', '_tweens')
779
+ js.if_block('!tw.active')
780
+ js.line('continue;')
781
+ js.block_close()
782
+ js.comment('Handle delay')
783
+ js.if_block('tw.delayElapsed < tw.delay')
784
+ js.line('tw.delayElapsed += dt / 60;')
785
+ js.line('continue;')
786
+ js.block_close()
787
+ js.comment('Init start value on first frame')
788
+ js.if_block('!tw.started')
789
+ js.const('info', '_sprites[tw.target]')
790
+ js.if_block('!info')
791
+ js.line('continue;')
792
+ js.block_close()
793
+ js.assign('tw.from', 'info.sprite[tw.prop]')
794
+ js.assign('tw.started', 'true')
795
+ js.block_close()
796
+ js.line('tw.elapsed += dt / 60;')
797
+ js.const('rawT', 'Math.min(tw.elapsed / tw.duration, 1)')
798
+ js.const('easeFn', "_easing[tw.easing] || _easing.linear")
799
+ js.const('t', 'easeFn(rawT)')
800
+ js.const('info', '_sprites[tw.target]')
801
+ js.if_block('info')
802
+ js.assign('info.sprite[tw.prop]', 'tw.from + (tw.to - tw.from) * t')
803
+ js.block_close()
804
+ js.if_block('rawT >= 1')
805
+ js.if_block('tw.loop')
806
+ js.assign('tw.elapsed', '0')
807
+ js.if_block('tw.yoyo')
808
+ js.const('tmp', 'tw.from')
809
+ js.assign('tw.from', 'tw.to')
810
+ js.assign('tw.to', 'tmp')
811
+ js.block_close()
812
+ js.else_block()
813
+ js.assign('tw.active', 'false')
814
+ js.block_close()
815
+ js.block_close() # rawT >= 1
816
+ js.block_close() # for
817
+ js.block_close() # function
818
+
819
+
820
+ def emit_particle_system(js: JsBuilder):
821
+ """Emit a real particle system with pooling, velocity, lifetime, alpha fade."""
822
+ js.comment("Particle System")
823
+ js.const('_particleSystems', '{}')
824
+ js.blank()
825
+
826
+ js.func('_createParticleSystem', 'config')
827
+ js.line('const ps = {')
828
+ js.indent()
829
+ js.line("id: config.id,")
830
+ js.line("follow: config.follow || null,")
831
+ js.line("trigger: config.trigger || null,")
832
+ js.line("count: config.count || 20,")
833
+ js.line("emitRate: config.emitRate || 10,")
834
+ js.line("lifetime: config.lifetime || 1,")
835
+ js.line("speedMin: config.speedMin || 1,")
836
+ js.line("speedMax: config.speedMax || 3,")
837
+ js.line("angleMin: (config.angleMin || 0) * Math.PI / 180,")
838
+ js.line("angleMax: (config.angleMax || 360) * Math.PI / 180,")
839
+ js.line("alphaStart: config.alphaStart !== undefined ? config.alphaStart : 1,")
840
+ js.line("alphaEnd: config.alphaEnd !== undefined ? config.alphaEnd : 0,")
841
+ js.line("active: false,")
842
+ js.line("emitTimer: 0,")
843
+ js.line("pool: [],")
844
+ js.line("container: new PIXI.Container(),")
845
+ js.dedent()
846
+ js.line("};")
847
+ js.blank()
848
+ js.comment('Pre-allocate particle pool')
849
+ js.for_range('i', '0', 'ps.count - 1')
850
+ js.const('p', 'new PIXI.Graphics()')
851
+ js.line("p.circle(0, 0, 3);")
852
+ js.line("p.fill({ color: 0xffffff });")
853
+ js.assign('p.visible', 'false')
854
+ js.line("p._life = 0; p._maxLife = 0; p._vx = 0; p._vy = 0;")
855
+ js.line("p._alphaStart = 1; p._alphaEnd = 0;")
856
+ js.line('ps.container.addChild(p);')
857
+ js.line('ps.pool.push(p);')
858
+ js.block_close()
859
+ js.blank()
860
+ js.line('_cameraContainer.addChild(ps.container);')
861
+ js.line('_particleSystems[config.id] = ps;')
862
+ js.ret('ps')
863
+ js.block_close() # function
864
+ js.blank()
865
+
866
+ js.func('_emitParticle', 'ps, x, y')
867
+ js.for_of('p', 'ps.pool')
868
+ js.if_block('!p.visible')
869
+ js.assign('p.x', 'x')
870
+ js.assign('p.y', 'y')
871
+ js.const('angle', 'ps.angleMin + Math.random() * (ps.angleMax - ps.angleMin)')
872
+ js.const('speed', 'ps.speedMin + Math.random() * (ps.speedMax - ps.speedMin)')
873
+ js.assign('p._vx', 'Math.cos(angle) * speed')
874
+ js.assign('p._vy', 'Math.sin(angle) * speed')
875
+ js.assign('p._life', '0')
876
+ js.assign('p._maxLife', 'ps.lifetime')
877
+ js.assign('p._alphaStart', 'ps.alphaStart')
878
+ js.assign('p._alphaEnd', 'ps.alphaEnd')
879
+ js.assign('p.alpha', 'ps.alphaStart')
880
+ js.assign('p.visible', 'true')
881
+ js.ret()
882
+ js.block_close() # if
883
+ js.block_close() # for
884
+ js.block_close() # function
885
+ js.blank()
886
+
887
+ js.func('_activateParticles', 'psId')
888
+ js.const('ps', '_particleSystems[psId]')
889
+ js.if_block('ps')
890
+ js.assign('ps.active', 'true')
891
+ js.block_close()
892
+ js.block_close()
893
+ js.blank()
894
+
895
+ js.func('_deactivateParticles', 'psId')
896
+ js.const('ps', '_particleSystems[psId]')
897
+ js.if_block('ps')
898
+ js.assign('ps.active', 'false')
899
+ js.block_close()
900
+ js.block_close()
901
+ js.blank()
902
+
903
+ js.func('_updateParticles', 'dt')
904
+ js.for_entries('id', 'ps', '_particleSystems')
905
+ js.comment('Emit new particles if active')
906
+ js.if_block('ps.active')
907
+ js.line('ps.emitTimer += dt / 60;')
908
+ js.const('interval', '1 / ps.emitRate')
909
+ js.block_open('while (ps.emitTimer >= interval)')
910
+ js.line('ps.emitTimer -= interval;')
911
+ js.let('ex', '0')
912
+ js.let('ey', '0')
913
+ js.if_block('ps.follow')
914
+ js.const('info', '_sprites[ps.follow]')
915
+ js.if_block('info')
916
+ js.assign('ex', 'info.sprite.x')
917
+ js.assign('ey', 'info.sprite.y')
918
+ js.block_close()
919
+ js.block_close()
920
+ js.line('_emitParticle(ps, ex, ey);')
921
+ js.block_close() # while
922
+ js.block_close() # if active
923
+ js.blank()
924
+ js.comment('Update living particles')
925
+ js.for_of('p', 'ps.pool')
926
+ js.if_block('!p.visible')
927
+ js.line('continue;')
928
+ js.block_close()
929
+ js.line('p._life += dt / 60;')
930
+ js.if_block('p._life >= p._maxLife')
931
+ js.assign('p.visible', 'false')
932
+ js.line('continue;')
933
+ js.block_close()
934
+ js.const('lifeT', 'p._life / p._maxLife')
935
+ js.assign('p.x', 'p.x + p._vx')
936
+ js.assign('p.y', 'p.y + p._vy')
937
+ js.assign('p.alpha', 'p._alphaStart + (p._alphaEnd - p._alphaStart) * lifeT')
938
+ js.block_close() # for particles
939
+ js.block_close() # for systems
940
+ js.block_close() # function
941
+
942
+
943
+ def emit_audio_system(js: JsBuilder):
944
+ """Emit robust audio system with proper autoplay handling."""
945
+ js.comment("Audio System")
946
+ js.const('_sounds', '{}')
947
+ js.let('_audioCtx', 'null')
948
+ js.let('_audioUnlocked', 'false')
949
+ js.const('_pendingSounds', '[]')
950
+ js.blank()
951
+
952
+ js.func('_initAudio')
953
+ js.if_block('_audioCtx')
954
+ js.ret()
955
+ js.block_close()
956
+ js.line("_audioCtx = new (window.AudioContext || window.webkitAudioContext)();")
957
+ js.blank()
958
+ js.comment('Unlock audio on any user interaction')
959
+ js.const('unlockEvents', "['click', 'keydown', 'touchstart', 'pointerdown']")
960
+ js.func('_unlock')
961
+ js.if_block('_audioUnlocked')
962
+ js.ret()
963
+ js.block_close()
964
+ js.if_block("_audioCtx.state === 'suspended'")
965
+ js.line("_audioCtx.resume().then(() => {")
966
+ js.indent()
967
+ js.assign('_audioUnlocked', 'true')
968
+ js.comment('Play any queued sounds')
969
+ js.block_open('while (_pendingSounds.length > 0)')
970
+ js.const('pending', '_pendingSounds.shift()')
971
+ js.const('_psrc', '_doPlaySound(pending.id, pending.opts)')
972
+ js.if_block('_psrc')
973
+ js.assign('_activeSources[pending.id]', '_psrc')
974
+ js.line("_psrc.onended = function() { if (_activeSources[pending.id] === _psrc) delete _activeSources[pending.id]; };")
975
+ js.block_close()
976
+ js.block_close()
977
+ js.dedent()
978
+ js.line("});")
979
+ js.else_block()
980
+ js.assign('_audioUnlocked', 'true')
981
+ js.block_close()
982
+ js.for_of('evt', 'unlockEvents')
983
+ js.line("document.removeEventListener(evt, _unlock);")
984
+ js.block_close()
985
+ js.block_close() # _unlock
986
+ js.blank()
987
+ js.for_of('evt', 'unlockEvents')
988
+ js.line("document.addEventListener(evt, _unlock, { once: false });")
989
+ js.block_close()
990
+ js.block_close() # _initAudio
991
+ js.blank()
992
+
993
+ js.block_open('async function _loadSound(id, url)')
994
+ js.line('_initAudio();')
995
+ js.block_open('try')
996
+ js.const('resp', 'await fetch(url)')
997
+ js.const('buf', 'await resp.arrayBuffer()')
998
+ js.assign('_sounds[id]', 'await _audioCtx.decodeAudioData(buf)')
999
+ js.block_close()
1000
+ js.block_open('catch(e)')
1001
+ js.line("console.warn('Sound load failed:', id, e);")
1002
+ js.block_close()
1003
+ js.block_close() # function
1004
+ js.blank()
1005
+
1006
+ js.func('_doPlaySound', 'id, opts')
1007
+ js.assign('opts', 'opts || {}')
1008
+ js.const('buf', '_sounds[id]')
1009
+ js.if_block('!buf')
1010
+ js.ret()
1011
+ js.block_close()
1012
+ js.const('src', '_audioCtx.createBufferSource()')
1013
+ js.assign('src.buffer', 'buf')
1014
+ js.const('gain', '_audioCtx.createGain()')
1015
+ js.assign('gain.gain.value', 'opts.volume !== undefined ? opts.volume : 1')
1016
+ js.line('src.connect(gain).connect(_audioCtx.destination);')
1017
+ js.assign('src.loop', '!!opts.loop')
1018
+ js.line('src.start(0);')
1019
+ js.ret('src')
1020
+ js.block_close()
1021
+ js.blank()
1022
+
1023
+ js.const('_activeSources', '{}')
1024
+ js.blank()
1025
+ js.func('_playSound', 'id, opts')
1026
+ js.if_block('!_audioUnlocked')
1027
+ js.line('_pendingSounds.push({ id: id, opts: opts });')
1028
+ js.ret()
1029
+ js.block_close()
1030
+ js.const('src', '_doPlaySound(id, opts)')
1031
+ js.if_block('src')
1032
+ js.assign('_activeSources[id]', 'src')
1033
+ js.line("src.onended = function() { if (_activeSources[id] === src) delete _activeSources[id]; };")
1034
+ js.block_close()
1035
+ js.ret('src')
1036
+ js.block_close()
1037
+ js.blank()
1038
+ js.func('_stopSound', 'id')
1039
+ js.const('src', '_activeSources[id]')
1040
+ js.if_block('src')
1041
+ js.block_open('try')
1042
+ js.line('src.stop();')
1043
+ js.block_close()
1044
+ js.block_open('catch(e)')
1045
+ js.block_close()
1046
+ js.line('delete _activeSources[id];')
1047
+ js.block_close()
1048
+ js.block_close()
1049
+
1050
+
1051
+ def emit_timer_system(js: JsBuilder):
1052
+ """Emit timer system without eval."""
1053
+ js.comment("Timer System")
1054
+ js.const('_timers', '{}')
1055
+ js.const('_timerActions', '{}')
1056
+ js.blank()
1057
+
1058
+ js.func('_createTimer', 'id, interval, repeat, autoStart, actionFn')
1059
+ js.line("_timers[id] = { elapsed: 0, interval: interval, repeat: repeat, count: 0, active: autoStart };")
1060
+ js.assign('_timerActions[id]', 'actionFn')
1061
+ js.block_close()
1062
+ js.blank()
1063
+
1064
+ js.func('_updateTimers', 'dt')
1065
+ js.for_entries('tid', 't', '_timers')
1066
+ js.if_block('!t.active')
1067
+ js.line('continue;')
1068
+ js.block_close()
1069
+ js.line('t.elapsed += dt / 60;')
1070
+ js.if_block('t.elapsed >= t.interval')
1071
+ js.assign('t.elapsed', '0')
1072
+ js.if_block('_timerActions[tid]')
1073
+ js.line('_timerActions[tid]();')
1074
+ js.block_close()
1075
+ js.line('t.count++;')
1076
+ js.if_block('t.repeat >= 0 && t.count >= t.repeat')
1077
+ js.assign('t.active', 'false')
1078
+ js.block_close()
1079
+ js.block_close() # elapsed >= interval
1080
+ js.block_close() # for
1081
+ js.block_close() # function
1082
+
1083
+
1084
+ def emit_event_bus(js: JsBuilder):
1085
+ """Emit a lightweight pub/sub event bus for game events."""
1086
+ js.comment("Event Bus")
1087
+ js.const('_gameEvents', '{ _listeners: {} }')
1088
+ js.line("_gameEvents.on = function(event, cb) {")
1089
+ js.indent()
1090
+ js.if_block('!this._listeners[event]')
1091
+ js.assign('this._listeners[event]', '[]')
1092
+ js.block_close()
1093
+ js.line('this._listeners[event].push(cb);')
1094
+ js.dedent()
1095
+ js.line("};")
1096
+ js.line("_gameEvents.off = function(event, cb) {")
1097
+ js.indent()
1098
+ js.if_block('this._listeners[event]')
1099
+ js.assign('this._listeners[event]', 'this._listeners[event].filter(fn => fn !== cb)')
1100
+ js.block_close()
1101
+ js.dedent()
1102
+ js.line("};")
1103
+ js.line("_gameEvents.emit = function(event, data) {")
1104
+ js.indent()
1105
+ js.if_block('this._listeners[event]')
1106
+ js.for_of('cb', 'this._listeners[event]')
1107
+ js.line('cb(data);')
1108
+ js.block_close()
1109
+ js.block_close()
1110
+ js.dedent()
1111
+ js.line("};")
1112
+
1113
+
1114
+ def emit_game_api(js: JsBuilder):
1115
+ """Emit the game API object with utility methods."""
1116
+ js.comment("Game API")
1117
+ js.const('game', '{ camera: {} }')
1118
+ js.blank()
1119
+
1120
+ # game.destroy(info)
1121
+ js.line("game.destroy = function(info) {")
1122
+ js.indent()
1123
+ js.if_block('!info')
1124
+ js.ret()
1125
+ js.block_close()
1126
+ js.comment('Find sprite id')
1127
+ js.let('spriteId', 'null')
1128
+ js.for_entries('id', 'entry', '_sprites')
1129
+ js.if_block('entry === info || entry.sprite === info')
1130
+ js.assign('spriteId', 'id')
1131
+ js.line('break;')
1132
+ js.block_close()
1133
+ js.block_close()
1134
+ js.if_block('!spriteId')
1135
+ js.ret()
1136
+ js.block_close()
1137
+ js.const('entry', '_sprites[spriteId]')
1138
+ js.if_block('entry.sprite && entry.sprite.parent')
1139
+ js.line('entry.sprite.parent.removeChild(entry.sprite);')
1140
+ js.block_close()
1141
+ js.if_block('entry.body')
1142
+ js.line('Matter.Composite.remove(mWorld, entry.body);')
1143
+ js.block_close()
1144
+ js.line('delete _sprites[spriteId];')
1145
+ js.dedent()
1146
+ js.line("};")
1147
+ js.blank()
1148
+
1149
+ # game.play(soundId, opts)
1150
+ js.line("game.play = function(soundId, opts) {")
1151
+ js.indent()
1152
+ js.if_block("typeof _playSound === 'function'")
1153
+ js.ret('_playSound(soundId, opts)')
1154
+ js.block_close()
1155
+ js.dedent()
1156
+ js.line("};")
1157
+ js.blank()
1158
+
1159
+ # game.stop(soundId)
1160
+ js.line("game.stop = function(soundId) {")
1161
+ js.indent()
1162
+ js.if_block("typeof _stopSound === 'function'")
1163
+ js.line('_stopSound(soundId);')
1164
+ js.block_close()
1165
+ js.dedent()
1166
+ js.line("};")
1167
+ js.blank()
1168
+
1169
+ # game.emit(info, event)
1170
+ js.line("game.emit = function(info, event) {")
1171
+ js.indent()
1172
+ js.line('_gameEvents.emit(event, info);')
1173
+ js.if_block('info && info.tag')
1174
+ js.line("_gameEvents.emit(info.tag + '.' + event, info);")
1175
+ js.block_close()
1176
+ js.dedent()
1177
+ js.line("};")
1178
+ js.blank()
1179
+
1180
+ # game.loadScene(name)
1181
+ js.line("game.loadScene = function(name) {")
1182
+ js.indent()
1183
+ js.if_block("typeof _loadScene === 'function'")
1184
+ js.line('_loadScene(name);')
1185
+ js.block_close()
1186
+ js.dedent()
1187
+ js.line("};")
1188
+ js.blank()
1189
+
1190
+ # game.respawn(id, x, y)
1191
+ js.line("game.respawn = function(id, x, y) {")
1192
+ js.indent()
1193
+ js.const('info', '_sprites[id]')
1194
+ js.if_block('!info')
1195
+ js.ret()
1196
+ js.block_close()
1197
+ js.if_block('info.sprite')
1198
+ js.assign('info.sprite.x', 'x')
1199
+ js.assign('info.sprite.y', 'y')
1200
+ js.assign('info.sprite.visible', 'true')
1201
+ js.block_close()
1202
+ js.if_block('info.body')
1203
+ js.line('Matter.Body.setPosition(info.body, { x: x, y: y });')
1204
+ js.line('Matter.Body.setVelocity(info.body, { x: 0, y: 0 });')
1205
+ js.block_close()
1206
+ js.dedent()
1207
+ js.line("};")
1208
+ js.blank()
1209
+
1210
+ # game.destroyGroup(name)
1211
+ js.line("game.destroyGroup = function(groupName) {")
1212
+ js.indent()
1213
+ js.const('toRemove', '[]')
1214
+ js.for_entries('id', 'info', '_sprites')
1215
+ js.if_block('info.group === groupName')
1216
+ js.line('toRemove.push(info);')
1217
+ js.block_close()
1218
+ js.block_close()
1219
+ js.for_of('info', 'toRemove')
1220
+ js.line('game.destroy(info);')
1221
+ js.block_close()
1222
+ js.dedent()
1223
+ js.line("};")
1224
+ js.blank()
1225
+
1226
+ # game.moveX(info, dx)
1227
+ js.line("game.moveX = function(info, dx) {")
1228
+ js.indent()
1229
+ js.if_block('info && info.body')
1230
+ js.line("Matter.Body.setVelocity(info.body, { x: dx, y: info.body.velocity.y });")
1231
+ js.block_close()
1232
+ js.dedent()
1233
+ js.line("};")
1234
+ js.blank()
1235
+
1236
+ # game.moveToward(info, targetId, speed)
1237
+ js.line("game.moveToward = function(info, targetId, speed) {")
1238
+ js.indent()
1239
+ js.if_block('!info || !info.body')
1240
+ js.ret()
1241
+ js.block_close()
1242
+ js.const('target', '_sprites[targetId]')
1243
+ js.if_block('!target')
1244
+ js.ret()
1245
+ js.block_close()
1246
+ js.const('dx', 'target.sprite.x - info.sprite.x')
1247
+ js.const('dy', 'target.sprite.y - info.sprite.y')
1248
+ js.const('dist', 'Math.sqrt(dx * dx + dy * dy)')
1249
+ js.if_block('dist < 1')
1250
+ js.ret()
1251
+ js.block_close()
1252
+ js.line("Matter.Body.setVelocity(info.body, { x: (dx / dist) * speed, y: (dy / dist) * speed });")
1253
+ js.dedent()
1254
+ js.line("};")
1255
+ js.blank()
1256
+
1257
+ # game.patrol(info, speed)
1258
+ js.line("game.patrol = function(info, speed) {")
1259
+ js.indent()
1260
+ js.line('game.moveX(info, speed);')
1261
+ js.dedent()
1262
+ js.line("};")
1263
+ js.blank()
1264
+
1265
+ # game.camera.shake(intensity, duration)
1266
+ js.line("game.camera.shake = function(intensity, duration) {")
1267
+ js.indent()
1268
+ js.let('_shakeFrames', 'Math.floor((duration || 0.3) * 60)')
1269
+ js.const('_shakeIntensity', 'intensity || 5')
1270
+ js.const('origX', '_cameraContainer.x')
1271
+ js.const('origY', '_cameraContainer.y')
1272
+ js.line("const _shakeTick = () => {")
1273
+ js.indent()
1274
+ js.if_block('_shakeFrames <= 0')
1275
+ js.assign('_cameraContainer.x', 'origX')
1276
+ js.assign('_cameraContainer.y', 'origY')
1277
+ js.ret()
1278
+ js.block_close()
1279
+ js.line('_cameraContainer.x = origX + (Math.random() - 0.5) * _shakeIntensity * 2;')
1280
+ js.line('_cameraContainer.y = origY + (Math.random() - 0.5) * _shakeIntensity * 2;')
1281
+ js.line('_shakeFrames--;')
1282
+ js.line('requestAnimationFrame(_shakeTick);')
1283
+ js.dedent()
1284
+ js.line("};")
1285
+ js.line('_shakeTick();')
1286
+ js.dedent()
1287
+ js.line("};")
1288
+ js.blank()
1289
+
1290
+ # game.spawn(id)
1291
+ js.line("game.spawn = function(id) {")
1292
+ js.indent()
1293
+ js.if_block("typeof _spawn === 'function'")
1294
+ js.line('_spawn(id);')
1295
+ js.block_close()
1296
+ js.dedent()
1297
+ js.line("};")
1298
+ js.blank()
1299
+
1300
+ # game.hitBlock(blockInfo, usedTextureSrc, coinTextureSrc, frameW, frameH)
1301
+ # Changes block to "used" texture and spawns coin effect
1302
+ js.line("game.hitBlock = function(blockInfo, usedTextureSrc, coinTextureSrc, frameW, frameH) {")
1303
+ js.indent()
1304
+ js.if_block('!blockInfo || blockInfo._blockUsed')
1305
+ js.ret()
1306
+ js.block_close()
1307
+ js.assign('blockInfo._blockUsed', 'true')
1308
+ js.blank()
1309
+
1310
+ js.comment('Change block texture to used')
1311
+ js.if_block('blockInfo.sprite && usedTextureSrc')
1312
+ js.const('usedTex', 'PIXI.Assets.get(usedTextureSrc)')
1313
+ js.if_block('usedTex')
1314
+ js.comment('Stop animation if playing')
1315
+ js.if_block('blockInfo.sprite.stop')
1316
+ js.line('blockInfo.sprite.stop();')
1317
+ js.block_close()
1318
+ js.comment('Get first frame of used texture')
1319
+ js.const('fw', 'frameW || 16')
1320
+ js.const('fh', 'frameH || 16')
1321
+ js.const('src', 'usedTex.source || usedTex.baseTexture || usedTex')
1322
+ js.const('rect', 'new PIXI.Rectangle(0, 0, fw, fh)')
1323
+ js.const('frameTex', 'new PIXI.Texture({ source: src, frame: rect })')
1324
+ js.comment('Replace with static sprite')
1325
+ js.const('newSprite', 'new PIXI.Sprite(frameTex)')
1326
+ js.line('newSprite.anchor.set(0.5);')
1327
+ js.assign('newSprite.x', 'blockInfo.sprite.x')
1328
+ js.assign('newSprite.y', 'blockInfo.sprite.y')
1329
+ js.const('parent', 'blockInfo.sprite.parent')
1330
+ js.if_block('parent')
1331
+ js.line('parent.removeChild(blockInfo.sprite);')
1332
+ js.line('parent.addChild(newSprite);')
1333
+ js.block_close()
1334
+ js.assign('blockInfo.sprite', 'newSprite')
1335
+ js.block_close()
1336
+ js.block_close()
1337
+ js.blank()
1338
+
1339
+ js.comment('Spawn coin effect above block')
1340
+ js.if_block('coinTextureSrc')
1341
+ js.const('coinTex', 'PIXI.Assets.get(coinTextureSrc)')
1342
+ js.if_block('coinTex')
1343
+ js.const('cfw', 'frameW || 16')
1344
+ js.const('cfh', 'frameH || 16')
1345
+ js.const('csrc', 'coinTex.source || coinTex.baseTexture || coinTex')
1346
+ js.comment('Create animated coin with 4 frames')
1347
+ js.const('coinFrames', '[]')
1348
+ js.line('for (let i = 0; i < 4; i++) {')
1349
+ js.indent()
1350
+ js.const('crect', 'new PIXI.Rectangle(i * cfw, 0, cfw, cfh)')
1351
+ js.line('coinFrames.push(new PIXI.Texture({ source: csrc, frame: crect }));')
1352
+ js.dedent()
1353
+ js.line('}')
1354
+ js.const('coinSprite', 'new PIXI.AnimatedSprite(coinFrames)')
1355
+ js.line('coinSprite.anchor.set(0.5);')
1356
+ js.assign('coinSprite.x', 'blockInfo.sprite.x')
1357
+ js.assign('coinSprite.y', 'blockInfo.sprite.y - 20')
1358
+ js.assign('coinSprite.animationSpeed', '0.15')
1359
+ js.line('coinSprite.play();')
1360
+ js.line('_cameraContainer.addChild(coinSprite);')
1361
+ js.blank()
1362
+ js.comment('Animate coin going up and fading')
1363
+ js.let('coinY', 'coinSprite.y')
1364
+ js.let('coinFrame', '0')
1365
+ js.const('coinAnim', '() => {')
1366
+ js.indent()
1367
+ js.line('coinFrame++;')
1368
+ js.assign('coinY', 'coinY - 2')
1369
+ js.assign('coinSprite.y', 'coinY')
1370
+ js.assign('coinSprite.alpha', '1 - (coinFrame / 30)')
1371
+ js.if_block('coinFrame < 30')
1372
+ js.line('requestAnimationFrame(coinAnim);')
1373
+ js.else_block()
1374
+ js.line('coinSprite.destroy();')
1375
+ js.block_close()
1376
+ js.dedent()
1377
+ js.line('};')
1378
+ js.line('coinAnim();')
1379
+ js.block_close()
1380
+ js.block_close()
1381
+ js.dedent()
1382
+ js.line("};")
1383
+ js.blank()
1384
+
1385
+ # game.spawnPowerUp(blockInfo, textureSrc, moveSpeed)
1386
+ # Spawns a mushroom that emerges from block and walks
1387
+ js.line("game.spawnPowerUp = function(blockInfo, textureSrc, moveSpeed) {")
1388
+ js.indent()
1389
+ js.if_block('!blockInfo || !blockInfo.sprite')
1390
+ js.ret()
1391
+ js.block_close()
1392
+
1393
+ js.const('tex', 'PIXI.Assets.get(textureSrc)')
1394
+ js.if_block('!tex')
1395
+ js.line("console.warn('PowerUp texture not loaded:', textureSrc);")
1396
+ js.ret()
1397
+ js.block_close()
1398
+ js.blank()
1399
+
1400
+ js.comment('Create mushroom sprite')
1401
+ js.const('mushSprite', 'new PIXI.Sprite(tex)')
1402
+ js.line('mushSprite.anchor.set(0.5);')
1403
+ js.assign('mushSprite.x', 'blockInfo.sprite.x')
1404
+ js.assign('mushSprite.y', 'blockInfo.sprite.y - 16')
1405
+ js.line('_cameraContainer.addChild(mushSprite);')
1406
+ js.blank()
1407
+
1408
+ js.comment('Create physics body for mushroom (no rotation)')
1409
+ js.const('mushBody', "Matter.Bodies.rectangle(mushSprite.x, mushSprite.y, 14, 14, { friction: 0, frictionAir: 0, restitution: 0, inertia: Infinity, label: 'mushroom' })")
1410
+ js.line('Matter.Composite.add(mWorld, mushBody);')
1411
+ js.blank()
1412
+
1413
+ js.comment('Register mushroom in sprites')
1414
+ js.const('mushId', "'powerup_' + Date.now()")
1415
+ js.const('mushInfo', '{ id: mushId, sprite: mushSprite, body: mushBody, tag: "powerup", _isPowerUp: true, behaviors: [], collisionHandlers: [] }')
1416
+ js.line('_sprites[mushId] = mushInfo;')
1417
+ js.line('_bodyToSprite[mushBody.id] = mushId;')
1418
+ js.blank()
1419
+
1420
+ js.comment('Start mushroom rising animation then walking')
1421
+ js.let('riseFrames', '0')
1422
+ js.const('spd', 'moveSpeed || 1.5')
1423
+ js.const('riseAnim', '() => {')
1424
+ js.indent()
1425
+ js.line('riseFrames++;')
1426
+ js.if_block('riseFrames < 16')
1427
+ js.comment('Rising from block')
1428
+ js.assign('mushSprite.y', 'mushSprite.y - 1')
1429
+ js.line('Matter.Body.setPosition(mushBody, { x: mushSprite.x, y: mushSprite.y });')
1430
+ js.line('requestAnimationFrame(riseAnim);')
1431
+ js.else_block()
1432
+ js.comment('Start walking')
1433
+ js.line('Matter.Body.setVelocity(mushBody, { x: spd, y: 0 });')
1434
+ js.block_close()
1435
+ js.dedent()
1436
+ js.line('};')
1437
+ js.line('riseAnim();')
1438
+ js.dedent()
1439
+ js.line("};")
1440
+ js.blank()
1441
+
1442
+ # game.powerUpPlayer(playerId, bigSpriteSrc, frameW, frameH)
1443
+ # Changes player sprite to big version with proper animations
1444
+ js.line("game.powerUpPlayer = function(playerId, bigSpriteSrc, frameW, frameH) {")
1445
+ js.indent()
1446
+ js.const('playerInfo', '_sprites[playerId]')
1447
+ js.if_block('!playerInfo || playerInfo._isBig')
1448
+ js.ret()
1449
+ js.block_close()
1450
+ js.assign('playerInfo._isBig', 'true')
1451
+ js.blank()
1452
+
1453
+ js.const('bigTex', 'PIXI.Assets.get(bigSpriteSrc)')
1454
+ js.if_block('!bigTex')
1455
+ js.line("console.warn('Big sprite not loaded:', bigSpriteSrc);")
1456
+ js.ret()
1457
+ js.block_close()
1458
+ js.blank()
1459
+
1460
+ js.comment('Extract frame textures from spritesheet')
1461
+ js.const('fw', 'frameW || 16')
1462
+ js.const('fh', 'frameH || 32')
1463
+ js.const('src', 'bigTex.source || bigTex.baseTexture || bigTex')
1464
+ js.blank()
1465
+
1466
+ js.comment('Create textures for each frame')
1467
+ js.const('allFrames', '[]')
1468
+ js.line('for (let i = 0; i < 5; i++) {')
1469
+ js.indent()
1470
+ js.const('rect', 'new PIXI.Rectangle(i * fw, 0, fw, fh)')
1471
+ js.line('allFrames.push(new PIXI.Texture({ source: src, frame: rect }));')
1472
+ js.dedent()
1473
+ js.line('}')
1474
+ js.blank()
1475
+
1476
+ js.comment('Create animation sets: idle(0), walk(1-3), jump(4)')
1477
+ js.const('idleFrames', '[allFrames[0]]')
1478
+ js.const('walkFrames', '[allFrames[1], allFrames[2], allFrames[3]]')
1479
+ js.const('jumpFrames', '[allFrames[4]]')
1480
+ js.blank()
1481
+
1482
+ js.comment('Create animated sprite starting with idle')
1483
+ js.const('bigSprite', 'new PIXI.AnimatedSprite(idleFrames)')
1484
+ js.line('bigSprite.anchor.set(0.5);')
1485
+ js.assign('bigSprite.x', 'playerInfo.sprite.x')
1486
+ js.assign('bigSprite.y', 'playerInfo.sprite.y - 4')
1487
+ js.assign('bigSprite.scale.x', 'playerInfo.sprite.scale.x')
1488
+ js.assign('bigSprite.animationSpeed', '0.15')
1489
+ js.assign('bigSprite.loop', 'true')
1490
+ js.line('bigSprite.play();')
1491
+ js.blank()
1492
+
1493
+ js.comment('Replace sprite in scene')
1494
+ js.const('parent', 'playerInfo.sprite.parent')
1495
+ js.if_block('parent')
1496
+ js.line('parent.removeChild(playerInfo.sprite);')
1497
+ js.line('parent.addChild(bigSprite);')
1498
+ js.block_close()
1499
+ js.assign('playerInfo.sprite', 'bigSprite')
1500
+ js.blank()
1501
+
1502
+ js.comment('Update physics body size for big Mario')
1503
+ js.if_block('playerInfo.body')
1504
+ js.const('pos', 'playerInfo.body.position')
1505
+ js.line('Matter.Composite.remove(mWorld, playerInfo.body);')
1506
+ js.const('newBody', "Matter.Bodies.rectangle(pos.x, pos.y - 4, 14, 30, { friction: 0, frictionAir: 0, inertia: Infinity, label: 'player' })")
1507
+ js.line('Matter.Composite.add(mWorld, newBody);')
1508
+ js.line('delete _bodyToSprite[playerInfo.body.id];')
1509
+ js.assign('playerInfo.body', 'newBody')
1510
+ js.line('_bodyToSprite[newBody.id] = playerId;')
1511
+ js.block_close()
1512
+ js.blank()
1513
+
1514
+ js.comment('Register animations in the animation system')
1515
+ js.line('_animatedSprites[playerId] = {')
1516
+ js.indent()
1517
+ js.line('sprite: bigSprite,')
1518
+ js.line("current: 'idle',")
1519
+ js.line('anims: {')
1520
+ js.indent()
1521
+ js.line('idle: { textures: idleFrames, speed: 0.1, loop: true },')
1522
+ js.line('walk: { textures: walkFrames, speed: 0.18, loop: true },')
1523
+ js.line('jump: { textures: jumpFrames, speed: 0.1, loop: false }')
1524
+ js.dedent()
1525
+ js.line('}')
1526
+ js.dedent()
1527
+ js.line('};')
1528
+ js.dedent()
1529
+ js.line("};")
1530
+ js.blank()
1531
+
1532
+ # game.setGoombaAI(goombaId, speed)
1533
+ # Makes the goomba walk with given speed and change direction at walls
1534
+ js.line("game.setGoombaAI = function(goombaId, speed) {")
1535
+ js.indent()
1536
+ js.const('info', '_sprites[goombaId]')
1537
+ js.if_block('!info || !info.body')
1538
+ js.ret()
1539
+ js.block_close()
1540
+ js.blank()
1541
+
1542
+ js.comment('Store the movement speed')
1543
+ js.assign('info._goombaSpeed', 'speed')
1544
+ js.assign('info._goombaDir', 'speed < 0 ? -1 : 1')
1545
+ js.blank()
1546
+
1547
+ js.comment('Add update behavior for goomba AI')
1548
+ js.if_block('!info.behaviors')
1549
+ js.assign('info.behaviors', '[]')
1550
+ js.block_close()
1551
+ js.line('info.behaviors.push({')
1552
+ js.indent()
1553
+ js.line('update: function() {')
1554
+ js.indent()
1555
+ js.comment('Skip if game is paused')
1556
+ js.line('if (game._paused) return;')
1557
+ js.line('const gInfo = _sprites[goombaId];')
1558
+ js.line('if (!gInfo || !gInfo.body) return;')
1559
+ js.blank()
1560
+ js.comment('Apply constant horizontal velocity')
1561
+ js.line('const currentSpeed = Math.abs(gInfo._goombaSpeed) * gInfo._goombaDir;')
1562
+ js.line('Matter.Body.setVelocity(gInfo.body, { x: currentSpeed, y: gInfo.body.velocity.y });')
1563
+ js.blank()
1564
+ js.comment('Flip sprite based on direction')
1565
+ js.line('if (gInfo.sprite) {')
1566
+ js.indent()
1567
+ js.line('gInfo.sprite.scale.x = gInfo._goombaDir > 0 ? -1 : 1;')
1568
+ js.dedent()
1569
+ js.line('}')
1570
+ js.dedent()
1571
+ js.line('}')
1572
+ js.dedent()
1573
+ js.line('});')
1574
+ js.blank()
1575
+
1576
+ js.comment('Listen for collisions to change direction (lateral only)')
1577
+ js.if_block('!info.collisionHandlers')
1578
+ js.assign('info.collisionHandlers', '[]')
1579
+ js.block_close()
1580
+ js.line('info.collisionHandlers.push(function(self, other) {')
1581
+ js.indent()
1582
+ js.line('const gInfo = _sprites[goombaId];')
1583
+ js.line('if (!gInfo || !gInfo.body || !other.body) return;')
1584
+ js.blank()
1585
+ js.comment('Only react to terrain/walls, not players or other enemies')
1586
+ js.line("const isWall = other.tag === 'terrain' || other.tag === 'tilemap-collision' || other.tag === 'wall' || other.tag === 'qblock';")
1587
+ js.line('if (!isWall) return;')
1588
+ js.blank()
1589
+ js.comment('Use body bounds to detect lateral vs vertical contact')
1590
+ js.line('const gb = gInfo.body.bounds;')
1591
+ js.line('const ob = other.body.bounds;')
1592
+ js.line('const overlapX = Math.min(gb.max.x, ob.max.x) - Math.max(gb.min.x, ob.min.x);')
1593
+ js.line('const overlapY = Math.min(gb.max.y, ob.max.y) - Math.max(gb.min.y, ob.min.y);')
1594
+ js.comment('Lateral collision: small horizontal overlap, larger vertical overlap')
1595
+ js.line('if (overlapX > 0 && overlapY > 0 && overlapX < overlapY) {')
1596
+ js.indent()
1597
+ js.comment('Cooldown to prevent rapid flipping')
1598
+ js.line('const now = Date.now();')
1599
+ js.line('if (!gInfo._lastFlipTime || now - gInfo._lastFlipTime > 200) {')
1600
+ js.indent()
1601
+ js.line('gInfo._goombaDir *= -1;')
1602
+ js.line('gInfo._lastFlipTime = now;')
1603
+ js.dedent()
1604
+ js.line('}')
1605
+ js.dedent()
1606
+ js.line('}')
1607
+ js.dedent()
1608
+ js.line('});')
1609
+
1610
+ js.dedent()
1611
+ js.line("};")
1612
+ js.blank()
1613
+
1614
+ # game.setRexAI - alias for goomba AI (same behavior)
1615
+ js.line("game.setRexAI = game.setGoombaAI;")
1616
+ js.blank()
1617
+
1618
+ # game.destroySprite(spriteId)
1619
+ # Removes a sprite and its physics body from the game
1620
+ js.line("game.destroySprite = function(spriteId) {")
1621
+ js.indent()
1622
+ js.const('info', '_sprites[spriteId]')
1623
+ js.if_block('!info')
1624
+ js.ret()
1625
+ js.block_close()
1626
+ js.blank()
1627
+
1628
+ js.comment('Remove physics body')
1629
+ js.if_block('info.body')
1630
+ js.line('Matter.Composite.remove(mWorld, info.body);')
1631
+ js.line('delete _bodyToSprite[info.body.id];')
1632
+ js.block_close()
1633
+ js.blank()
1634
+
1635
+ js.comment('Remove sprite from display')
1636
+ js.if_block('info.sprite && info.sprite.parent')
1637
+ js.line('info.sprite.parent.removeChild(info.sprite);')
1638
+ js.block_close()
1639
+ js.blank()
1640
+
1641
+ js.comment('Remove from animated sprites')
1642
+ js.line('delete _animatedSprites[spriteId];')
1643
+ js.blank()
1644
+
1645
+ js.comment('Remove from sprites registry')
1646
+ js.line('delete _sprites[spriteId];')
1647
+ js.dedent()
1648
+ js.line("};")
1649
+ js.blank()
1650
+
1651
+ # game.pause() - Pause the game (physics, animations, enemy AI)
1652
+ js.line("game.pause = function() {")
1653
+ js.indent()
1654
+ js.line("game._paused = true;")
1655
+ js.comment('Stop physics engine')
1656
+ js.line("mEngine.timing.timeScale = 0;")
1657
+ js.comment('Stop all animated sprites')
1658
+ js.line("for (const id in _animatedSprites) {")
1659
+ js.indent()
1660
+ js.line("const anim = _animatedSprites[id];")
1661
+ js.line("if (anim && anim.sprite && anim.sprite.stop) anim.sprite.stop();")
1662
+ js.dedent()
1663
+ js.line("}")
1664
+ js.comment('Store velocities and stop all bodies')
1665
+ js.line("game._savedVelocities = {};")
1666
+ js.line("for (const id in _sprites) {")
1667
+ js.indent()
1668
+ js.line("const info = _sprites[id];")
1669
+ js.line("if (info.body) {")
1670
+ js.indent()
1671
+ js.line("game._savedVelocities[id] = { x: info.body.velocity.x, y: info.body.velocity.y };")
1672
+ js.line("Matter.Body.setVelocity(info.body, { x: 0, y: 0 });")
1673
+ js.dedent()
1674
+ js.line("}")
1675
+ js.dedent()
1676
+ js.line("}")
1677
+ js.dedent()
1678
+ js.line("};")
1679
+ js.blank()
1680
+
1681
+ # game.resume() - Resume the game
1682
+ js.line("game.resume = function() {")
1683
+ js.indent()
1684
+ js.line("game._paused = false;")
1685
+ js.comment('Resume physics engine')
1686
+ js.line("mEngine.timing.timeScale = 1;")
1687
+ js.comment('Resume all animated sprites')
1688
+ js.line("for (const id in _animatedSprites) {")
1689
+ js.indent()
1690
+ js.line("const anim = _animatedSprites[id];")
1691
+ js.line("if (anim && anim.sprite && anim.sprite.play) anim.sprite.play();")
1692
+ js.dedent()
1693
+ js.line("}")
1694
+ js.comment('Restore velocities')
1695
+ js.line("if (game._savedVelocities) {")
1696
+ js.indent()
1697
+ js.line("for (const id in game._savedVelocities) {")
1698
+ js.indent()
1699
+ js.line("const info = _sprites[id];")
1700
+ js.line("if (info && info.body) {")
1701
+ js.indent()
1702
+ js.line("Matter.Body.setVelocity(info.body, game._savedVelocities[id]);")
1703
+ js.dedent()
1704
+ js.line("}")
1705
+ js.dedent()
1706
+ js.line("}")
1707
+ js.dedent()
1708
+ js.line("}")
1709
+ js.dedent()
1710
+ js.line("};")
1711
+ js.blank()
1712
+
1713
+ # game.playSound(soundId, opts) - Play a sound by ID
1714
+ js.line("game.playSound = function(soundId, opts) {")
1715
+ js.indent()
1716
+ js.line("_playSound(soundId, opts || {});")
1717
+ js.dedent()
1718
+ js.line("};")
1719
+ js.blank()
1720
+
1721
+ # game.stopSound(soundId) - Stop a looping sound
1722
+ js.line("game.stopSound = function(soundId) {")
1723
+ js.indent()
1724
+ js.if_block('_activeSources[soundId]')
1725
+ js.line('_activeSources[soundId].stop();')
1726
+ js.line('delete _activeSources[soundId];')
1727
+ js.block_close()
1728
+ js.dedent()
1729
+ js.line("};")
1730
+ js.blank()
1731
+
1732
+ # game.stopAllSounds() - Stop all sounds
1733
+ js.line("game.stopAllSounds = function() {")
1734
+ js.indent()
1735
+ js.line("for (const id in _activeSources) {")
1736
+ js.indent()
1737
+ js.line("_activeSources[id].stop();")
1738
+ js.line("delete _activeSources[id];")
1739
+ js.dedent()
1740
+ js.line("}")
1741
+ js.dedent()
1742
+ js.line("};")
1743
+ js.blank()
1744
+
1745
+ # game.irisIn(callback, duration) - Circle wipe in (level start)
1746
+ js.line("game.irisIn = function(callback, duration) {")
1747
+ js.indent()
1748
+ js.line("duration = duration || 30;")
1749
+ js.line("const overlay = new PIXI.Graphics();")
1750
+ js.line("overlay.zIndex = 9999;")
1751
+ js.line("app.stage.addChild(overlay);")
1752
+ js.line("app.stage.sortChildren();")
1753
+ js.line("let frame = 0;")
1754
+ js.line("const centerX = app.screen.width / 2;")
1755
+ js.line("const centerY = app.screen.height / 2;")
1756
+ js.line("const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY) + 50;")
1757
+ js.line("const animate = () => {")
1758
+ js.indent()
1759
+ js.line("frame++;")
1760
+ js.line("const t = frame / duration;")
1761
+ js.line("const radius = maxRadius * t;")
1762
+ js.line("overlay.clear();")
1763
+ js.line("overlay.rect(0, 0, app.screen.width, app.screen.height);")
1764
+ js.line("overlay.fill(0x000000);")
1765
+ js.line("overlay.circle(centerX, centerY, radius);")
1766
+ js.line("overlay.cut();")
1767
+ js.line("if (frame < duration) {")
1768
+ js.indent()
1769
+ js.line("requestAnimationFrame(animate);")
1770
+ js.dedent()
1771
+ js.line("} else {")
1772
+ js.indent()
1773
+ js.line("app.stage.removeChild(overlay);")
1774
+ js.line("if (callback) callback();")
1775
+ js.dedent()
1776
+ js.line("}")
1777
+ js.dedent()
1778
+ js.line("};")
1779
+ js.line("animate();")
1780
+ js.dedent()
1781
+ js.line("};")
1782
+ js.blank()
1783
+
1784
+ # game.irisOut(callback, duration) - Circle wipe out (death/level end)
1785
+ js.line("game.irisOut = function(callback, duration) {")
1786
+ js.indent()
1787
+ js.line("duration = duration || 30;")
1788
+ js.line("const overlay = new PIXI.Graphics();")
1789
+ js.line("overlay.zIndex = 9999;")
1790
+ js.line("app.stage.addChild(overlay);")
1791
+ js.line("app.stage.sortChildren();")
1792
+ js.line("let frame = 0;")
1793
+ js.line("const centerX = app.screen.width / 2;")
1794
+ js.line("const centerY = app.screen.height / 2;")
1795
+ js.line("const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY) + 50;")
1796
+ js.line("const animate = () => {")
1797
+ js.indent()
1798
+ js.line("frame++;")
1799
+ js.line("const t = 1 - (frame / duration);")
1800
+ js.line("const radius = maxRadius * t;")
1801
+ js.line("overlay.clear();")
1802
+ js.line("overlay.rect(0, 0, app.screen.width, app.screen.height);")
1803
+ js.line("overlay.fill(0x000000);")
1804
+ js.line("if (radius > 0) {")
1805
+ js.indent()
1806
+ js.line("overlay.circle(centerX, centerY, radius);")
1807
+ js.line("overlay.cut();")
1808
+ js.dedent()
1809
+ js.line("}")
1810
+ js.line("if (frame < duration) {")
1811
+ js.indent()
1812
+ js.line("requestAnimationFrame(animate);")
1813
+ js.dedent()
1814
+ js.line("} else {")
1815
+ js.indent()
1816
+ js.line("if (callback) callback();")
1817
+ js.dedent()
1818
+ js.line("}")
1819
+ js.dedent()
1820
+ js.line("};")
1821
+ js.line("animate();")
1822
+ js.dedent()
1823
+ js.line("};")
1824
+ js.blank()
1825
+
1826
+ # game.killPlayer(playerId, deathSpriteSrc) - Kill player with death animation
1827
+ js.line("game.killPlayer = function(playerId, deathSpriteSrc) {")
1828
+ js.indent()
1829
+ js.const('info', '_sprites[playerId]')
1830
+ js.if_block('!info || info._isDying')
1831
+ js.ret()
1832
+ js.block_close()
1833
+ js.assign('info._isDying', 'true')
1834
+ js.blank()
1835
+
1836
+ js.comment('Pause game and stop background music')
1837
+ js.line('game.pause();')
1838
+ js.line("game.stopSound('bgm-level');")
1839
+ js.blank()
1840
+
1841
+ js.comment('Swap to death sprite if provided')
1842
+ js.const('deathSrc', "deathSpriteSrc || 'assets/smw/sprites/mario_death.png'")
1843
+ js.const('deathTex', 'PIXI.Assets.get(deathSrc)')
1844
+ js.if_block('deathTex')
1845
+ js.comment('Store original sprite and create death sprite')
1846
+ js.assign('info._originalSprite', 'info.sprite')
1847
+ js.const('deathSprite', 'PIXI.Sprite.from(deathTex)')
1848
+ js.line('deathSprite.anchor.set(0.5);')
1849
+ js.assign('deathSprite.x', 'info.sprite.x')
1850
+ js.assign('deathSprite.y', 'info.sprite.y')
1851
+ js.const('parent', 'info.sprite.parent')
1852
+ js.if_block('parent')
1853
+ js.line('parent.removeChild(info.sprite);')
1854
+ js.line('parent.addChild(deathSprite);')
1855
+ js.block_close()
1856
+ js.assign('info.sprite', 'deathSprite')
1857
+ js.block_close()
1858
+ js.blank()
1859
+
1860
+ js.comment('Remove Mario from physics (so he can do death animation)')
1861
+ js.if_block('info.body')
1862
+ js.line('Matter.Composite.remove(mWorld, info.body);')
1863
+ js.line('delete _bodyToSprite[info.body.id];')
1864
+ js.assign('info._deathBody', 'info.body')
1865
+ js.assign('info.body', 'null')
1866
+ js.block_close()
1867
+ js.blank()
1868
+
1869
+ js.comment('Death animation - Mario jumps up then falls (slower, wait for sound)')
1870
+ js.let('deathY', 'info.sprite.y')
1871
+ js.let('deathVelY', '-4')
1872
+ js.let('deathFrame', '0')
1873
+ js.let('waitFrames', '0')
1874
+ js.const('deathAnim', '() => {')
1875
+ js.indent()
1876
+ js.line('deathFrame++;')
1877
+ js.blank()
1878
+ js.comment('First phase: jump up and fall')
1879
+ js.if_block('deathY < 600')
1880
+ js.line('deathVelY += 0.15;') # slower gravity
1881
+ js.line('deathY += deathVelY;')
1882
+ js.assign('info.sprite.y', 'deathY')
1883
+ js.line('requestAnimationFrame(deathAnim);')
1884
+ js.else_block()
1885
+ js.comment('Second phase: wait for death sound to finish (~4 seconds = 240 frames)')
1886
+ js.line('waitFrames++;')
1887
+ js.if_block('waitFrames < 240')
1888
+ js.line('requestAnimationFrame(deathAnim);')
1889
+ js.else_block()
1890
+ js.comment('Death animation complete - emit event')
1891
+ js.line("_gameEvents.emit('death-animation-complete', { playerId: playerId });")
1892
+ js.block_close()
1893
+ js.block_close()
1894
+ js.dedent()
1895
+ js.line('};')
1896
+ js.line('requestAnimationFrame(deathAnim);')
1897
+ js.dedent()
1898
+ js.line("};")
1899
+
1900
+
1901
+ def emit_spawn_system(js: JsBuilder):
1902
+ """Emit the spawn/pool system."""
1903
+ js.comment("Spawn System")
1904
+ js.const('_spawners', '{}')
1905
+ js.const('_prefabConfigs', '{}')
1906
+ js.blank()
1907
+
1908
+ js.func('_registerPrefab', 'name, config')
1909
+ js.assign('_prefabConfigs[name]', 'config')
1910
+ js.block_close()
1911
+ js.blank()
1912
+
1913
+ js.func('_createSpawner', 'config')
1914
+ js.line("_spawners[config.id] = {")
1915
+ js.indent()
1916
+ js.line("id: config.id,")
1917
+ js.line("prefab: config.prefab,")
1918
+ js.line("poolSize: config.poolSize || 10,")
1919
+ js.line("spawnX: config.x || 0,")
1920
+ js.line("spawnY: config.y || 0,")
1921
+ js.line("interval: config.interval || null,")
1922
+ js.line("pool: [],")
1923
+ js.line("active: 0,")
1924
+ js.dedent()
1925
+ js.line("};")
1926
+ js.block_close()
1927
+ js.blank()
1928
+
1929
+ # Helper to create a sprite from prefab config
1930
+ js.func('_createPrefabSprite', 'prefabName, x, y')
1931
+ js.const('config', '_prefabConfigs[prefabName]')
1932
+ js.if_block('!config')
1933
+ js.line("console.warn('Unknown prefab:', prefabName);")
1934
+ js.ret('null')
1935
+ js.block_close()
1936
+ js.const('w', 'config.width || 20')
1937
+ js.const('h', 'config.height || 20')
1938
+ js.const('spr', 'new PIXI.Graphics()')
1939
+ js.comment('Center the rectangle like main sprites')
1940
+ js.line('spr.rect(-w/2, -h/2, w, h);')
1941
+ js.if_block('config.color')
1942
+ js.line("spr.fill({ color: parseInt(config.color.replace('#',''), 16) });")
1943
+ js.else_block()
1944
+ js.line("spr.fill({ color: 0x22c55e });")
1945
+ js.block_close()
1946
+ js.assign('spr.x', 'x')
1947
+ js.assign('spr.y', 'y')
1948
+ js.line('_cameraContainer.addChild(spr);')
1949
+ js.ret('spr')
1950
+ js.block_close()
1951
+ js.blank()
1952
+
1953
+ js.func('_spawn', 'spawnerId')
1954
+ js.const('sp', '_spawners[spawnerId]')
1955
+ js.if_block('!sp')
1956
+ js.ret('null')
1957
+ js.block_close()
1958
+ js.comment('Find an inactive pool member')
1959
+ js.for_of('item', 'sp.pool')
1960
+ js.if_block('!item.active')
1961
+ js.assign('item.active', 'true')
1962
+ js.if_block('item.sprite')
1963
+ js.assign('item.sprite.visible', 'true')
1964
+ js.assign('item.sprite.x', 'sp.spawnX')
1965
+ js.assign('item.sprite.y', 'sp.spawnY')
1966
+ js.block_close()
1967
+ js.if_block('item.body')
1968
+ js.line('Matter.Body.setPosition(item.body, { x: sp.spawnX, y: sp.spawnY });')
1969
+ js.block_close()
1970
+ js.ret('item.sprite')
1971
+ js.block_close()
1972
+ js.block_close()
1973
+ js.comment('No inactive items, create new if under pool limit')
1974
+ js.if_block('sp.pool.length < sp.poolSize')
1975
+ js.const('newSprite', '_createPrefabSprite(sp.prefab, sp.spawnX, sp.spawnY)')
1976
+ js.if_block('newSprite')
1977
+ js.const('newItem', '{ sprite: newSprite, active: true, body: null }')
1978
+ js.line('sp.pool.push(newItem);')
1979
+ js.ret('newSprite')
1980
+ js.block_close()
1981
+ js.block_close()
1982
+ js.ret('null')
1983
+ js.block_close()
1984
+
1985
+
1986
+ def emit_mouse_system(js: JsBuilder):
1987
+ """Emit global mouse position tracking and coordinate conversion."""
1988
+ js.comment("Mouse System")
1989
+ js.let('_mouseX', '0')
1990
+ js.let('_mouseY', '0')
1991
+ js.let('_mouseWorldX', '0')
1992
+ js.let('_mouseWorldY', '0')
1993
+ js.line("app.canvas.addEventListener('pointermove', (e) => {")
1994
+ js.indent()
1995
+ js.const('rect', 'app.canvas.getBoundingClientRect()')
1996
+ js.assign('_mouseX', 'e.clientX - rect.left')
1997
+ js.assign('_mouseY', 'e.clientY - rect.top')
1998
+ js.comment('Convert to world coordinates (accounting for camera)')
1999
+ js.assign('_mouseWorldX', '_mouseX - _cameraContainer.x')
2000
+ js.assign('_mouseWorldY', '_mouseY - _cameraContainer.y')
2001
+ js.dedent()
2002
+ js.line("});")
2003
+
2004
+
2005
+ # HUD element template (HTML)
2006
+ HUD_DIV_TEMPLATE = '<div class="qg-hud qg-hud-{position}" id="qg-hud-{index}">{content}</div>'