将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
681 lines
25 KiB
GDScript
681 lines
25 KiB
GDScript
@tool
|
|
extends "res://addons/godot_mcp/commands/base_command.gd"
|
|
|
|
const PropertyParser := preload("res://addons/godot_mcp/utils/property_parser.gd")
|
|
const NodeUtils := preload("res://addons/godot_mcp/utils/node_utils.gd")
|
|
|
|
|
|
func get_commands() -> Dictionary:
|
|
return {
|
|
"add_mesh_instance": _add_mesh_instance,
|
|
"setup_lighting": _setup_lighting,
|
|
"set_material_3d": _set_material_3d,
|
|
"setup_environment": _setup_environment,
|
|
"setup_camera_3d": _setup_camera_3d,
|
|
"add_gridmap": _add_gridmap,
|
|
}
|
|
|
|
|
|
## ─── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
func _optional_float(params: Dictionary, key: String, default: float) -> float:
|
|
if params.has(key):
|
|
return float(params[key])
|
|
return default
|
|
|
|
|
|
func _parse_color_param(params: Dictionary, key: String, default: Color) -> Color:
|
|
if not params.has(key):
|
|
return default
|
|
var val: Variant = params[key]
|
|
if val is String:
|
|
return PropertyParser.parse_value(val, TYPE_COLOR)
|
|
if val is Dictionary:
|
|
return Color(
|
|
float(val.get("r", default.r)),
|
|
float(val.get("g", default.g)),
|
|
float(val.get("b", default.b)),
|
|
float(val.get("a", default.a))
|
|
)
|
|
return default
|
|
|
|
|
|
func _parse_vector3_param(params: Dictionary, key: String, default: Vector3) -> Vector3:
|
|
if not params.has(key):
|
|
return default
|
|
var val: Variant = params[key]
|
|
if val is String:
|
|
return PropertyParser.parse_value(val, TYPE_VECTOR3)
|
|
if val is Dictionary:
|
|
return Vector3(
|
|
float(val.get("x", default.x)),
|
|
float(val.get("y", default.y)),
|
|
float(val.get("z", default.z))
|
|
)
|
|
if val is Array and val.size() >= 3:
|
|
return Vector3(float(val[0]), float(val[1]), float(val[2]))
|
|
return default
|
|
|
|
|
|
func _add_child_with_undo(node: Node, parent: Node, root: Node, action_name: String) -> void:
|
|
var undo_redo := get_undo_redo()
|
|
undo_redo.create_action(action_name)
|
|
undo_redo.add_do_method(parent, "add_child", node)
|
|
undo_redo.add_do_method(node, "set_owner", root)
|
|
undo_redo.add_do_reference(node)
|
|
undo_redo.add_undo_method(parent, "remove_child", node)
|
|
undo_redo.commit_action()
|
|
|
|
|
|
## ─── 1. add_mesh_instance ──────────────────────────────────────────────────
|
|
|
|
func _add_mesh_instance(params: Dictionary) -> Dictionary:
|
|
var root := get_edited_root()
|
|
if root == null:
|
|
return error_no_scene()
|
|
|
|
var parent_path: String = optional_string(params, "parent_path", ".")
|
|
var parent := find_node_by_path(parent_path)
|
|
if parent == null:
|
|
return error_not_found("Parent node '%s'" % parent_path)
|
|
|
|
var node_name: String = optional_string(params, "name", "MeshInstance3D")
|
|
var mesh_type: String = optional_string(params, "mesh_type", "")
|
|
var mesh_file: String = optional_string(params, "mesh_file", "")
|
|
|
|
if mesh_type.is_empty() and mesh_file.is_empty():
|
|
return error_invalid_params("Either 'mesh_type' or 'mesh_file' is required")
|
|
|
|
var mesh_instance := MeshInstance3D.new()
|
|
mesh_instance.name = node_name
|
|
|
|
if not mesh_file.is_empty():
|
|
# Load .glb / .gltf / .obj
|
|
if not ResourceLoader.exists(mesh_file):
|
|
mesh_instance.queue_free()
|
|
return error_not_found("Mesh file '%s'" % mesh_file, "Provide a valid res:// path to .glb, .gltf, or .obj")
|
|
var loaded: Resource = load(mesh_file)
|
|
if loaded is Mesh:
|
|
mesh_instance.mesh = loaded as Mesh
|
|
elif loaded is PackedScene:
|
|
# For .glb/.gltf we instantiate and steal the first MeshInstance3D's mesh
|
|
var scene_instance: Node = (loaded as PackedScene).instantiate()
|
|
var found_mesh: Mesh = null
|
|
var search_nodes: Array[Node] = [scene_instance]
|
|
while not search_nodes.is_empty():
|
|
var n: Node = search_nodes.pop_front()
|
|
if n is MeshInstance3D and (n as MeshInstance3D).mesh != null:
|
|
found_mesh = (n as MeshInstance3D).mesh
|
|
break
|
|
for child in n.get_children():
|
|
search_nodes.append(child)
|
|
scene_instance.queue_free()
|
|
if found_mesh == null:
|
|
mesh_instance.queue_free()
|
|
return error_invalid_params("No mesh found in '%s'" % mesh_file)
|
|
mesh_instance.mesh = found_mesh
|
|
else:
|
|
mesh_instance.queue_free()
|
|
return error_invalid_params("'%s' is not a Mesh or PackedScene" % mesh_file)
|
|
else:
|
|
# Primitive mesh
|
|
var mesh_classes := {
|
|
"BoxMesh": BoxMesh,
|
|
"SphereMesh": SphereMesh,
|
|
"CylinderMesh": CylinderMesh,
|
|
"CapsuleMesh": CapsuleMesh,
|
|
"PlaneMesh": PlaneMesh,
|
|
"PrismMesh": PrismMesh,
|
|
"TorusMesh": TorusMesh,
|
|
"QuadMesh": QuadMesh,
|
|
}
|
|
if not mesh_classes.has(mesh_type):
|
|
mesh_instance.queue_free()
|
|
return error_invalid_params("Unknown mesh_type '%s'. Available: %s" % [mesh_type, mesh_classes.keys()])
|
|
var mesh_res: Mesh = mesh_classes[mesh_type].new()
|
|
# Apply mesh properties if provided
|
|
var mesh_properties: Dictionary = params.get("mesh_properties", {})
|
|
for prop_name: String in mesh_properties:
|
|
if prop_name in mesh_res:
|
|
var current: Variant = mesh_res.get(prop_name)
|
|
mesh_res.set(prop_name, PropertyParser.parse_value(mesh_properties[prop_name], typeof(current)))
|
|
mesh_instance.mesh = mesh_res
|
|
|
|
# Transform
|
|
var position := _parse_vector3_param(params, "position", Vector3.ZERO)
|
|
var rotation_deg := _parse_vector3_param(params, "rotation", Vector3.ZERO)
|
|
var scale_vec := _parse_vector3_param(params, "scale", Vector3.ONE)
|
|
|
|
mesh_instance.position = position
|
|
mesh_instance.rotation_degrees = rotation_deg
|
|
mesh_instance.scale = scale_vec
|
|
|
|
_add_child_with_undo(mesh_instance, parent, root, "MCP: Add MeshInstance3D")
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(mesh_instance)),
|
|
"name": str(mesh_instance.name),
|
|
"mesh_type": mesh_type if mesh_file.is_empty() else mesh_file,
|
|
})
|
|
|
|
|
|
## ─── 2. setup_lighting ────────────────────────────────────────────────────
|
|
|
|
func _setup_lighting(params: Dictionary) -> Dictionary:
|
|
var root := get_edited_root()
|
|
if root == null:
|
|
return error_no_scene()
|
|
|
|
var parent_path: String = optional_string(params, "parent_path", ".")
|
|
var parent := find_node_by_path(parent_path)
|
|
if parent == null:
|
|
return error_not_found("Parent node '%s'" % parent_path)
|
|
|
|
var light_type: String = optional_string(params, "light_type", "")
|
|
var preset: String = optional_string(params, "preset", "")
|
|
var node_name: String = optional_string(params, "name", "")
|
|
|
|
# Preset configurations
|
|
if not preset.is_empty():
|
|
match preset:
|
|
"sun":
|
|
light_type = "DirectionalLight3D"
|
|
if node_name.is_empty():
|
|
node_name = "SunLight"
|
|
"indoor":
|
|
light_type = "OmniLight3D"
|
|
if node_name.is_empty():
|
|
node_name = "IndoorLight"
|
|
"dramatic":
|
|
light_type = "SpotLight3D"
|
|
if node_name.is_empty():
|
|
node_name = "DramaticLight"
|
|
_:
|
|
return error_invalid_params("Unknown preset '%s'. Available: sun, indoor, dramatic" % preset)
|
|
|
|
if light_type.is_empty():
|
|
return error_invalid_params("Either 'light_type' or 'preset' is required")
|
|
|
|
var light: Light3D
|
|
match light_type:
|
|
"DirectionalLight3D":
|
|
light = DirectionalLight3D.new()
|
|
"OmniLight3D":
|
|
light = OmniLight3D.new()
|
|
"SpotLight3D":
|
|
light = SpotLight3D.new()
|
|
_:
|
|
return error_invalid_params("Unknown light_type '%s'. Available: DirectionalLight3D, OmniLight3D, SpotLight3D" % light_type)
|
|
|
|
if node_name.is_empty():
|
|
node_name = light_type
|
|
light.name = node_name
|
|
|
|
# Common properties
|
|
light.light_color = _parse_color_param(params, "color", Color.WHITE)
|
|
light.light_energy = _optional_float(params, "energy", 1.0)
|
|
light.shadow_enabled = optional_bool(params, "shadows", false)
|
|
|
|
# Type-specific properties
|
|
if light is OmniLight3D:
|
|
var omni: OmniLight3D = light as OmniLight3D
|
|
omni.omni_range = _optional_float(params, "range", 5.0)
|
|
omni.omni_attenuation = _optional_float(params, "attenuation", 1.0)
|
|
elif light is SpotLight3D:
|
|
var spot: SpotLight3D = light as SpotLight3D
|
|
spot.spot_range = _optional_float(params, "range", 5.0)
|
|
spot.spot_attenuation = _optional_float(params, "attenuation", 1.0)
|
|
spot.spot_angle = _optional_float(params, "spot_angle", 45.0)
|
|
spot.spot_angle_attenuation = _optional_float(params, "spot_angle_attenuation", 1.0)
|
|
|
|
# Apply preset defaults after type creation
|
|
if not preset.is_empty():
|
|
match preset:
|
|
"sun":
|
|
light.light_energy = _optional_float(params, "energy", 1.0)
|
|
light.shadow_enabled = optional_bool(params, "shadows", true)
|
|
light.rotation_degrees = _parse_vector3_param(params, "rotation", Vector3(-45, -30, 0))
|
|
"indoor":
|
|
light.light_energy = _optional_float(params, "energy", 0.8)
|
|
light.light_color = _parse_color_param(params, "color", Color(1.0, 0.95, 0.85))
|
|
if light is OmniLight3D:
|
|
(light as OmniLight3D).omni_range = _optional_float(params, "range", 8.0)
|
|
"dramatic":
|
|
light.light_energy = _optional_float(params, "energy", 2.0)
|
|
light.shadow_enabled = optional_bool(params, "shadows", true)
|
|
if light is SpotLight3D:
|
|
(light as SpotLight3D).spot_angle = _optional_float(params, "spot_angle", 25.0)
|
|
(light as SpotLight3D).spot_range = _optional_float(params, "range", 10.0)
|
|
|
|
# Position / rotation
|
|
light.position = _parse_vector3_param(params, "position", Vector3.ZERO)
|
|
if params.has("rotation"):
|
|
light.rotation_degrees = _parse_vector3_param(params, "rotation", light.rotation_degrees)
|
|
|
|
_add_child_with_undo(light, parent, root, "MCP: Add %s" % light_type)
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(light)),
|
|
"name": str(light.name),
|
|
"light_type": light_type,
|
|
"preset": preset,
|
|
})
|
|
|
|
|
|
## ─── 3. set_material_3d ───────────────────────────────────────────────────
|
|
|
|
func _set_material_3d(params: Dictionary) -> Dictionary:
|
|
var result := require_string(params, "node_path")
|
|
if result[1] != null:
|
|
return result[1]
|
|
var node_path: String = result[0]
|
|
|
|
var root := get_edited_root()
|
|
if root == null:
|
|
return error_no_scene()
|
|
|
|
var node := find_node_by_path(node_path)
|
|
if node == null:
|
|
return error_not_found("Node '%s'" % node_path)
|
|
|
|
if not node is MeshInstance3D:
|
|
return error_invalid_params("Node '%s' is not a MeshInstance3D (is %s)" % [node_path, node.get_class()])
|
|
|
|
var mesh_inst: MeshInstance3D = node as MeshInstance3D
|
|
var surface_index: int = optional_int(params, "surface_index", 0)
|
|
|
|
var mat := StandardMaterial3D.new()
|
|
|
|
# Albedo
|
|
mat.albedo_color = _parse_color_param(params, "albedo_color", Color.WHITE)
|
|
if params.has("albedo_texture"):
|
|
var tex_path: String = params["albedo_texture"]
|
|
if ResourceLoader.exists(tex_path):
|
|
mat.albedo_texture = load(tex_path) as Texture2D
|
|
|
|
# PBR
|
|
mat.metallic = _optional_float(params, "metallic", 0.0)
|
|
mat.roughness = _optional_float(params, "roughness", 1.0)
|
|
if params.has("metallic_texture"):
|
|
var tex_path: String = params["metallic_texture"]
|
|
if ResourceLoader.exists(tex_path):
|
|
mat.metallic_texture = load(tex_path) as Texture2D
|
|
if params.has("roughness_texture"):
|
|
var tex_path: String = params["roughness_texture"]
|
|
if ResourceLoader.exists(tex_path):
|
|
mat.roughness_texture = load(tex_path) as Texture2D
|
|
if params.has("normal_texture"):
|
|
mat.normal_enabled = true
|
|
var tex_path: String = params["normal_texture"]
|
|
if ResourceLoader.exists(tex_path):
|
|
mat.normal_texture = load(tex_path) as Texture2D
|
|
|
|
# Emission
|
|
if params.has("emission") or params.has("emission_color"):
|
|
mat.emission_enabled = true
|
|
mat.emission = _parse_color_param(params, "emission", _parse_color_param(params, "emission_color", Color.BLACK))
|
|
mat.emission_energy_multiplier = _optional_float(params, "emission_energy", 1.0)
|
|
if params.has("emission_texture"):
|
|
mat.emission_enabled = true
|
|
var tex_path: String = params["emission_texture"]
|
|
if ResourceLoader.exists(tex_path):
|
|
mat.emission_texture = load(tex_path) as Texture2D
|
|
|
|
# Transparency
|
|
if params.has("transparency"):
|
|
var transparency_val: String = str(params["transparency"])
|
|
match transparency_val.to_upper():
|
|
"DISABLED", "0":
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
|
|
"ALPHA", "1":
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
"ALPHA_SCISSOR", "2":
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
|
"ALPHA_HASH", "3":
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_HASH
|
|
"ALPHA_DEPTH_PRE_PASS", "4":
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS
|
|
|
|
# Cull mode
|
|
if params.has("cull_mode"):
|
|
var cull_val: String = str(params["cull_mode"])
|
|
match cull_val.to_upper():
|
|
"BACK", "0":
|
|
mat.cull_mode = BaseMaterial3D.CULL_BACK
|
|
"FRONT", "1":
|
|
mat.cull_mode = BaseMaterial3D.CULL_FRONT
|
|
"DISABLED", "2":
|
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
|
|
# Apply
|
|
var old_mat: Material = mesh_inst.get_surface_override_material(surface_index)
|
|
var undo_redo := get_undo_redo()
|
|
undo_redo.create_action("MCP: Set material on %s" % mesh_inst.name)
|
|
undo_redo.add_do_method(mesh_inst, "set_surface_override_material", surface_index, mat)
|
|
undo_redo.add_undo_method(mesh_inst, "set_surface_override_material", surface_index, old_mat)
|
|
undo_redo.commit_action()
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(mesh_inst)),
|
|
"surface_index": surface_index,
|
|
"albedo_color": str(mat.albedo_color),
|
|
"metallic": mat.metallic,
|
|
"roughness": mat.roughness,
|
|
})
|
|
|
|
|
|
## ─── 4. setup_environment ─────────────────────────────────────────────────
|
|
|
|
func _setup_environment(params: Dictionary) -> Dictionary:
|
|
var root := get_edited_root()
|
|
if root == null:
|
|
return error_no_scene()
|
|
|
|
var parent_path: String = optional_string(params, "parent_path", ".")
|
|
var parent := find_node_by_path(parent_path)
|
|
if parent == null:
|
|
return error_not_found("Parent node '%s'" % parent_path)
|
|
|
|
var node_name: String = optional_string(params, "name", "WorldEnvironment")
|
|
|
|
# Check if a WorldEnvironment already exists at the target
|
|
var node_path: String = optional_string(params, "node_path", "")
|
|
var world_env: WorldEnvironment = null
|
|
var is_existing := false
|
|
|
|
if not node_path.is_empty():
|
|
var existing := find_node_by_path(node_path)
|
|
if existing != null and existing is WorldEnvironment:
|
|
world_env = existing as WorldEnvironment
|
|
is_existing = true
|
|
|
|
if world_env == null:
|
|
world_env = WorldEnvironment.new()
|
|
world_env.name = node_name
|
|
|
|
var env: Environment = world_env.environment
|
|
if env == null:
|
|
env = Environment.new()
|
|
|
|
# Background / Sky
|
|
var bg_mode: String = optional_string(params, "background_mode", "sky")
|
|
match bg_mode.to_lower():
|
|
"sky":
|
|
env.background_mode = Environment.BG_SKY
|
|
"color":
|
|
env.background_mode = Environment.BG_COLOR
|
|
env.background_color = _parse_color_param(params, "background_color", Color(0.3, 0.3, 0.3))
|
|
"canvas":
|
|
env.background_mode = Environment.BG_CANVAS
|
|
"clear_color":
|
|
env.background_mode = Environment.BG_CLEAR_COLOR
|
|
|
|
# Procedural sky
|
|
if params.has("sky") and params["sky"] is Dictionary:
|
|
var sky_params: Dictionary = params["sky"]
|
|
var sky_mat := ProceduralSkyMaterial.new()
|
|
sky_mat.sky_top_color = _parse_color_param(sky_params, "sky_top_color", Color(0.385, 0.454, 0.55))
|
|
sky_mat.sky_horizon_color = _parse_color_param(sky_params, "sky_horizon_color", Color(0.646, 0.654, 0.67))
|
|
sky_mat.ground_bottom_color = _parse_color_param(sky_params, "ground_bottom_color", Color(0.2, 0.169, 0.133))
|
|
sky_mat.ground_horizon_color = _parse_color_param(sky_params, "ground_horizon_color", Color(0.646, 0.654, 0.67))
|
|
sky_mat.sun_angle_max = _optional_float(sky_params, "sun_angle_max", 30.0) if sky_params.has("sun_angle_max") else 30.0
|
|
sky_mat.sky_curve = _optional_float(sky_params, "sky_curve", 0.15) if sky_params.has("sky_curve") else 0.15
|
|
|
|
var sky := Sky.new()
|
|
sky.sky_material = sky_mat
|
|
env.sky = sky
|
|
env.background_mode = Environment.BG_SKY
|
|
|
|
# Ambient light
|
|
if params.has("ambient_light_color"):
|
|
env.ambient_light_color = _parse_color_param(params, "ambient_light_color", Color.WHITE)
|
|
env.ambient_light_energy = _optional_float(params, "ambient_light_energy", 1.0) if params.has("ambient_light_energy") else env.ambient_light_energy
|
|
if params.has("ambient_light_source"):
|
|
var src: String = str(params["ambient_light_source"])
|
|
match src.to_upper():
|
|
"BACKGROUND", "0":
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_BG
|
|
"DISABLED", "1":
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_DISABLED
|
|
"COLOR", "2":
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
"SKY", "3":
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
|
|
|
# Tonemap
|
|
if params.has("tonemap_mode"):
|
|
var tm: String = str(params["tonemap_mode"])
|
|
match tm.to_upper():
|
|
"LINEAR", "0":
|
|
env.tonemap_mode = Environment.TONE_MAPPER_LINEAR
|
|
"REINHARDT", "1":
|
|
env.tonemap_mode = Environment.TONE_MAPPER_REINHARDT
|
|
"FILMIC", "2":
|
|
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
|
"ACES", "3":
|
|
env.tonemap_mode = Environment.TONE_MAPPER_ACES
|
|
"AGX", "4":
|
|
env.tonemap_mode = 4 # Environment.TONE_MAPPER_AGX (Godot 4.4+)
|
|
if params.has("tonemap_exposure"):
|
|
env.tonemap_exposure = _optional_float(params, "tonemap_exposure", 1.0)
|
|
if params.has("tonemap_white"):
|
|
env.tonemap_white = _optional_float(params, "tonemap_white", 1.0)
|
|
|
|
# Fog
|
|
if params.has("fog_enabled"):
|
|
env.fog_enabled = optional_bool(params, "fog_enabled", false)
|
|
if env.fog_enabled or params.has("fog_light_color"):
|
|
env.fog_light_color = _parse_color_param(params, "fog_light_color", Color(0.518, 0.553, 0.608))
|
|
env.fog_density = _optional_float(params, "fog_density", 0.01) if params.has("fog_density") else env.fog_density
|
|
env.fog_light_energy = _optional_float(params, "fog_light_energy", 1.0) if params.has("fog_light_energy") else env.fog_light_energy
|
|
|
|
# Glow
|
|
if params.has("glow_enabled"):
|
|
env.glow_enabled = optional_bool(params, "glow_enabled", false)
|
|
if env.glow_enabled:
|
|
env.glow_intensity = _optional_float(params, "glow_intensity", 0.8) if params.has("glow_intensity") else env.glow_intensity
|
|
env.glow_strength = _optional_float(params, "glow_strength", 1.0) if params.has("glow_strength") else env.glow_strength
|
|
env.glow_bloom = _optional_float(params, "glow_bloom", 0.0) if params.has("glow_bloom") else env.glow_bloom
|
|
|
|
# SSAO
|
|
if params.has("ssao_enabled"):
|
|
env.ssao_enabled = optional_bool(params, "ssao_enabled", false)
|
|
if env.ssao_enabled:
|
|
env.ssao_radius = _optional_float(params, "ssao_radius", 1.0) if params.has("ssao_radius") else env.ssao_radius
|
|
env.ssao_intensity = _optional_float(params, "ssao_intensity", 2.0) if params.has("ssao_intensity") else env.ssao_intensity
|
|
|
|
# SSR
|
|
if params.has("ssr_enabled"):
|
|
env.ssr_enabled = optional_bool(params, "ssr_enabled", false)
|
|
if env.ssr_enabled:
|
|
env.ssr_max_steps = optional_int(params, "ssr_max_steps", 64) if params.has("ssr_max_steps") else env.ssr_max_steps
|
|
env.ssr_fade_in = _optional_float(params, "ssr_fade_in", 0.15) if params.has("ssr_fade_in") else env.ssr_fade_in
|
|
env.ssr_fade_out = _optional_float(params, "ssr_fade_out", 2.0) if params.has("ssr_fade_out") else env.ssr_fade_out
|
|
|
|
# SDFGI
|
|
if params.has("sdfgi_enabled"):
|
|
env.sdfgi_enabled = optional_bool(params, "sdfgi_enabled", false)
|
|
|
|
world_env.environment = env
|
|
|
|
if not is_existing:
|
|
_add_child_with_undo(world_env, parent, root, "MCP: Add WorldEnvironment")
|
|
|
|
var features: Array = []
|
|
if env.fog_enabled: features.append("fog")
|
|
if env.glow_enabled: features.append("glow")
|
|
if env.ssao_enabled: features.append("ssao")
|
|
if env.ssr_enabled: features.append("ssr")
|
|
if env.sdfgi_enabled: features.append("sdfgi")
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(world_env)),
|
|
"name": str(world_env.name),
|
|
"background_mode": bg_mode,
|
|
"features": features,
|
|
"is_existing": is_existing,
|
|
})
|
|
|
|
|
|
## ─── 5. setup_camera_3d ──────────────────────────────────────────────────
|
|
|
|
func _setup_camera_3d(params: Dictionary) -> Dictionary:
|
|
var root := get_edited_root()
|
|
if root == null:
|
|
return error_no_scene()
|
|
|
|
var parent_path: String = optional_string(params, "parent_path", ".")
|
|
var parent := find_node_by_path(parent_path)
|
|
if parent == null:
|
|
return error_not_found("Parent node '%s'" % parent_path)
|
|
|
|
# Check if we're configuring an existing camera
|
|
var node_path: String = optional_string(params, "node_path", "")
|
|
var camera: Camera3D = null
|
|
var is_existing := false
|
|
|
|
if not node_path.is_empty():
|
|
var existing := find_node_by_path(node_path)
|
|
if existing != null and existing is Camera3D:
|
|
camera = existing as Camera3D
|
|
is_existing = true
|
|
elif existing != null:
|
|
return error_invalid_params("Node '%s' is not a Camera3D (is %s)" % [node_path, existing.get_class()])
|
|
|
|
if camera == null:
|
|
camera = Camera3D.new()
|
|
camera.name = optional_string(params, "name", "Camera3D")
|
|
|
|
# Projection
|
|
var projection_str: String = optional_string(params, "projection", "")
|
|
if not projection_str.is_empty():
|
|
match projection_str.to_lower():
|
|
"perspective", "0":
|
|
camera.projection = Camera3D.PROJECTION_PERSPECTIVE
|
|
"orthogonal", "orthographic", "1":
|
|
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
|
"frustum", "2":
|
|
camera.projection = Camera3D.PROJECTION_FRUSTUM
|
|
|
|
# Properties
|
|
if params.has("fov"):
|
|
camera.fov = _optional_float(params, "fov", 75.0)
|
|
if params.has("size"):
|
|
camera.size = _optional_float(params, "size", 1.0)
|
|
if params.has("near"):
|
|
camera.near = _optional_float(params, "near", 0.05)
|
|
if params.has("far"):
|
|
camera.far = _optional_float(params, "far", 4000.0)
|
|
if params.has("cull_mask"):
|
|
camera.cull_mask = optional_int(params, "cull_mask", 1048575)
|
|
|
|
# Make current
|
|
camera.current = optional_bool(params, "current", false)
|
|
|
|
# Transform
|
|
camera.position = _parse_vector3_param(params, "position", camera.position if is_existing else Vector3(0, 1, 3))
|
|
if params.has("rotation"):
|
|
camera.rotation_degrees = _parse_vector3_param(params, "rotation", camera.rotation_degrees)
|
|
if params.has("look_at"):
|
|
var target := _parse_vector3_param(params, "look_at", Vector3.ZERO)
|
|
# We need to set position first, then use look_at
|
|
camera.look_at(target)
|
|
|
|
# Environment override
|
|
if params.has("environment_path"):
|
|
var env_path: String = params["environment_path"]
|
|
if ResourceLoader.exists(env_path):
|
|
var env_res: Resource = load(env_path)
|
|
if env_res is Environment:
|
|
camera.environment = env_res as Environment
|
|
|
|
if not is_existing:
|
|
_add_child_with_undo(camera, parent, root, "MCP: Add Camera3D")
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(camera)),
|
|
"name": str(camera.name),
|
|
"projection": "perspective" if camera.projection == Camera3D.PROJECTION_PERSPECTIVE else "orthogonal",
|
|
"fov": camera.fov,
|
|
"position": str(camera.position),
|
|
"is_existing": is_existing,
|
|
})
|
|
|
|
|
|
## ─── 6. add_gridmap ──────────────────────────────────────────────────────
|
|
|
|
func _add_gridmap(params: Dictionary) -> Dictionary:
|
|
var root := get_edited_root()
|
|
if root == null:
|
|
return error_no_scene()
|
|
|
|
var parent_path: String = optional_string(params, "parent_path", ".")
|
|
var parent := find_node_by_path(parent_path)
|
|
if parent == null:
|
|
return error_not_found("Parent node '%s'" % parent_path)
|
|
|
|
var node_name: String = optional_string(params, "name", "GridMap")
|
|
|
|
# Check for existing GridMap to configure
|
|
var node_path: String = optional_string(params, "node_path", "")
|
|
var gridmap: GridMap = null
|
|
var is_existing := false
|
|
|
|
if not node_path.is_empty():
|
|
var existing := find_node_by_path(node_path)
|
|
if existing != null and existing is GridMap:
|
|
gridmap = existing as GridMap
|
|
is_existing = true
|
|
elif existing != null:
|
|
return error_invalid_params("Node '%s' is not a GridMap (is %s)" % [node_path, existing.get_class()])
|
|
|
|
if gridmap == null:
|
|
gridmap = GridMap.new()
|
|
gridmap.name = node_name
|
|
|
|
# Mesh library
|
|
if params.has("mesh_library_path"):
|
|
var lib_path: String = params["mesh_library_path"]
|
|
if not ResourceLoader.exists(lib_path):
|
|
if not is_existing:
|
|
gridmap.queue_free()
|
|
return error_not_found("MeshLibrary '%s'" % lib_path, "Provide a valid res:// path to a .meshlib or .tres file")
|
|
var lib: Resource = load(lib_path)
|
|
if lib is MeshLibrary:
|
|
gridmap.mesh_library = lib as MeshLibrary
|
|
else:
|
|
if not is_existing:
|
|
gridmap.queue_free()
|
|
return error_invalid_params("'%s' is not a MeshLibrary" % lib_path)
|
|
|
|
# Cell size
|
|
if params.has("cell_size"):
|
|
gridmap.cell_size = _parse_vector3_param(params, "cell_size", Vector3(2, 2, 2))
|
|
|
|
# Position
|
|
gridmap.position = _parse_vector3_param(params, "position", gridmap.position if is_existing else Vector3.ZERO)
|
|
|
|
if not is_existing:
|
|
_add_child_with_undo(gridmap, parent, root, "MCP: Add GridMap")
|
|
|
|
# Set cells
|
|
var cells: Array = params.get("cells", [])
|
|
var cells_set: int = 0
|
|
for cell in cells:
|
|
if cell is Dictionary:
|
|
var x: int = int(cell.get("x", 0))
|
|
var y: int = int(cell.get("y", 0))
|
|
var z: int = int(cell.get("z", 0))
|
|
var item: int = int(cell.get("item", 0))
|
|
var orientation: int = int(cell.get("orientation", 0))
|
|
gridmap.set_cell_item(Vector3i(x, y, z), item, orientation)
|
|
cells_set += 1
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(gridmap)),
|
|
"name": str(gridmap.name),
|
|
"cells_set": cells_set,
|
|
"is_existing": is_existing,
|
|
"has_mesh_library": gridmap.mesh_library != null,
|
|
})
|