758 lines
26 KiB
GDScript
758 lines
26 KiB
GDScript
@tool
|
|
extends "res://addons/godot_mcp/commands/base_command.gd"
|
|
|
|
const PropertyParser := preload("res://addons/godot_mcp/utils/property_parser.gd")
|
|
|
|
|
|
func get_commands() -> Dictionary:
|
|
return {
|
|
"setup_collision": _setup_collision,
|
|
"set_physics_layers": _set_physics_layers,
|
|
"get_physics_layers": _get_physics_layers,
|
|
"add_raycast": _add_raycast,
|
|
"setup_physics_body": _setup_physics_body,
|
|
"get_collision_info": _get_collision_info,
|
|
}
|
|
|
|
|
|
## Determine if a node (or its ancestors) lives in a 2D or 3D context.
|
|
## Returns "2d", "3d", or "" if undetermined.
|
|
func _detect_dimension(node: Node) -> String:
|
|
if node is Node2D or node is Control:
|
|
return "2d"
|
|
if node is Node3D:
|
|
return "3d"
|
|
# Walk up the tree
|
|
var parent := node.get_parent()
|
|
while parent != null:
|
|
if parent is Node2D or parent is Control:
|
|
return "2d"
|
|
if parent is Node3D:
|
|
return "3d"
|
|
parent = parent.get_parent()
|
|
return ""
|
|
|
|
|
|
func _setup_collision(params: Dictionary) -> Dictionary:
|
|
var result := require_string(params, "node_path")
|
|
if result[1] != null:
|
|
return result[1]
|
|
var node_path: String = result[0]
|
|
|
|
var result2 := require_string(params, "shape")
|
|
if result2[1] != null:
|
|
return result2[1]
|
|
var shape_name: String = result2[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, "Use get_scene_tree to see available nodes")
|
|
|
|
var dim := _detect_dimension(node)
|
|
if dim.is_empty():
|
|
# Allow explicit override
|
|
dim = optional_string(params, "dimension", "2d")
|
|
|
|
# Validate parent can have collision children
|
|
var valid_parents_2d := ["PhysicsBody2D", "Area2D", "StaticBody2D", "CharacterBody2D", "RigidBody2D", "AnimatableBody2D"]
|
|
var valid_parents_3d := ["PhysicsBody3D", "Area3D", "StaticBody3D", "CharacterBody3D", "RigidBody3D", "AnimatableBody3D"]
|
|
|
|
var is_valid_parent := false
|
|
if dim == "2d":
|
|
for vp: String in valid_parents_2d:
|
|
if node.is_class(vp):
|
|
is_valid_parent = true
|
|
break
|
|
else:
|
|
for vp: String in valid_parents_3d:
|
|
if node.is_class(vp):
|
|
is_valid_parent = true
|
|
break
|
|
|
|
if not is_valid_parent:
|
|
return error_invalid_params("Node '%s' (%s) is not a physics body or area. CollisionShape should be added to a PhysicsBody or Area node." % [node_path, node.get_class()])
|
|
|
|
# Create shape resource
|
|
var shape: Resource = null
|
|
var child_name := "CollisionShape"
|
|
|
|
if dim == "2d":
|
|
match shape_name:
|
|
"rectangle", "rect":
|
|
shape = RectangleShape2D.new()
|
|
var w: float = float(params.get("width", 32.0))
|
|
var h: float = float(params.get("height", 32.0))
|
|
shape.size = Vector2(w, h)
|
|
"circle":
|
|
shape = CircleShape2D.new()
|
|
shape.radius = float(params.get("radius", 16.0))
|
|
"capsule":
|
|
shape = CapsuleShape2D.new()
|
|
shape.radius = float(params.get("radius", 16.0))
|
|
shape.height = float(params.get("height", 40.0))
|
|
"segment":
|
|
shape = SegmentShape2D.new()
|
|
shape.a = Vector2(float(params.get("ax", 0.0)), float(params.get("ay", 0.0)))
|
|
shape.b = Vector2(float(params.get("bx", 32.0)), float(params.get("by", 0.0)))
|
|
"custom":
|
|
# ConvexPolygonShape2D — expects "points" as array of [x,y] pairs
|
|
shape = ConvexPolygonShape2D.new()
|
|
var points_data: Array = params.get("points", [])
|
|
var pool: PackedVector2Array = PackedVector2Array()
|
|
for p: Variant in points_data:
|
|
if p is Array and p.size() >= 2:
|
|
pool.append(Vector2(float(p[0]), float(p[1])))
|
|
if pool.size() >= 3:
|
|
shape.points = pool
|
|
_:
|
|
return error_invalid_params("Unknown 2D shape: '%s'. Available: rectangle, circle, capsule, segment, custom" % shape_name)
|
|
|
|
var collision_node := CollisionShape2D.new()
|
|
collision_node.shape = shape
|
|
collision_node.name = child_name
|
|
|
|
var disabled: bool = optional_bool(params, "disabled", false)
|
|
collision_node.disabled = disabled
|
|
var one_way: bool = optional_bool(params, "one_way_collision", false)
|
|
collision_node.one_way_collision = one_way
|
|
|
|
var undo_redo := get_undo_redo()
|
|
undo_redo.create_action("MCP: Add CollisionShape2D to %s" % node.name)
|
|
undo_redo.add_do_method(node, "add_child", collision_node)
|
|
undo_redo.add_do_method(collision_node, "set_owner", root)
|
|
undo_redo.add_do_reference(collision_node)
|
|
undo_redo.add_undo_method(node, "remove_child", collision_node)
|
|
undo_redo.commit_action()
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(collision_node)),
|
|
"shape_type": shape.get_class(),
|
|
"dimension": "2D",
|
|
})
|
|
|
|
else:
|
|
# 3D shapes
|
|
match shape_name:
|
|
"box", "rectangle", "rect":
|
|
shape = BoxShape3D.new()
|
|
var sx: float = float(params.get("width", 1.0))
|
|
var sy: float = float(params.get("height", 1.0))
|
|
var sz: float = float(params.get("depth", 1.0))
|
|
shape.size = Vector3(sx, sy, sz)
|
|
"sphere", "circle":
|
|
shape = SphereShape3D.new()
|
|
shape.radius = float(params.get("radius", 0.5))
|
|
"capsule":
|
|
shape = CapsuleShape3D.new()
|
|
shape.radius = float(params.get("radius", 0.5))
|
|
shape.height = float(params.get("height", 2.0))
|
|
"cylinder":
|
|
shape = CylinderShape3D.new()
|
|
shape.radius = float(params.get("radius", 0.5))
|
|
shape.height = float(params.get("height", 2.0))
|
|
"convex", "custom":
|
|
shape = ConvexPolygonShape3D.new()
|
|
var points_data: Array = params.get("points", [])
|
|
var pool: PackedVector3Array = PackedVector3Array()
|
|
for p: Variant in points_data:
|
|
if p is Array and p.size() >= 3:
|
|
pool.append(Vector3(float(p[0]), float(p[1]), float(p[2])))
|
|
if pool.size() >= 4:
|
|
shape.points = pool
|
|
_:
|
|
return error_invalid_params("Unknown 3D shape: '%s'. Available: box, sphere, capsule, cylinder, convex" % shape_name)
|
|
|
|
var collision_node := CollisionShape3D.new()
|
|
collision_node.shape = shape
|
|
collision_node.name = child_name
|
|
|
|
var disabled: bool = optional_bool(params, "disabled", false)
|
|
collision_node.disabled = disabled
|
|
|
|
var undo_redo := get_undo_redo()
|
|
undo_redo.create_action("MCP: Add CollisionShape3D to %s" % node.name)
|
|
undo_redo.add_do_method(node, "add_child", collision_node)
|
|
undo_redo.add_do_method(collision_node, "set_owner", root)
|
|
undo_redo.add_do_reference(collision_node)
|
|
undo_redo.add_undo_method(node, "remove_child", collision_node)
|
|
undo_redo.commit_action()
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(collision_node)),
|
|
"shape_type": shape.get_class(),
|
|
"dimension": "3D",
|
|
})
|
|
|
|
|
|
func _get_layer_name(dim: String, layer_index: int) -> String:
|
|
var setting_key := "layer_names/%s_physics/layer_%d" % [dim, layer_index]
|
|
if ProjectSettings.has_setting(setting_key):
|
|
var name_val: Variant = ProjectSettings.get_setting(setting_key)
|
|
if name_val is String and not (name_val as String).is_empty():
|
|
return name_val as String
|
|
return ""
|
|
|
|
|
|
func _layer_bitmask_to_info(bitmask: int, dim: String) -> Array:
|
|
var layers: Array = []
|
|
for i in range(1, 33):
|
|
if bitmask & (1 << (i - 1)):
|
|
var layer_name := _get_layer_name(dim, i)
|
|
var entry: Dictionary = {"layer": i}
|
|
if not layer_name.is_empty():
|
|
entry["name"] = layer_name
|
|
layers.append(entry)
|
|
return layers
|
|
|
|
|
|
func _set_physics_layers(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, "Use get_scene_tree to see available nodes")
|
|
|
|
# Check node has collision_layer/collision_mask properties
|
|
if not "collision_layer" in node:
|
|
return error_invalid_params("Node '%s' (%s) does not have collision_layer property" % [node_path, node.get_class()])
|
|
|
|
var undo_redo := get_undo_redo()
|
|
undo_redo.create_action("MCP: Set physics layers on %s" % node.name)
|
|
|
|
var changes: Dictionary = {}
|
|
|
|
if params.has("collision_layer"):
|
|
var old_layer: int = node.get("collision_layer")
|
|
var new_layer: int = _parse_layer_value(params["collision_layer"])
|
|
undo_redo.add_do_property(node, "collision_layer", new_layer)
|
|
undo_redo.add_undo_property(node, "collision_layer", old_layer)
|
|
changes["collision_layer"] = new_layer
|
|
|
|
if params.has("collision_mask"):
|
|
var old_mask: int = node.get("collision_mask")
|
|
var new_mask: int = _parse_layer_value(params["collision_mask"])
|
|
undo_redo.add_do_property(node, "collision_mask", new_mask)
|
|
undo_redo.add_undo_property(node, "collision_mask", old_mask)
|
|
changes["collision_mask"] = new_mask
|
|
|
|
if changes.is_empty():
|
|
return error_invalid_params("Must provide collision_layer and/or collision_mask")
|
|
|
|
undo_redo.commit_action()
|
|
|
|
var dim := _detect_dimension(node)
|
|
if dim.is_empty():
|
|
dim = "2d"
|
|
|
|
var result_data: Dictionary = {
|
|
"node_path": str(root.get_path_to(node)),
|
|
}
|
|
if changes.has("collision_layer"):
|
|
result_data["collision_layer"] = changes["collision_layer"]
|
|
result_data["collision_layer_info"] = _layer_bitmask_to_info(changes["collision_layer"], dim)
|
|
if changes.has("collision_mask"):
|
|
result_data["collision_mask"] = changes["collision_mask"]
|
|
result_data["collision_mask_info"] = _layer_bitmask_to_info(changes["collision_mask"], dim)
|
|
|
|
return success(result_data)
|
|
|
|
|
|
## Parse layer value: can be an int bitmask, or an array of layer numbers [1, 3, 5]
|
|
func _parse_layer_value(value: Variant) -> int:
|
|
if value is int or value is float:
|
|
return int(value)
|
|
if value is Array:
|
|
var bitmask: int = 0
|
|
for layer_num: Variant in value:
|
|
var n: int = int(layer_num)
|
|
if n >= 1 and n <= 32:
|
|
bitmask |= (1 << (n - 1))
|
|
return bitmask
|
|
# Try parsing as int
|
|
return int(value)
|
|
|
|
|
|
func _get_physics_layers(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, "Use get_scene_tree to see available nodes")
|
|
|
|
if not "collision_layer" in node:
|
|
return error_invalid_params("Node '%s' (%s) does not have collision_layer property" % [node_path, node.get_class()])
|
|
|
|
var layer: int = node.get("collision_layer")
|
|
var mask: int = node.get("collision_mask")
|
|
|
|
var dim := _detect_dimension(node)
|
|
if dim.is_empty():
|
|
dim = "2d"
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(node)),
|
|
"type": node.get_class(),
|
|
"collision_layer": layer,
|
|
"collision_layer_info": _layer_bitmask_to_info(layer, dim),
|
|
"collision_mask": mask,
|
|
"collision_mask_info": _layer_bitmask_to_info(mask, dim),
|
|
})
|
|
|
|
|
|
func _add_raycast(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, "Use get_scene_tree to see available nodes")
|
|
|
|
var dim := _detect_dimension(node)
|
|
if dim.is_empty():
|
|
dim = optional_string(params, "dimension", "2d")
|
|
|
|
var ray_name: String = optional_string(params, "name", "RayCast")
|
|
var enabled: bool = optional_bool(params, "enabled", true)
|
|
var collision_mask: int = optional_int(params, "collision_mask", 1)
|
|
var collide_with_areas: bool = optional_bool(params, "collide_with_areas", false)
|
|
var collide_with_bodies: bool = optional_bool(params, "collide_with_bodies", true)
|
|
var hit_from_inside: bool = optional_bool(params, "hit_from_inside", false)
|
|
|
|
var undo_redo := get_undo_redo()
|
|
|
|
if dim == "2d":
|
|
var ray := RayCast2D.new()
|
|
ray.name = ray_name
|
|
ray.enabled = enabled
|
|
ray.collision_mask = collision_mask
|
|
ray.collide_with_areas = collide_with_areas
|
|
ray.collide_with_bodies = collide_with_bodies
|
|
ray.hit_from_inside = hit_from_inside
|
|
|
|
var tx: float = float(params.get("target_x", 0.0))
|
|
var ty: float = float(params.get("target_y", 50.0))
|
|
ray.target_position = Vector2(tx, ty)
|
|
|
|
undo_redo.create_action("MCP: Add RayCast2D to %s" % node.name)
|
|
undo_redo.add_do_method(node, "add_child", ray)
|
|
undo_redo.add_do_method(ray, "set_owner", root)
|
|
undo_redo.add_do_reference(ray)
|
|
undo_redo.add_undo_method(node, "remove_child", ray)
|
|
undo_redo.commit_action()
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(ray)),
|
|
"type": "RayCast2D",
|
|
"target_position": "Vector2(%s, %s)" % [tx, ty],
|
|
"collision_mask": collision_mask,
|
|
})
|
|
|
|
else:
|
|
var ray := RayCast3D.new()
|
|
ray.name = ray_name
|
|
ray.enabled = enabled
|
|
ray.collision_mask = collision_mask
|
|
ray.collide_with_areas = collide_with_areas
|
|
ray.collide_with_bodies = collide_with_bodies
|
|
ray.hit_from_inside = hit_from_inside
|
|
|
|
var tx: float = float(params.get("target_x", 0.0))
|
|
var ty: float = float(params.get("target_y", -1.0))
|
|
var tz: float = float(params.get("target_z", 0.0))
|
|
ray.target_position = Vector3(tx, ty, tz)
|
|
|
|
undo_redo.create_action("MCP: Add RayCast3D to %s" % node.name)
|
|
undo_redo.add_do_method(node, "add_child", ray)
|
|
undo_redo.add_do_method(ray, "set_owner", root)
|
|
undo_redo.add_do_reference(ray)
|
|
undo_redo.add_undo_method(node, "remove_child", ray)
|
|
undo_redo.commit_action()
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(ray)),
|
|
"type": "RayCast3D",
|
|
"target_position": "Vector3(%s, %s, %s)" % [tx, ty, tz],
|
|
"collision_mask": collision_mask,
|
|
})
|
|
|
|
|
|
func _setup_physics_body(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, "Use get_scene_tree to see available nodes")
|
|
|
|
var undo_redo := get_undo_redo()
|
|
undo_redo.create_action("MCP: Setup physics body %s" % node.name)
|
|
|
|
var applied: Dictionary = {}
|
|
|
|
if node is CharacterBody2D or node is CharacterBody3D:
|
|
# CharacterBody properties
|
|
if params.has("floor_stop_on_slope"):
|
|
var old_val: bool = node.floor_stop_on_slope
|
|
var new_val: bool = bool(params["floor_stop_on_slope"])
|
|
undo_redo.add_do_property(node, "floor_stop_on_slope", new_val)
|
|
undo_redo.add_undo_property(node, "floor_stop_on_slope", old_val)
|
|
applied["floor_stop_on_slope"] = new_val
|
|
|
|
if params.has("floor_max_angle"):
|
|
var old_val: float = node.floor_max_angle
|
|
var new_val: float = float(params["floor_max_angle"])
|
|
undo_redo.add_do_property(node, "floor_max_angle", new_val)
|
|
undo_redo.add_undo_property(node, "floor_max_angle", old_val)
|
|
applied["floor_max_angle"] = new_val
|
|
|
|
if params.has("floor_snap_length"):
|
|
var old_val: float = node.floor_snap_length
|
|
var new_val: float = float(params["floor_snap_length"])
|
|
undo_redo.add_do_property(node, "floor_snap_length", new_val)
|
|
undo_redo.add_undo_property(node, "floor_snap_length", old_val)
|
|
applied["floor_snap_length"] = new_val
|
|
|
|
if params.has("wall_min_slide_angle"):
|
|
var old_val: float = node.wall_min_slide_angle
|
|
var new_val: float = float(params["wall_min_slide_angle"])
|
|
undo_redo.add_do_property(node, "wall_min_slide_angle", new_val)
|
|
undo_redo.add_undo_property(node, "wall_min_slide_angle", old_val)
|
|
applied["wall_min_slide_angle"] = new_val
|
|
|
|
if params.has("motion_mode"):
|
|
var mode_str: String = str(params["motion_mode"])
|
|
var mode_val: int = 0
|
|
if node is CharacterBody2D:
|
|
match mode_str.to_lower():
|
|
"grounded":
|
|
mode_val = CharacterBody2D.MOTION_MODE_GROUNDED
|
|
"floating":
|
|
mode_val = CharacterBody2D.MOTION_MODE_FLOATING
|
|
_:
|
|
mode_val = int(params["motion_mode"])
|
|
else:
|
|
match mode_str.to_lower():
|
|
"grounded":
|
|
mode_val = CharacterBody3D.MOTION_MODE_GROUNDED
|
|
"floating":
|
|
mode_val = CharacterBody3D.MOTION_MODE_FLOATING
|
|
_:
|
|
mode_val = int(params["motion_mode"])
|
|
var old_val: int = node.motion_mode
|
|
undo_redo.add_do_property(node, "motion_mode", mode_val)
|
|
undo_redo.add_undo_property(node, "motion_mode", old_val)
|
|
applied["motion_mode"] = mode_str
|
|
|
|
if params.has("max_slides"):
|
|
var old_val: int = node.max_slides
|
|
var new_val: int = int(params["max_slides"])
|
|
undo_redo.add_do_property(node, "max_slides", new_val)
|
|
undo_redo.add_undo_property(node, "max_slides", old_val)
|
|
applied["max_slides"] = new_val
|
|
|
|
if params.has("slide_on_ceiling"):
|
|
var old_val: bool = node.slide_on_ceiling
|
|
var new_val: bool = bool(params["slide_on_ceiling"])
|
|
undo_redo.add_do_property(node, "slide_on_ceiling", new_val)
|
|
undo_redo.add_undo_property(node, "slide_on_ceiling", old_val)
|
|
applied["slide_on_ceiling"] = new_val
|
|
|
|
elif node is RigidBody2D or node is RigidBody3D:
|
|
# RigidBody properties
|
|
if params.has("mass"):
|
|
var old_val: float = node.mass
|
|
var new_val: float = float(params["mass"])
|
|
undo_redo.add_do_property(node, "mass", new_val)
|
|
undo_redo.add_undo_property(node, "mass", old_val)
|
|
applied["mass"] = new_val
|
|
|
|
if params.has("gravity_scale"):
|
|
var old_val: float = node.gravity_scale
|
|
var new_val: float = float(params["gravity_scale"])
|
|
undo_redo.add_do_property(node, "gravity_scale", new_val)
|
|
undo_redo.add_undo_property(node, "gravity_scale", old_val)
|
|
applied["gravity_scale"] = new_val
|
|
|
|
if params.has("linear_damp"):
|
|
var old_val: float = node.linear_damp
|
|
var new_val: float = float(params["linear_damp"])
|
|
undo_redo.add_do_property(node, "linear_damp", new_val)
|
|
undo_redo.add_undo_property(node, "linear_damp", old_val)
|
|
applied["linear_damp"] = new_val
|
|
|
|
if params.has("angular_damp"):
|
|
var old_val: float = node.angular_damp
|
|
var new_val: float = float(params["angular_damp"])
|
|
undo_redo.add_do_property(node, "angular_damp", new_val)
|
|
undo_redo.add_undo_property(node, "angular_damp", old_val)
|
|
applied["angular_damp"] = new_val
|
|
|
|
if params.has("freeze"):
|
|
var old_val: bool = node.freeze
|
|
var new_val: bool = bool(params["freeze"])
|
|
undo_redo.add_do_property(node, "freeze", new_val)
|
|
undo_redo.add_undo_property(node, "freeze", old_val)
|
|
applied["freeze"] = new_val
|
|
|
|
if params.has("freeze_mode"):
|
|
var mode_str: String = str(params["freeze_mode"])
|
|
var mode_val: int = 0
|
|
if node is RigidBody2D:
|
|
match mode_str.to_lower():
|
|
"static":
|
|
mode_val = RigidBody2D.FREEZE_MODE_STATIC
|
|
"kinematic":
|
|
mode_val = RigidBody2D.FREEZE_MODE_KINEMATIC
|
|
_:
|
|
mode_val = int(params["freeze_mode"])
|
|
else:
|
|
match mode_str.to_lower():
|
|
"static":
|
|
mode_val = RigidBody3D.FREEZE_MODE_STATIC
|
|
"kinematic":
|
|
mode_val = RigidBody3D.FREEZE_MODE_KINEMATIC
|
|
_:
|
|
mode_val = int(params["freeze_mode"])
|
|
var old_val: int = node.freeze_mode
|
|
undo_redo.add_do_property(node, "freeze_mode", mode_val)
|
|
undo_redo.add_undo_property(node, "freeze_mode", old_val)
|
|
applied["freeze_mode"] = mode_str
|
|
|
|
if params.has("continuous_cd"):
|
|
if node is RigidBody2D:
|
|
var ccd_str: String = str(params["continuous_cd"])
|
|
var ccd_val: int = 0
|
|
match ccd_str.to_lower():
|
|
"disabled":
|
|
ccd_val = RigidBody2D.CCD_MODE_DISABLED
|
|
"cast_ray":
|
|
ccd_val = RigidBody2D.CCD_MODE_CAST_RAY
|
|
"cast_shape":
|
|
ccd_val = RigidBody2D.CCD_MODE_CAST_SHAPE
|
|
_:
|
|
ccd_val = int(params["continuous_cd"])
|
|
var old_val: int = node.continuous_cd
|
|
undo_redo.add_do_property(node, "continuous_cd", ccd_val)
|
|
undo_redo.add_undo_property(node, "continuous_cd", old_val)
|
|
applied["continuous_cd"] = ccd_str
|
|
else:
|
|
var old_val: bool = node.continuous_cd
|
|
var new_val: bool = bool(params["continuous_cd"])
|
|
undo_redo.add_do_property(node, "continuous_cd", new_val)
|
|
undo_redo.add_undo_property(node, "continuous_cd", old_val)
|
|
applied["continuous_cd"] = new_val
|
|
|
|
if params.has("contact_monitor"):
|
|
var old_val: bool = node.contact_monitor
|
|
var new_val: bool = bool(params["contact_monitor"])
|
|
undo_redo.add_do_property(node, "contact_monitor", new_val)
|
|
undo_redo.add_undo_property(node, "contact_monitor", old_val)
|
|
applied["contact_monitor"] = new_val
|
|
|
|
if params.has("max_contacts_reported"):
|
|
var old_val: int = node.max_contacts_reported
|
|
var new_val: int = int(params["max_contacts_reported"])
|
|
undo_redo.add_do_property(node, "max_contacts_reported", new_val)
|
|
undo_redo.add_undo_property(node, "max_contacts_reported", old_val)
|
|
applied["max_contacts_reported"] = new_val
|
|
|
|
elif node is StaticBody2D or node is StaticBody3D or node is AnimatableBody2D or node is AnimatableBody3D:
|
|
# StaticBody / AnimatableBody shared properties
|
|
if params.has("physics_material_override"):
|
|
# We just note it — use add_resource for complex resource assignment
|
|
return error_invalid_params("Use add_resource to set physics_material_override")
|
|
else:
|
|
return error_invalid_params("Node '%s' (%s) is not a recognized physics body type. Supported: CharacterBody2D/3D, RigidBody2D/3D, StaticBody2D/3D, AnimatableBody2D/3D" % [node_path, node.get_class()])
|
|
|
|
if applied.is_empty():
|
|
undo_redo.commit_action()
|
|
return error_invalid_params("No valid properties provided for %s" % node.get_class())
|
|
|
|
undo_redo.commit_action()
|
|
|
|
return success({
|
|
"node_path": str(root.get_path_to(node)),
|
|
"type": node.get_class(),
|
|
"applied": applied,
|
|
})
|
|
|
|
|
|
func _get_collision_info(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, "Use get_scene_tree to see available nodes")
|
|
|
|
var include_children: bool = optional_bool(params, "include_children", true)
|
|
|
|
var info: Dictionary = {
|
|
"node_path": str(root.get_path_to(node)),
|
|
"type": node.get_class(),
|
|
}
|
|
|
|
# Collect physics body properties
|
|
if "collision_layer" in node:
|
|
var dim := _detect_dimension(node)
|
|
if dim.is_empty():
|
|
dim = "2d"
|
|
info["collision_layer"] = node.get("collision_layer")
|
|
info["collision_layer_info"] = _layer_bitmask_to_info(int(node.get("collision_layer")), dim)
|
|
info["collision_mask"] = node.get("collision_mask")
|
|
info["collision_mask_info"] = _layer_bitmask_to_info(int(node.get("collision_mask")), dim)
|
|
|
|
# Collect body-specific properties
|
|
if node is CharacterBody2D or node is CharacterBody3D:
|
|
info["body_settings"] = {
|
|
"motion_mode": node.motion_mode,
|
|
"floor_stop_on_slope": node.floor_stop_on_slope,
|
|
"floor_max_angle": node.floor_max_angle,
|
|
"floor_snap_length": node.floor_snap_length,
|
|
"wall_min_slide_angle": node.wall_min_slide_angle,
|
|
"max_slides": node.max_slides,
|
|
"slide_on_ceiling": node.slide_on_ceiling,
|
|
}
|
|
elif node is RigidBody2D or node is RigidBody3D:
|
|
info["body_settings"] = {
|
|
"mass": node.mass,
|
|
"gravity_scale": node.gravity_scale,
|
|
"linear_damp": node.linear_damp,
|
|
"angular_damp": node.angular_damp,
|
|
"freeze": node.freeze,
|
|
"freeze_mode": node.freeze_mode,
|
|
"contact_monitor": node.contact_monitor,
|
|
"max_contacts_reported": node.max_contacts_reported,
|
|
}
|
|
|
|
# Collect collision shapes
|
|
var shapes: Array = []
|
|
var raycasts: Array = []
|
|
var nodes_to_check: Array = [node]
|
|
|
|
if include_children:
|
|
var queue: Array = [node]
|
|
while queue.size() > 0:
|
|
var current: Node = queue.pop_front()
|
|
for child_idx in current.get_child_count():
|
|
var child := current.get_child(child_idx)
|
|
nodes_to_check.append(child)
|
|
queue.append(child)
|
|
|
|
for check_node: Node in nodes_to_check:
|
|
if check_node is CollisionShape2D:
|
|
var shape_info: Dictionary = {
|
|
"node_path": str(root.get_path_to(check_node)),
|
|
"disabled": check_node.disabled,
|
|
"one_way_collision": check_node.one_way_collision,
|
|
}
|
|
if check_node.shape != null:
|
|
shape_info["shape_type"] = check_node.shape.get_class()
|
|
if check_node.shape is RectangleShape2D:
|
|
shape_info["size"] = "Vector2(%s, %s)" % [check_node.shape.size.x, check_node.shape.size.y]
|
|
elif check_node.shape is CircleShape2D:
|
|
shape_info["radius"] = check_node.shape.radius
|
|
elif check_node.shape is CapsuleShape2D:
|
|
shape_info["radius"] = check_node.shape.radius
|
|
shape_info["height"] = check_node.shape.height
|
|
shapes.append(shape_info)
|
|
|
|
elif check_node is CollisionShape3D:
|
|
var shape_info: Dictionary = {
|
|
"node_path": str(root.get_path_to(check_node)),
|
|
"disabled": check_node.disabled,
|
|
}
|
|
if check_node.shape != null:
|
|
shape_info["shape_type"] = check_node.shape.get_class()
|
|
if check_node.shape is BoxShape3D:
|
|
shape_info["size"] = "Vector3(%s, %s, %s)" % [check_node.shape.size.x, check_node.shape.size.y, check_node.shape.size.z]
|
|
elif check_node.shape is SphereShape3D:
|
|
shape_info["radius"] = check_node.shape.radius
|
|
elif check_node.shape is CapsuleShape3D:
|
|
shape_info["radius"] = check_node.shape.radius
|
|
shape_info["height"] = check_node.shape.height
|
|
elif check_node.shape is CylinderShape3D:
|
|
shape_info["radius"] = check_node.shape.radius
|
|
shape_info["height"] = check_node.shape.height
|
|
shapes.append(shape_info)
|
|
|
|
elif check_node is CollisionPolygon2D:
|
|
shapes.append({
|
|
"node_path": str(root.get_path_to(check_node)),
|
|
"shape_type": "CollisionPolygon2D",
|
|
"disabled": check_node.disabled,
|
|
"one_way_collision": check_node.one_way_collision,
|
|
"polygon_points": check_node.polygon.size(),
|
|
})
|
|
|
|
elif check_node is CollisionPolygon3D:
|
|
shapes.append({
|
|
"node_path": str(root.get_path_to(check_node)),
|
|
"shape_type": "CollisionPolygon3D",
|
|
"disabled": check_node.disabled,
|
|
"polygon_points": check_node.polygon.size(),
|
|
})
|
|
|
|
elif check_node is RayCast2D:
|
|
raycasts.append({
|
|
"node_path": str(root.get_path_to(check_node)),
|
|
"type": "RayCast2D",
|
|
"enabled": check_node.enabled,
|
|
"target_position": "Vector2(%s, %s)" % [check_node.target_position.x, check_node.target_position.y],
|
|
"collision_mask": check_node.collision_mask,
|
|
"collide_with_areas": check_node.collide_with_areas,
|
|
"collide_with_bodies": check_node.collide_with_bodies,
|
|
})
|
|
|
|
elif check_node is RayCast3D:
|
|
raycasts.append({
|
|
"node_path": str(root.get_path_to(check_node)),
|
|
"type": "RayCast3D",
|
|
"enabled": check_node.enabled,
|
|
"target_position": "Vector3(%s, %s, %s)" % [check_node.target_position.x, check_node.target_position.y, check_node.target_position.z],
|
|
"collision_mask": check_node.collision_mask,
|
|
"collide_with_areas": check_node.collide_with_areas,
|
|
"collide_with_bodies": check_node.collide_with_bodies,
|
|
})
|
|
|
|
info["collision_shapes"] = shapes
|
|
info["raycasts"] = raycasts
|
|
|
|
return success(info)
|