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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- 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
|