"""Script Templates -- Boilerplate generators for new nodes and projects.
Provides templates for each major node type so that users can quickly scaffold
new scripts via ``File > New Script``. Also includes project wizard templates
for creating new projects (Empty, 2D Game, 3D Game).
Template variables:
${class_name} - PascalCase class name
${base_class} - Base class to inherit from
${file_name} - Snake_case file name (without .py)
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
log = logging.getLogger(__name__)
__all__ = [
"ScriptTemplate",
"TEMPLATES",
"generate_script",
"ProjectTemplate",
"PROJECT_TEMPLATES",
"generate_project",
]
# ============================================================================
# Script Templates
# ============================================================================
[docs]
@dataclass(frozen=True)
class ScriptTemplate:
"""A script template with a name, base class, and code body."""
name: str
base_class: str
body: str
description: str = ""
# -- Template bodies --------------------------------------------------------
_NODE_BODY = '''\
"""${class_name} -- ${base_class} script."""
from simvx.core import ${base_class}, Signal, Property
class ${class_name}(${base_class}):
def ready(self):
pass
def process(self, dt: float):
pass
'''
_NODE2D_BODY = '''\
"""${class_name} -- 2D node script."""
from simvx.core import ${base_class}, Signal, Property, Vec2
class ${class_name}(${base_class}):
speed = Property(100.0, range=(0, 500), hint="Movement speed")
def ready(self):
pass
def process(self, dt: float):
pass
def draw(self, renderer):
pass
'''
_NODE3D_BODY = '''\
"""${class_name} -- 3D node script."""
from simvx.core import ${base_class}, Signal, Property, Vec3, Quat
class ${class_name}(${base_class}):
speed = Property(5.0, range=(0, 50), hint="Movement speed")
def ready(self):
pass
def process(self, dt: float):
pass
'''
_CHARACTER2D_BODY = '''\
"""${class_name} -- 2D character with physics."""
from simvx.core import ${base_class}, Signal, Property, Vec2, Input
class ${class_name}(${base_class}):
speed = Property(200.0, range=(0, 1000), hint="Movement speed")
jump_force = Property(400.0, range=(0, 1000), hint="Jump velocity")
def ready(self):
self.velocity = Vec2()
def physics_process(self, dt: float):
# Horizontal movement
direction = Vec2()
if Input.is_key_pressed("d"):
direction.x += 1
if Input.is_key_pressed("a"):
direction.x -= 1
self.velocity.x = direction.x * self.speed
# Gravity and jump
self.velocity.y += 980.0 * dt
if self.is_on_floor() and Input.is_key_pressed("space"):
self.velocity.y = -self.jump_force
self.move_and_slide(dt)
'''
_CHARACTER3D_BODY = '''\
"""${class_name} -- 3D character with physics."""
from simvx.core import ${base_class}, Signal, Property, Vec3, Input
class ${class_name}(${base_class}):
speed = Property(5.0, range=(0, 50), hint="Movement speed")
jump_force = Property(8.0, range=(0, 20), hint="Jump velocity")
def ready(self):
self.velocity = Vec3()
def physics_process(self, dt: float):
direction = Vec3()
if Input.is_key_pressed("w"):
direction.z -= 1
if Input.is_key_pressed("s"):
direction.z += 1
if Input.is_key_pressed("a"):
direction.x -= 1
if Input.is_key_pressed("d"):
direction.x += 1
if direction.length() > 0:
direction = direction.normalized()
self.velocity.x = direction.x * self.speed
self.velocity.z = direction.z * self.speed
# Gravity and jump
self.velocity.y -= 9.8 * dt
if self.is_on_floor() and Input.is_key_pressed("space"):
self.velocity.y = self.jump_force
self.move_and_slide(dt)
'''
_CAMERA3D_BODY = '''\
"""${class_name} -- Camera3D script."""
from simvx.core import ${base_class}, Property
class ${class_name}(${base_class}):
fov = Property(70.0, range=(30, 120), hint="Field of view")
def ready(self):
pass
def process(self, dt: float):
pass
'''
_MESH3D_BODY = '''\
"""${class_name} -- Mesh instance script."""
from simvx.core import ${base_class}, Property, Vec3
class ${class_name}(${base_class}):
spin_speed = Property(1.0, range=(0, 10), hint="Rotation speed")
def ready(self):
pass
def process(self, dt: float):
self.rotation.y += self.spin_speed * dt
'''
_TIMER_BODY = '''\
"""${class_name} -- Timer script."""
from simvx.core import ${base_class}, Signal, Property
class ${class_name}(${base_class}):
interval = Property(1.0, range=(0.01, 60), hint="Timer interval")
def ready(self):
self.timeout.connect(self._on_timeout)
def _on_timeout(self):
pass
'''
_AREA2D_BODY = '''\
"""${class_name} -- Area2D script for detecting overlaps."""
from simvx.core import ${base_class}, Signal, Property
class ${class_name}(${base_class}):
def ready(self):
pass
def _on_body_entered(self, body):
pass
def _on_body_exited(self, body):
pass
'''
_AREA3D_BODY = '''\
"""${class_name} -- Area3D script for detecting overlaps."""
from simvx.core import ${base_class}, Signal, Property
class ${class_name}(${base_class}):
def ready(self):
pass
def _on_body_entered(self, body):
pass
def _on_body_exited(self, body):
pass
'''
_UI_CONTROL_BODY = '''\
"""${class_name} -- Custom UI control."""
from simvx.core import ${base_class}, Signal, Property, Vec2
class ${class_name}(${base_class}):
def ready(self):
self.size = Vec2(200, 100)
def draw(self, renderer):
x, y, w, h = self.get_global_rect()
renderer.draw_filled_rect(x, y, w, h, (0.2, 0.2, 0.2, 1.0))
def _on_gui_input(self, event):
pass
'''
# -- Template registry -------------------------------------------------------
TEMPLATES: dict[str, ScriptTemplate] = {
"Node": ScriptTemplate("Node", "Node", _NODE_BODY, "Basic node"),
"Node2D": ScriptTemplate("Node2D", "Node2D", _NODE2D_BODY, "2D node with transform"),
"Node3D": ScriptTemplate("Node3D", "Node3D", _NODE3D_BODY, "3D node with transform"),
"CharacterBody2D": ScriptTemplate("CharacterBody2D", "CharacterBody2D", _CHARACTER2D_BODY, "2D physics character"),
"CharacterBody3D": ScriptTemplate("CharacterBody3D", "CharacterBody3D", _CHARACTER3D_BODY, "3D physics character"),
"Camera3D": ScriptTemplate("Camera3D", "Camera3D", _CAMERA3D_BODY, "3D camera"),
"MeshInstance3D": ScriptTemplate("MeshInstance3D", "MeshInstance3D", _MESH3D_BODY, "3D mesh"),
"Timer": ScriptTemplate("Timer", "Timer", _TIMER_BODY, "Timer node"),
"Area2D": ScriptTemplate("Area2D", "Area2D", _AREA2D_BODY, "2D overlap detector"),
"Area3D": ScriptTemplate("Area3D", "Area3D", _AREA3D_BODY, "3D overlap detector"),
"Control": ScriptTemplate("Control", "Control", _UI_CONTROL_BODY, "Custom UI control"),
}
[docs]
def generate_script(
template_name: str,
class_name: str,
file_name: str | None = None,
output_path: str | Path | None = None,
) -> str:
"""Generate a script from a template.
Args:
template_name: Key in TEMPLATES (e.g. "Node3D").
class_name: PascalCase class name.
file_name: Snake_case file name (auto-derived if None).
output_path: If given, write the script to this path.
Returns:
The generated script text.
"""
tmpl = TEMPLATES.get(template_name)
if tmpl is None:
raise ValueError(f"Unknown template: {template_name!r}. Available: {list(TEMPLATES)}")
if file_name is None:
file_name = _to_snake_case(class_name)
code = tmpl.body
code = code.replace("${class_name}", class_name)
code = code.replace("${base_class}", tmpl.base_class)
code = code.replace("${file_name}", file_name)
if output_path:
p = Path(output_path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(code)
return code
[docs]
def generate_script_text(template_name: str, class_name: str) -> str:
"""Generate script source text from a template (no file I/O)."""
return generate_script(template_name, class_name)
def _to_snake_case(name: str) -> str:
"""Convert PascalCase to snake_case.
Inserts underscores before uppercase letters that follow lowercase letters,
producing names like ``node3d``, ``character_body2d``. Digit-to-uppercase
transitions (e.g. ``3D``) do NOT get an underscore.
"""
result = []
for i, ch in enumerate(name):
if ch.isupper() and i > 0:
prev = name[i - 1]
if prev.islower():
result.append("_")
result.append(ch.lower())
return "".join(result)
# ============================================================================
# Project Templates
# ============================================================================
[docs]
@dataclass(frozen=True)
class ProjectTemplate:
"""Template for creating a new project."""
name: str
description: str
files: dict[str, str] # relative_path -> content
_EMPTY_PROJECT_JSON = """\
{
"project_name": "${project_name}",
"default_scene": "",
"window_width": 1280,
"window_height": 720,
"fullscreen": false,
"vsync": true,
"msaa": 4,
"gravity": 9.8,
"physics_fps": 60,
"bloom_enabled": false,
"shadows_enabled": true,
"shadow_resolution": 1024,
"input_actions": [],
"autoloads": []
}
"""
_GAME2D_MAIN = '''\
"""Main scene for 2D game."""
from simvx.core import Node2D, Camera2D, Property, Vec2
from simvx.graphics import App
class Main(Node2D):
def ready(self):
camera = Camera2D(name="Camera")
self.add_child(camera)
def process(self, dt: float):
pass
if __name__ == "__main__":
app = App(width=1280, height=720, title="${project_name}")
app.run(Main())
'''
_GAME3D_MAIN = '''\
"""Main scene for 3D game."""
from simvx.core import Node3D, Camera3D, MeshInstance3D, DirectionalLight3D, Property, Vec3
from simvx.graphics import App
class Main(Node3D):
def ready(self):
camera = Camera3D(name="Camera")
camera.position = Vec3(0, 5, 10)
self.add_child(camera)
light = DirectionalLight3D(name="Sun")
self.add_child(light)
def process(self, dt: float):
pass
if __name__ == "__main__":
app = App(width=1280, height=720, title="${project_name}")
app.run(Main())
'''
PROJECT_TEMPLATES: dict[str, ProjectTemplate] = {
"Empty": ProjectTemplate(
name="Empty",
description="Empty project with just a project file.",
files={
"project.simvx": _EMPTY_PROJECT_JSON,
},
),
"2D Game": ProjectTemplate(
name="2D Game",
description="2D game starter with camera and main scene.",
files={
"project.simvx": _EMPTY_PROJECT_JSON,
"main.py": _GAME2D_MAIN,
},
),
"3D Game": ProjectTemplate(
name="3D Game",
description="3D game starter with camera, light, and main scene.",
files={
"project.simvx": _EMPTY_PROJECT_JSON,
"main.py": _GAME3D_MAIN,
},
),
}
[docs]
def generate_project(
template_name: str,
project_name: str,
output_dir: str | Path,
) -> Path:
"""Create a new project from a template.
Args:
template_name: Key in PROJECT_TEMPLATES (e.g. "3D Game").
project_name: Name for the project.
output_dir: Directory to create the project in.
Returns:
Path to the created project directory.
"""
tmpl = PROJECT_TEMPLATES.get(template_name)
if tmpl is None:
raise ValueError(f"Unknown project template: {template_name!r}. Available: {list(PROJECT_TEMPLATES)}")
out = Path(output_dir) / _to_snake_case(project_name)
out.mkdir(parents=True, exist_ok=True)
for rel_path, content in tmpl.files.items():
content = content.replace("${project_name}", project_name)
file_path = out / rel_path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
# Create standard directories
for subdir in ("scenes", "scripts", "assets", "addons"):
(out / subdir).mkdir(exist_ok=True)
log.info("Created project '%s' at %s", project_name, out)
return out