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,1719 @@
1
+ """
2
+ AST Nodes for Game Engine 2D (qg: namespace)
3
+
4
+ All game-specific AST nodes for the Quantum 2D game engine.
5
+ These represent game objects, systems, behaviors, and abstractions
6
+ that compile to PixiJS + Matter.js JavaScript.
7
+ """
8
+
9
+ from typing import Dict, Any, List, Optional
10
+ from dataclasses import dataclass, field
11
+ import sys
12
+ from pathlib import Path
13
+ from quantum.core.ast_nodes import QuantumNode
14
+
15
+
16
+ # ============================================
17
+ # CORE GAME NODES
18
+ # ============================================
19
+
20
+ class SceneNode(QuantumNode):
21
+ """Represents <qg:scene> - A game scene with auto game loop."""
22
+
23
+ VALID_SCENE_TYPES = ('level', 'title', 'map', 'overlay')
24
+
25
+ VALID_DEATH_SEQUENCES = ('smw', 'instant', 'respawn')
26
+
27
+ def __init__(self, name: str):
28
+ self.name = name
29
+ self.width: int = 800
30
+ self.height: int = 600
31
+ self.viewport_width: Optional[int] = None # Canvas width (defaults to width)
32
+ self.viewport_height: Optional[int] = None # Canvas height (defaults to height)
33
+ self.background: str = "#000000"
34
+ self.active: bool = True
35
+ self.scene_type: str = "level" # level, title, map, overlay
36
+ self.initial: bool = False # marks as entry scene
37
+ self.children: List[QuantumNode] = []
38
+
39
+ # Death / game over
40
+ self.death_sequence: Optional[str] = None # "smw", "instant", "respawn"
41
+ self.death_timer: float = 3.0 # Seconds before reload after death
42
+ self.game_over_jingle: Optional[str] = None # Audio path for game over jingle
43
+ self.game_over_alphabet: Optional[str] = None # Sprite path for alphabet tileset
44
+
45
+ def add_child(self, child: QuantumNode):
46
+ self.children.append(child)
47
+
48
+ def to_dict(self) -> Dict[str, Any]:
49
+ result = {
50
+ "type": "game_scene",
51
+ "name": self.name,
52
+ "width": self.width,
53
+ "height": self.height,
54
+ "background": self.background,
55
+ "active": self.active,
56
+ "scene_type": self.scene_type,
57
+ "initial": self.initial,
58
+ "children_count": len(self.children),
59
+ }
60
+ if self.death_sequence is not None:
61
+ result["death_sequence"] = self.death_sequence
62
+ if self.game_over_jingle is not None:
63
+ result["game_over_jingle"] = self.game_over_jingle
64
+ if self.game_over_alphabet is not None:
65
+ result["game_over_alphabet"] = self.game_over_alphabet
66
+ return result
67
+
68
+ def validate(self) -> List[str]:
69
+ errors = []
70
+ if not self.name:
71
+ errors.append("Scene name is required")
72
+ if self.width <= 0:
73
+ errors.append("Scene width must be positive")
74
+ if self.height <= 0:
75
+ errors.append("Scene height must be positive")
76
+ if self.scene_type not in self.VALID_SCENE_TYPES:
77
+ errors.append(f"Invalid scene type: {self.scene_type}. Must be one of {', '.join(self.VALID_SCENE_TYPES)}")
78
+ for child in self.children:
79
+ if hasattr(child, 'validate'):
80
+ errors.extend(child.validate())
81
+ return errors
82
+
83
+
84
+ class SpriteNode(QuantumNode):
85
+ """Represents <qg:sprite> - A game sprite with optional physics."""
86
+
87
+ def __init__(self, sprite_id: str):
88
+ self.sprite_id = sprite_id
89
+ self.src: str = ""
90
+ self.x: float = 0
91
+ self.y: float = 0
92
+ self.width: Optional[float] = None
93
+ self.height: Optional[float] = None
94
+ self.anchor_x: float = 0.5
95
+ self.anchor_y: float = 0.5
96
+ self.rotation: float = 0
97
+ self.scale_x: float = 1
98
+ self.scale_y: float = 1
99
+ self.alpha: float = 1.0
100
+ self.visible: bool = True
101
+ self.color: Optional[str] = None # Hex color for sprites without images
102
+ self.tag: Optional[str] = None
103
+ self.layer: int = 0
104
+
105
+ # Spritesheet
106
+ self.frame_width: Optional[int] = None
107
+ self.frame_height: Optional[int] = None
108
+
109
+ # Physics (Matter.js)
110
+ self.body: Optional[str] = None # static, dynamic, kinematic
111
+ self.shape: str = 'box' # box, circle, slope-left, slope-right
112
+ self.bounce: float = 0.1
113
+ self.friction: float = 0.1
114
+ self.mass: Optional[float] = None
115
+ self.sensor: bool = False
116
+
117
+ # Magic controls
118
+ self.controls: Optional[str] = None # wasd, arrows, custom
119
+ self.speed: float = 5.0
120
+ self.jump_force: float = 10.0
121
+
122
+ # SMW-style jump physics
123
+ self.gravity_up: Optional[float] = None # Gravity while ascending (lower = floatier)
124
+ self.gravity_down: Optional[float] = None # Gravity while descending (higher = snappier)
125
+ self.jump_hold_boost: float = 0.4 # Extra upward force while holding jump
126
+ self.coyote_frames: int = 6 # Frames allowed to jump after leaving platform
127
+ self.max_fall_speed: float = 15.0 # Terminal velocity
128
+
129
+ # Collision layers (int bitmask or str named layer)
130
+ self.collision_layer = None # int bitmask or str layer name
131
+ self.collision_mask = None # int bitmask or str comma-separated layer names
132
+ self.floor_max_angle: Optional[float] = None # Max floor angle in degrees (default 45)
133
+
134
+ # Special abilities
135
+ self.spin_jump: bool = False # SMW spin jump (kills enemies instantly, rotates sprite)
136
+
137
+ # Children (animations, colliders, behaviors)
138
+ self.children: List[QuantumNode] = []
139
+
140
+ def add_child(self, child: QuantumNode):
141
+ self.children.append(child)
142
+
143
+ def to_dict(self) -> Dict[str, Any]:
144
+ return {
145
+ "type": "game_sprite",
146
+ "id": self.sprite_id,
147
+ "src": self.src,
148
+ "x": self.x,
149
+ "y": self.y,
150
+ "body": self.body,
151
+ "controls": self.controls,
152
+ "tag": self.tag,
153
+ "children_count": len(self.children),
154
+ }
155
+
156
+ def validate(self) -> List[str]:
157
+ errors = []
158
+ if not self.sprite_id:
159
+ errors.append("Sprite id is required")
160
+ if self.body and self.body not in ('static', 'dynamic', 'kinematic'):
161
+ errors.append(f"Invalid body type: {self.body}. Must be static, dynamic, or kinematic")
162
+ for child in self.children:
163
+ if hasattr(child, 'validate'):
164
+ errors.extend(child.validate())
165
+ return errors
166
+
167
+
168
+ class PhysicsNode(QuantumNode):
169
+ """Represents <qg:physics> - Scene-level physics configuration."""
170
+
171
+ def __init__(self):
172
+ self.gravity_x: float = 0
173
+ self.gravity_y: float = 9.8
174
+ self.bounds: Optional[str] = None # canvas, scene, none
175
+ self.debug: bool = False
176
+ self.collision_layers: Dict[str, int] = {} # name -> bitmask value
177
+ self.children: List[QuantumNode] = []
178
+
179
+ def add_child(self, child: QuantumNode):
180
+ self.children.append(child)
181
+
182
+ def to_dict(self) -> Dict[str, Any]:
183
+ result = {
184
+ "type": "game_physics",
185
+ "gravity_x": self.gravity_x,
186
+ "gravity_y": self.gravity_y,
187
+ "bounds": self.bounds,
188
+ "debug": self.debug,
189
+ }
190
+ if self.collision_layers:
191
+ result["collision_layers"] = self.collision_layers
192
+ return result
193
+
194
+ def validate(self) -> List[str]:
195
+ return []
196
+
197
+
198
+ class ColliderNode(QuantumNode):
199
+ """Represents <qg:collider> - Override collision shape for a sprite."""
200
+
201
+ def __init__(self):
202
+ self.shape: str = "box" # box, circle, polygon
203
+ self.width: Optional[float] = None
204
+ self.height: Optional[float] = None
205
+ self.radius: Optional[float] = None
206
+ self.vertices: Optional[str] = None # JSON array of points
207
+ self.offset_x: float = 0
208
+ self.offset_y: float = 0
209
+
210
+ def to_dict(self) -> Dict[str, Any]:
211
+ return {
212
+ "type": "game_collider",
213
+ "shape": self.shape,
214
+ }
215
+
216
+ def validate(self) -> List[str]:
217
+ errors = []
218
+ if self.shape not in ('box', 'circle', 'polygon'):
219
+ errors.append(f"Invalid collider shape: {self.shape}")
220
+ return errors
221
+
222
+
223
+ class AnimationNode(QuantumNode):
224
+ """Represents <qg:animation> - Sprite animation from spritesheet."""
225
+
226
+ def __init__(self, name: str):
227
+ self.name = name
228
+ self.frames: str = "" # e.g. "0-3", "0,1,2,3"
229
+ self.speed: float = 0.1
230
+ self.loop: bool = True
231
+ self.auto_play: bool = False
232
+
233
+ def to_dict(self) -> Dict[str, Any]:
234
+ return {
235
+ "type": "game_animation",
236
+ "name": self.name,
237
+ "frames": self.frames,
238
+ "speed": self.speed,
239
+ "loop": self.loop,
240
+ "auto_play": self.auto_play,
241
+ }
242
+
243
+ def validate(self) -> List[str]:
244
+ errors = []
245
+ if not self.name:
246
+ errors.append("Animation name is required")
247
+ if not self.frames:
248
+ errors.append("Animation frames are required")
249
+ return errors
250
+
251
+
252
+ class OnCollisionNode(QuantumNode):
253
+ """Represents <qg:on-collision> - Inline collision handler for sprites.
254
+
255
+ Allows simple collision responses without creating behaviors:
256
+ <qg:sprite id="mario" tag="player">
257
+ <qg:on-collision with-tag="coin" action="destroy-other" />
258
+ <qg:on-collision with-tag="enemy" action="emit:player-hit" />
259
+ </qg:sprite>
260
+
261
+ Supported actions:
262
+ - destroy-self: Remove this sprite
263
+ - destroy-other: Remove the colliding sprite
264
+ - emit:eventName: Emit a custom event
265
+ """
266
+
267
+ def __init__(self):
268
+ self.with_tag: Optional[str] = None # Tag to collide with
269
+ self.with_id: Optional[str] = None # Specific sprite ID to collide with
270
+ self.action: str = "" # Action to perform
271
+
272
+ def to_dict(self) -> Dict[str, Any]:
273
+ return {
274
+ "type": "game_on_collision",
275
+ "with_tag": self.with_tag,
276
+ "with_id": self.with_id,
277
+ "action": self.action,
278
+ }
279
+
280
+ def validate(self) -> List[str]:
281
+ errors = []
282
+ if not self.with_tag and not self.with_id:
283
+ errors.append("on-collision requires with-tag or with-id")
284
+ if not self.action:
285
+ errors.append("on-collision requires an action")
286
+ return errors
287
+
288
+
289
+ class CameraNode(QuantumNode):
290
+ """Represents <qg:camera> - Camera system with follow and bounds."""
291
+
292
+ def __init__(self):
293
+ self.follow: Optional[str] = None # sprite id to follow
294
+ self.lerp: float = 0.1
295
+ self.bounds: Optional[str] = None # scene, none
296
+ self.zoom: float = 1.0
297
+ self.offset_x: float = 0
298
+ self.offset_y: float = 0
299
+
300
+ def to_dict(self) -> Dict[str, Any]:
301
+ return {
302
+ "type": "game_camera",
303
+ "follow": self.follow,
304
+ "lerp": self.lerp,
305
+ "bounds": self.bounds,
306
+ }
307
+
308
+ def validate(self) -> List[str]:
309
+ return []
310
+
311
+
312
+ class InputNode(QuantumNode):
313
+ """Represents <qg:input> - Custom input mapping."""
314
+
315
+ def __init__(self):
316
+ self.key: str = ""
317
+ self.action: str = ""
318
+ self.input_type: str = "press" # press, hold, release, click, mousedown, mouseup, mousemove
319
+
320
+ def to_dict(self) -> Dict[str, Any]:
321
+ return {
322
+ "type": "game_input",
323
+ "key": self.key,
324
+ "action": self.action,
325
+ "input_type": self.input_type,
326
+ }
327
+
328
+ def validate(self) -> List[str]:
329
+ errors = []
330
+ if not self.key:
331
+ errors.append("Input key is required")
332
+ if not self.action:
333
+ errors.append("Input action is required")
334
+ return errors
335
+
336
+
337
+ class SoundNode(QuantumNode):
338
+ """Represents <qg:sound> - Audio with trigger-based playback."""
339
+
340
+ def __init__(self, sound_id: str):
341
+ self.sound_id = sound_id
342
+ self.src: str = ""
343
+ self.volume: float = 1.0
344
+ self.loop: bool = False
345
+ self.trigger: Optional[str] = None # e.g. "player.jump", "scene.start"
346
+ self.channel: str = "sfx" # sfx, music
347
+
348
+ def to_dict(self) -> Dict[str, Any]:
349
+ return {
350
+ "type": "game_sound",
351
+ "id": self.sound_id,
352
+ "src": self.src,
353
+ "trigger": self.trigger,
354
+ "channel": self.channel,
355
+ }
356
+
357
+ def validate(self) -> List[str]:
358
+ errors = []
359
+ if not self.sound_id:
360
+ errors.append("Sound id is required")
361
+ if not self.src:
362
+ errors.append("Sound src is required")
363
+ return errors
364
+
365
+
366
+ class ParticleNode(QuantumNode):
367
+ """Represents <qg:particle> - Particle emitter."""
368
+
369
+ def __init__(self, particle_id: str):
370
+ self.particle_id = particle_id
371
+ self.src: str = ""
372
+ self.follow: Optional[str] = None
373
+ self.trigger: Optional[str] = None
374
+ self.count: int = 20
375
+ self.emit_rate: int = 10
376
+ self.lifetime: float = 1.0
377
+ self.speed_min: float = 1.0
378
+ self.speed_max: float = 3.0
379
+ self.angle_min: float = 0
380
+ self.angle_max: float = 360
381
+ self.alpha_start: float = 1.0
382
+ self.alpha_end: float = 0.0
383
+
384
+ def to_dict(self) -> Dict[str, Any]:
385
+ return {
386
+ "type": "game_particle",
387
+ "id": self.particle_id,
388
+ "follow": self.follow,
389
+ "trigger": self.trigger,
390
+ }
391
+
392
+ def validate(self) -> List[str]:
393
+ errors = []
394
+ if not self.particle_id:
395
+ errors.append("Particle id is required")
396
+ return errors
397
+
398
+
399
+ class TimerNode(QuantumNode):
400
+ """Represents <qg:timer> - Declarative game timer."""
401
+
402
+ def __init__(self, timer_id: str):
403
+ self.timer_id = timer_id
404
+ self.interval: float = 1.0
405
+ self.repeat: int = -1 # -1 = infinite
406
+ self.auto_start: bool = True
407
+ self.action: str = ""
408
+
409
+ def to_dict(self) -> Dict[str, Any]:
410
+ return {
411
+ "type": "game_timer",
412
+ "id": self.timer_id,
413
+ "interval": self.interval,
414
+ "action": self.action,
415
+ }
416
+
417
+ def validate(self) -> List[str]:
418
+ errors = []
419
+ if not self.timer_id:
420
+ errors.append("Timer id is required")
421
+ if not self.action:
422
+ errors.append("Timer action is required")
423
+ return errors
424
+
425
+
426
+ class SpawnNode(QuantumNode):
427
+ """Represents <qg:spawn> - Object spawner with pooling."""
428
+
429
+ def __init__(self, spawn_id: str):
430
+ self.spawn_id = spawn_id
431
+ self.prefab: str = ""
432
+ self.count: int = 1
433
+ self.interval: Optional[float] = None
434
+ self.x: Optional[str] = None
435
+ self.y: Optional[str] = None
436
+ self.pool_size: int = 10
437
+
438
+ def to_dict(self) -> Dict[str, Any]:
439
+ return {
440
+ "type": "game_spawn",
441
+ "id": self.spawn_id,
442
+ "prefab": self.prefab,
443
+ }
444
+
445
+ def validate(self) -> List[str]:
446
+ errors = []
447
+ if not self.spawn_id:
448
+ errors.append("Spawn id is required")
449
+ if not self.prefab:
450
+ errors.append("Spawn prefab is required")
451
+ return errors
452
+
453
+
454
+ class HudNode(QuantumNode):
455
+ """Represents <qg:hud> - Fixed overlay on camera.
456
+
457
+ Supports two modes:
458
+ 1. HTML mode (legacy): children are HTMLNode/TextNode elements
459
+ 2. Tile-based mode: children are HudTileNode, HudCounterNode, etc.
460
+
461
+ Tile-based mode is detected by presence of background/sprite-prefix attrs
462
+ or HudTile*/HudCounter*/HudCollection* children.
463
+ """
464
+
465
+ def __init__(self):
466
+ self.position: str = "top-left"
467
+ self.background: Optional[str] = None # e.g. "rgba(0,0,0,0.55)"
468
+ self.background_height: Optional[int] = None # px height of bg bar
469
+ self.sprite_prefix: Optional[str] = None # e.g. "assets/smw/sprites/hud_"
470
+ self.children: List[QuantumNode] = []
471
+
472
+ def add_child(self, child: QuantumNode):
473
+ self.children.append(child)
474
+
475
+ def to_dict(self) -> Dict[str, Any]:
476
+ result = {
477
+ "type": "game_hud",
478
+ "position": self.position,
479
+ "children_count": len(self.children),
480
+ }
481
+ if self.background is not None:
482
+ result["background"] = self.background
483
+ if self.background_height is not None:
484
+ result["background_height"] = self.background_height
485
+ if self.sprite_prefix is not None:
486
+ result["sprite_prefix"] = self.sprite_prefix
487
+ return result
488
+
489
+ def validate(self) -> List[str]:
490
+ errors = []
491
+ valid = ('top-left', 'top-right', 'top-center',
492
+ 'bottom-left', 'bottom-right', 'bottom-center', 'center')
493
+ # Only validate position for HTML-mode HUDs (no background = legacy)
494
+ if self.background is None and self.position not in valid:
495
+ errors.append(f"Invalid HUD position: {self.position}")
496
+ for child in self.children:
497
+ if hasattr(child, 'validate'):
498
+ errors.extend(child.validate())
499
+ return errors
500
+
501
+
502
+ class HudTileNode(QuantumNode):
503
+ """Represents <qg:tile> inside <qg:hud> - Static sprite tile."""
504
+
505
+ def __init__(self):
506
+ self.sprite: str = "" # Sprite filename (resolved with prefix)
507
+ self.x: int = 0
508
+ self.y: int = 0
509
+ self.width: Optional[int] = None
510
+ self.height: Optional[int] = None
511
+
512
+ def to_dict(self) -> Dict[str, Any]:
513
+ return {
514
+ "type": "hud_tile",
515
+ "sprite": self.sprite,
516
+ "x": self.x,
517
+ "y": self.y,
518
+ }
519
+
520
+ def validate(self) -> List[str]:
521
+ errors = []
522
+ if not self.sprite:
523
+ errors.append("HUD tile sprite is required")
524
+ return errors
525
+
526
+
527
+ class HudCounterNode(QuantumNode):
528
+ """Represents <qg:counter> inside <qg:hud> - Numeric counter from digit strip."""
529
+
530
+ def __init__(self):
531
+ self.strip: str = "" # Digit strip image (0-9 horizontally)
532
+ self.x: int = 0
533
+ self.y: int = 0
534
+ self.digits: int = 2
535
+ self.bind: str = "" # e.g. "{coins}"
536
+ self.digit_width: int = 8
537
+ self.digit_height: int = 8
538
+ self.countdown: bool = False
539
+ self.hurry_at: Optional[int] = None # Flash when value reaches this
540
+ self.flash: Optional[str] = None # Flash color name (e.g. "red")
541
+ self.align: str = "left" # "left" or "right"
542
+ self.right_edge: Optional[int] = None # Right edge x for align="right"
543
+ self.tint: Optional[str] = None # Constant tint color name (e.g. "yellow")
544
+ self.intelligent: bool = False # No zero-padding, show only needed digits
545
+ self.icon: Optional[str] = None # Prefix icon sprite (e.g. "coin_icon.png")
546
+ self.symbol: Optional[str] = None # Prefix symbol sprite (e.g. "x_symbol.png")
547
+ self.extra_life_at: Optional[int] = None # Add life and reset at this value
548
+
549
+ def to_dict(self) -> Dict[str, Any]:
550
+ result = {
551
+ "type": "hud_counter",
552
+ "strip": self.strip,
553
+ "x": self.x,
554
+ "y": self.y,
555
+ "digits": self.digits,
556
+ "bind": self.bind,
557
+ }
558
+ if self.countdown:
559
+ result["countdown"] = True
560
+ if self.hurry_at is not None:
561
+ result["hurry_at"] = self.hurry_at
562
+ if self.flash is not None:
563
+ result["flash"] = self.flash
564
+ if self.align != "left":
565
+ result["align"] = self.align
566
+ if self.right_edge is not None:
567
+ result["right_edge"] = self.right_edge
568
+ if self.tint is not None:
569
+ result["tint"] = self.tint
570
+ if self.intelligent:
571
+ result["intelligent"] = True
572
+ if self.icon is not None:
573
+ result["icon"] = self.icon
574
+ if self.symbol is not None:
575
+ result["symbol"] = self.symbol
576
+ if self.extra_life_at is not None:
577
+ result["extra_life_at"] = self.extra_life_at
578
+ return result
579
+
580
+ def validate(self) -> List[str]:
581
+ errors = []
582
+ if not self.strip:
583
+ errors.append("HUD counter strip is required")
584
+ if not self.bind:
585
+ errors.append("HUD counter bind is required")
586
+ if self.digits < 1:
587
+ errors.append("HUD counter digits must be >= 1")
588
+ return errors
589
+
590
+
591
+ class HudCollectionNode(QuantumNode):
592
+ """Represents <qg:collection> inside <qg:hud> - Repeated sprites (e.g. dragon coins)."""
593
+
594
+ def __init__(self):
595
+ self.sprite: str = ""
596
+ self.x: int = 0
597
+ self.y: int = 0
598
+ self.spacing: int = 8
599
+ self.max: int = 5
600
+ self.bind: str = "" # e.g. "{yoshi_coins}"
601
+
602
+ def to_dict(self) -> Dict[str, Any]:
603
+ return {
604
+ "type": "hud_collection",
605
+ "sprite": self.sprite,
606
+ "x": self.x,
607
+ "y": self.y,
608
+ "spacing": self.spacing,
609
+ "max": self.max,
610
+ "bind": self.bind,
611
+ }
612
+
613
+ def validate(self) -> List[str]:
614
+ errors = []
615
+ if not self.sprite:
616
+ errors.append("HUD collection sprite is required")
617
+ if not self.bind:
618
+ errors.append("HUD collection bind is required")
619
+ if self.max < 1:
620
+ errors.append("HUD collection max must be >= 1")
621
+ return errors
622
+
623
+
624
+ class HudSlotNode(QuantumNode):
625
+ """Represents <qg:slot> inside <qg:hud> - Switchable sprite based on variable."""
626
+
627
+ def __init__(self):
628
+ self.x: int = 0
629
+ self.y: int = 0
630
+ self.bind: str = "" # e.g. "{powerup_state}"
631
+ self.options: List['HudOptionNode'] = []
632
+
633
+ def add_option(self, option: 'HudOptionNode'):
634
+ self.options.append(option)
635
+
636
+ def to_dict(self) -> Dict[str, Any]:
637
+ return {
638
+ "type": "hud_slot",
639
+ "x": self.x,
640
+ "y": self.y,
641
+ "bind": self.bind,
642
+ "options_count": len(self.options),
643
+ }
644
+
645
+ def validate(self) -> List[str]:
646
+ errors = []
647
+ if not self.bind:
648
+ errors.append("HUD slot bind is required")
649
+ if not self.options:
650
+ errors.append("HUD slot must have at least one option")
651
+ for opt in self.options:
652
+ if hasattr(opt, 'validate'):
653
+ errors.extend(opt.validate())
654
+ return errors
655
+
656
+
657
+ class HudOptionNode(QuantumNode):
658
+ """Represents <qg:option> inside <qg:slot> - One sprite option."""
659
+
660
+ def __init__(self):
661
+ self.value: str = ""
662
+ self.sprite: str = ""
663
+
664
+ def to_dict(self) -> Dict[str, Any]:
665
+ return {
666
+ "type": "hud_option",
667
+ "value": self.value,
668
+ "sprite": self.sprite,
669
+ }
670
+
671
+ def validate(self) -> List[str]:
672
+ errors = []
673
+ if not self.value:
674
+ errors.append("HUD option value is required")
675
+ if not self.sprite:
676
+ errors.append("HUD option sprite is required")
677
+ return errors
678
+
679
+
680
+ class HudBehaviorNode(QuantumNode):
681
+ """Represents <qg:hud-behavior> - Event trigger based on HUD variable state."""
682
+
683
+ def __init__(self):
684
+ self.target: str = "" # Variable name (e.g. "time_left")
685
+ self.event: str = "" # Event type (e.g. "reach")
686
+ self.value: Optional[int] = None # Threshold value
687
+ self.actions: List['HudActionNode'] = []
688
+
689
+ def add_action(self, action: 'HudActionNode'):
690
+ self.actions.append(action)
691
+
692
+ def to_dict(self) -> Dict[str, Any]:
693
+ result = {
694
+ "type": "hud_behavior",
695
+ "target": self.target,
696
+ "event": self.event,
697
+ }
698
+ if self.value is not None:
699
+ result["value"] = self.value
700
+ result["actions_count"] = len(self.actions)
701
+ return result
702
+
703
+ def validate(self) -> List[str]:
704
+ errors = []
705
+ if not self.target:
706
+ errors.append("HUD behavior target is required")
707
+ if not self.event:
708
+ errors.append("HUD behavior event is required")
709
+ if not self.actions:
710
+ errors.append("HUD behavior must have at least one action")
711
+ for action in self.actions:
712
+ if hasattr(action, 'validate'):
713
+ errors.extend(action.validate())
714
+ return errors
715
+
716
+
717
+ class HudActionNode(QuantumNode):
718
+ """Represents <qg:action> inside <qg:hud-behavior> - Action to perform."""
719
+
720
+ def __init__(self):
721
+ self.action_type: str = "" # "emit", "set", etc.
722
+ self.event: Optional[str] = None # For emit type
723
+ self.rate: Optional[float] = None # For countdown rate
724
+
725
+ def to_dict(self) -> Dict[str, Any]:
726
+ result = {
727
+ "type": "hud_action",
728
+ "action_type": self.action_type,
729
+ }
730
+ if self.event is not None:
731
+ result["event"] = self.event
732
+ if self.rate is not None:
733
+ result["rate"] = self.rate
734
+ return result
735
+
736
+ def validate(self) -> List[str]:
737
+ errors = []
738
+ if not self.action_type:
739
+ errors.append("HUD action type is required")
740
+ return errors
741
+
742
+
743
+ class TweenNode(QuantumNode):
744
+ """Represents <qg:tween> - Declarative animation tween."""
745
+
746
+ def __init__(self, tween_id: str):
747
+ self.tween_id = tween_id
748
+ self.target: str = ""
749
+ self.property: str = "" # x, y, alpha, rotation, scale
750
+ self.to_value: float = 0
751
+ self.duration: float = 1.0
752
+ self.easing: str = "linear"
753
+ self.loop: bool = False
754
+ self.yoyo: bool = False
755
+ self.delay: float = 0
756
+ self.auto_start: bool = True
757
+
758
+ def to_dict(self) -> Dict[str, Any]:
759
+ return {
760
+ "type": "game_tween",
761
+ "id": self.tween_id,
762
+ "target": self.target,
763
+ "property": self.property,
764
+ }
765
+
766
+ def validate(self) -> List[str]:
767
+ errors = []
768
+ if not self.tween_id:
769
+ errors.append("Tween id is required")
770
+ if not self.target:
771
+ errors.append("Tween target is required")
772
+ if not self.property:
773
+ errors.append("Tween property is required")
774
+ return errors
775
+
776
+
777
+ class TileAnimationNode(QuantumNode):
778
+ """Represents <qg:tile-animation> - Animated tile definition."""
779
+
780
+ def __init__(self, tile_id: int):
781
+ self.tile_id = tile_id # Which tile ID to animate
782
+ self.frames: List[int] = [] # List of tile IDs for animation frames
783
+ self.speed: float = 0.15 # Seconds per frame
784
+
785
+ def to_dict(self) -> Dict[str, Any]:
786
+ return {
787
+ "type": "game_tile_animation",
788
+ "tile_id": self.tile_id,
789
+ "frames": self.frames,
790
+ "speed": self.speed,
791
+ }
792
+
793
+ def validate(self) -> List[str]:
794
+ errors = []
795
+ if not self.frames:
796
+ errors.append("Tile animation must have at least one frame")
797
+ return errors
798
+
799
+
800
+ class TilemapNode(QuantumNode):
801
+ """Represents <qg:tilemap> - Tile-based map."""
802
+
803
+ def __init__(self, tilemap_id: str):
804
+ self.tilemap_id = tilemap_id
805
+ self.src: str = "" # tileset image or .tres resource
806
+ self.data_src: Optional[str] = None # External JSON file with grid data
807
+ self.tile_width: int = 32
808
+ self.tile_height: int = 32
809
+ self.layers: List['TilemapLayerNode'] = []
810
+ self.tile_animations: List['TileAnimationNode'] = [] # Animated tile definitions
811
+
812
+ def add_layer(self, layer: 'TilemapLayerNode'):
813
+ self.layers.append(layer)
814
+
815
+ def add_tile_animation(self, anim: 'TileAnimationNode'):
816
+ self.tile_animations.append(anim)
817
+
818
+ def to_dict(self) -> Dict[str, Any]:
819
+ return {
820
+ "type": "game_tilemap",
821
+ "id": self.tilemap_id,
822
+ "tile_width": self.tile_width,
823
+ "tile_height": self.tile_height,
824
+ "layers_count": len(self.layers),
825
+ }
826
+
827
+ def validate(self) -> List[str]:
828
+ errors = []
829
+ if not self.tilemap_id:
830
+ errors.append("Tilemap id is required")
831
+ for layer in self.layers:
832
+ errors.extend(layer.validate())
833
+ return errors
834
+
835
+
836
+ class TilemapLayerNode(QuantumNode):
837
+ """Represents <qg:layer> - A layer within a tilemap."""
838
+
839
+ def __init__(self, name: str):
840
+ self.name = name
841
+ self.data: str = "" # CSV tile data (empty when using data-src on parent)
842
+ self.collision: bool = False
843
+ self.external_data: bool = False # True when parent tilemap uses data-src
844
+
845
+ def to_dict(self) -> Dict[str, Any]:
846
+ return {
847
+ "type": "game_tilemap_layer",
848
+ "name": self.name,
849
+ "collision": self.collision,
850
+ }
851
+
852
+ def validate(self) -> List[str]:
853
+ errors = []
854
+ if not self.name:
855
+ errors.append("Tilemap layer name is required")
856
+ if not self.data and not self.external_data:
857
+ errors.append("Tilemap layer data is required")
858
+ return errors
859
+
860
+
861
+ # ============================================
862
+ # BEHAVIOR & ABSTRACTION NODES
863
+ # ============================================
864
+
865
+ class BehaviorNode(QuantumNode):
866
+ """Represents <qg:behavior> - Reusable behavior encapsulating state + functions."""
867
+
868
+ def __init__(self, name: str):
869
+ self.name = name
870
+ self.children: List[QuantumNode] = [] # q:set, q:function, qg:state-machine
871
+
872
+ def add_child(self, child: QuantumNode):
873
+ self.children.append(child)
874
+
875
+ def to_dict(self) -> Dict[str, Any]:
876
+ return {
877
+ "type": "game_behavior",
878
+ "name": self.name,
879
+ "children_count": len(self.children),
880
+ }
881
+
882
+ def validate(self) -> List[str]:
883
+ errors = []
884
+ if not self.name:
885
+ errors.append("Behavior name is required")
886
+ for child in self.children:
887
+ if hasattr(child, 'validate'):
888
+ errors.extend(child.validate())
889
+ return errors
890
+
891
+
892
+ class UseNode(QuantumNode):
893
+ """Represents <qg:use> - Attach a behavior to a sprite/group."""
894
+
895
+ def __init__(self, behavior: str):
896
+ self.behavior = behavior
897
+ self.overrides: Dict[str, str] = {}
898
+ self.on_collision: Optional[str] = None
899
+ self.collision_tag: Optional[str] = None
900
+ # Handlers past the first, from <qg:on-collision> children: a patrol
901
+ # that turns at a wall AND at a ledge needs two, and one attribute
902
+ # holds one.
903
+ self.extra_collisions: List[Dict[str, Optional[str]]] = []
904
+
905
+ def to_dict(self) -> Dict[str, Any]:
906
+ return {
907
+ "type": "game_use",
908
+ "behavior": self.behavior,
909
+ "overrides": self.overrides,
910
+ "on_collision": self.on_collision,
911
+ "collision_tag": self.collision_tag,
912
+ "extra_collisions": self.extra_collisions,
913
+ }
914
+
915
+ def validate(self) -> List[str]:
916
+ errors = []
917
+ if not self.behavior:
918
+ errors.append("Use behavior name is required")
919
+ return errors
920
+
921
+
922
+ class PrefabNode(QuantumNode):
923
+ """Represents <qg:prefab> - Reusable game object template with semantic attributes.
924
+
925
+ Semantic attributes allow declarative specification of game entity behavior
926
+ (combat, movement, rewards, etc.) without manual JavaScript. These attributes
927
+ are optional and default to None/False/0 for backward compatibility.
928
+ """
929
+
930
+ # Valid values for validated fields
931
+ VALID_ENTITY_TYPES = ('enemy', 'item', 'block', 'hazard', 'goal', 'projectile')
932
+ VALID_DEFEATED_BY = ('stomp', 'fire', 'star', 'shell', 'any')
933
+ VALID_MOVEMENTS = ('patrol', 'chase', 'fly-patrol', 'hop', 'path', 'none')
934
+ VALID_EFFECTS = ('grow', 'fire-power', 'invincible', 'extra-life', 'speed-up')
935
+ VALID_FACINGS = ('left', 'right')
936
+
937
+ def __init__(self, name: str):
938
+ self.name = name
939
+ self.children: List[QuantumNode] = [] # Typically one sprite with children
940
+
941
+ # --- Classification ---
942
+ self.entity_type: Optional[str] = None # enemy, item, block, hazard, goal, projectile
943
+
944
+ # --- Combat ---
945
+ self.health: Optional[int] = None # Hit points (e.g. 2 = dies after 2 stomps)
946
+ self.defeated_by: Optional[str] = None # stomp, fire, star, shell, any
947
+ self.on_defeat: Optional[str] = None # Event/action on defeat (e.g. "emit:enemy-killed")
948
+ self.contact_damage: Optional[int] = None # Damage dealt on contact
949
+ self.knockback: Optional[float] = None # Knockback force applied to player
950
+ self.i_frames: Optional[int] = None # Invincibility frames after hit
951
+ self.stomp_bounce: Optional[float] = None # Bounce force when stomped
952
+
953
+ # --- Movement ---
954
+ self.movement: Optional[str] = None # patrol, chase, fly-patrol, hop, path, none
955
+ self.move_speed: Optional[float] = None # Movement speed
956
+ self.turn_on_edge: Optional[bool] = None # Turn around at platform edges
957
+ self.facing: Optional[str] = None # Initial facing direction: left, right
958
+ self.turn_cooldown: Optional[float] = None # Delay before allowing next turn (anti-oscillation)
959
+
960
+ # --- Reward ---
961
+ self.reward: Optional[str] = None # Reward on collect/interact (e.g. "score:100")
962
+ self.reward_kill: Optional[str] = None # Reward on kill (e.g. "score:200")
963
+
964
+ # --- Respawn ---
965
+ self.respawns: Optional[bool] = None # Whether entity respawns after defeat
966
+ self.respawn_delay: Optional[float] = None # Seconds before respawn
967
+
968
+ # --- Item ---
969
+ self.effect: Optional[str] = None # grow, fire-power, invincible, extra-life, speed-up
970
+ self.duration: Optional[float] = None # Effect duration in seconds
971
+ self.auto_move: Optional[bool] = None # Item moves after spawning (e.g. mushroom)
972
+ self.collectible: Optional[bool] = None # Can be collected on contact
973
+ self.carryable: Optional[bool] = None # Can be picked up and carried
974
+
975
+ # --- Block ---
976
+ self.content: Optional[str] = None # What's inside: coin, mushroom, star, etc.
977
+ self.hits: Optional[int] = None # Hits before empty (e.g. 1 for ? block)
978
+ self.breakable: Optional[bool] = None # Can be broken (e.g. brick block)
979
+ self.invisible: Optional[bool] = None # Hidden until hit from below
980
+
981
+ # --- Checkpoint ---
982
+ self.checkpoint: Optional[bool] = None # Acts as a checkpoint (one-time activation)
983
+
984
+ # --- Flying ---
985
+ self.flying: Optional[bool] = None # Block with wings (oscillating movement)
986
+
987
+ def add_child(self, child: QuantumNode):
988
+ self.children.append(child)
989
+
990
+ def to_dict(self) -> Dict[str, Any]:
991
+ result = {
992
+ "type": "game_prefab",
993
+ "name": self.name,
994
+ "children_count": len(self.children),
995
+ }
996
+ # Include semantic attributes only when set (non-None)
997
+ if self.entity_type is not None:
998
+ result["entity_type"] = self.entity_type
999
+ if self.health is not None:
1000
+ result["health"] = self.health
1001
+ if self.defeated_by is not None:
1002
+ result["defeated_by"] = self.defeated_by
1003
+ if self.on_defeat is not None:
1004
+ result["on_defeat"] = self.on_defeat
1005
+ if self.contact_damage is not None:
1006
+ result["contact_damage"] = self.contact_damage
1007
+ if self.knockback is not None:
1008
+ result["knockback"] = self.knockback
1009
+ if self.i_frames is not None:
1010
+ result["i_frames"] = self.i_frames
1011
+ if self.stomp_bounce is not None:
1012
+ result["stomp_bounce"] = self.stomp_bounce
1013
+ if self.movement is not None:
1014
+ result["movement"] = self.movement
1015
+ if self.move_speed is not None:
1016
+ result["move_speed"] = self.move_speed
1017
+ if self.turn_on_edge is not None:
1018
+ result["turn_on_edge"] = self.turn_on_edge
1019
+ if self.facing is not None:
1020
+ result["facing"] = self.facing
1021
+ if self.turn_cooldown is not None:
1022
+ result["turn_cooldown"] = self.turn_cooldown
1023
+ if self.reward is not None:
1024
+ result["reward"] = self.reward
1025
+ if self.reward_kill is not None:
1026
+ result["reward_kill"] = self.reward_kill
1027
+ if self.respawns is not None:
1028
+ result["respawns"] = self.respawns
1029
+ if self.respawn_delay is not None:
1030
+ result["respawn_delay"] = self.respawn_delay
1031
+ if self.effect is not None:
1032
+ result["effect"] = self.effect
1033
+ if self.duration is not None:
1034
+ result["duration"] = self.duration
1035
+ if self.auto_move is not None:
1036
+ result["auto_move"] = self.auto_move
1037
+ if self.collectible is not None:
1038
+ result["collectible"] = self.collectible
1039
+ if self.carryable is not None:
1040
+ result["carryable"] = self.carryable
1041
+ if self.content is not None:
1042
+ result["content"] = self.content
1043
+ if self.hits is not None:
1044
+ result["hits"] = self.hits
1045
+ if self.breakable is not None:
1046
+ result["breakable"] = self.breakable
1047
+ if self.invisible is not None:
1048
+ result["invisible"] = self.invisible
1049
+ if self.checkpoint is not None:
1050
+ result["checkpoint"] = self.checkpoint
1051
+ if self.flying is not None:
1052
+ result["flying"] = self.flying
1053
+ return result
1054
+
1055
+ def validate(self) -> List[str]:
1056
+ errors = []
1057
+ if not self.name:
1058
+ errors.append("Prefab name is required")
1059
+ if not self.children:
1060
+ errors.append("Prefab must contain at least one child")
1061
+
1062
+ # Validate semantic attribute values
1063
+ if self.entity_type is not None and self.entity_type not in self.VALID_ENTITY_TYPES:
1064
+ errors.append(f"Invalid entity type: {self.entity_type}. Must be one of {', '.join(self.VALID_ENTITY_TYPES)}")
1065
+ if self.defeated_by is not None and self.defeated_by not in self.VALID_DEFEATED_BY:
1066
+ errors.append(f"Invalid defeated-by: {self.defeated_by}. Must be one of {', '.join(self.VALID_DEFEATED_BY)}")
1067
+ if self.movement is not None and self.movement not in self.VALID_MOVEMENTS:
1068
+ errors.append(f"Invalid movement: {self.movement}. Must be one of {', '.join(self.VALID_MOVEMENTS)}")
1069
+ if self.effect is not None and self.effect not in self.VALID_EFFECTS:
1070
+ errors.append(f"Invalid effect: {self.effect}. Must be one of {', '.join(self.VALID_EFFECTS)}")
1071
+ if self.facing is not None and self.facing not in self.VALID_FACINGS:
1072
+ errors.append(f"Invalid facing: {self.facing}. Must be one of {', '.join(self.VALID_FACINGS)}")
1073
+ if self.health is not None and self.health < 0:
1074
+ errors.append("Health must be non-negative")
1075
+ if self.hits is not None and self.hits < 0:
1076
+ errors.append("Hits must be non-negative")
1077
+
1078
+ for child in self.children:
1079
+ if hasattr(child, 'validate'):
1080
+ errors.extend(child.validate())
1081
+ return errors
1082
+
1083
+
1084
+ class InstanceNode(QuantumNode):
1085
+ """Represents <qg:instance> - Instantiate a prefab."""
1086
+
1087
+ def __init__(self, prefab: str):
1088
+ self.prefab = prefab
1089
+ self.instance_id: Optional[str] = None
1090
+ self.x: float = 0
1091
+ self.y: float = 0
1092
+ self.overrides: Dict[str, str] = {}
1093
+
1094
+ def to_dict(self) -> Dict[str, Any]:
1095
+ return {
1096
+ "type": "game_instance",
1097
+ "prefab": self.prefab,
1098
+ "id": self.instance_id,
1099
+ "x": self.x,
1100
+ "y": self.y,
1101
+ }
1102
+
1103
+ def validate(self) -> List[str]:
1104
+ errors = []
1105
+ if not self.prefab:
1106
+ errors.append("Instance prefab name is required")
1107
+ return errors
1108
+
1109
+
1110
+ # ============================================
1111
+ # ENEMY SYSTEM NODES
1112
+ # ============================================
1113
+
1114
+ VALID_AI_TYPES = ('patrol', 'chase', 'fly-patrol', 'hop', 'path', 'charge', 'emerge')
1115
+ VALID_DEFEAT_BY = ('stomp', 'fire', 'star', 'shell')
1116
+ VALID_HIT_EFFECTS = ('squish', 'shrink', 'flash')
1117
+ VALID_KILL_EFFECTS = ('puff', 'explode', 'fade')
1118
+
1119
+
1120
+ class EnemyOnHitNode(QuantumNode):
1121
+ """Represents <qg:on-hit> - What happens when enemy is hit (not killed)."""
1122
+
1123
+ def __init__(self):
1124
+ self.score: int = 0
1125
+ self.effect: Optional[str] = None
1126
+ self.speed_multiply: Optional[float] = None
1127
+
1128
+ def to_dict(self) -> Dict[str, Any]:
1129
+ return {
1130
+ "type": "game_enemy_on_hit",
1131
+ "score": self.score,
1132
+ "effect": self.effect,
1133
+ "speed_multiply": self.speed_multiply,
1134
+ }
1135
+
1136
+ def validate(self) -> List[str]:
1137
+ errors = []
1138
+ if self.effect is not None and self.effect not in VALID_HIT_EFFECTS:
1139
+ errors.append(f"Invalid hit effect: {self.effect}. Must be one of {', '.join(VALID_HIT_EFFECTS)}")
1140
+ return errors
1141
+
1142
+
1143
+ class EnemyOnKillNode(QuantumNode):
1144
+ """Represents <qg:on-kill> - What happens when enemy is killed."""
1145
+
1146
+ def __init__(self):
1147
+ self.score: int = 0
1148
+ self.effect: Optional[str] = None
1149
+ self.drop: Optional[str] = None
1150
+
1151
+ def to_dict(self) -> Dict[str, Any]:
1152
+ return {
1153
+ "type": "game_enemy_on_kill",
1154
+ "score": self.score,
1155
+ "effect": self.effect,
1156
+ "drop": self.drop,
1157
+ }
1158
+
1159
+ def validate(self) -> List[str]:
1160
+ errors = []
1161
+ if self.effect is not None and self.effect not in VALID_KILL_EFFECTS:
1162
+ errors.append(f"Invalid kill effect: {self.effect}. Must be one of {', '.join(VALID_KILL_EFFECTS)}")
1163
+ return errors
1164
+
1165
+
1166
+ class EnemyDefeatNode(QuantumNode):
1167
+ """Represents <qg:defeat> - How enemy can be defeated."""
1168
+
1169
+ def __init__(self):
1170
+ self.by: str = 'stomp'
1171
+ self.health: int = 1
1172
+ self.bounce: Optional[float] = None
1173
+ self.on_hit: Optional[EnemyOnHitNode] = None
1174
+ self.on_kill: Optional[EnemyOnKillNode] = None
1175
+
1176
+ def to_dict(self) -> Dict[str, Any]:
1177
+ return {
1178
+ "type": "game_enemy_defeat",
1179
+ "by": self.by,
1180
+ "health": self.health,
1181
+ "bounce": self.bounce,
1182
+ "on_hit": self.on_hit.to_dict() if self.on_hit else None,
1183
+ "on_kill": self.on_kill.to_dict() if self.on_kill else None,
1184
+ }
1185
+
1186
+ def validate(self) -> List[str]:
1187
+ errors = []
1188
+ if self.by not in VALID_DEFEAT_BY:
1189
+ errors.append(f"Invalid defeat by: {self.by}. Must be one of {', '.join(VALID_DEFEAT_BY)}")
1190
+ if self.health < 1:
1191
+ errors.append("Defeat health must be >= 1")
1192
+ if self.on_hit:
1193
+ errors.extend(self.on_hit.validate())
1194
+ if self.on_kill:
1195
+ errors.extend(self.on_kill.validate())
1196
+ return errors
1197
+
1198
+
1199
+ class EnemyAiNode(QuantumNode):
1200
+ """Represents <qg:ai> - Enemy AI behavior."""
1201
+
1202
+ def __init__(self):
1203
+ self.ai_type: str = 'patrol'
1204
+ self.speed: float = 30.0
1205
+ self.turn_cooldown: Optional[float] = None
1206
+ self.turn_on_edge: bool = False
1207
+ self.facing: str = 'left'
1208
+
1209
+ def to_dict(self) -> Dict[str, Any]:
1210
+ return {
1211
+ "type": "game_enemy_ai",
1212
+ "ai_type": self.ai_type,
1213
+ "speed": self.speed,
1214
+ "turn_cooldown": self.turn_cooldown,
1215
+ "turn_on_edge": self.turn_on_edge,
1216
+ "facing": self.facing,
1217
+ }
1218
+
1219
+ def validate(self) -> List[str]:
1220
+ errors = []
1221
+ if self.ai_type not in VALID_AI_TYPES:
1222
+ errors.append(f"Invalid AI type: {self.ai_type}. Must be one of {', '.join(VALID_AI_TYPES)}")
1223
+ if self.speed < 0:
1224
+ errors.append("AI speed must be non-negative")
1225
+ if self.facing not in ('left', 'right'):
1226
+ errors.append(f"Invalid facing: {self.facing}. Must be 'left' or 'right'")
1227
+ return errors
1228
+
1229
+
1230
+ class EnemyNode(QuantumNode):
1231
+ """Represents <qg:enemy> - Enemy behavior definition (AI + defeat + rewards).
1232
+
1233
+ Separates behavior from visual (prefab) and placement (instance).
1234
+ """
1235
+
1236
+ def __init__(self, prefab: str):
1237
+ self.prefab = prefab
1238
+ self.ai: Optional[EnemyAiNode] = None
1239
+ self.defeat: Optional[EnemyDefeatNode] = None
1240
+
1241
+ def to_dict(self) -> Dict[str, Any]:
1242
+ return {
1243
+ "type": "game_enemy",
1244
+ "prefab": self.prefab,
1245
+ "ai": self.ai.to_dict() if self.ai else None,
1246
+ "defeat": self.defeat.to_dict() if self.defeat else None,
1247
+ }
1248
+
1249
+ def validate(self) -> List[str]:
1250
+ errors = []
1251
+ if not self.prefab:
1252
+ errors.append("Enemy prefab name is required")
1253
+ if self.ai:
1254
+ errors.extend(self.ai.validate())
1255
+ if self.defeat:
1256
+ errors.extend(self.defeat.validate())
1257
+ return errors
1258
+
1259
+
1260
+ class GroupNode(QuantumNode):
1261
+ """Represents <qg:group> - Group sprites with shared behavior."""
1262
+
1263
+ def __init__(self, name: str):
1264
+ self.name = name
1265
+ self.tag: Optional[str] = None
1266
+ self.children: List[QuantumNode] = []
1267
+
1268
+ def add_child(self, child: QuantumNode):
1269
+ self.children.append(child)
1270
+
1271
+ def to_dict(self) -> Dict[str, Any]:
1272
+ return {
1273
+ "type": "game_group",
1274
+ "name": self.name,
1275
+ "tag": self.tag,
1276
+ "children_count": len(self.children),
1277
+ }
1278
+
1279
+ def validate(self) -> List[str]:
1280
+ errors = []
1281
+ if not self.name:
1282
+ errors.append("Group name is required")
1283
+ for child in self.children:
1284
+ if hasattr(child, 'validate'):
1285
+ errors.extend(child.validate())
1286
+ return errors
1287
+
1288
+
1289
+ # ============================================
1290
+ # STATE MACHINE NODES
1291
+ # ============================================
1292
+
1293
+ class StateMachineNode(QuantumNode):
1294
+ """Represents <qg:state-machine> - Declarative FSM."""
1295
+
1296
+ def __init__(self, initial: str):
1297
+ self.initial = initial
1298
+ self.states: List['StateNode'] = []
1299
+
1300
+ def add_state(self, state: 'StateNode'):
1301
+ self.states.append(state)
1302
+
1303
+ def to_dict(self) -> Dict[str, Any]:
1304
+ return {
1305
+ "type": "game_state_machine",
1306
+ "initial": self.initial,
1307
+ "states_count": len(self.states),
1308
+ }
1309
+
1310
+ def validate(self) -> List[str]:
1311
+ errors = []
1312
+ if not self.initial:
1313
+ errors.append("State machine initial state is required")
1314
+ if not self.states:
1315
+ errors.append("State machine must have at least one state")
1316
+ state_names = [s.name for s in self.states]
1317
+ if self.initial not in state_names:
1318
+ errors.append(f"Initial state '{self.initial}' not found in states")
1319
+ for state in self.states:
1320
+ errors.extend(state.validate())
1321
+ return errors
1322
+
1323
+
1324
+ class StateNode(QuantumNode):
1325
+ """Represents <qg:state> - Individual state in a state machine."""
1326
+
1327
+ def __init__(self, name: str):
1328
+ self.name = name
1329
+ self.transitions: List['TransitionNode'] = []
1330
+ self.children: List[QuantumNode] = [] # q:function nodes (enter, update, exit)
1331
+
1332
+ def add_transition(self, transition: 'TransitionNode'):
1333
+ self.transitions.append(transition)
1334
+
1335
+ def add_child(self, child: QuantumNode):
1336
+ self.children.append(child)
1337
+
1338
+ def to_dict(self) -> Dict[str, Any]:
1339
+ return {
1340
+ "type": "game_state",
1341
+ "name": self.name,
1342
+ "transitions_count": len(self.transitions),
1343
+ "children_count": len(self.children),
1344
+ }
1345
+
1346
+ def validate(self) -> List[str]:
1347
+ errors = []
1348
+ if not self.name:
1349
+ errors.append("State name is required")
1350
+ for t in self.transitions:
1351
+ errors.extend(t.validate())
1352
+ for child in self.children:
1353
+ if hasattr(child, 'validate'):
1354
+ errors.extend(child.validate())
1355
+ return errors
1356
+
1357
+
1358
+ class RawCodeNode(QuantumNode):
1359
+ """Represents raw JavaScript code inside a q:function in game context.
1360
+
1361
+ Allows free-form JS text (e.g. game.destroy(self)) alongside XML-parsed nodes.
1362
+ """
1363
+
1364
+ def __init__(self, code: str):
1365
+ self.code = code
1366
+
1367
+ def to_dict(self) -> Dict[str, Any]:
1368
+ return {
1369
+ "type": "raw_code",
1370
+ "code": self.code,
1371
+ }
1372
+
1373
+ def validate(self) -> List[str]:
1374
+ errors = []
1375
+ if not self.code or not self.code.strip():
1376
+ errors.append("RawCodeNode code is empty")
1377
+ return errors
1378
+
1379
+
1380
+ class ClickableNode(QuantumNode):
1381
+ """Represents <qg:clickable> - Makes a sprite respond to mouse/pointer events."""
1382
+
1383
+ def __init__(self):
1384
+ self.action: str = ""
1385
+ self.cursor: str = "pointer"
1386
+
1387
+ def to_dict(self) -> Dict[str, Any]:
1388
+ return {
1389
+ "type": "game_clickable",
1390
+ "action": self.action,
1391
+ "cursor": self.cursor,
1392
+ }
1393
+
1394
+ def validate(self) -> List[str]:
1395
+ errors = []
1396
+ if not self.action:
1397
+ errors.append("Clickable action is required")
1398
+ return errors
1399
+
1400
+
1401
+ class TransitionNode(QuantumNode):
1402
+ """Represents <qg:on> - State transition triggered by event."""
1403
+
1404
+ def __init__(self, event: str, transition: str):
1405
+ self.event = event
1406
+ self.transition = transition
1407
+
1408
+ def to_dict(self) -> Dict[str, Any]:
1409
+ return {
1410
+ "type": "game_transition",
1411
+ "event": self.event,
1412
+ "transition": self.transition,
1413
+ }
1414
+
1415
+ def validate(self) -> List[str]:
1416
+ errors = []
1417
+ if not self.event:
1418
+ errors.append("Transition event is required")
1419
+ if not self.transition:
1420
+ errors.append("Transition target state is required")
1421
+ return errors
1422
+
1423
+
1424
+ class EventNode(QuantumNode):
1425
+ """Represents <qg:event> - Declarative event listener.
1426
+
1427
+ Allows registering event handlers at scene level:
1428
+ <qg:event name="coinCollected" handler="onCoinCollected" />
1429
+ <qg:event name="playerDied" handler="onPlayerDied" filter-tag="player" />
1430
+
1431
+ Attributes:
1432
+ name: Event name to listen for (e.g., "coinCollected", "player.jump")
1433
+ handler: Function name to call when event fires
1434
+ filter_tag: Optional tag filter - only handle events from sprites with this tag
1435
+ filter_id: Optional sprite ID filter - only handle events from this specific sprite
1436
+ scope: Event scope - "scene" (default), "sprite", or "global"
1437
+ """
1438
+
1439
+ def __init__(self, name: str, handler: str):
1440
+ self.name = name
1441
+ self.handler = handler
1442
+ self.filter_tag: Optional[str] = None
1443
+ self.filter_id: Optional[str] = None
1444
+ self.scope: str = "scene"
1445
+ self.children: List[QuantumNode] = [] # inline children (e.g., SceneTransitionNode)
1446
+
1447
+ def add_child(self, child: QuantumNode):
1448
+ self.children.append(child)
1449
+
1450
+ def to_dict(self) -> Dict[str, Any]:
1451
+ return {
1452
+ "type": "game_event",
1453
+ "name": self.name,
1454
+ "handler": self.handler,
1455
+ "filter_tag": self.filter_tag,
1456
+ "filter_id": self.filter_id,
1457
+ "scope": self.scope,
1458
+ "children_count": len(self.children),
1459
+ }
1460
+
1461
+ def validate(self) -> List[str]:
1462
+ errors = []
1463
+ if not self.name:
1464
+ errors.append("Event name is required")
1465
+ if not self.handler and not self.children:
1466
+ errors.append("Event must have a handler or inline children")
1467
+ if self.scope not in ("scene", "sprite", "global"):
1468
+ errors.append(f"Invalid event scope: {self.scope}. Must be scene, sprite, or global")
1469
+ for child in self.children:
1470
+ if hasattr(child, 'validate'):
1471
+ errors.extend(child.validate())
1472
+ return errors
1473
+
1474
+
1475
+ # ============================================
1476
+ # MULTI-SCENE NODES
1477
+ # ============================================
1478
+
1479
+ class SceneTransitionNode(QuantumNode):
1480
+ """Represents <qg:transition> - Transition between game scenes.
1481
+
1482
+ Used inside <qg:event>, <q:function>, or <qg:hud-behavior> to trigger
1483
+ a scene change with an optional visual effect.
1484
+
1485
+ Attributes:
1486
+ to: Target scene name
1487
+ effect: Transition effect (fade, iris-out, iris-in, cut)
1488
+ duration: Effect duration in seconds
1489
+ data: Optional dictionary of data to pass to target scene
1490
+ """
1491
+
1492
+ VALID_EFFECTS = ('fade', 'iris-out', 'iris-in', 'cut')
1493
+
1494
+ def __init__(self, to: str):
1495
+ self.to = to
1496
+ self.effect: str = "fade"
1497
+ self.duration: float = 0.5
1498
+ self.data: Optional[Dict[str, str]] = None
1499
+
1500
+ def to_dict(self) -> Dict[str, Any]:
1501
+ result = {
1502
+ "type": "game_scene_transition",
1503
+ "to": self.to,
1504
+ "effect": self.effect,
1505
+ "duration": self.duration,
1506
+ }
1507
+ if self.data:
1508
+ result["data"] = self.data
1509
+ return result
1510
+
1511
+ def validate(self) -> List[str]:
1512
+ errors = []
1513
+ if not self.to:
1514
+ errors.append("Scene transition target (to) is required")
1515
+ if self.effect not in self.VALID_EFFECTS:
1516
+ errors.append(f"Invalid transition effect: {self.effect}. Must be one of {', '.join(self.VALID_EFFECTS)}")
1517
+ if self.duration < 0:
1518
+ errors.append("Transition duration must be non-negative")
1519
+ return errors
1520
+
1521
+
1522
+ class PersistentNode(QuantumNode):
1523
+ """Represents <qg:persistent> - State that survives scene changes.
1524
+
1525
+ Wraps SetNode children that should persist across scene transitions.
1526
+ These become part of SceneManager's persistent_state dictionary.
1527
+
1528
+ Example:
1529
+ <qg:persistent>
1530
+ <q:set name="lives" value="5" />
1531
+ <q:set name="score" value="0" />
1532
+ </qg:persistent>
1533
+ """
1534
+
1535
+ def __init__(self):
1536
+ self.children: List[QuantumNode] = []
1537
+
1538
+ def add_child(self, child: QuantumNode):
1539
+ self.children.append(child)
1540
+
1541
+ def to_dict(self) -> Dict[str, Any]:
1542
+ return {
1543
+ "type": "game_persistent",
1544
+ "children_count": len(self.children),
1545
+ }
1546
+
1547
+ def validate(self) -> List[str]:
1548
+ errors = []
1549
+ if not self.children:
1550
+ errors.append("Persistent block must contain at least one child")
1551
+ for child in self.children:
1552
+ if hasattr(child, 'validate'):
1553
+ errors.extend(child.validate())
1554
+ return errors
1555
+
1556
+
1557
+ class MapNodeDef(QuantumNode):
1558
+ """Represents <qg:map-node> - A point on the world map.
1559
+
1560
+ Used inside <qg:scene type="map"> to define navigable nodes
1561
+ that the player can move between (SMW-style world map).
1562
+
1563
+ Attributes:
1564
+ id: Unique identifier for this map node
1565
+ x: X position on the map
1566
+ y: Y position on the map
1567
+ scene: Scene name to load when entering this node (None = decorative)
1568
+ locked: Whether this node starts locked
1569
+ icon: Optional sprite for the node marker
1570
+ """
1571
+
1572
+ def __init__(self, node_id: str):
1573
+ self.node_id = node_id
1574
+ self.x: float = 0
1575
+ self.y: float = 0
1576
+ self.scene: Optional[str] = None
1577
+ self.locked: bool = False
1578
+ self.icon: Optional[str] = None
1579
+
1580
+ def to_dict(self) -> Dict[str, Any]:
1581
+ result = {
1582
+ "type": "game_map_node",
1583
+ "id": self.node_id,
1584
+ "x": self.x,
1585
+ "y": self.y,
1586
+ }
1587
+ if self.scene:
1588
+ result["scene"] = self.scene
1589
+ if self.locked:
1590
+ result["locked"] = True
1591
+ if self.icon:
1592
+ result["icon"] = self.icon
1593
+ return result
1594
+
1595
+ def validate(self) -> List[str]:
1596
+ errors = []
1597
+ if not self.node_id:
1598
+ errors.append("Map node id is required")
1599
+ return errors
1600
+
1601
+
1602
+ class MapPathNode(QuantumNode):
1603
+ """Represents <qg:map-path> - A path between two map nodes.
1604
+
1605
+ Defines connectivity in the world map graph. Mario can only
1606
+ move between connected nodes via defined paths.
1607
+
1608
+ Attributes:
1609
+ from_node: Source node ID
1610
+ to_node: Destination node ID
1611
+ unlock: Condition to unlock this path (e.g., "level-1-cleared")
1612
+ direction: Visual hint for path direction (right, up, etc.)
1613
+ """
1614
+
1615
+ def __init__(self, from_node: str, to_node: str):
1616
+ self.from_node = from_node
1617
+ self.to_node = to_node
1618
+ self.unlock: Optional[str] = None
1619
+ self.direction: Optional[str] = None
1620
+
1621
+ def to_dict(self) -> Dict[str, Any]:
1622
+ result = {
1623
+ "type": "game_map_path",
1624
+ "from": self.from_node,
1625
+ "to": self.to_node,
1626
+ }
1627
+ if self.unlock:
1628
+ result["unlock"] = self.unlock
1629
+ if self.direction:
1630
+ result["direction"] = self.direction
1631
+ return result
1632
+
1633
+ def validate(self) -> List[str]:
1634
+ errors = []
1635
+ if not self.from_node:
1636
+ errors.append("Map path from_node is required")
1637
+ if not self.to_node:
1638
+ errors.append("Map path to_node is required")
1639
+ return errors
1640
+
1641
+
1642
+ # ============================================
1643
+ # EVENT ACTION NODES
1644
+ # ============================================
1645
+
1646
+ class EventActionNode(QuantumNode):
1647
+ """Represents <qg:action> inside <qg:event> - Inline event action.
1648
+
1649
+ Generates a single line of GDScript inside an event handler:
1650
+ type="score" amount="10" -> score += 10
1651
+ type="destroy" target="{data.other}" -> data.other.queue_free()
1652
+ type="sound" target="sfx-coin" -> QuantumBridge.play_sound("sfx-coin")
1653
+ type="camera-shake" value="3,0.15" -> camera.shake(3.0, 0.15)
1654
+ type="set" target="coins" value="{coins+1}" -> coins += 1
1655
+ type="emit" value="coin-collected" -> QuantumEventBus.emit_event("coin-collected", {})
1656
+ type="spawn" target="puff" value="x,y" -> spawn puff particle
1657
+ """
1658
+
1659
+ VALID_ACTION_TYPES = ('score', 'destroy', 'sound', 'set', 'camera-shake', 'emit', 'spawn')
1660
+
1661
+ def __init__(self):
1662
+ self.action_type: str = "" # score, destroy, sound, set, camera-shake, emit, spawn
1663
+ self.target: Optional[str] = None
1664
+ self.value: Optional[str] = None
1665
+ self.amount: Optional[int] = None
1666
+
1667
+ def to_dict(self) -> Dict[str, Any]:
1668
+ result = {
1669
+ "type": "event_action",
1670
+ "action_type": self.action_type,
1671
+ }
1672
+ if self.target is not None:
1673
+ result["target"] = self.target
1674
+ if self.value is not None:
1675
+ result["value"] = self.value
1676
+ if self.amount is not None:
1677
+ result["amount"] = self.amount
1678
+ return result
1679
+
1680
+ def validate(self) -> List[str]:
1681
+ errors = []
1682
+ if not self.action_type:
1683
+ errors.append("Event action type is required")
1684
+ if self.action_type and self.action_type not in self.VALID_ACTION_TYPES:
1685
+ errors.append(f"Invalid event action type: {self.action_type}. Must be one of {', '.join(self.VALID_ACTION_TYPES)}")
1686
+ return errors
1687
+
1688
+
1689
+ # ============================================
1690
+ # COLLISION LAYER DEFINITIONS
1691
+ # ============================================
1692
+
1693
+ class CollisionLayerDef(QuantumNode):
1694
+ """Represents <qg:collision-layer> inside <qg:physics> - Named collision layer.
1695
+
1696
+ Maps a human-readable name to a Godot collision layer bitmask value.
1697
+ Example:
1698
+ <qg:collision-layer id="1" name="world" />
1699
+ <qg:collision-layer id="2" name="player" />
1700
+ """
1701
+
1702
+ def __init__(self, layer_id: int, name: str):
1703
+ self.layer_id = layer_id
1704
+ self.name = name
1705
+
1706
+ def to_dict(self) -> Dict[str, Any]:
1707
+ return {
1708
+ "type": "collision_layer_def",
1709
+ "layer_id": self.layer_id,
1710
+ "name": self.name,
1711
+ }
1712
+
1713
+ def validate(self) -> List[str]:
1714
+ errors = []
1715
+ if self.layer_id < 1 or self.layer_id > 32:
1716
+ errors.append(f"Collision layer id must be between 1 and 32, got {self.layer_id}")
1717
+ if not self.name:
1718
+ errors.append("Collision layer name is required")
1719
+ return errors