quantum-framework 0.18.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/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -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 +548 -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 +565 -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 +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -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_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -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 +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -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/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -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 +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -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 +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -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 +79 -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 +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -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/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 +164 -0
- quantum/core/xml_lines.py +81 -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 +25 -0
- quantum/runtime/action_handler.py +532 -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/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -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 +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -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 +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -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 +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -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/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -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/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1449 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Godot 4 - GDScript/TSCN Templates and Builders
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- GdScriptBuilder: Structured GDScript code emitter with proper indentation
|
|
6
|
+
- TscnBuilder: Godot .tscn scene file builder (text-based format)
|
|
7
|
+
- GDScript templates for runtime scripts (bridge, event bus, player controller, etc.)
|
|
8
|
+
- Scale constants for Quantum → Godot unit conversion
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ==========================================================================
|
|
17
|
+
# SNES Hardware Constants (Super Nintendo Entertainment System)
|
|
18
|
+
# ==========================================================================
|
|
19
|
+
# Source: SNES dev wiki, SMW disassembly, TASVideos
|
|
20
|
+
# Resolution: 256×224 (Mode 1, NTSC)
|
|
21
|
+
# Tile size: 16×16 px (BG), sprites 8/16/32/64
|
|
22
|
+
# Framerate: 60 fps (NTSC)
|
|
23
|
+
# Subpixel resolution: 1/16 pixel (4-bit fractional)
|
|
24
|
+
|
|
25
|
+
SNES_WIDTH = 256
|
|
26
|
+
SNES_HEIGHT = 224
|
|
27
|
+
SNES_TILE = 16
|
|
28
|
+
SNES_FPS = 60
|
|
29
|
+
SNES_WINDOW_SCALE = 3 # 3x upscale → 768×672 window
|
|
30
|
+
|
|
31
|
+
# ==========================================================================
|
|
32
|
+
# SMW Physics Constants (for Godot: px/s and px/s² at 60fps)
|
|
33
|
+
# ==========================================================================
|
|
34
|
+
# Derived from SMW disassembly + frame analysis:
|
|
35
|
+
# Walk max: ~21 subpx/frame = 1.3 px/frame → 78.75 px/s
|
|
36
|
+
# Run max: ~37 subpx/frame = 2.3 px/frame → 138.75 px/s
|
|
37
|
+
# Sprint: ~49 subpx/frame = 3.1 px/frame → 183.75 px/s
|
|
38
|
+
# Max fall: ~64 subpx/frame = 4.0 px/frame → 240 px/s
|
|
39
|
+
# Jump initial: ~-5 px/frame → -300 px/s
|
|
40
|
+
# Gravity rise (A held): ~0.19 px/frame² → 675 px/s²
|
|
41
|
+
# Gravity fall: ~0.44 px/frame² → 1575 px/s²
|
|
42
|
+
#
|
|
43
|
+
# Tuned values (balanced for playability in Godot):
|
|
44
|
+
|
|
45
|
+
SMW_WALK_SPEED = 90.0 # px/s (1.5 px/frame)
|
|
46
|
+
SMW_JUMP_VELOCITY = -280.0 # px/s (initial upward, negative = up)
|
|
47
|
+
SMW_GRAVITY = 750.0 # px/s² (project default)
|
|
48
|
+
SMW_GRAVITY_RISING = 600.0 # px/s² (holding jump, lighter feel)
|
|
49
|
+
SMW_GRAVITY_FALLING = 1050.0 # px/s² (falling, snappy)
|
|
50
|
+
SMW_MAX_FALL = 240.0 # px/s (4 px/frame terminal velocity)
|
|
51
|
+
SMW_JUMP_HOLD_BOOST = 50.0 # px/s² (extra lift while holding jump)
|
|
52
|
+
SMW_COYOTE_FRAMES = 6 # frames of grace after leaving edge
|
|
53
|
+
|
|
54
|
+
# Validation: jump height = v² / (2 * g_rise) = 280² / (2*600) = 65px ≈ 4 tiles ✓
|
|
55
|
+
# With hold boost over 0.4s rise: ~80px ≈ 5 tiles ✓ (SMW standard)
|
|
56
|
+
|
|
57
|
+
# ==========================================================================
|
|
58
|
+
# Legacy scale helpers (kept for backward compat with tests)
|
|
59
|
+
# ==========================================================================
|
|
60
|
+
|
|
61
|
+
QUANTUM_SCALE = 100 # Legacy — NOT used for positions or SNES physics
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def q2g(value: float) -> float:
|
|
65
|
+
"""Convert Quantum abstract unit to Godot value (legacy)."""
|
|
66
|
+
return value * QUANTUM_SCALE
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def q2g_int(value: float) -> int:
|
|
70
|
+
"""Convert Quantum abstract unit to integer (legacy)."""
|
|
71
|
+
return int(value * QUANTUM_SCALE)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def q2g_gravity(value: float) -> float:
|
|
75
|
+
"""Convert Quantum gravity to Godot gravity (legacy)."""
|
|
76
|
+
return value * QUANTUM_SCALE
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def q2g_speed(value: float) -> float:
|
|
80
|
+
"""Convert Quantum speed to Godot speed (legacy)."""
|
|
81
|
+
return value * QUANTUM_SCALE
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def q2g_jump(value: float) -> float:
|
|
85
|
+
"""Convert Quantum jump force to Godot velocity (legacy)."""
|
|
86
|
+
return -(value * QUANTUM_SCALE * 0.6)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ==========================================================================
|
|
90
|
+
# GdScriptBuilder - Structured GDScript code emitter
|
|
91
|
+
# ==========================================================================
|
|
92
|
+
|
|
93
|
+
class GdScriptBuilder:
|
|
94
|
+
"""Builds syntactically valid GDScript code with proper indentation.
|
|
95
|
+
|
|
96
|
+
Every append method returns self for chaining.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, indent_char: str = '\t'):
|
|
100
|
+
self._lines: list[str] = []
|
|
101
|
+
self._indent: int = 0
|
|
102
|
+
self._indent_char: str = indent_char
|
|
103
|
+
|
|
104
|
+
# -- core --
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def _pad(self) -> str:
|
|
108
|
+
return self._indent_char * self._indent
|
|
109
|
+
|
|
110
|
+
def line(self, code: str = '') -> 'GdScriptBuilder':
|
|
111
|
+
"""Emit a single line at current indent."""
|
|
112
|
+
self._lines.append(f"{self._pad}{code}" if code else '')
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def raw(self, code: str) -> 'GdScriptBuilder':
|
|
116
|
+
"""Emit raw code preserving its own indentation at current base indent."""
|
|
117
|
+
for ln in code.split('\n'):
|
|
118
|
+
self._lines.append(f"{self._pad}{ln}")
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def raw_unindented(self, code: str) -> 'GdScriptBuilder':
|
|
122
|
+
"""Emit raw code without adding base indentation."""
|
|
123
|
+
for ln in code.split('\n'):
|
|
124
|
+
self._lines.append(ln)
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def blank(self) -> 'GdScriptBuilder':
|
|
128
|
+
self._lines.append('')
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def comment(self, text: str) -> 'GdScriptBuilder':
|
|
132
|
+
return self.line(f"# {text}")
|
|
133
|
+
|
|
134
|
+
def section(self, title: str) -> 'GdScriptBuilder':
|
|
135
|
+
return self.blank().line(f"# === {title} ===")
|
|
136
|
+
|
|
137
|
+
def indent(self) -> 'GdScriptBuilder':
|
|
138
|
+
self._indent += 1
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def dedent(self) -> 'GdScriptBuilder':
|
|
142
|
+
self._indent = max(0, self._indent - 1)
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
# -- GDScript specific --
|
|
146
|
+
|
|
147
|
+
def extends(self, base_class: str) -> 'GdScriptBuilder':
|
|
148
|
+
return self.line(f"extends {base_class}")
|
|
149
|
+
|
|
150
|
+
def class_name(self, name: str) -> 'GdScriptBuilder':
|
|
151
|
+
return self.line(f"class_name {name}")
|
|
152
|
+
|
|
153
|
+
def var(self, name: str, value: str = '', type_hint: str = '') -> 'GdScriptBuilder':
|
|
154
|
+
if type_hint and value:
|
|
155
|
+
return self.line(f"var {name}: {type_hint} = {value}")
|
|
156
|
+
elif type_hint:
|
|
157
|
+
return self.line(f"var {name}: {type_hint}")
|
|
158
|
+
elif value:
|
|
159
|
+
return self.line(f"var {name} = {value}")
|
|
160
|
+
else:
|
|
161
|
+
return self.line(f"var {name}")
|
|
162
|
+
|
|
163
|
+
def const(self, name: str, value: str) -> 'GdScriptBuilder':
|
|
164
|
+
return self.line(f"const {name} = {value}")
|
|
165
|
+
|
|
166
|
+
def assign(self, target: str, value: str) -> 'GdScriptBuilder':
|
|
167
|
+
return self.line(f"{target} = {value}")
|
|
168
|
+
|
|
169
|
+
def export_var(self, name: str, type_hint: str, value: str = '') -> 'GdScriptBuilder':
|
|
170
|
+
if value:
|
|
171
|
+
return self.line(f"@export var {name}: {type_hint} = {value}")
|
|
172
|
+
return self.line(f"@export var {name}: {type_hint}")
|
|
173
|
+
|
|
174
|
+
def onready_var(self, name: str, path: str) -> 'GdScriptBuilder':
|
|
175
|
+
return self.line(f'@onready var {name} = ${path}')
|
|
176
|
+
|
|
177
|
+
def signal_decl(self, name: str, params: str = '') -> 'GdScriptBuilder':
|
|
178
|
+
if params:
|
|
179
|
+
return self.line(f"signal {name}({params})")
|
|
180
|
+
return self.line(f"signal {name}")
|
|
181
|
+
|
|
182
|
+
def preload(self, name: str, path: str) -> 'GdScriptBuilder':
|
|
183
|
+
return self.line(f'var {name} = preload("{path}")')
|
|
184
|
+
|
|
185
|
+
# -- blocks --
|
|
186
|
+
|
|
187
|
+
def func(self, name: str, params: str = '', return_type: str = '') -> 'GdScriptBuilder':
|
|
188
|
+
"""Open a function block."""
|
|
189
|
+
sig = f"func {name}({params})"
|
|
190
|
+
if return_type:
|
|
191
|
+
sig += f" -> {return_type}"
|
|
192
|
+
sig += ":"
|
|
193
|
+
self.line(sig)
|
|
194
|
+
self._indent += 1
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
def func_close(self) -> 'GdScriptBuilder':
|
|
198
|
+
"""Close a function block (just dedent in GDScript)."""
|
|
199
|
+
self._indent = max(0, self._indent - 1)
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def if_block(self, condition: str) -> 'GdScriptBuilder':
|
|
203
|
+
self.line(f"if {condition}:")
|
|
204
|
+
self._indent += 1
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def elif_block(self, condition: str) -> 'GdScriptBuilder':
|
|
208
|
+
self._indent = max(0, self._indent - 1)
|
|
209
|
+
self.line(f"elif {condition}:")
|
|
210
|
+
self._indent += 1
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def else_block(self) -> 'GdScriptBuilder':
|
|
214
|
+
self._indent = max(0, self._indent - 1)
|
|
215
|
+
self.line("else:")
|
|
216
|
+
self._indent += 1
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def for_block(self, var: str, iterable: str) -> 'GdScriptBuilder':
|
|
220
|
+
self.line(f"for {var} in {iterable}:")
|
|
221
|
+
self._indent += 1
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def while_block(self, condition: str) -> 'GdScriptBuilder':
|
|
225
|
+
self.line(f"while {condition}:")
|
|
226
|
+
self._indent += 1
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
def match_block(self, value: str) -> 'GdScriptBuilder':
|
|
230
|
+
self.line(f"match {value}:")
|
|
231
|
+
self._indent += 1
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
def match_case(self, pattern: str) -> 'GdScriptBuilder':
|
|
235
|
+
self.line(f"{pattern}:")
|
|
236
|
+
self._indent += 1
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
def block_close(self) -> 'GdScriptBuilder':
|
|
240
|
+
"""Generic block close (dedent)."""
|
|
241
|
+
self._indent = max(0, self._indent - 1)
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def pass_stmt(self) -> 'GdScriptBuilder':
|
|
245
|
+
return self.line("pass")
|
|
246
|
+
|
|
247
|
+
def return_stmt(self, value: str = '') -> 'GdScriptBuilder':
|
|
248
|
+
if value:
|
|
249
|
+
return self.line(f"return {value}")
|
|
250
|
+
return self.line("return")
|
|
251
|
+
|
|
252
|
+
def build(self) -> str:
|
|
253
|
+
"""Return the complete GDScript source."""
|
|
254
|
+
return '\n'.join(self._lines) + '\n'
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ==========================================================================
|
|
258
|
+
# TscnBuilder - Godot .tscn scene file builder
|
|
259
|
+
# ==========================================================================
|
|
260
|
+
|
|
261
|
+
class TscnBuilder:
|
|
262
|
+
"""Builds Godot .tscn (text scene) files programmatically.
|
|
263
|
+
|
|
264
|
+
TSCN format:
|
|
265
|
+
[gd_scene load_steps=N format=3 uid="uid://..."]
|
|
266
|
+
[ext_resource type="..." path="..." id="1"]
|
|
267
|
+
[sub_resource type="..." id="sub_1"]
|
|
268
|
+
[node name="Root" type="Node2D"]
|
|
269
|
+
[node name="Child" type="Sprite2D" parent="."]
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self):
|
|
273
|
+
self._ext_resources: List[Dict[str, str]] = []
|
|
274
|
+
self._sub_resources: List[Dict[str, Any]] = []
|
|
275
|
+
self._nodes: List[Dict[str, Any]] = []
|
|
276
|
+
self._ext_id_counter: int = 0
|
|
277
|
+
self._sub_id_counter: int = 0
|
|
278
|
+
self._ext_resource_cache: Dict[str, str] = {} # path → id
|
|
279
|
+
|
|
280
|
+
def add_ext_resource(self, res_type: str, path: str) -> str:
|
|
281
|
+
"""Add an external resource. Returns the resource ID string."""
|
|
282
|
+
if path in self._ext_resource_cache:
|
|
283
|
+
return self._ext_resource_cache[path]
|
|
284
|
+
self._ext_id_counter += 1
|
|
285
|
+
rid = str(self._ext_id_counter)
|
|
286
|
+
self._ext_resources.append({
|
|
287
|
+
'type': res_type,
|
|
288
|
+
'path': path,
|
|
289
|
+
'id': rid,
|
|
290
|
+
})
|
|
291
|
+
self._ext_resource_cache[path] = rid
|
|
292
|
+
return rid
|
|
293
|
+
|
|
294
|
+
def add_sub_resource(self, res_type: str, properties: Dict[str, Any] = None) -> str:
|
|
295
|
+
"""Add a sub-resource. Returns the sub-resource ID string."""
|
|
296
|
+
self._sub_id_counter += 1
|
|
297
|
+
sid = f"SubResource(\"{self._sub_id_counter}\")"
|
|
298
|
+
self._sub_resources.append({
|
|
299
|
+
'type': res_type,
|
|
300
|
+
'id': str(self._sub_id_counter),
|
|
301
|
+
'properties': properties or {},
|
|
302
|
+
})
|
|
303
|
+
return sid
|
|
304
|
+
|
|
305
|
+
def add_node(self, name: str, node_type: str, parent: Optional[str] = None,
|
|
306
|
+
properties: Dict[str, Any] = None, groups: List[str] = None,
|
|
307
|
+
instance: Optional[str] = None) -> 'TscnBuilder':
|
|
308
|
+
"""Add a scene node."""
|
|
309
|
+
self._nodes.append({
|
|
310
|
+
'name': name,
|
|
311
|
+
'type': node_type,
|
|
312
|
+
'parent': parent,
|
|
313
|
+
'properties': properties or {},
|
|
314
|
+
'groups': groups or [],
|
|
315
|
+
'instance': instance,
|
|
316
|
+
})
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def add_node_with_script(self, name: str, node_type: str, script_path: str,
|
|
320
|
+
parent: Optional[str] = None,
|
|
321
|
+
properties: Dict[str, Any] = None,
|
|
322
|
+
groups: List[str] = None) -> 'TscnBuilder':
|
|
323
|
+
"""Add a scene node with an attached script."""
|
|
324
|
+
script_id = self.add_ext_resource('Script', script_path)
|
|
325
|
+
props = properties or {}
|
|
326
|
+
props['script'] = f'ExtResource("{script_id}")'
|
|
327
|
+
return self.add_node(name, node_type, parent, props, groups)
|
|
328
|
+
|
|
329
|
+
def build(self) -> str:
|
|
330
|
+
"""Return the complete .tscn file content."""
|
|
331
|
+
lines = []
|
|
332
|
+
load_steps = 1 + len(self._ext_resources) + len(self._sub_resources)
|
|
333
|
+
|
|
334
|
+
# Header
|
|
335
|
+
lines.append(f'[gd_scene load_steps={load_steps} format=3]')
|
|
336
|
+
lines.append('')
|
|
337
|
+
|
|
338
|
+
# External resources
|
|
339
|
+
for res in self._ext_resources:
|
|
340
|
+
lines.append(
|
|
341
|
+
f'[ext_resource type="{res["type"]}" path="res://{res["path"]}" id="{res["id"]}"]'
|
|
342
|
+
)
|
|
343
|
+
if self._ext_resources:
|
|
344
|
+
lines.append('')
|
|
345
|
+
|
|
346
|
+
# Sub-resources
|
|
347
|
+
for sub in self._sub_resources:
|
|
348
|
+
props_str = self._format_sub_resource_props(sub['properties'])
|
|
349
|
+
lines.append(f'[sub_resource type="{sub["type"]}" id="{sub["id"]}"]')
|
|
350
|
+
if props_str:
|
|
351
|
+
lines.append(props_str)
|
|
352
|
+
lines.append('')
|
|
353
|
+
|
|
354
|
+
# Nodes
|
|
355
|
+
for node in self._nodes:
|
|
356
|
+
lines.append(self._format_node(node))
|
|
357
|
+
props_lines = self._format_node_props(node['properties'])
|
|
358
|
+
if props_lines:
|
|
359
|
+
lines.append(props_lines)
|
|
360
|
+
lines.append('')
|
|
361
|
+
|
|
362
|
+
return '\n'.join(lines)
|
|
363
|
+
|
|
364
|
+
def _format_node(self, node: Dict[str, Any]) -> str:
|
|
365
|
+
"""Format a [node ...] header line."""
|
|
366
|
+
parts = [f'name="{node["name"]}"']
|
|
367
|
+
|
|
368
|
+
if node.get('instance'):
|
|
369
|
+
parts.append(f'instance={node["instance"]}')
|
|
370
|
+
elif node['type']:
|
|
371
|
+
parts.append(f'type="{node["type"]}"')
|
|
372
|
+
|
|
373
|
+
if node['parent'] is not None:
|
|
374
|
+
parts.append(f'parent="{node["parent"]}"')
|
|
375
|
+
|
|
376
|
+
if node.get('groups'):
|
|
377
|
+
groups_str = ', '.join(f'"{g}"' for g in node['groups'])
|
|
378
|
+
parts.append(f'groups=[{groups_str}]')
|
|
379
|
+
|
|
380
|
+
return '[node ' + ' '.join(parts) + ']'
|
|
381
|
+
|
|
382
|
+
def _format_node_props(self, props: Dict[str, Any]) -> str:
|
|
383
|
+
"""Format node properties as key = value lines."""
|
|
384
|
+
lines = []
|
|
385
|
+
for key, value in props.items():
|
|
386
|
+
lines.append(f'{key} = {self._format_value(value)}')
|
|
387
|
+
return '\n'.join(lines)
|
|
388
|
+
|
|
389
|
+
def _format_sub_resource_props(self, props: Dict[str, Any]) -> str:
|
|
390
|
+
"""Format sub-resource properties."""
|
|
391
|
+
lines = []
|
|
392
|
+
for key, value in props.items():
|
|
393
|
+
lines.append(f'{key} = {self._format_value(value)}')
|
|
394
|
+
return '\n'.join(lines)
|
|
395
|
+
|
|
396
|
+
def _format_value(self, value: Any) -> str:
|
|
397
|
+
"""Format a value for .tscn file."""
|
|
398
|
+
if isinstance(value, bool):
|
|
399
|
+
return 'true' if value else 'false'
|
|
400
|
+
elif isinstance(value, int):
|
|
401
|
+
return str(value)
|
|
402
|
+
elif isinstance(value, float):
|
|
403
|
+
return f'{value:.6g}'
|
|
404
|
+
elif isinstance(value, str):
|
|
405
|
+
# Check for special Godot references
|
|
406
|
+
if value.startswith('ExtResource(') or value.startswith('SubResource('):
|
|
407
|
+
return value
|
|
408
|
+
if value.startswith('Vector2(') or value.startswith('Vector2i('):
|
|
409
|
+
return value
|
|
410
|
+
if value.startswith('Rect2(') or value.startswith('Color('):
|
|
411
|
+
return value
|
|
412
|
+
if value.startswith('PackedScene('):
|
|
413
|
+
return value
|
|
414
|
+
return f'"{value}"'
|
|
415
|
+
elif isinstance(value, list):
|
|
416
|
+
items = ', '.join(self._format_value(v) for v in value)
|
|
417
|
+
return f'[{items}]'
|
|
418
|
+
elif isinstance(value, dict):
|
|
419
|
+
items = ', '.join(
|
|
420
|
+
f'{self._format_value(k)}: {self._format_value(v)}'
|
|
421
|
+
for k, v in value.items()
|
|
422
|
+
)
|
|
423
|
+
return f'{{{items}}}'
|
|
424
|
+
else:
|
|
425
|
+
return str(value)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ==========================================================================
|
|
429
|
+
# GDScript helper functions
|
|
430
|
+
# ==========================================================================
|
|
431
|
+
|
|
432
|
+
def gd_string(s: str) -> str:
|
|
433
|
+
"""Escape a string for GDScript."""
|
|
434
|
+
return '"' + s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + '"'
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def gd_bool(b: bool) -> str:
|
|
438
|
+
"""Convert Python bool to GDScript bool literal."""
|
|
439
|
+
return 'true' if b else 'false'
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def gd_vector2(x: float, y: float) -> str:
|
|
443
|
+
"""Format a Vector2."""
|
|
444
|
+
return f'Vector2({x:.6g}, {y:.6g})'
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def gd_vector2i(x: int, y: int) -> str:
|
|
448
|
+
"""Format a Vector2i."""
|
|
449
|
+
return f'Vector2i({x}, {y})'
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def gd_color(hex_color: str) -> str:
|
|
453
|
+
"""Convert hex color to Godot Color."""
|
|
454
|
+
c = hex_color.lstrip('#')
|
|
455
|
+
if len(c) == 3:
|
|
456
|
+
c = ''.join(ch * 2 for ch in c)
|
|
457
|
+
if len(c) == 6:
|
|
458
|
+
r = int(c[0:2], 16) / 255.0
|
|
459
|
+
g = int(c[2:4], 16) / 255.0
|
|
460
|
+
b = int(c[4:6], 16) / 255.0
|
|
461
|
+
return f'Color({r:.3f}, {g:.3f}, {b:.3f}, 1.0)'
|
|
462
|
+
return f'Color("{hex_color}")'
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ==========================================================================
|
|
466
|
+
# Easing name mapping: Quantum → Godot
|
|
467
|
+
# ==========================================================================
|
|
468
|
+
|
|
469
|
+
EASING_MAP = {
|
|
470
|
+
'linear': 'Tween.EASE_IN', # Godot TRANS_LINEAR
|
|
471
|
+
'ease-in': 'Tween.EASE_IN',
|
|
472
|
+
'ease-out': 'Tween.EASE_OUT',
|
|
473
|
+
'ease-in-out': 'Tween.EASE_IN_OUT',
|
|
474
|
+
'bounce': 'Tween.EASE_OUT', # with TRANS_BOUNCE
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
TRANS_MAP = {
|
|
478
|
+
'linear': 'Tween.TRANS_LINEAR',
|
|
479
|
+
'ease-in': 'Tween.TRANS_QUAD',
|
|
480
|
+
'ease-out': 'Tween.TRANS_QUAD',
|
|
481
|
+
'ease-in-out': 'Tween.TRANS_QUAD',
|
|
482
|
+
'bounce': 'Tween.TRANS_BOUNCE',
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ==========================================================================
|
|
487
|
+
# project.godot template
|
|
488
|
+
# ==========================================================================
|
|
489
|
+
|
|
490
|
+
def build_project_godot(config: Dict[str, Any]) -> str:
|
|
491
|
+
"""Generate project.godot file content."""
|
|
492
|
+
lines = []
|
|
493
|
+
lines.append('; Engine configuration file.')
|
|
494
|
+
lines.append('; Generated by Quantum Framework - Godot Backend')
|
|
495
|
+
lines.append('')
|
|
496
|
+
lines.append('config_version=5')
|
|
497
|
+
lines.append('')
|
|
498
|
+
|
|
499
|
+
# [application]
|
|
500
|
+
lines.append('[application]')
|
|
501
|
+
lines.append('')
|
|
502
|
+
lines.append(f'config/name="{config.get("name", "Quantum Game")}"')
|
|
503
|
+
lines.append(f'run/main_scene="res://{config.get("main_scene", "main.tscn")}"')
|
|
504
|
+
lines.append('config/features=PackedStringArray("4.4")')
|
|
505
|
+
lines.append('')
|
|
506
|
+
|
|
507
|
+
# [autoload]
|
|
508
|
+
autoloads = config.get('autoloads', {})
|
|
509
|
+
if autoloads:
|
|
510
|
+
lines.append('[autoload]')
|
|
511
|
+
lines.append('')
|
|
512
|
+
for name, path in autoloads.items():
|
|
513
|
+
lines.append(f'{name}="*res://{path}"')
|
|
514
|
+
lines.append('')
|
|
515
|
+
|
|
516
|
+
# [display]
|
|
517
|
+
lines.append('[display]')
|
|
518
|
+
lines.append('')
|
|
519
|
+
vp_width = config.get('viewport_width', SNES_WIDTH)
|
|
520
|
+
vp_height = config.get('viewport_height', SNES_HEIGHT)
|
|
521
|
+
lines.append(f'window/size/viewport_width={vp_width}')
|
|
522
|
+
lines.append(f'window/size/viewport_height={vp_height}')
|
|
523
|
+
|
|
524
|
+
# Window override: scale up for modern monitors
|
|
525
|
+
scale = config.get('window_scale', SNES_WINDOW_SCALE)
|
|
526
|
+
win_width = vp_width * scale
|
|
527
|
+
win_height = vp_height * scale
|
|
528
|
+
lines.append(f'window/size/window_width_override={win_width}')
|
|
529
|
+
lines.append(f'window/size/window_height_override={win_height}')
|
|
530
|
+
|
|
531
|
+
# Stretch: "viewport" = pixel-perfect upscale (no subpixel blending)
|
|
532
|
+
stretch_mode = config.get('stretch_mode', 'viewport')
|
|
533
|
+
lines.append(f'window/stretch/mode="{stretch_mode}"')
|
|
534
|
+
stretch_aspect = config.get('stretch_aspect', 'keep')
|
|
535
|
+
lines.append(f'window/stretch/aspect="{stretch_aspect}"')
|
|
536
|
+
lines.append('')
|
|
537
|
+
|
|
538
|
+
# [physics]
|
|
539
|
+
lines.append('[physics]')
|
|
540
|
+
lines.append('')
|
|
541
|
+
gravity = config.get('gravity', 980.0)
|
|
542
|
+
lines.append(f'2d/default_gravity={gravity:.1f}')
|
|
543
|
+
lines.append('')
|
|
544
|
+
|
|
545
|
+
# [input] - input map
|
|
546
|
+
input_map = config.get('input_map', {})
|
|
547
|
+
if input_map:
|
|
548
|
+
lines.append('[input]')
|
|
549
|
+
lines.append('')
|
|
550
|
+
for action_name, keys in input_map.items():
|
|
551
|
+
events = []
|
|
552
|
+
for key in keys:
|
|
553
|
+
events.append(_input_event_for_key(key))
|
|
554
|
+
events_str = ', '.join(events)
|
|
555
|
+
lines.append(f'{action_name}={{"deadzone": 0.5, "events": [{events_str}]}}')
|
|
556
|
+
lines.append('')
|
|
557
|
+
|
|
558
|
+
# [layer_names]
|
|
559
|
+
collision_layer_names = config.get('collision_layer_names', {})
|
|
560
|
+
if collision_layer_names:
|
|
561
|
+
lines.append('[layer_names]')
|
|
562
|
+
lines.append('')
|
|
563
|
+
# Sort by layer id value, map name->id to id->name
|
|
564
|
+
id_to_name = {}
|
|
565
|
+
for name, layer_id in collision_layer_names.items():
|
|
566
|
+
# Convert bitmask to layer number (1-based)
|
|
567
|
+
bit = 1
|
|
568
|
+
while (1 << (bit - 1)) < layer_id:
|
|
569
|
+
bit += 1
|
|
570
|
+
if (1 << (bit - 1)) == layer_id:
|
|
571
|
+
id_to_name[bit] = name
|
|
572
|
+
else:
|
|
573
|
+
# Not a power of 2, use as direct layer number
|
|
574
|
+
id_to_name[layer_id] = name
|
|
575
|
+
for layer_num in sorted(id_to_name.keys()):
|
|
576
|
+
lines.append(f'2d_physics/layer_{layer_num}="{id_to_name[layer_num]}"')
|
|
577
|
+
lines.append('')
|
|
578
|
+
else:
|
|
579
|
+
# Default SMW layer names
|
|
580
|
+
lines.append('[layer_names]')
|
|
581
|
+
lines.append('')
|
|
582
|
+
lines.append('2d_physics/layer_1="world"')
|
|
583
|
+
lines.append('2d_physics/layer_2="player"')
|
|
584
|
+
lines.append('2d_physics/layer_3="enemies"')
|
|
585
|
+
lines.append('2d_physics/layer_4="collectibles"')
|
|
586
|
+
lines.append('2d_physics/layer_5="sensors"')
|
|
587
|
+
lines.append('')
|
|
588
|
+
|
|
589
|
+
# [rendering]
|
|
590
|
+
lines.append('[rendering]')
|
|
591
|
+
lines.append('')
|
|
592
|
+
lines.append('textures/canvas_textures/default_texture_filter=0')
|
|
593
|
+
lines.append('')
|
|
594
|
+
|
|
595
|
+
return '\n'.join(lines)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _input_event_for_key(key: str) -> str:
|
|
599
|
+
"""Convert a key name to a Godot InputEvent resource string.
|
|
600
|
+
|
|
601
|
+
Godot 4 project.godot uses integer physical keycodes, not symbolic names.
|
|
602
|
+
"""
|
|
603
|
+
# Godot 4 physical keycode integer values
|
|
604
|
+
key_map = {
|
|
605
|
+
'ArrowLeft': 4194319, 'ArrowRight': 4194321,
|
|
606
|
+
'ArrowUp': 4194320, 'ArrowDown': 4194322,
|
|
607
|
+
'a': 65, 'd': 68, 'w': 87, 's': 83,
|
|
608
|
+
'Space': 32, ' ': 32,
|
|
609
|
+
'Enter': 4194309, 'Escape': 4194305,
|
|
610
|
+
'Shift': 4194325, 'Control': 4194326,
|
|
611
|
+
'z': 90, 'x': 88, 'c': 67,
|
|
612
|
+
}
|
|
613
|
+
godot_key = key_map.get(key, ord(key.upper()) if len(key) == 1 else 0)
|
|
614
|
+
return f'Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":{godot_key},"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)'
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# ==========================================================================
|
|
618
|
+
# export_presets.cfg template
|
|
619
|
+
# ==========================================================================
|
|
620
|
+
|
|
621
|
+
def build_export_presets() -> str:
|
|
622
|
+
"""Generate export_presets.cfg for HTML5 web export."""
|
|
623
|
+
return """[preset.0]
|
|
624
|
+
|
|
625
|
+
name="Web"
|
|
626
|
+
platform="Web"
|
|
627
|
+
runnable=true
|
|
628
|
+
dedicated_server=false
|
|
629
|
+
custom_features=""
|
|
630
|
+
export_filter="all_resources"
|
|
631
|
+
include_filter=""
|
|
632
|
+
exclude_filter=""
|
|
633
|
+
export_path="dist/index.html"
|
|
634
|
+
encryption_include_filters=""
|
|
635
|
+
encryption_exclude_filters=""
|
|
636
|
+
encrypt_pck=false
|
|
637
|
+
encrypt_directory=false
|
|
638
|
+
|
|
639
|
+
[preset.0.options]
|
|
640
|
+
|
|
641
|
+
custom_template/debug=""
|
|
642
|
+
custom_template/release=""
|
|
643
|
+
variant/extensions_support=false
|
|
644
|
+
vram_texture_compression/for_desktop=true
|
|
645
|
+
vram_texture_compression/for_mobile=false
|
|
646
|
+
html/export_icon=true
|
|
647
|
+
html/custom_html_shell=""
|
|
648
|
+
html/head_include=""
|
|
649
|
+
html/canvas_resize_policy=2
|
|
650
|
+
html/focus_canvas_on_start=true
|
|
651
|
+
html/experimental_virtual_keyboard=false
|
|
652
|
+
progressive_web_app/enabled=false
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ==========================================================================
|
|
657
|
+
# GDScript templates for runtime scripts
|
|
658
|
+
# ==========================================================================
|
|
659
|
+
|
|
660
|
+
QUANTUM_BRIDGE_GD = '''\
|
|
661
|
+
extends Node
|
|
662
|
+
## Quantum Bridge - Game API autoload
|
|
663
|
+
## Provides centralized game object management and utility functions.
|
|
664
|
+
|
|
665
|
+
var sprites: Dictionary = {}
|
|
666
|
+
var event_bus: Node
|
|
667
|
+
var game_state: Dictionary = {}
|
|
668
|
+
|
|
669
|
+
func save_game_state(data: Dictionary):
|
|
670
|
+
\tgame_state = data.duplicate()
|
|
671
|
+
|
|
672
|
+
func get_game_state() -> Dictionary:
|
|
673
|
+
\treturn game_state
|
|
674
|
+
|
|
675
|
+
func clear_game_state():
|
|
676
|
+
\tgame_state = {}
|
|
677
|
+
|
|
678
|
+
func _ready():
|
|
679
|
+
\tevent_bus = get_node("/root/QuantumEventBus")
|
|
680
|
+
|
|
681
|
+
func register_sprite(id: String, node: Node):
|
|
682
|
+
\tsprites[id] = node
|
|
683
|
+
|
|
684
|
+
func destroy_sprite(id: String):
|
|
685
|
+
\tif id in sprites:
|
|
686
|
+
\t\tsprites[id].queue_free()
|
|
687
|
+
\t\tsprites.erase(id)
|
|
688
|
+
|
|
689
|
+
func respawn(id: String, x: float, y: float):
|
|
690
|
+
\tif id in sprites:
|
|
691
|
+
\t\tsprites[id].position = Vector2(x, y)
|
|
692
|
+
\t\tif sprites[id] is CharacterBody2D:
|
|
693
|
+
\t\t\tsprites[id].velocity = Vector2.ZERO
|
|
694
|
+
|
|
695
|
+
func get_sprite(id: String) -> Node:
|
|
696
|
+
\tif id in sprites:
|
|
697
|
+
\t\treturn sprites[id]
|
|
698
|
+
\treturn null
|
|
699
|
+
|
|
700
|
+
func emit_event(event_name: String, data: Dictionary = {}):
|
|
701
|
+
\tif event_bus:
|
|
702
|
+
\t\tevent_bus.emit_event(event_name, data)
|
|
703
|
+
|
|
704
|
+
func camera_shake(intensity: float = 5.0, duration: float = 0.3):
|
|
705
|
+
\tvar cam = get_viewport().get_camera_2d()
|
|
706
|
+
\tif cam and cam.has_method("shake"):
|
|
707
|
+
\t\tcam.shake(intensity, duration)
|
|
708
|
+
|
|
709
|
+
func set_patrol_ai(id: String, speed: float):
|
|
710
|
+
\tif id in sprites and sprites[id].has_method("set_patrol"):
|
|
711
|
+
\t\tsprites[id].set_patrol(speed)
|
|
712
|
+
|
|
713
|
+
func kill_player(id: String):
|
|
714
|
+
\tif id in sprites and sprites[id].has_method("die"):
|
|
715
|
+
\t\tsprites[id].die()
|
|
716
|
+
|
|
717
|
+
func play_sound(id: String, opts: Dictionary = {}):
|
|
718
|
+
\tvar player = get_node_or_null("/root/Main/Sounds/" + id)
|
|
719
|
+
\tif player and player is AudioStreamPlayer:
|
|
720
|
+
\t\tif opts.has("volume"):
|
|
721
|
+
\t\t\tplayer.volume_db = linear_to_db(opts["volume"])
|
|
722
|
+
\t\tplayer.play()
|
|
723
|
+
|
|
724
|
+
func pause():
|
|
725
|
+
\tget_tree().paused = true
|
|
726
|
+
|
|
727
|
+
func resume():
|
|
728
|
+
\tget_tree().paused = false
|
|
729
|
+
|
|
730
|
+
func load_scene(scene_name: String):
|
|
731
|
+
\tvar path = "res://scenes/" + scene_name + ".tscn"
|
|
732
|
+
\tget_tree().change_scene_to_file(path)
|
|
733
|
+
|
|
734
|
+
func iris_in(duration: float = 0.5):
|
|
735
|
+
\temit_event("iris_in", {"duration": duration})
|
|
736
|
+
|
|
737
|
+
func iris_out(duration: float = 0.5):
|
|
738
|
+
\temit_event("iris_out", {"duration": duration})
|
|
739
|
+
|
|
740
|
+
func save_state(key: String, value) -> void:
|
|
741
|
+
\tvar save_data: Dictionary = {}
|
|
742
|
+
\tvar path = "user://quantum_save.json"
|
|
743
|
+
\tif FileAccess.file_exists(path):
|
|
744
|
+
\t\tvar f = FileAccess.open(path, FileAccess.READ)
|
|
745
|
+
\t\tsave_data = JSON.parse_string(f.get_as_text()) if f else {}
|
|
746
|
+
\t\tif f: f.close()
|
|
747
|
+
\tif save_data == null:
|
|
748
|
+
\t\tsave_data = {}
|
|
749
|
+
\tsave_data[key] = value
|
|
750
|
+
\tvar f = FileAccess.open(path, FileAccess.WRITE)
|
|
751
|
+
\tif f:
|
|
752
|
+
\t\tf.store_string(JSON.stringify(save_data))
|
|
753
|
+
\t\tf.close()
|
|
754
|
+
|
|
755
|
+
func load_state(key: String, default_value = null):
|
|
756
|
+
\tvar path = "user://quantum_save.json"
|
|
757
|
+
\tif not FileAccess.file_exists(path):
|
|
758
|
+
\t\treturn default_value
|
|
759
|
+
\tvar f = FileAccess.open(path, FileAccess.READ)
|
|
760
|
+
\tif not f:
|
|
761
|
+
\t\treturn default_value
|
|
762
|
+
\tvar data = JSON.parse_string(f.get_as_text())
|
|
763
|
+
\tf.close()
|
|
764
|
+
\tif data is Dictionary and data.has(key):
|
|
765
|
+
\t\treturn data[key]
|
|
766
|
+
\treturn default_value
|
|
767
|
+
'''
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
QUANTUM_EVENT_BUS_GD = '''\
|
|
771
|
+
extends Node
|
|
772
|
+
## Quantum Event Bus - Global event system autoload
|
|
773
|
+
## Allows decoupled communication between game objects.
|
|
774
|
+
|
|
775
|
+
var _listeners: Dictionary = {}
|
|
776
|
+
|
|
777
|
+
func emit_event(event_name: String, data: Dictionary = {}):
|
|
778
|
+
\tif event_name in _listeners:
|
|
779
|
+
\t\tfor callback in _listeners[event_name]:
|
|
780
|
+
\t\t\tif callback.is_valid():
|
|
781
|
+
\t\t\t\tcallback.call(data)
|
|
782
|
+
|
|
783
|
+
func listen(event_name: String, callback: Callable):
|
|
784
|
+
\tif event_name not in _listeners:
|
|
785
|
+
\t\t_listeners[event_name] = []
|
|
786
|
+
\t_listeners[event_name].append(callback)
|
|
787
|
+
|
|
788
|
+
func remove_listener(event_name: String, callback: Callable):
|
|
789
|
+
\tif event_name in _listeners:
|
|
790
|
+
\t\t_listeners[event_name].erase(callback)
|
|
791
|
+
|
|
792
|
+
func clear(event_name: String = ""):
|
|
793
|
+
\tif event_name:
|
|
794
|
+
\t\t_listeners.erase(event_name)
|
|
795
|
+
\telse:
|
|
796
|
+
\t\t_listeners.clear()
|
|
797
|
+
'''
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
PLAYER_CONTROLLER_GD = '''\
|
|
801
|
+
extends CharacterBody2D
|
|
802
|
+
## Player Controller - SMW-style platformer physics
|
|
803
|
+
## Generated by Quantum Framework
|
|
804
|
+
|
|
805
|
+
@export var move_speed: float = {speed}
|
|
806
|
+
@export var jump_velocity: float = {jump_velocity}
|
|
807
|
+
@export var gravity_up: float = {gravity_up}
|
|
808
|
+
@export var gravity_down: float = {gravity_down}
|
|
809
|
+
@export var jump_hold_boost: float = {jump_hold_boost}
|
|
810
|
+
@export var coyote_frames: int = {coyote_frames}
|
|
811
|
+
@export var max_fall_speed: float = {max_fall_speed}
|
|
812
|
+
|
|
813
|
+
var _coyote_counter: int = 0
|
|
814
|
+
var _jump_held: bool = false
|
|
815
|
+
var _facing_right: bool = true
|
|
816
|
+
var _stomp_bouncing: bool = false
|
|
817
|
+
|
|
818
|
+
# Death state
|
|
819
|
+
var is_dead: bool = false
|
|
820
|
+
var _death_texture: Texture2D = null
|
|
821
|
+
var _normal_texture: Texture2D = null
|
|
822
|
+
|
|
823
|
+
# Animation
|
|
824
|
+
var _anim_timer: float = 0.0
|
|
825
|
+
var _walk_frames: Array = {walk_frames}
|
|
826
|
+
var _walk_frame_idx: int = 0
|
|
827
|
+
var _walk_speed: float = {walk_anim_speed}
|
|
828
|
+
var _idle_frame: int = {idle_frame}
|
|
829
|
+
var _jump_frame: int = {jump_frame}
|
|
830
|
+
|
|
831
|
+
func _ready():
|
|
832
|
+
\tcollision_layer = {collision_layer}
|
|
833
|
+
\tcollision_mask = {collision_mask}
|
|
834
|
+
\tfloor_max_angle = deg_to_rad({floor_max_angle})
|
|
835
|
+
\tfloor_snap_length = 4.0
|
|
836
|
+
\tfloor_block_on_wall = true
|
|
837
|
+
\tmax_slides = 6
|
|
838
|
+
|
|
839
|
+
func _physics_process(delta: float):
|
|
840
|
+
\t# Death: only apply gravity and slide
|
|
841
|
+
\tif is_dead:
|
|
842
|
+
\t\tvelocity.y += gravity_down * delta
|
|
843
|
+
\t\tmove_and_slide()
|
|
844
|
+
\t\treturn
|
|
845
|
+
|
|
846
|
+
\t# Enemy hit cooldown
|
|
847
|
+
\tif _enemy_hit_cooldown > 0:
|
|
848
|
+
\t\t_enemy_hit_cooldown -= delta
|
|
849
|
+
|
|
850
|
+
\t# Gravity
|
|
851
|
+
\tif not is_on_floor():
|
|
852
|
+
\t\tvar grav = gravity_down if velocity.y > 0 else gravity_up
|
|
853
|
+
\t\tvelocity.y += grav * delta
|
|
854
|
+
\t\tvelocity.y = min(velocity.y, max_fall_speed)
|
|
855
|
+
\t\t_coyote_counter -= 1
|
|
856
|
+
\telse:
|
|
857
|
+
\t\tif _stomp_bouncing:
|
|
858
|
+
\t\t\t_stomp_bouncing = false
|
|
859
|
+
\t\telse:
|
|
860
|
+
\t\t\tvelocity.y = 0
|
|
861
|
+
\t\t\t_coyote_counter = coyote_frames
|
|
862
|
+
|
|
863
|
+
\t# Jump
|
|
864
|
+
\tif Input.is_action_just_pressed("jump") and _coyote_counter > 0:
|
|
865
|
+
\t\tvelocity.y = jump_velocity
|
|
866
|
+
\t\t_jump_held = true
|
|
867
|
+
\t\t_coyote_counter = 0
|
|
868
|
+
\t\tprint("[JUMP] vel_y=", velocity.y, " pos_y=", global_position.y)
|
|
869
|
+
|
|
870
|
+
\t# Variable jump height
|
|
871
|
+
\tif Input.is_action_just_released("jump") and velocity.y < 0:
|
|
872
|
+
\t\tvelocity.y *= 0.5
|
|
873
|
+
\t\t_jump_held = false
|
|
874
|
+
|
|
875
|
+
\t# Jump hold boost
|
|
876
|
+
\tif _jump_held and Input.is_action_pressed("jump") and velocity.y < 0:
|
|
877
|
+
\t\tvelocity.y -= jump_hold_boost * delta
|
|
878
|
+
|
|
879
|
+
\t# Horizontal movement
|
|
880
|
+
\tvar direction = Input.get_axis("move_left", "move_right")
|
|
881
|
+
\tif direction:
|
|
882
|
+
\t\tvelocity.x = direction * move_speed
|
|
883
|
+
\t\t_facing_right = direction > 0
|
|
884
|
+
\telse:
|
|
885
|
+
\t\tvelocity.x = move_toward(velocity.x, 0, move_speed * 0.2)
|
|
886
|
+
|
|
887
|
+
\tmove_and_slide()
|
|
888
|
+
\t_check_enemy_collisions()
|
|
889
|
+
\t_update_animation(delta)
|
|
890
|
+
|
|
891
|
+
var _enemy_hit_cooldown: float = 0.0
|
|
892
|
+
|
|
893
|
+
func _check_enemy_collisions():
|
|
894
|
+
\tif _enemy_hit_cooldown > 0:
|
|
895
|
+
\t\treturn
|
|
896
|
+
\tfor i in range(get_slide_collision_count()):
|
|
897
|
+
\t\tvar col = get_slide_collision(i)
|
|
898
|
+
\t\tvar collider = col.get_collider()
|
|
899
|
+
\t\tif not collider or not collider.has_meta("tag"):
|
|
900
|
+
\t\t\tcontinue
|
|
901
|
+
\t\tvar tag = collider.get_meta("tag")
|
|
902
|
+
\t\tvar normal = col.get_normal()
|
|
903
|
+
\t\tif tag == "enemy":
|
|
904
|
+
\t\t\t_enemy_hit_cooldown = 0.3
|
|
905
|
+
\t\t\tQuantumEventBus.emit_event("enemy-collision", {{
|
|
906
|
+
\t\t\t\t"other": collider,
|
|
907
|
+
\t\t\t\t"normal_y": normal.y
|
|
908
|
+
\t\t\t}})
|
|
909
|
+
\t\t\tbreak
|
|
910
|
+
\t\telif tag == "qblock" and normal.y > 0.5:
|
|
911
|
+
\t\t\tif collider.has_method("hit"):
|
|
912
|
+
\t\t\t\tcollider.hit()
|
|
913
|
+
\t\t\tQuantumEventBus.emit_event("qblock-hit", {{
|
|
914
|
+
\t\t\t\t"other": collider,
|
|
915
|
+
\t\t\t\t"normal_y": normal.y
|
|
916
|
+
\t\t\t}})
|
|
917
|
+
\t\telif tag == "rotating_block" and normal.y > 0.5:
|
|
918
|
+
\t\t\tif collider.has_method("hit"):
|
|
919
|
+
\t\t\t\tcollider.hit()
|
|
920
|
+
\t\t\tQuantumEventBus.emit_event("rotating-block-hit", {{
|
|
921
|
+
\t\t\t\t"other": collider,
|
|
922
|
+
\t\t\t\t"normal_y": normal.y
|
|
923
|
+
\t\t\t}})
|
|
924
|
+
|
|
925
|
+
func die():
|
|
926
|
+
\tif is_dead:
|
|
927
|
+
\t\treturn
|
|
928
|
+
\tis_dead = true
|
|
929
|
+
\tprocess_mode = Node.PROCESS_MODE_ALWAYS
|
|
930
|
+
\tvar sprite = get_node_or_null("Sprite2D")
|
|
931
|
+
\tif sprite and _death_texture:
|
|
932
|
+
\t\t_normal_texture = sprite.texture
|
|
933
|
+
\t\tsprite.texture = _death_texture
|
|
934
|
+
\t\tsprite.hframes = 1
|
|
935
|
+
\t\tsprite.vframes = 1
|
|
936
|
+
\t\tsprite.frame = 0
|
|
937
|
+
\t\tsprite.flip_h = false
|
|
938
|
+
\tvelocity = Vector2(0, -200)
|
|
939
|
+
\tset_deferred("collision_layer", 0)
|
|
940
|
+
\tset_deferred("collision_mask", 0)
|
|
941
|
+
|
|
942
|
+
func set_death_texture(tex: Texture2D):
|
|
943
|
+
\t_death_texture = tex
|
|
944
|
+
|
|
945
|
+
func stomp_bounce(vel: float):
|
|
946
|
+
\tprint("[STOMP_BOUNCE] before_vel_y=", velocity.y, " bounce_vel=", vel, " pos_y=", global_position.y)
|
|
947
|
+
\tvelocity.y = vel
|
|
948
|
+
\t_jump_held = false
|
|
949
|
+
\t_coyote_counter = 0
|
|
950
|
+
\t_stomp_bouncing = true
|
|
951
|
+
|
|
952
|
+
func _update_animation(delta: float):
|
|
953
|
+
\tvar sprite = get_node_or_null("Sprite2D")
|
|
954
|
+
\tif not sprite:
|
|
955
|
+
\t\treturn
|
|
956
|
+
|
|
957
|
+
\t# Flip sprite based on facing direction
|
|
958
|
+
\t# SMW spritesheets face LEFT by default, flip when facing right
|
|
959
|
+
\tsprite.flip_h = _facing_right
|
|
960
|
+
|
|
961
|
+
\t# Frame selection
|
|
962
|
+
\tif not is_on_floor():
|
|
963
|
+
\t\t# Airborne: jump frame
|
|
964
|
+
\t\tsprite.frame = _jump_frame
|
|
965
|
+
\t\t_anim_timer = 0.0
|
|
966
|
+
\t\t_walk_frame_idx = 0
|
|
967
|
+
\telif abs(velocity.x) > 5:
|
|
968
|
+
\t\t# Walking: cycle through walk frames
|
|
969
|
+
\t\t_anim_timer += delta
|
|
970
|
+
\t\tif _anim_timer >= _walk_speed:
|
|
971
|
+
\t\t\t_anim_timer = 0.0
|
|
972
|
+
\t\t\t_walk_frame_idx = (_walk_frame_idx + 1) % _walk_frames.size()
|
|
973
|
+
\t\tsprite.frame = _walk_frames[_walk_frame_idx]
|
|
974
|
+
\telse:
|
|
975
|
+
\t\t# Idle
|
|
976
|
+
\t\tsprite.frame = _idle_frame
|
|
977
|
+
\t\t_anim_timer = 0.0
|
|
978
|
+
\t\t_walk_frame_idx = 0
|
|
979
|
+
'''
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
# Spin jump section — appended to player controller when spin_jump=true
|
|
983
|
+
SPIN_JUMP_SECTION = '''
|
|
984
|
+
|
|
985
|
+
# --- Spin Jump (SMW-style) ---
|
|
986
|
+
var _is_spin_jumping: bool = false
|
|
987
|
+
var _spin_jump_velocity_mult: float = 0.85
|
|
988
|
+
|
|
989
|
+
func _unhandled_input(event):
|
|
990
|
+
\tif event.is_action_pressed("spin_jump") and is_on_floor() and not is_dead:
|
|
991
|
+
\t\tvelocity.y = jump_velocity * _spin_jump_velocity_mult
|
|
992
|
+
\t\t_is_spin_jumping = true
|
|
993
|
+
\t\t_jump_held = true
|
|
994
|
+
\t\t_coyote_counter = 0
|
|
995
|
+
\t\tQuantumBridge.play_sound("sfx-spin")
|
|
996
|
+
|
|
997
|
+
func _check_spin_landing():
|
|
998
|
+
\tif _is_spin_jumping and is_on_floor():
|
|
999
|
+
\t\t_is_spin_jumping = false
|
|
1000
|
+
\t\tvar sprite = get_node_or_null("Sprite2D")
|
|
1001
|
+
\t\tif sprite:
|
|
1002
|
+
\t\t\tsprite.rotation = 0.0
|
|
1003
|
+
|
|
1004
|
+
func _spin_visual(delta: float):
|
|
1005
|
+
\tif _is_spin_jumping:
|
|
1006
|
+
\t\tvar sprite = get_node_or_null("Sprite2D")
|
|
1007
|
+
\t\tif sprite:
|
|
1008
|
+
\t\t\tsprite.rotation += delta * 20.0
|
|
1009
|
+
'''
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
CAMERA_FOLLOW_GD = '''\
|
|
1013
|
+
extends Camera2D
|
|
1014
|
+
## Camera Follow - Smooth follow with bounds
|
|
1015
|
+
## Generated by Quantum Framework
|
|
1016
|
+
|
|
1017
|
+
@export var follow_target: NodePath
|
|
1018
|
+
@export var smoothing_speed: float = {lerp_speed}
|
|
1019
|
+
@export var use_bounds: bool = {use_bounds}
|
|
1020
|
+
@export var bound_left: float = 0.0
|
|
1021
|
+
@export var bound_top: float = 0.0
|
|
1022
|
+
@export var bound_right: float = {bound_right}
|
|
1023
|
+
@export var bound_bottom: float = {bound_bottom}
|
|
1024
|
+
|
|
1025
|
+
var _shake_intensity: float = 0.0
|
|
1026
|
+
var _shake_timer: float = 0.0
|
|
1027
|
+
var _base_offset: Vector2
|
|
1028
|
+
|
|
1029
|
+
func _ready():
|
|
1030
|
+
\t_base_offset = offset
|
|
1031
|
+
\tposition_smoothing_enabled = true
|
|
1032
|
+
\tposition_smoothing_speed = smoothing_speed
|
|
1033
|
+
|
|
1034
|
+
\tif use_bounds:
|
|
1035
|
+
\t\tlimit_left = int(bound_left)
|
|
1036
|
+
\t\tlimit_top = int(bound_top)
|
|
1037
|
+
\t\tlimit_right = int(bound_right)
|
|
1038
|
+
\t\tlimit_bottom = int(bound_bottom)
|
|
1039
|
+
|
|
1040
|
+
func _process(delta: float):
|
|
1041
|
+
\tif _shake_timer > 0:
|
|
1042
|
+
\t\t_shake_timer -= delta
|
|
1043
|
+
\t\toffset = _base_offset + Vector2(
|
|
1044
|
+
\t\t\trandf_range(-_shake_intensity, _shake_intensity),
|
|
1045
|
+
\t\t\trandf_range(-_shake_intensity, _shake_intensity)
|
|
1046
|
+
\t\t)
|
|
1047
|
+
\telse:
|
|
1048
|
+
\t\toffset = _base_offset
|
|
1049
|
+
|
|
1050
|
+
func shake(intensity: float = 5.0, duration: float = 0.3):
|
|
1051
|
+
\t_shake_intensity = intensity
|
|
1052
|
+
\t_shake_timer = duration
|
|
1053
|
+
'''
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
PATROL_AI_GD = '''\
|
|
1057
|
+
extends CharacterBody2D
|
|
1058
|
+
## Patrol AI - Simple back-and-forth movement
|
|
1059
|
+
## Generated by Quantum Framework
|
|
1060
|
+
|
|
1061
|
+
@export var patrol_speed: float = {speed}
|
|
1062
|
+
@export var turn_on_edge: bool = {turn_on_edge}
|
|
1063
|
+
@export var gravity: float = {gravity}
|
|
1064
|
+
|
|
1065
|
+
var _direction: float = {initial_direction}
|
|
1066
|
+
|
|
1067
|
+
func _physics_process(delta: float):
|
|
1068
|
+
\tif not is_on_floor():
|
|
1069
|
+
\t\tvelocity.y += gravity * delta
|
|
1070
|
+
\telse:
|
|
1071
|
+
\t\tvelocity.y = 0
|
|
1072
|
+
|
|
1073
|
+
\tvelocity.x = _direction * patrol_speed
|
|
1074
|
+
|
|
1075
|
+
\tif is_on_wall():
|
|
1076
|
+
\t\t_direction *= -1
|
|
1077
|
+
|
|
1078
|
+
\tif turn_on_edge and is_on_floor():
|
|
1079
|
+
\t\tvar ray_pos = Vector2(_direction * 16, 2)
|
|
1080
|
+
\t\tvar space = get_world_2d().direct_space_state
|
|
1081
|
+
\t\tvar query = PhysicsRayQueryParameters2D.create(
|
|
1082
|
+
\t\t\tglobal_position + ray_pos,
|
|
1083
|
+
\t\t\tglobal_position + ray_pos + Vector2(0, 32)
|
|
1084
|
+
\t\t)
|
|
1085
|
+
\t\tvar result = space.intersect_ray(query)
|
|
1086
|
+
\t\tif not result:
|
|
1087
|
+
\t\t\t_direction *= -1
|
|
1088
|
+
|
|
1089
|
+
\tmove_and_slide()
|
|
1090
|
+
|
|
1091
|
+
func set_patrol(speed: float):
|
|
1092
|
+
\tpatrol_speed = speed
|
|
1093
|
+
|
|
1094
|
+
func die():
|
|
1095
|
+
\tqueue_free()
|
|
1096
|
+
'''
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
HUD_MANAGER_GD = '''\
|
|
1100
|
+
extends CanvasLayer
|
|
1101
|
+
## HUD Manager - UI overlay
|
|
1102
|
+
## Generated by Quantum Framework
|
|
1103
|
+
|
|
1104
|
+
{label_vars}
|
|
1105
|
+
|
|
1106
|
+
func _ready():
|
|
1107
|
+
{label_ready}
|
|
1108
|
+
|
|
1109
|
+
func update_label(label_name: String, value):
|
|
1110
|
+
\tvar label = get_node_or_null(label_name)
|
|
1111
|
+
\tif label and label is Label:
|
|
1112
|
+
\t\tlabel.text = str(value)
|
|
1113
|
+
'''
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
SPRITE_ANIMATOR_GD = '''\
|
|
1117
|
+
extends {base_class}
|
|
1118
|
+
## Sprite Animator - Auto-cycling sprite frames
|
|
1119
|
+
## Generated by Quantum Framework
|
|
1120
|
+
|
|
1121
|
+
var _anim_timer: float = 0.0
|
|
1122
|
+
var _anim_frames: Array = {frames}
|
|
1123
|
+
var _anim_idx: int = 0
|
|
1124
|
+
var _anim_speed: float = {speed}
|
|
1125
|
+
var _sprite: Sprite2D
|
|
1126
|
+
|
|
1127
|
+
func _ready():
|
|
1128
|
+
\t_sprite = find_child("Sprite2D", true, false)
|
|
1129
|
+
|
|
1130
|
+
func _process(delta: float):
|
|
1131
|
+
\tif not _sprite or _anim_frames.size() <= 1:
|
|
1132
|
+
\t\treturn
|
|
1133
|
+
\t_anim_timer += delta
|
|
1134
|
+
\tif _anim_timer >= _anim_speed:
|
|
1135
|
+
\t\t_anim_timer = 0.0
|
|
1136
|
+
\t\t_anim_idx = (_anim_idx + 1) % _anim_frames.size()
|
|
1137
|
+
\t\t_sprite.frame = _anim_frames[_anim_idx]
|
|
1138
|
+
'''
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
# ==========================================================================
|
|
1142
|
+
# SceneManager autoload — handles multi-scene transitions & persistent state
|
|
1143
|
+
# ==========================================================================
|
|
1144
|
+
|
|
1145
|
+
SCENE_MANAGER_GD = '''extends Node
|
|
1146
|
+
## SceneManager — handles scene transitions and persistent state.
|
|
1147
|
+
## Generated by Quantum Framework - Godot Backend
|
|
1148
|
+
|
|
1149
|
+
var persistent_state: Dictionary = {}
|
|
1150
|
+
var current_scene_name: String = ""
|
|
1151
|
+
|
|
1152
|
+
var _transition_layer: CanvasLayer
|
|
1153
|
+
var _color_rect: ColorRect
|
|
1154
|
+
var _transitioning: bool = false
|
|
1155
|
+
|
|
1156
|
+
func _ready():
|
|
1157
|
+
\t_setup_transition_layer()
|
|
1158
|
+
|
|
1159
|
+
func _setup_transition_layer():
|
|
1160
|
+
\t_transition_layer = CanvasLayer.new()
|
|
1161
|
+
\t_transition_layer.layer = 100
|
|
1162
|
+
\tadd_child(_transition_layer)
|
|
1163
|
+
\t_color_rect = ColorRect.new()
|
|
1164
|
+
\t_color_rect.color = Color.BLACK
|
|
1165
|
+
\t_color_rect.anchors_preset = Control.PRESET_FULL_RECT
|
|
1166
|
+
\t_color_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
1167
|
+
\t_color_rect.modulate.a = 0.0
|
|
1168
|
+
\t_transition_layer.add_child(_color_rect)
|
|
1169
|
+
|
|
1170
|
+
func transition_to(scene_name: String, effect: String = "fade", duration: float = 0.5, data: Dictionary = {}):
|
|
1171
|
+
\tif _transitioning:
|
|
1172
|
+
\t\treturn
|
|
1173
|
+
\t_transitioning = true
|
|
1174
|
+
\tfor key in data:
|
|
1175
|
+
\t\tpersistent_state[key] = data[key]
|
|
1176
|
+
\tvar scene_path = "res://scenes/" + scene_name + ".tscn"
|
|
1177
|
+
\tmatch effect:
|
|
1178
|
+
\t\t"fade":
|
|
1179
|
+
\t\t\tawait _fade_out(duration / 2.0)
|
|
1180
|
+
\t\t\tget_tree().change_scene_to_file(scene_path)
|
|
1181
|
+
\t\t\tawait get_tree().process_frame
|
|
1182
|
+
\t\t\tcurrent_scene_name = scene_name
|
|
1183
|
+
\t\t\tawait _fade_in(duration / 2.0)
|
|
1184
|
+
\t\t"iris-out":
|
|
1185
|
+
\t\t\tawait _fade_out(duration)
|
|
1186
|
+
\t\t\tget_tree().change_scene_to_file(scene_path)
|
|
1187
|
+
\t\t\tawait get_tree().process_frame
|
|
1188
|
+
\t\t\tcurrent_scene_name = scene_name
|
|
1189
|
+
\t\t\t_color_rect.modulate.a = 0.0
|
|
1190
|
+
\t\t"iris-in":
|
|
1191
|
+
\t\t\t_color_rect.modulate.a = 1.0
|
|
1192
|
+
\t\t\tget_tree().change_scene_to_file(scene_path)
|
|
1193
|
+
\t\t\tawait get_tree().process_frame
|
|
1194
|
+
\t\t\tcurrent_scene_name = scene_name
|
|
1195
|
+
\t\t\tawait _fade_in(duration)
|
|
1196
|
+
\t\t"cut":
|
|
1197
|
+
\t\t\tget_tree().change_scene_to_file(scene_path)
|
|
1198
|
+
\t\t\tawait get_tree().process_frame
|
|
1199
|
+
\t\t\tcurrent_scene_name = scene_name
|
|
1200
|
+
\t_transitioning = false
|
|
1201
|
+
|
|
1202
|
+
func _fade_out(duration: float):
|
|
1203
|
+
\tvar tween = create_tween()
|
|
1204
|
+
\ttween.tween_property(_color_rect, "modulate:a", 1.0, duration)
|
|
1205
|
+
\tawait tween.finished
|
|
1206
|
+
|
|
1207
|
+
func _fade_in(duration: float):
|
|
1208
|
+
\tvar tween = create_tween()
|
|
1209
|
+
\ttween.tween_property(_color_rect, "modulate:a", 0.0, duration)
|
|
1210
|
+
\tawait tween.finished
|
|
1211
|
+
|
|
1212
|
+
func set_state(key: String, value):
|
|
1213
|
+
\tpersistent_state[key] = value
|
|
1214
|
+
|
|
1215
|
+
func get_state(key: String, default = null):
|
|
1216
|
+
\treturn persistent_state.get(key, default)
|
|
1217
|
+
'''
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
MAP_CONTROLLER_GD = '''extends Node2D
|
|
1221
|
+
## MapController — handles world map navigation between nodes.
|
|
1222
|
+
## Generated by Quantum Framework - Godot Backend
|
|
1223
|
+
|
|
1224
|
+
@export var map_data: Dictionary = {}
|
|
1225
|
+
@export var path_data: Array = []
|
|
1226
|
+
|
|
1227
|
+
var current_node: String = ""
|
|
1228
|
+
var _mario_sprite: Sprite2D
|
|
1229
|
+
var _moving: bool = false
|
|
1230
|
+
|
|
1231
|
+
func _ready():
|
|
1232
|
+
\t_mario_sprite = $Mario if has_node("Mario") else null
|
|
1233
|
+
\tif map_data.has("start_node"):
|
|
1234
|
+
\t\tcurrent_node = map_data["start_node"]
|
|
1235
|
+
\tif _mario_sprite and map_data.has("nodes") and map_data["nodes"].has(current_node):
|
|
1236
|
+
\t\tvar node_info = map_data["nodes"][current_node]
|
|
1237
|
+
\t\t_mario_sprite.position = Vector2(node_info["x"], node_info["y"])
|
|
1238
|
+
|
|
1239
|
+
func _input(event: InputEvent):
|
|
1240
|
+
\tif _moving:
|
|
1241
|
+
\t\treturn
|
|
1242
|
+
\tif event.is_action_pressed("ui_accept"):
|
|
1243
|
+
\t\t_enter_node()
|
|
1244
|
+
\telif event.is_action_pressed("ui_right"):
|
|
1245
|
+
\t\t_try_move("right")
|
|
1246
|
+
\telif event.is_action_pressed("ui_left"):
|
|
1247
|
+
\t\t_try_move("left")
|
|
1248
|
+
\telif event.is_action_pressed("ui_up"):
|
|
1249
|
+
\t\t_try_move("up")
|
|
1250
|
+
\telif event.is_action_pressed("ui_down"):
|
|
1251
|
+
\t\t_try_move("down")
|
|
1252
|
+
|
|
1253
|
+
func _enter_node():
|
|
1254
|
+
\tif not map_data.has("nodes"):
|
|
1255
|
+
\t\treturn
|
|
1256
|
+
\tvar node_info = map_data["nodes"].get(current_node, {})
|
|
1257
|
+
\tvar scene_name = node_info.get("scene", "")
|
|
1258
|
+
\tif scene_name != "" and not node_info.get("locked", false):
|
|
1259
|
+
\t\tSceneManager.transition_to(scene_name, "iris-in", 0.5)
|
|
1260
|
+
|
|
1261
|
+
func _try_move(direction: String):
|
|
1262
|
+
\tvar neighbors = _get_neighbors(current_node)
|
|
1263
|
+
\tvar best_node = ""
|
|
1264
|
+
\tvar best_dist = INF
|
|
1265
|
+
\tvar current_pos = _get_node_pos(current_node)
|
|
1266
|
+
\tfor neighbor in neighbors:
|
|
1267
|
+
\t\tvar neighbor_pos = _get_node_pos(neighbor)
|
|
1268
|
+
\t\tvar delta = neighbor_pos - current_pos
|
|
1269
|
+
\t\tvar valid = false
|
|
1270
|
+
\t\tmatch direction:
|
|
1271
|
+
\t\t\t"right": valid = delta.x > 0 and abs(delta.x) >= abs(delta.y)
|
|
1272
|
+
\t\t\t"left": valid = delta.x < 0 and abs(delta.x) >= abs(delta.y)
|
|
1273
|
+
\t\t\t"up": valid = delta.y < 0 and abs(delta.y) >= abs(delta.x)
|
|
1274
|
+
\t\t\t"down": valid = delta.y > 0 and abs(delta.y) >= abs(delta.x)
|
|
1275
|
+
\t\tif valid:
|
|
1276
|
+
\t\t\tvar dist = delta.length()
|
|
1277
|
+
\t\t\tif dist < best_dist:
|
|
1278
|
+
\t\t\t\tbest_dist = dist
|
|
1279
|
+
\t\t\t\tbest_node = neighbor
|
|
1280
|
+
\tif best_node != "":
|
|
1281
|
+
\t\t_move_to(best_node)
|
|
1282
|
+
|
|
1283
|
+
func _move_to(target_node: String):
|
|
1284
|
+
\t_moving = true
|
|
1285
|
+
\tvar target_pos = _get_node_pos(target_node)
|
|
1286
|
+
\tif _mario_sprite:
|
|
1287
|
+
\t\tvar tween = create_tween()
|
|
1288
|
+
\t\ttween.tween_property(_mario_sprite, "position", target_pos, 0.2)
|
|
1289
|
+
\t\tawait tween.finished
|
|
1290
|
+
\tcurrent_node = target_node
|
|
1291
|
+
\t_moving = false
|
|
1292
|
+
|
|
1293
|
+
func _get_neighbors(node_id: String) -> Array:
|
|
1294
|
+
\tvar neighbors = []
|
|
1295
|
+
\tfor path in path_data:
|
|
1296
|
+
\t\tvar unlocked = true
|
|
1297
|
+
\t\tif path.has("unlock") and path["unlock"] != "":
|
|
1298
|
+
\t\t\tunlocked = SceneManager.get_state(path["unlock"], false)
|
|
1299
|
+
\t\tif not unlocked:
|
|
1300
|
+
\t\t\tcontinue
|
|
1301
|
+
\t\tif path["from"] == node_id:
|
|
1302
|
+
\t\t\tneighbors.append(path["to"])
|
|
1303
|
+
\t\telif path["to"] == node_id:
|
|
1304
|
+
\t\t\tneighbors.append(path["from"])
|
|
1305
|
+
\treturn neighbors
|
|
1306
|
+
|
|
1307
|
+
func _get_node_pos(node_id: String) -> Vector2:
|
|
1308
|
+
\tif map_data.has("nodes") and map_data["nodes"].has(node_id):
|
|
1309
|
+
\t\tvar info = map_data["nodes"][node_id]
|
|
1310
|
+
\t\treturn Vector2(info["x"], info["y"])
|
|
1311
|
+
\treturn Vector2.ZERO
|
|
1312
|
+
'''
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
# ==========================================================================
|
|
1316
|
+
# Tilemap Loader — loads tile data from external JSON at runtime
|
|
1317
|
+
# ==========================================================================
|
|
1318
|
+
|
|
1319
|
+
TILEMAP_LOADER_GD = '''\
|
|
1320
|
+
extends TileMapLayer
|
|
1321
|
+
## Loads tile data from {json_path} and populates the TileMapLayer at runtime.
|
|
1322
|
+
|
|
1323
|
+
func _ready():
|
|
1324
|
+
\tvar file = FileAccess.open("{json_path}", FileAccess.READ)
|
|
1325
|
+
\tif not file:
|
|
1326
|
+
\t\tpush_error("tilemap_loader: Could not open {json_path}")
|
|
1327
|
+
\t\treturn
|
|
1328
|
+
\tvar json_text = file.get_as_text()
|
|
1329
|
+
\tfile.close()
|
|
1330
|
+
|
|
1331
|
+
\tvar json = JSON.parse_string(json_text)
|
|
1332
|
+
\tif json == null:
|
|
1333
|
+
\t\tpush_error("tilemap_loader: Failed to parse {json_path}")
|
|
1334
|
+
\t\treturn
|
|
1335
|
+
|
|
1336
|
+
\tvar grid = json["grid"]
|
|
1337
|
+
\tvar count = 0
|
|
1338
|
+
\tfor row_idx in grid.size():
|
|
1339
|
+
\t\tvar row = grid[row_idx]
|
|
1340
|
+
\t\tfor col_idx in row.size():
|
|
1341
|
+
\t\t\tvar cell = row[col_idx]
|
|
1342
|
+
\t\t\tif cell != null:
|
|
1343
|
+
\t\t\t\tvar atlas_x = int(cell[0])
|
|
1344
|
+
\t\t\t\tvar atlas_y = int(cell[1])
|
|
1345
|
+
\t\t\t\tset_cell(Vector2i(col_idx, row_idx), {source_id}, Vector2i(atlas_x, atlas_y))
|
|
1346
|
+
\t\t\t\tcount += 1
|
|
1347
|
+
|
|
1348
|
+
\tprint("TileMapLayer: loaded ", count, " tiles")
|
|
1349
|
+
'''
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
# ==========================================================================
|
|
1353
|
+
# Game Over Screen
|
|
1354
|
+
# ==========================================================================
|
|
1355
|
+
|
|
1356
|
+
GAME_OVER_SCREEN_GD = '''\
|
|
1357
|
+
extends CanvasLayer
|
|
1358
|
+
## Game Over Screen - SMW-style slide-in animation
|
|
1359
|
+
## "GAME" slides from left, "OVER" from right, meet at center
|
|
1360
|
+
## Generated by Quantum Framework
|
|
1361
|
+
|
|
1362
|
+
var _alphabet_tex: Texture2D
|
|
1363
|
+
var _canvas: Control
|
|
1364
|
+
var _game_x: float = -64.0
|
|
1365
|
+
var _over_x: float = 320.0
|
|
1366
|
+
var _target_game_x: float
|
|
1367
|
+
var _target_over_x: float
|
|
1368
|
+
var _black_alpha: float = 0.0
|
|
1369
|
+
var _phase: int = 0 # 0=fade-in, 1=slide, 2=hold
|
|
1370
|
+
var _timer: float = 0.0
|
|
1371
|
+
var _sfx: AudioStreamPlayer
|
|
1372
|
+
|
|
1373
|
+
const CHAR_W: int = 8
|
|
1374
|
+
const CHAR_H: int = 8
|
|
1375
|
+
const SCALE: float = 2.0
|
|
1376
|
+
|
|
1377
|
+
signal game_over_finished
|
|
1378
|
+
|
|
1379
|
+
func _ready():
|
|
1380
|
+
\tlayer = 100
|
|
1381
|
+
\tprocess_mode = Node.PROCESS_MODE_ALWAYS
|
|
1382
|
+
|
|
1383
|
+
\t_alphabet_tex = load("res://{alphabet_path}")
|
|
1384
|
+
|
|
1385
|
+
\t_canvas = Control.new()
|
|
1386
|
+
\t_canvas.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
1387
|
+
\t_canvas.connect("draw", _on_draw)
|
|
1388
|
+
\tadd_child(_canvas)
|
|
1389
|
+
|
|
1390
|
+
\t# "GAME OVER" layout
|
|
1391
|
+
\tvar vp_w: float = {viewport_width}.0
|
|
1392
|
+
\tvar game_width = 4 * CHAR_W * SCALE
|
|
1393
|
+
\tvar over_width = 4 * CHAR_W * SCALE
|
|
1394
|
+
\tvar space_w = CHAR_W * SCALE
|
|
1395
|
+
\tvar total_width = game_width + space_w + over_width
|
|
1396
|
+
\tvar center_x = vp_w / 2.0
|
|
1397
|
+
|
|
1398
|
+
\t_target_game_x = center_x - total_width / 2.0
|
|
1399
|
+
\t_target_over_x = _target_game_x + game_width + space_w
|
|
1400
|
+
|
|
1401
|
+
\t_game_x = -game_width
|
|
1402
|
+
\t_over_x = vp_w + over_width
|
|
1403
|
+
|
|
1404
|
+
func _process(delta: float):
|
|
1405
|
+
\tmatch _phase:
|
|
1406
|
+
\t\t0: # Fade to black
|
|
1407
|
+
\t\t\t_black_alpha = min(1.0, _black_alpha + delta * 2.0)
|
|
1408
|
+
\t\t\tif _black_alpha >= 1.0:
|
|
1409
|
+
\t\t\t\t_phase = 1
|
|
1410
|
+
\t\t\t\t_timer = 0.0
|
|
1411
|
+
\t\t\t\t_sfx = AudioStreamPlayer.new()
|
|
1412
|
+
\t\t\t\t_sfx.stream = load("res://{jingle_path}")
|
|
1413
|
+
\t\t\t\t_sfx.process_mode = Node.PROCESS_MODE_ALWAYS
|
|
1414
|
+
\t\t\t\tadd_child(_sfx)
|
|
1415
|
+
\t\t\t\t_sfx.play()
|
|
1416
|
+
\t\t1: # Slide in
|
|
1417
|
+
\t\t\t_timer += delta
|
|
1418
|
+
\t\t\tvar t = clamp(_timer / 0.8, 0.0, 1.0)
|
|
1419
|
+
\t\t\tvar ease_t = 1.0 - pow(1.0 - t, 3.0)
|
|
1420
|
+
\t\t\t_game_x = lerp(-64.0, _target_game_x, ease_t)
|
|
1421
|
+
\t\t\t_over_x = lerp({viewport_width}.0 + 64.0, _target_over_x, ease_t)
|
|
1422
|
+
\t\t\tif t >= 1.0:
|
|
1423
|
+
\t\t\t\t_phase = 2
|
|
1424
|
+
\t\t\t\t_timer = 0.0
|
|
1425
|
+
\t\t2: # Hold until jingle ends
|
|
1426
|
+
\t\t\t_timer += delta
|
|
1427
|
+
\t\t\tif _timer >= 6.0:
|
|
1428
|
+
\t\t\t\temit_signal("game_over_finished")
|
|
1429
|
+
\t\t\t\tset_process(false)
|
|
1430
|
+
\t_canvas.queue_redraw()
|
|
1431
|
+
|
|
1432
|
+
func _on_draw():
|
|
1433
|
+
\tvar size = _canvas.size
|
|
1434
|
+
\t_canvas.draw_rect(Rect2(0, 0, size.x, size.y), Color(0, 0, 0, _black_alpha))
|
|
1435
|
+
\tif _phase >= 1:
|
|
1436
|
+
\t\tvar center_y = size.y / 2.0 - CHAR_H * SCALE / 2.0
|
|
1437
|
+
\t\t_draw_word("GAME", _game_x, center_y)
|
|
1438
|
+
\t\t_draw_word("OVER", _over_x, center_y)
|
|
1439
|
+
|
|
1440
|
+
func _draw_word(word: String, x: float, y: float):
|
|
1441
|
+
\tfor i in range(word.length()):
|
|
1442
|
+
\t\tvar ch = word[i]
|
|
1443
|
+
\t\tvar idx = ch.unicode_at(0) - "A".unicode_at(0)
|
|
1444
|
+
\t\tif idx < 0 or idx > 25:
|
|
1445
|
+
\t\t\tcontinue
|
|
1446
|
+
\t\tvar src = Rect2(idx * CHAR_W, 0, CHAR_W, CHAR_H)
|
|
1447
|
+
\t\tvar dst = Rect2(x + i * CHAR_W * SCALE, y, CHAR_W * SCALE, CHAR_H * SCALE)
|
|
1448
|
+
\t\t_canvas.draw_texture_rect_region(_alphabet_tex, dst, src)
|
|
1449
|
+
'''
|