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