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,983 @@
1
+ """
2
+ Game Engine 2D Parser - Parse qg: namespace elements into Game AST nodes.
3
+
4
+ This module provides parse functions for all game-specific tags.
5
+ It is called from the main QuantumParser when a qg: prefixed tag is encountered.
6
+ """
7
+
8
+ from xml.etree import ElementTree as ET
9
+ from typing import Optional, List
10
+
11
+ from .ast_nodes import (
12
+ SceneNode, SpriteNode, PhysicsNode, ColliderNode, AnimationNode,
13
+ CameraNode, InputNode, SoundNode, ParticleNode, TimerNode,
14
+ SpawnNode, HudNode, TweenNode, TilemapNode, TilemapLayerNode,
15
+ TileAnimationNode, BehaviorNode, UseNode, PrefabNode, InstanceNode,
16
+ EnemyNode, EnemyAiNode, EnemyDefeatNode, EnemyOnHitNode, EnemyOnKillNode,
17
+ GroupNode, StateMachineNode, StateNode, TransitionNode, RawCodeNode,
18
+ ClickableNode, EventNode, OnCollisionNode,
19
+ HudTileNode, HudCounterNode, HudCollectionNode,
20
+ HudSlotNode, HudOptionNode, HudBehaviorNode, HudActionNode,
21
+ SceneTransitionNode, PersistentNode, MapNodeDef, MapPathNode,
22
+ EventActionNode, CollisionLayerDef,
23
+ )
24
+
25
+
26
+ class GameParseError(Exception):
27
+ """Game-specific parse error."""
28
+ pass
29
+
30
+
31
+ class GameParser:
32
+ """Parser for qg: namespace game elements."""
33
+
34
+ def __init__(self, parent_parser):
35
+ """
36
+ Args:
37
+ parent_parser: The main QuantumParser instance, used to parse
38
+ q: namespace children (q:set, q:function, q:if, etc.)
39
+ """
40
+ self.parent = parent_parser
41
+
42
+ # ------------------------------------------------------------------
43
+ # Dispatch
44
+ # ------------------------------------------------------------------
45
+
46
+ GAME_TAG_MAP = {
47
+ 'scene': '_parse_game_scene',
48
+ 'sprite': '_parse_game_sprite',
49
+ 'physics': '_parse_game_physics',
50
+ 'collider': '_parse_game_collider',
51
+ 'animation': '_parse_game_animation',
52
+ 'camera': '_parse_game_camera',
53
+ 'input': '_parse_game_input',
54
+ 'sound': '_parse_game_sound',
55
+ 'particle': '_parse_game_particle',
56
+ 'timer': '_parse_game_timer',
57
+ 'spawn': '_parse_game_spawn',
58
+ 'hud': '_parse_game_hud',
59
+ 'tween': '_parse_game_tween',
60
+ 'tilemap': '_parse_game_tilemap',
61
+ 'layer': '_parse_game_layer',
62
+ 'behavior': '_parse_game_behavior',
63
+ 'use': '_parse_game_use',
64
+ 'prefab': '_parse_game_prefab',
65
+ 'instance': '_parse_game_instance',
66
+ 'group': '_parse_game_group',
67
+ 'state-machine': '_parse_game_state_machine',
68
+ 'state': '_parse_game_state',
69
+ 'on': '_parse_game_transition',
70
+ 'clickable': '_parse_game_clickable',
71
+ 'event': '_parse_game_event',
72
+ 'on-collision': '_parse_game_on_collision',
73
+ 'tile': '_parse_game_hud_tile',
74
+ 'counter': '_parse_game_hud_counter',
75
+ 'collection': '_parse_game_hud_collection',
76
+ 'slot': '_parse_game_hud_slot',
77
+ 'option': '_parse_game_hud_option',
78
+ 'hud-behavior': '_parse_game_hud_behavior',
79
+ 'action': '_parse_game_action_dispatch',
80
+ 'transition': '_parse_game_scene_transition',
81
+ 'collision-layer': '_parse_game_collision_layer',
82
+ 'persistent': '_parse_game_persistent',
83
+ 'map-node': '_parse_game_map_node',
84
+ 'map-path': '_parse_game_map_path',
85
+ 'enemy': '_parse_game_enemy',
86
+ }
87
+
88
+ def parse_game_element(self, local_name: str, element: ET.Element):
89
+ """Dispatch a qg: element to the correct parse method."""
90
+ method_name = self.GAME_TAG_MAP.get(local_name)
91
+ if method_name is None:
92
+ raise GameParseError(f"Unknown game tag: qg:{local_name}")
93
+ method = getattr(self, method_name)
94
+ return method(element)
95
+
96
+ # ------------------------------------------------------------------
97
+ # Helper: get local name from element
98
+ # ------------------------------------------------------------------
99
+
100
+ def _get_local_name(self, element: ET.Element) -> str:
101
+ tag = element.tag
102
+ if '}' in tag:
103
+ return tag.split('}')[-1]
104
+ if ':' in tag:
105
+ return tag.split(':')[-1]
106
+ return tag
107
+
108
+ def _get_namespace(self, element: ET.Element) -> Optional[str]:
109
+ tag = element.tag
110
+ if '{https://quantum.lang/game}' in tag:
111
+ return 'game'
112
+ if '{https://quantum.lang/ns}' in tag:
113
+ return 'quantum'
114
+ if tag.startswith('qg:'):
115
+ return 'game'
116
+ if tag.startswith('q:'):
117
+ return 'quantum'
118
+ return None
119
+
120
+ def _parse_float(self, value: Optional[str], default: float = 0) -> float:
121
+ if value is None:
122
+ return default
123
+ try:
124
+ return float(value)
125
+ except ValueError:
126
+ return default
127
+
128
+ def _parse_int(self, value: Optional[str], default: int = 0) -> int:
129
+ if value is None:
130
+ return default
131
+ try:
132
+ return int(value)
133
+ except ValueError:
134
+ return default
135
+
136
+ def _parse_bool(self, value: Optional[str], default: bool = False) -> bool:
137
+ if value is None:
138
+ return default
139
+ return value.lower() in ('true', '1', 'yes')
140
+
141
+ # ------------------------------------------------------------------
142
+ # Parse children – dispatches both qg: and q: tags
143
+ # ------------------------------------------------------------------
144
+
145
+ def _parse_child(self, child: ET.Element):
146
+ """Parse a child element that can be either qg: or q: namespace."""
147
+ ns = self._get_namespace(child)
148
+ local = self._get_local_name(child)
149
+
150
+ if ns == 'game':
151
+ return self.parse_game_element(local, child)
152
+ elif ns == 'quantum':
153
+ if local == 'function':
154
+ return self._parse_game_function(child)
155
+ return self.parent._parse_statement(child)
156
+ else:
157
+ # Could be HTML inside HUD
158
+ if hasattr(self.parent, '_parse_html_element'):
159
+ return self.parent._parse_html_element(child)
160
+ return None
161
+
162
+ def _parse_game_function(self, element: ET.Element):
163
+ """Parse q:function with mixed raw code text and XML statement children.
164
+
165
+ Captures element.text and child.tail as RawCodeNode, alongside
166
+ regular q:set / q:if / etc children parsed normally.
167
+ """
168
+ # Use the parent parser to get the FunctionNode shell (name, params)
169
+ func_node = self.parent._parse_function(element)
170
+
171
+ # Now re-parse the body to capture raw text
172
+ mixed_body = []
173
+
174
+ # Leading text before first child
175
+ if element.text and element.text.strip():
176
+ for line in element.text.strip().splitlines():
177
+ line = line.strip()
178
+ if line:
179
+ mixed_body.append(RawCodeNode(line))
180
+
181
+ # Process children: XML elements + their tail text
182
+ for child in element:
183
+ parsed = self._parse_child(child)
184
+ if parsed:
185
+ mixed_body.append(parsed)
186
+ # Tail text after each child
187
+ if child.tail and child.tail.strip():
188
+ for line in child.tail.strip().splitlines():
189
+ line = line.strip()
190
+ if line:
191
+ mixed_body.append(RawCodeNode(line))
192
+
193
+ # Only replace body if we found raw code nodes
194
+ has_raw = any(isinstance(n, RawCodeNode) for n in mixed_body)
195
+ if has_raw:
196
+ func_node.body = mixed_body
197
+
198
+ return func_node
199
+
200
+ # ------------------------------------------------------------------
201
+ # Core game nodes
202
+ # ------------------------------------------------------------------
203
+
204
+ def _parse_game_scene(self, element: ET.Element) -> SceneNode:
205
+ name = element.get('name', 'main')
206
+ node = SceneNode(name)
207
+ node.width = self._parse_int(element.get('width'), 800)
208
+ node.height = self._parse_int(element.get('height'), 600)
209
+ # Viewport size (canvas size) - defaults to scene size if not specified
210
+ node.viewport_width = self._parse_int(element.get('viewport-width')) if element.get('viewport-width') else None
211
+ node.viewport_height = self._parse_int(element.get('viewport-height')) if element.get('viewport-height') else None
212
+ node.background = element.get('background', '#000000')
213
+ node.active = self._parse_bool(element.get('active'), True)
214
+ # Multi-scene attributes
215
+ node.scene_type = element.get('type', 'level')
216
+ node.initial = self._parse_bool(element.get('initial'))
217
+ # Death / game over
218
+ node.death_sequence = element.get('death-sequence')
219
+ if element.get('death-timer') is not None:
220
+ node.death_timer = self._parse_float(element.get('death-timer'), 3.0)
221
+ node.game_over_jingle = element.get('game-over-jingle')
222
+ node.game_over_alphabet = element.get('game-over-alphabet')
223
+
224
+ for child in element:
225
+ parsed = self._parse_child(child)
226
+ if parsed:
227
+ node.add_child(parsed)
228
+ return node
229
+
230
+ def _parse_game_sprite(self, element: ET.Element) -> SpriteNode:
231
+ sprite_id = element.get('id', '')
232
+ node = SpriteNode(sprite_id)
233
+ node.src = element.get('src', '')
234
+ node.x = self._parse_float(element.get('x'))
235
+ node.y = self._parse_float(element.get('y'))
236
+ node.width = self._parse_float(element.get('width')) if element.get('width') else None
237
+ node.height = self._parse_float(element.get('height')) if element.get('height') else None
238
+ node.anchor_x = self._parse_float(element.get('anchor-x'), 0.5)
239
+ node.anchor_y = self._parse_float(element.get('anchor-y'), 0.5)
240
+ node.rotation = self._parse_float(element.get('rotation'))
241
+ node.scale_x = self._parse_float(element.get('scale-x'), 1)
242
+ node.scale_y = self._parse_float(element.get('scale-y'), 1)
243
+ node.alpha = self._parse_float(element.get('alpha'), 1.0)
244
+ node.visible = self._parse_bool(element.get('visible'), True)
245
+ node.color = element.get('color') # Hex color for sprites without images
246
+ node.tag = element.get('tag')
247
+ node.layer = self._parse_int(element.get('layer'))
248
+
249
+ # Spritesheet
250
+ node.frame_width = self._parse_int(element.get('frame-width')) if element.get('frame-width') else None
251
+ node.frame_height = self._parse_int(element.get('frame-height')) if element.get('frame-height') else None
252
+
253
+ # Physics
254
+ node.body = element.get('body')
255
+ node.shape = element.get('shape', 'box') # box, circle, slope-left, slope-right
256
+ node.bounce = self._parse_float(element.get('bounce'), 0.1)
257
+ node.friction = self._parse_float(element.get('friction'), 0.1)
258
+ node.mass = self._parse_float(element.get('mass')) if element.get('mass') else None
259
+ node.sensor = self._parse_bool(element.get('sensor'))
260
+
261
+ # Controls
262
+ node.controls = element.get('controls')
263
+ node.speed = self._parse_float(element.get('speed'), 5.0)
264
+ node.jump_force = self._parse_float(element.get('jump-force'), 10.0)
265
+
266
+ # SMW-style jump physics
267
+ node.gravity_up = self._parse_float(element.get('gravity-up')) if element.get('gravity-up') else None
268
+ node.gravity_down = self._parse_float(element.get('gravity-down')) if element.get('gravity-down') else None
269
+ node.jump_hold_boost = self._parse_float(element.get('jump-hold-boost'), 0.4)
270
+ node.coyote_frames = self._parse_int(element.get('coyote-frames'), 6)
271
+ node.max_fall_speed = self._parse_float(element.get('max-fall-speed'), 15.0)
272
+
273
+ # Collision layers (numeric bitmask or named layer)
274
+ if element.get('collision-layer') is not None:
275
+ raw = element.get('collision-layer')
276
+ try:
277
+ node.collision_layer = int(raw)
278
+ except ValueError:
279
+ # Named layer — store as string, resolve in codegen
280
+ node.collision_layer = raw
281
+ if element.get('collision-mask') is not None:
282
+ raw = element.get('collision-mask')
283
+ try:
284
+ node.collision_mask = int(raw)
285
+ except ValueError:
286
+ # Named layers (comma-separated) — store as string, resolve in codegen
287
+ node.collision_mask = raw
288
+ if element.get('floor-max-angle') is not None:
289
+ node.floor_max_angle = self._parse_float(element.get('floor-max-angle'))
290
+ if element.get('spin-jump') is not None:
291
+ node.spin_jump = self._parse_bool(element.get('spin-jump'))
292
+
293
+ for child in element:
294
+ parsed = self._parse_child(child)
295
+ if parsed:
296
+ node.add_child(parsed)
297
+ return node
298
+
299
+ def _parse_game_physics(self, element: ET.Element) -> PhysicsNode:
300
+ node = PhysicsNode()
301
+ node.gravity_x = self._parse_float(element.get('gravity-x'))
302
+ node.gravity_y = self._parse_float(element.get('gravity-y'), 9.8)
303
+ node.bounds = element.get('bounds')
304
+ node.debug = self._parse_bool(element.get('debug'))
305
+
306
+ # Parse collision-layer children
307
+ for child in element:
308
+ local = self._get_local_name(child)
309
+ if local == 'collision-layer':
310
+ layer_def = self._parse_game_collision_layer(child)
311
+ node.collision_layers[layer_def.name] = layer_def.layer_id
312
+ node.add_child(layer_def)
313
+ return node
314
+
315
+ def _parse_game_collider(self, element: ET.Element) -> ColliderNode:
316
+ node = ColliderNode()
317
+ node.shape = element.get('shape', 'box')
318
+ node.width = self._parse_float(element.get('width')) if element.get('width') else None
319
+ node.height = self._parse_float(element.get('height')) if element.get('height') else None
320
+ node.radius = self._parse_float(element.get('radius')) if element.get('radius') else None
321
+ node.vertices = element.get('vertices')
322
+ node.offset_x = self._parse_float(element.get('offset-x'))
323
+ node.offset_y = self._parse_float(element.get('offset-y'))
324
+ return node
325
+
326
+ def _parse_game_animation(self, element: ET.Element) -> AnimationNode:
327
+ name = element.get('name', '')
328
+ node = AnimationNode(name)
329
+ node.frames = element.get('frames', '')
330
+ node.speed = self._parse_float(element.get('speed'), 0.1)
331
+ node.loop = self._parse_bool(element.get('loop'), True)
332
+ node.auto_play = self._parse_bool(element.get('auto-play'))
333
+ return node
334
+
335
+ def _parse_game_camera(self, element: ET.Element) -> CameraNode:
336
+ node = CameraNode()
337
+ node.follow = element.get('follow')
338
+ node.lerp = self._parse_float(element.get('lerp'), 0.1)
339
+ node.bounds = element.get('bounds')
340
+ node.zoom = self._parse_float(element.get('zoom'), 1.0)
341
+ node.offset_x = self._parse_float(element.get('offset-x'))
342
+ node.offset_y = self._parse_float(element.get('offset-y'))
343
+ return node
344
+
345
+ def _parse_game_input(self, element: ET.Element) -> InputNode:
346
+ node = InputNode()
347
+ node.key = element.get('key', '')
348
+ node.action = element.get('action', '')
349
+ node.input_type = element.get('type', 'press')
350
+ return node
351
+
352
+ def _parse_game_sound(self, element: ET.Element) -> SoundNode:
353
+ sound_id = element.get('id', '')
354
+ node = SoundNode(sound_id)
355
+ node.src = element.get('src', '')
356
+ node.volume = self._parse_float(element.get('volume'), 1.0)
357
+ node.loop = self._parse_bool(element.get('loop'))
358
+ node.trigger = element.get('trigger')
359
+ node.channel = element.get('channel', 'sfx')
360
+ return node
361
+
362
+ def _parse_game_particle(self, element: ET.Element) -> ParticleNode:
363
+ pid = element.get('id', '')
364
+ node = ParticleNode(pid)
365
+ node.src = element.get('src', '')
366
+ node.follow = element.get('follow')
367
+ node.trigger = element.get('trigger')
368
+ node.count = self._parse_int(element.get('count'), 20)
369
+ node.emit_rate = self._parse_int(element.get('emit-rate'), 10)
370
+ node.lifetime = self._parse_float(element.get('lifetime'), 1.0)
371
+ node.speed_min = self._parse_float(element.get('speed-min'), 1.0)
372
+ node.speed_max = self._parse_float(element.get('speed-max'), 3.0)
373
+ node.angle_min = self._parse_float(element.get('angle-min'), 0)
374
+ node.angle_max = self._parse_float(element.get('angle-max'), 360)
375
+ node.alpha_start = self._parse_float(element.get('alpha-start'), 1.0)
376
+ node.alpha_end = self._parse_float(element.get('alpha-end'), 0.0)
377
+ return node
378
+
379
+ def _parse_game_timer(self, element: ET.Element) -> TimerNode:
380
+ tid = element.get('id', '')
381
+ node = TimerNode(tid)
382
+ node.interval = self._parse_float(element.get('interval'), 1.0)
383
+ node.repeat = self._parse_int(element.get('repeat'), -1)
384
+ node.auto_start = self._parse_bool(element.get('auto-start'), True)
385
+ node.action = element.get('action', '')
386
+ return node
387
+
388
+ def _parse_game_spawn(self, element: ET.Element) -> SpawnNode:
389
+ sid = element.get('id', '')
390
+ node = SpawnNode(sid)
391
+ node.prefab = element.get('prefab', '')
392
+ node.count = self._parse_int(element.get('count'), 1)
393
+ node.interval = self._parse_float(element.get('interval')) if element.get('interval') else None
394
+ node.x = element.get('x')
395
+ node.y = element.get('y')
396
+ node.pool_size = self._parse_int(element.get('pool-size'), 10)
397
+ return node
398
+
399
+ def _parse_game_hud(self, element: ET.Element) -> HudNode:
400
+ node = HudNode()
401
+ node.position = element.get('position', 'top-left')
402
+ node.background = element.get('background')
403
+ node.background_height = self._parse_int(element.get('background-height')) if element.get('background-height') else None
404
+ node.sprite_prefix = element.get('sprite-prefix')
405
+
406
+ for child in element:
407
+ ns = self._get_namespace(child)
408
+ local = self._get_local_name(child)
409
+
410
+ if ns == 'game' and local in self.GAME_TAG_MAP:
411
+ parsed = self.parse_game_element(local, child)
412
+ if parsed:
413
+ node.add_child(parsed)
414
+ else:
415
+ # Fallback: parse as HTML (backward compat)
416
+ parsed = self._parse_child(child)
417
+ if parsed:
418
+ node.add_child(parsed)
419
+ elif hasattr(self.parent, '_parse_html_element'):
420
+ html_node = self.parent._parse_html_element(child)
421
+ if html_node:
422
+ node.add_child(html_node)
423
+ return node
424
+
425
+ def _parse_game_hud_tile(self, element: ET.Element) -> HudTileNode:
426
+ node = HudTileNode()
427
+ node.sprite = element.get('sprite', '')
428
+ node.x = self._parse_int(element.get('x'))
429
+ node.y = self._parse_int(element.get('y'))
430
+ node.width = self._parse_int(element.get('width')) if element.get('width') else None
431
+ node.height = self._parse_int(element.get('height')) if element.get('height') else None
432
+ return node
433
+
434
+ def _parse_game_hud_counter(self, element: ET.Element) -> HudCounterNode:
435
+ node = HudCounterNode()
436
+ node.strip = element.get('strip', '')
437
+ node.x = self._parse_int(element.get('x'))
438
+ node.y = self._parse_int(element.get('y'))
439
+ node.digits = self._parse_int(element.get('digits'), 2)
440
+ node.bind = element.get('bind', '')
441
+ node.digit_width = self._parse_int(element.get('digit-width'), 8)
442
+ node.digit_height = self._parse_int(element.get('digit-height'), 8)
443
+ node.countdown = self._parse_bool(element.get('countdown'))
444
+ node.hurry_at = self._parse_int(element.get('hurry-at')) if element.get('hurry-at') else None
445
+ node.flash = element.get('flash')
446
+ node.align = element.get('align', 'left')
447
+ node.right_edge = self._parse_int(element.get('right-edge')) if element.get('right-edge') else None
448
+ node.tint = element.get('tint')
449
+ node.intelligent = self._parse_bool(element.get('intelligent'))
450
+ node.icon = element.get('icon')
451
+ node.symbol = element.get('symbol')
452
+ node.extra_life_at = self._parse_int(element.get('extra-life-at')) if element.get('extra-life-at') else None
453
+ return node
454
+
455
+ def _parse_game_hud_collection(self, element: ET.Element) -> HudCollectionNode:
456
+ node = HudCollectionNode()
457
+ node.sprite = element.get('sprite', '')
458
+ node.x = self._parse_int(element.get('x'))
459
+ node.y = self._parse_int(element.get('y'))
460
+ node.spacing = self._parse_int(element.get('spacing'), 8)
461
+ node.max = self._parse_int(element.get('max'), 5)
462
+ node.bind = element.get('bind', '')
463
+ return node
464
+
465
+ def _parse_game_hud_slot(self, element: ET.Element) -> HudSlotNode:
466
+ node = HudSlotNode()
467
+ node.x = self._parse_int(element.get('x'))
468
+ node.y = self._parse_int(element.get('y'))
469
+ node.bind = element.get('bind', '')
470
+ for child in element:
471
+ local = self._get_local_name(child)
472
+ if local == 'option':
473
+ option = self._parse_game_hud_option(child)
474
+ node.add_option(option)
475
+ return node
476
+
477
+ def _parse_game_hud_option(self, element: ET.Element) -> HudOptionNode:
478
+ node = HudOptionNode()
479
+ node.value = element.get('value', '')
480
+ node.sprite = element.get('sprite', '')
481
+ return node
482
+
483
+ def _parse_game_hud_behavior(self, element: ET.Element) -> HudBehaviorNode:
484
+ node = HudBehaviorNode()
485
+ node.target = element.get('target', '')
486
+ node.event = element.get('event', '')
487
+ node.value = self._parse_int(element.get('value')) if element.get('value') else None
488
+ for child in element:
489
+ local = self._get_local_name(child)
490
+ if local == 'action':
491
+ action = self._parse_game_hud_action(child)
492
+ node.add_action(action)
493
+ return node
494
+
495
+ def _parse_game_hud_action(self, element: ET.Element) -> HudActionNode:
496
+ node = HudActionNode()
497
+ node.action_type = element.get('type', '')
498
+ node.event = element.get('event')
499
+ node.rate = self._parse_float(element.get('rate')) if element.get('rate') else None
500
+ return node
501
+
502
+ def _parse_game_tween(self, element: ET.Element) -> TweenNode:
503
+ tid = element.get('id', '')
504
+ node = TweenNode(tid)
505
+ node.target = element.get('target', '')
506
+ node.property = element.get('property', '')
507
+ node.to_value = self._parse_float(element.get('to'))
508
+ node.duration = self._parse_float(element.get('duration'), 1.0)
509
+ node.easing = element.get('easing', 'linear')
510
+ node.loop = self._parse_bool(element.get('loop'))
511
+ node.yoyo = self._parse_bool(element.get('yoyo'))
512
+ node.delay = self._parse_float(element.get('delay'))
513
+ node.auto_start = self._parse_bool(element.get('auto-start'), True)
514
+ return node
515
+
516
+ def _parse_game_tilemap(self, element: ET.Element) -> TilemapNode:
517
+ tid = element.get('id', '')
518
+ node = TilemapNode(tid)
519
+ node.src = element.get('src', '')
520
+ node.data_src = element.get('data-src')
521
+ node.tile_width = self._parse_int(element.get('tile-width'), 32)
522
+ node.tile_height = self._parse_int(element.get('tile-height'), 32)
523
+
524
+ has_external_data = node.data_src is not None
525
+ for child in element:
526
+ local = self._get_local_name(child)
527
+ if local == 'layer':
528
+ layer = self._parse_game_layer(child)
529
+ if has_external_data:
530
+ layer.external_data = True
531
+ node.add_layer(layer)
532
+ elif local == 'tile-animation':
533
+ anim = self._parse_game_tile_animation(child)
534
+ node.add_tile_animation(anim)
535
+ return node
536
+
537
+ def _parse_game_tile_animation(self, element: ET.Element) -> TileAnimationNode:
538
+ """Parse <qg:tile-animation tile-id="5" frames="5,6,7,8" speed="0.15" />"""
539
+ tile_id = self._parse_int(element.get('tile-id'), 0)
540
+ node = TileAnimationNode(tile_id)
541
+ node.speed = self._parse_float(element.get('speed'), 0.15)
542
+
543
+ # Parse frames as comma-separated list of integers
544
+ frames_str = element.get('frames', '')
545
+ if frames_str:
546
+ node.frames = [int(f.strip()) for f in frames_str.split(',') if f.strip()]
547
+
548
+ return node
549
+
550
+ def _parse_game_layer(self, element: ET.Element) -> TilemapLayerNode:
551
+ name = element.get('name', '')
552
+ node = TilemapLayerNode(name)
553
+ node.collision = self._parse_bool(element.get('collision'))
554
+ node.data = (element.text or '').strip()
555
+ return node
556
+
557
+ # ------------------------------------------------------------------
558
+ # Behavior abstractions
559
+ # ------------------------------------------------------------------
560
+
561
+ def _parse_game_behavior(self, element: ET.Element) -> BehaviorNode:
562
+ name = element.get('name', '')
563
+ node = BehaviorNode(name)
564
+
565
+ for child in element:
566
+ parsed = self._parse_child(child)
567
+ if parsed:
568
+ node.add_child(parsed)
569
+ return node
570
+
571
+ def _parse_game_use(self, element: ET.Element) -> UseNode:
572
+ behavior = element.get('behavior', '')
573
+ node = UseNode(behavior)
574
+ node.on_collision = element.get('on-collision')
575
+ node.collision_tag = element.get('collision-tag')
576
+
577
+ # A behavior often needs more than one collision handler — a patrol
578
+ # that turns at a wall AND at a ledge. The attribute form allows one,
579
+ # and examples/smw_polished.q tried to write two by repeating
580
+ # on-collision=, which is a duplicate attribute and killed the parse
581
+ # of the whole file. Repeating the <qg:use> instead would instantiate
582
+ # the behavior twice, which is not the same thing.
583
+ #
584
+ # So: extra handlers as children.
585
+ #
586
+ # <qg:use behavior="PatrolBehavior" speed="0.8">
587
+ # <qg:on-collision handler="onWallHit" tag="solid" />
588
+ # <qg:on-collision handler="onEdgeDetected" tag="edge" />
589
+ # </qg:use>
590
+ for child in element:
591
+ tag = child.tag.split('}')[-1].split(':')[-1]
592
+ if tag != 'on-collision':
593
+ continue
594
+ handler = child.get('handler') or child.get('on-collision')
595
+ if not handler:
596
+ continue
597
+ collision_tag = (child.get('tag') or child.get('collision-tag')
598
+ or child.get('with-tag'))
599
+ if node.on_collision is None:
600
+ node.on_collision = handler
601
+ node.collision_tag = collision_tag
602
+ else:
603
+ node.extra_collisions.append(
604
+ {'handler': handler, 'tag': collision_tag})
605
+
606
+ # All other attributes are overrides
607
+ reserved = {'behavior', 'on-collision', 'collision-tag'}
608
+ for attr_name, attr_val in element.attrib.items():
609
+ if attr_name not in reserved:
610
+ node.overrides[attr_name] = attr_val
611
+ return node
612
+
613
+ def _parse_game_prefab(self, element: ET.Element) -> PrefabNode:
614
+ name = element.get('name', '')
615
+ node = PrefabNode(name)
616
+
617
+ # --- Classification ---
618
+ node.entity_type = element.get('type')
619
+
620
+ # --- Combat ---
621
+ if element.get('health') is not None:
622
+ node.health = self._parse_int(element.get('health'), 1)
623
+ node.defeated_by = element.get('defeated-by')
624
+ node.on_defeat = element.get('on-defeat')
625
+ if element.get('contact-damage') is not None:
626
+ node.contact_damage = self._parse_int(element.get('contact-damage'), 1)
627
+ if element.get('knockback') is not None:
628
+ node.knockback = self._parse_float(element.get('knockback'), 0.0)
629
+ if element.get('i-frames') is not None:
630
+ node.i_frames = self._parse_int(element.get('i-frames'), 0)
631
+ if element.get('stomp-bounce') is not None:
632
+ node.stomp_bounce = self._parse_float(element.get('stomp-bounce'), 0.0)
633
+
634
+ # --- Movement ---
635
+ node.movement = element.get('movement')
636
+ if element.get('move-speed') is not None:
637
+ node.move_speed = self._parse_float(element.get('move-speed'), 1.0)
638
+ if element.get('turn-on-edge') is not None:
639
+ node.turn_on_edge = self._parse_bool(element.get('turn-on-edge'))
640
+ node.facing = element.get('facing')
641
+ if element.get('turn-cooldown') is not None:
642
+ node.turn_cooldown = self._parse_float(element.get('turn-cooldown'), 0.0)
643
+
644
+ # --- Reward ---
645
+ node.reward = element.get('reward')
646
+ node.reward_kill = element.get('reward-kill')
647
+
648
+ # --- Respawn ---
649
+ if element.get('respawns') is not None:
650
+ node.respawns = self._parse_bool(element.get('respawns'))
651
+ if element.get('respawn-delay') is not None:
652
+ node.respawn_delay = self._parse_float(element.get('respawn-delay'), 0.0)
653
+
654
+ # --- Item ---
655
+ node.effect = element.get('effect')
656
+ if element.get('duration') is not None:
657
+ node.duration = self._parse_float(element.get('duration'), 0.0)
658
+ if element.get('auto-move') is not None:
659
+ node.auto_move = self._parse_bool(element.get('auto-move'))
660
+ if element.get('collectible') is not None:
661
+ node.collectible = self._parse_bool(element.get('collectible'))
662
+ if element.get('carryable') is not None:
663
+ node.carryable = self._parse_bool(element.get('carryable'))
664
+
665
+ # --- Block ---
666
+ node.content = element.get('content')
667
+ if element.get('hits') is not None:
668
+ node.hits = self._parse_int(element.get('hits'), 1)
669
+ if element.get('breakable') is not None:
670
+ node.breakable = self._parse_bool(element.get('breakable'))
671
+ if element.get('invisible') is not None:
672
+ node.invisible = self._parse_bool(element.get('invisible'))
673
+ if element.get('checkpoint') is not None:
674
+ node.checkpoint = self._parse_bool(element.get('checkpoint'))
675
+ if element.get('flying') is not None:
676
+ node.flying = self._parse_bool(element.get('flying'))
677
+
678
+ for child in element:
679
+ parsed = self._parse_child(child)
680
+ if parsed:
681
+ node.add_child(parsed)
682
+ return node
683
+
684
+ def _parse_game_instance(self, element: ET.Element) -> InstanceNode:
685
+ prefab = element.get('prefab', '')
686
+ node = InstanceNode(prefab)
687
+ node.instance_id = element.get('id')
688
+ node.x = self._parse_float(element.get('x'))
689
+ node.y = self._parse_float(element.get('y'))
690
+
691
+ reserved = {'prefab', 'id', 'x', 'y'}
692
+ for attr_name, attr_val in element.attrib.items():
693
+ if attr_name not in reserved:
694
+ node.overrides[attr_name] = attr_val
695
+ return node
696
+
697
+ def _parse_game_group(self, element: ET.Element) -> GroupNode:
698
+ name = element.get('name', '')
699
+ node = GroupNode(name)
700
+ node.tag = element.get('tag')
701
+
702
+ for child in element:
703
+ parsed = self._parse_child(child)
704
+ if parsed:
705
+ node.add_child(parsed)
706
+ return node
707
+
708
+ # ------------------------------------------------------------------
709
+ # State machine
710
+ # ------------------------------------------------------------------
711
+
712
+ def _parse_game_state_machine(self, element: ET.Element) -> StateMachineNode:
713
+ initial = element.get('initial', '')
714
+ node = StateMachineNode(initial)
715
+
716
+ for child in element:
717
+ local = self._get_local_name(child)
718
+ if local == 'state':
719
+ state = self._parse_game_state(child)
720
+ node.add_state(state)
721
+ return node
722
+
723
+ def _parse_game_state(self, element: ET.Element) -> StateNode:
724
+ name = element.get('name', '')
725
+ node = StateNode(name)
726
+
727
+ for child in element:
728
+ ns = self._get_namespace(child)
729
+ local = self._get_local_name(child)
730
+
731
+ if ns == 'game' and local == 'on':
732
+ transition = self._parse_game_transition(child)
733
+ node.add_transition(transition)
734
+ else:
735
+ parsed = self._parse_child(child)
736
+ if parsed:
737
+ node.add_child(parsed)
738
+ return node
739
+
740
+ def _parse_game_clickable(self, element: ET.Element) -> ClickableNode:
741
+ node = ClickableNode()
742
+ node.action = element.get('action', '')
743
+ node.cursor = element.get('cursor', 'pointer')
744
+ return node
745
+
746
+ def _parse_game_transition(self, element: ET.Element) -> TransitionNode:
747
+ event = element.get('event', '')
748
+ transition = element.get('transition', '')
749
+ return TransitionNode(event, transition)
750
+
751
+ def _parse_game_event(self, element: ET.Element) -> EventNode:
752
+ """Parse <qg:event> - Declarative event listener.
753
+
754
+ Supports both handler-reference style and inline children:
755
+ <qg:event name="press-start" handler="onStart" />
756
+ <qg:event name="press-start">
757
+ <qg:transition to="world-map" effect="fade" />
758
+ </qg:event>
759
+
760
+ Attributes:
761
+ name: Event name to listen for
762
+ handler or action: Function to call when event fires
763
+ filter-tag: Optional tag filter
764
+ filter-id: Optional sprite ID filter
765
+ scope: Event scope (scene, sprite, global)
766
+ """
767
+ name = element.get('name', '')
768
+ handler = element.get('handler') or element.get('action', '')
769
+ node = EventNode(name, handler)
770
+ node.filter_tag = element.get('filter-tag')
771
+ node.filter_id = element.get('filter-id')
772
+ node.scope = element.get('scope', 'scene')
773
+
774
+ # Parse inline children (e.g., <qg:transition> inside <qg:event>)
775
+ for child in element:
776
+ parsed = self._parse_child(child)
777
+ if parsed:
778
+ if not hasattr(node, 'children'):
779
+ node.children = []
780
+ node.children.append(parsed)
781
+ return node
782
+
783
+ def _parse_game_on_collision(self, element: ET.Element) -> OnCollisionNode:
784
+ """Parse <qg:on-collision> - Inline collision handler for sprites.
785
+
786
+ Attributes:
787
+ with-tag: Tag of target sprites to collide with
788
+ with-id: Specific sprite ID to collide with
789
+ action: Action to perform (destroy-self, destroy-other, emit:eventName, call:funcName)
790
+
791
+ Example:
792
+ <qg:sprite id="mario" tag="player">
793
+ <qg:on-collision with-tag="coin" action="destroy-other" />
794
+ <qg:on-collision with-tag="enemy" action="emit:player-hit" />
795
+ </qg:sprite>
796
+ """
797
+ node = OnCollisionNode()
798
+ node.with_tag = element.get('with-tag')
799
+ node.with_id = element.get('with-id')
800
+ node.action = element.get('action', '')
801
+ return node
802
+
803
+ # ------------------------------------------------------------------
804
+ # Multi-scene nodes
805
+ # ------------------------------------------------------------------
806
+
807
+ def _parse_game_scene_transition(self, element: ET.Element) -> SceneTransitionNode:
808
+ """Parse <qg:transition> - Scene transition.
809
+
810
+ Example:
811
+ <qg:transition to="world-map" effect="fade" duration="0.5" />
812
+ """
813
+ to = element.get('to', '')
814
+ node = SceneTransitionNode(to)
815
+ node.effect = element.get('effect', 'fade')
816
+ node.duration = self._parse_float(element.get('duration'), 0.5)
817
+
818
+ # Parse optional data attributes (data-* pattern)
819
+ data = {}
820
+ for attr_name, attr_val in element.attrib.items():
821
+ if attr_name.startswith('data-'):
822
+ data[attr_name[5:]] = attr_val
823
+ if data:
824
+ node.data = data
825
+
826
+ return node
827
+
828
+ def _parse_game_persistent(self, element: ET.Element) -> PersistentNode:
829
+ """Parse <qg:persistent> - Persistent state block.
830
+
831
+ Example:
832
+ <qg:persistent>
833
+ <q:set name="lives" value="5" />
834
+ <q:set name="score" value="0" />
835
+ </qg:persistent>
836
+ """
837
+ node = PersistentNode()
838
+ for child in element:
839
+ parsed = self._parse_child(child)
840
+ if parsed:
841
+ node.add_child(parsed)
842
+ return node
843
+
844
+ def _parse_game_map_node(self, element: ET.Element) -> MapNodeDef:
845
+ """Parse <qg:map-node> - World map node.
846
+
847
+ Example:
848
+ <qg:map-node id="yoshi-island-1" x="80" y="180" scene="yoshi-island-1" />
849
+ """
850
+ node_id = element.get('id', '')
851
+ node = MapNodeDef(node_id)
852
+ node.x = self._parse_float(element.get('x'))
853
+ node.y = self._parse_float(element.get('y'))
854
+ node.scene = element.get('scene')
855
+ node.locked = self._parse_bool(element.get('locked'))
856
+ node.icon = element.get('icon')
857
+ return node
858
+
859
+ def _parse_game_map_path(self, element: ET.Element) -> MapPathNode:
860
+ """Parse <qg:map-path> - World map path between nodes.
861
+
862
+ Example:
863
+ <qg:map-path from="start" to="yoshi-island-1" />
864
+ <qg:map-path from="yoshi-island-1" to="yoshi-island-2" unlock="yoshi-island-1-cleared" />
865
+ """
866
+ from_node = element.get('from', '')
867
+ to_node = element.get('to', '')
868
+ node = MapPathNode(from_node, to_node)
869
+ node.unlock = element.get('unlock')
870
+ node.direction = element.get('direction')
871
+ return node
872
+
873
+ # ------------------------------------------------------------------
874
+ # Collision layer definitions
875
+ # ------------------------------------------------------------------
876
+
877
+ def _parse_game_collision_layer(self, element: ET.Element) -> CollisionLayerDef:
878
+ """Parse <qg:collision-layer> - Named collision layer definition.
879
+
880
+ Example:
881
+ <qg:collision-layer id="1" name="world" />
882
+ <qg:collision-layer id="2" name="player" />
883
+ """
884
+ layer_id = self._parse_int(element.get('id'), 1)
885
+ name = element.get('name', '')
886
+ return CollisionLayerDef(layer_id, name)
887
+
888
+ # ------------------------------------------------------------------
889
+ # Enemy system
890
+ # ------------------------------------------------------------------
891
+
892
+ def _parse_game_enemy(self, element: ET.Element) -> EnemyNode:
893
+ """Parse <qg:enemy> - Enemy behavior definition.
894
+
895
+ Children are parsed contextually: <qg:ai> and <qg:defeat>.
896
+ """
897
+ prefab = element.get('prefab', '')
898
+ node = EnemyNode(prefab)
899
+
900
+ for child in element:
901
+ local = self._get_local_name(child)
902
+ if local == 'ai':
903
+ node.ai = self._parse_enemy_ai(child)
904
+ elif local == 'defeat':
905
+ node.defeat = self._parse_enemy_defeat(child)
906
+
907
+ return node
908
+
909
+ def _parse_enemy_ai(self, element: ET.Element) -> EnemyAiNode:
910
+ """Parse <qg:ai> - Enemy AI behavior."""
911
+ node = EnemyAiNode()
912
+ node.ai_type = element.get('type', 'patrol')
913
+ node.speed = self._parse_float(element.get('speed'), 30.0)
914
+ node.turn_cooldown = self._parse_float(element.get('turn-cooldown')) if element.get('turn-cooldown') else None
915
+ node.turn_on_edge = self._parse_bool(element.get('turn-on-edge'))
916
+ node.facing = element.get('facing', 'left')
917
+ return node
918
+
919
+ def _parse_enemy_defeat(self, element: ET.Element) -> EnemyDefeatNode:
920
+ """Parse <qg:defeat> - How enemy can be defeated.
921
+
922
+ Children: <qg:on-hit>, <qg:on-kill>.
923
+ """
924
+ node = EnemyDefeatNode()
925
+ node.by = element.get('by', 'stomp')
926
+ node.health = self._parse_int(element.get('health'), 1)
927
+ node.bounce = self._parse_float(element.get('bounce')) if element.get('bounce') else None
928
+
929
+ for child in element:
930
+ local = self._get_local_name(child)
931
+ if local == 'on-hit':
932
+ node.on_hit = self._parse_enemy_on_hit(child)
933
+ elif local == 'on-kill':
934
+ node.on_kill = self._parse_enemy_on_kill(child)
935
+
936
+ return node
937
+
938
+ def _parse_enemy_on_hit(self, element: ET.Element) -> EnemyOnHitNode:
939
+ """Parse <qg:on-hit> - Reward/effect on non-lethal hit."""
940
+ node = EnemyOnHitNode()
941
+ node.score = self._parse_int(element.get('score'))
942
+ node.effect = element.get('effect')
943
+ node.speed_multiply = self._parse_float(element.get('speed-multiply')) if element.get('speed-multiply') else None
944
+ return node
945
+
946
+ def _parse_enemy_on_kill(self, element: ET.Element) -> EnemyOnKillNode:
947
+ """Parse <qg:on-kill> - Reward/effect on kill."""
948
+ node = EnemyOnKillNode()
949
+ node.score = self._parse_int(element.get('score'))
950
+ node.effect = element.get('effect')
951
+ node.drop = element.get('drop')
952
+ return node
953
+
954
+ # ------------------------------------------------------------------
955
+ # Event action dispatch (qg:action can be HUD or Event context)
956
+ # ------------------------------------------------------------------
957
+
958
+ def _parse_game_action_dispatch(self, element: ET.Element):
959
+ """Dispatch <qg:action> based on context attributes.
960
+
961
+ If element has 'type' in EventActionNode.VALID_ACTION_TYPES -> EventActionNode
962
+ Otherwise fall back to HudActionNode (legacy behavior).
963
+ """
964
+ action_type = element.get('type', '')
965
+ if action_type in EventActionNode.VALID_ACTION_TYPES:
966
+ return self._parse_game_event_action(element)
967
+ return self._parse_game_hud_action(element)
968
+
969
+ def _parse_game_event_action(self, element: ET.Element) -> EventActionNode:
970
+ """Parse <qg:action> inside <qg:event> - Inline event action.
971
+
972
+ Example:
973
+ <qg:action type="score" amount="10" />
974
+ <qg:action type="destroy" target="{data.other}" />
975
+ <qg:action type="sound" target="sfx-coin" />
976
+ """
977
+ node = EventActionNode()
978
+ node.action_type = element.get('type', '')
979
+ node.target = element.get('target')
980
+ node.value = element.get('value')
981
+ if element.get('amount') is not None:
982
+ node.amount = self._parse_int(element.get('amount'))
983
+ return node