- Introduced StageGizmos for hover highlighting, selection outlines, and rotation handles in the sandbox stage builder. - Added StageGrid for an optional world-space grid overlay that adjusts with camera panning and zooming. - Implemented StageSelection for geometric hit-testing and selection management of nodes in the sandbox. - Created StageSpawner as a registry-driven factory for spawning terrain, props, and stickmen, allowing for dynamic template management. - Each script includes necessary constants, state management, and public API methods for interaction.
645 lines
19 KiB
GDScript
645 lines
19 KiB
GDScript
class_name SandboxStage
|
|
extends Node2D
|
|
## SandboxStage - Kid-friendly director sandbox stage (Phase 2).
|
|
##
|
|
## A self-contained visual stage for placing terrain, props and stickmen from a
|
|
## palette, with EDIT (build) and PLAY (simulate) modes. Selection uses geometric
|
|
## hit-testing; objects are dragged directly (no move handle) and rotated via a
|
|
## ring handle. A placement ghost previews the object under the cursor, and an
|
|
## optional snap-to-grid + grid overlay ease placement. NOT wired into the editor
|
|
## - run standalone via F6 on res://scenes/sandbox_stage.tscn.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Preloaded helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd")
|
|
const STAGE_SELECTION := preload("res://scripts/stage_selection.gd")
|
|
const STAGE_GIZMOS := preload("res://scripts/stage_gizmos.gd")
|
|
const STAGE_GRID := preload("res://scripts/stage_grid.gd")
|
|
const STICKMAN_RIG := preload("res://scripts/stickman_rig.gd")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Enums
|
|
# ---------------------------------------------------------------------------
|
|
|
|
enum StageMode { EDIT, PLAY }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
signal mode_changed(mode: int)
|
|
signal object_placed(node: Node2D)
|
|
signal object_selected(nodes: Array[Node2D])
|
|
signal object_deselected()
|
|
signal object_deleted(nodes: Array[Node2D])
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
const ZOOM_STEP: float = 1.10
|
|
const MIN_BOX_AREA: float = 16.0
|
|
const RAGDOLL_CONTAINER_NAME := "RagdollBodyContainer"
|
|
|
|
const SETTINGS_PATH := "user://sandbox_settings.json"
|
|
const DEFAULT_GRID_SIZE := 15.0
|
|
const MIN_GRID_SIZE := 1.0
|
|
const MAX_GRID_SIZE := 100.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Exported properties
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@export var min_zoom: float = 0.1
|
|
@export var max_zoom: float = 6.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Node references
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@onready var _camera: Camera2D = $Camera2D
|
|
@onready var _world: Node2D = $World
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State
|
|
# ---------------------------------------------------------------------------
|
|
|
|
var current_mode: StageMode = StageMode.EDIT
|
|
|
|
var _spawner: StageSpawner
|
|
var _selection: StageSelection
|
|
var _gizmos: StageGizmos
|
|
var _grid: StageGrid
|
|
|
|
var _placement_id: String = ""
|
|
|
|
var _mode_button: Button
|
|
var _palette_buttons: Dictionary = {}
|
|
var _status_label: Label
|
|
var _grid_check: CheckBox
|
|
var _snap_check: CheckBox
|
|
var _grid_size_spin: SpinBox
|
|
|
|
var _grid_size: float = DEFAULT_GRID_SIZE
|
|
var _snap_enabled: bool = false
|
|
var _show_grid: bool = true
|
|
|
|
var _panning: bool = false
|
|
var _pan_last: Vector2 = Vector2.ZERO
|
|
|
|
var _box_selecting: bool = false
|
|
var _box_start: Vector2 = Vector2.ZERO
|
|
var _box_rect: Rect2 = Rect2()
|
|
|
|
var _ghost: Node2D = null
|
|
var _ghost_holder: Node2D = null
|
|
|
|
## Authored object state (instance id -> {node, position, rotation}), updated on
|
|
## every place/move/rotate and restored when returning to EDIT.
|
|
var _authored: Dictionary = {}
|
|
|
|
## Physics frames remaining before re-asserting the authored restore (a safety
|
|
## net for engines where a same-frame freeze + teleport does not stick).
|
|
var _restore_frames_left: int = 0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _ready() -> void:
|
|
_camera.make_current()
|
|
_load_settings()
|
|
|
|
_spawner = STAGE_SPAWNER.new(_world)
|
|
_selection = STAGE_SELECTION.new(_world, _camera)
|
|
_selection.selection_changed.connect(_on_selection_changed)
|
|
_selection.hover_changed.connect(_on_hover_changed)
|
|
|
|
_build_grid_layer()
|
|
_build_gizmo_layer()
|
|
_build_ghost_holder()
|
|
_build_ui()
|
|
_apply_grid_settings()
|
|
_refresh_status()
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if _ghost != null and is_instance_valid(_ghost) and current_mode == StageMode.EDIT:
|
|
_update_ghost_position()
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
if _restore_frames_left > 0:
|
|
_restore_frames_left -= 1
|
|
_restore_authored_state()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Input
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if event is InputEventMouseButton:
|
|
_handle_mouse_button(event)
|
|
elif event is InputEventMouseMotion:
|
|
_handle_mouse_motion(event)
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event is InputEventMouseButton:
|
|
_handle_world_click(event)
|
|
|
|
|
|
func _unhandled_key_input(event: InputEvent) -> void:
|
|
if not (event is InputEventKey) or not event.pressed or event.echo:
|
|
return
|
|
match event.keycode:
|
|
KEY_DELETE, KEY_BACKSPACE:
|
|
if current_mode == StageMode.EDIT:
|
|
delete_selected()
|
|
KEY_ESCAPE:
|
|
if _placement_id != "":
|
|
set_placement_mode("")
|
|
elif not _selection.get_selected().is_empty():
|
|
_selection.clear_selection()
|
|
|
|
|
|
func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
|
match mb.button_index:
|
|
MOUSE_BUTTON_WHEEL_UP:
|
|
if mb.pressed:
|
|
_set_zoom(_camera.zoom.x * ZOOM_STEP)
|
|
MOUSE_BUTTON_WHEEL_DOWN:
|
|
if mb.pressed:
|
|
_set_zoom(_camera.zoom.x / ZOOM_STEP)
|
|
MOUSE_BUTTON_MIDDLE:
|
|
_panning = mb.pressed
|
|
_pan_last = mb.position
|
|
|
|
|
|
func _handle_world_click(mb: InputEventMouseButton) -> void:
|
|
if mb.button_index != MOUSE_BUTTON_LEFT:
|
|
return
|
|
if current_mode != StageMode.EDIT:
|
|
return
|
|
var world_pos := _camera.get_global_mouse_position()
|
|
if mb.pressed:
|
|
if _gizmos.hit_test(world_pos) != STAGE_GIZMOS.Handle.NONE:
|
|
_gizmos.begin_drag(world_pos)
|
|
elif _placement_id != "":
|
|
_place_at(world_pos)
|
|
else:
|
|
_begin_click_select(world_pos, mb.shift_pressed)
|
|
else:
|
|
if _gizmos.is_dragging():
|
|
_gizmos.end_drag()
|
|
elif _box_selecting:
|
|
_finish_box_select(mb.shift_pressed)
|
|
|
|
|
|
func _handle_mouse_motion(mm: InputEventMouseMotion) -> void:
|
|
if _panning:
|
|
_camera.position -= mm.relative / _camera.zoom.x
|
|
return
|
|
if current_mode != StageMode.EDIT:
|
|
return
|
|
var world_pos := _camera.get_global_mouse_position()
|
|
if _gizmos.is_dragging():
|
|
_gizmos.drag_to(world_pos)
|
|
elif _box_selecting:
|
|
_box_rect = Rect2(_box_start, world_pos - _box_start)
|
|
_gizmos.set_box_rect(_box_rect)
|
|
elif not _is_mouse_over_ui():
|
|
_selection.update_hover(world_pos)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mode management
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func set_mode(mode: StageMode) -> void:
|
|
if mode == current_mode:
|
|
return
|
|
current_mode = mode
|
|
if mode == StageMode.EDIT:
|
|
_enter_edit_mode()
|
|
else:
|
|
_enter_play_mode()
|
|
mode_changed.emit(int(mode))
|
|
_refresh_status()
|
|
|
|
|
|
func _enter_edit_mode() -> void:
|
|
# Snap stickmen straight back to their standing pose/position (no stand-up
|
|
# tween glide).
|
|
for node: Node2D in _world_children_selectable():
|
|
if node is STICKMAN_RIG:
|
|
(node as STICKMAN_RIG).snap_to_standing()
|
|
# Freeze every prop (kinematic) so nothing keeps falling in EDIT — including
|
|
# any object that may not be in the authored map. freeze_mode is set BEFORE
|
|
# freeze so the body freezes directly as kinematic, never via the static
|
|
# layer (whose transform sync can drop a subsequent position/rotation set).
|
|
for node: Node2D in _world_children_selectable():
|
|
if node is RigidBody2D:
|
|
var body := node as RigidBody2D
|
|
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
|
body.freeze = true
|
|
_restore_authored_state()
|
|
# The freeze/teleport can take a physics frame or two to settle in the engine,
|
|
# so re-assert the authored transform on the next few physics frames.
|
|
_restore_frames_left = 3
|
|
_gizmos.set_enabled(true)
|
|
|
|
|
|
func _enter_play_mode() -> void:
|
|
_gizmos.set_enabled(false)
|
|
_selection.clear_selection()
|
|
set_placement_mode("")
|
|
for node: Node2D in _world_children_selectable():
|
|
if node is RigidBody2D:
|
|
(node as RigidBody2D).freeze = false
|
|
for node: Node2D in _world_children_selectable():
|
|
if node is STICKMAN_RIG:
|
|
var rig := node as STICKMAN_RIG
|
|
rig.auto_recover = false
|
|
rig.set_ragdoll(true)
|
|
|
|
|
|
func _save_object_state(node: Node2D) -> void:
|
|
_authored[node.get_instance_id()] = {
|
|
"node": node,
|
|
"position": node.global_position,
|
|
"rotation": node.global_rotation,
|
|
}
|
|
|
|
|
|
func _clear_object_state(node: Node2D) -> void:
|
|
_authored.erase(node.get_instance_id())
|
|
|
|
|
|
func _restore_authored_state() -> void:
|
|
for id: int in _authored.keys():
|
|
var entry: Dictionary = _authored[id]
|
|
var node := entry.get("node") as Node2D
|
|
if node == null or not is_instance_valid(node):
|
|
_authored.erase(id)
|
|
continue
|
|
node.global_position = entry.get("position", node.global_position)
|
|
node.global_rotation = entry.get("rotation", node.global_rotation)
|
|
if node is RigidBody2D:
|
|
var body := node as RigidBody2D
|
|
body.linear_velocity = Vector2.ZERO
|
|
body.angular_velocity = 0.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Placement
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func set_placement_mode(id: String) -> void:
|
|
_placement_id = id
|
|
for pid: String in _palette_buttons:
|
|
var btn: Button = _palette_buttons[pid]
|
|
btn.set_pressed_no_signal(pid == id)
|
|
if id == "":
|
|
_free_ghost()
|
|
else:
|
|
_spawn_ghost()
|
|
|
|
|
|
func _place_at(world_pos: Vector2) -> void:
|
|
if _snap_enabled:
|
|
world_pos = _snap_to_grid(world_pos)
|
|
var node := _spawner.spawn(_placement_id, world_pos)
|
|
if node == null:
|
|
return
|
|
if current_mode == StageMode.EDIT and node is RigidBody2D:
|
|
var body := node as RigidBody2D
|
|
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
|
body.freeze = true
|
|
_save_object_state(node)
|
|
object_placed.emit(node)
|
|
_refresh_status()
|
|
_spawn_ghost()
|
|
|
|
|
|
func _spawn_ghost() -> void:
|
|
_free_ghost()
|
|
if _placement_id == "":
|
|
return
|
|
var ghost := _spawner.spawn(_placement_id, Vector2.ZERO)
|
|
if ghost == null:
|
|
return
|
|
# Reparent out of World so the ghost is not selectable/counted.
|
|
if ghost.get_parent() == _world:
|
|
_world.remove_child(ghost)
|
|
_ghost_holder.add_child(ghost)
|
|
ghost.modulate = Color(1.0, 1.0, 1.0, 0.5)
|
|
if ghost is RigidBody2D:
|
|
var body := ghost as RigidBody2D
|
|
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
|
body.freeze = true
|
|
body.collision_layer = 0
|
|
body.collision_mask = 0
|
|
elif ghost is StaticBody2D:
|
|
var sb := ghost as StaticBody2D
|
|
sb.collision_layer = 0
|
|
sb.collision_mask = 0
|
|
elif ghost is STICKMAN_RIG:
|
|
# Freeze the ghost rig into a static standing figure: disable IK solving
|
|
# and animation so its limbs don't flex/follow anything while the ghost
|
|
# tracks the cursor.
|
|
var rig := ghost as STICKMAN_RIG
|
|
var skeleton := rig.get_node_or_null(NodePath("Skeleton2D")) as Skeleton2D
|
|
if skeleton != null and skeleton.modification_stack != null:
|
|
skeleton.modification_stack.enabled = false
|
|
var anim := rig.get_node_or_null(NodePath("AnimationPlayer")) as AnimationPlayer
|
|
if anim != null:
|
|
anim.stop()
|
|
_ghost = ghost
|
|
_update_ghost_position()
|
|
|
|
|
|
func _free_ghost() -> void:
|
|
if _ghost != null and is_instance_valid(_ghost):
|
|
_ghost.queue_free()
|
|
_ghost = null
|
|
|
|
|
|
func _update_ghost_position() -> void:
|
|
if _ghost == null or not is_instance_valid(_ghost):
|
|
return
|
|
var pos := _camera.get_global_mouse_position() + _spawner.get_spawn_offset(_placement_id)
|
|
if _snap_enabled:
|
|
pos = _snap_to_grid(pos)
|
|
_ghost.position = pos
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Selection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _begin_click_select(world_pos: Vector2, shift: bool) -> void:
|
|
var hit := _selection.hit_test(world_pos)
|
|
if hit != null:
|
|
if shift:
|
|
_selection.toggle_selection(hit)
|
|
if _selection.is_selected(hit):
|
|
_gizmos.begin_translate_drag(hit, world_pos)
|
|
else:
|
|
_selection.select_only(hit)
|
|
_gizmos.begin_translate_drag(hit, world_pos)
|
|
else:
|
|
_box_selecting = true
|
|
_box_start = world_pos
|
|
_box_rect = Rect2(world_pos, Vector2.ZERO)
|
|
_gizmos.set_box_rect(_box_rect)
|
|
|
|
|
|
func _finish_box_select(shift: bool) -> void:
|
|
_box_selecting = false
|
|
_gizmos.set_box_rect(Rect2())
|
|
var rect := _box_rect.abs()
|
|
if rect.get_area() < MIN_BOX_AREA:
|
|
if not shift:
|
|
_selection.clear_selection()
|
|
else:
|
|
_selection.box_select(rect, shift)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Deletion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func delete_selected() -> void:
|
|
var selected := _selection.get_selected().duplicate()
|
|
if selected.is_empty():
|
|
return
|
|
for node: Node2D in selected:
|
|
if is_instance_valid(node):
|
|
_clear_object_state(node)
|
|
node.queue_free()
|
|
_selection.clear_selection()
|
|
object_deleted.emit(selected)
|
|
_refresh_status()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Status / UI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _refresh_status() -> void:
|
|
if _status_label == null:
|
|
return
|
|
var mode_text := "EDIT" if current_mode == StageMode.EDIT else "PLAY"
|
|
var count := _world_children_selectable().size()
|
|
var sel := _selection.get_selected()
|
|
var sel_text: String
|
|
if sel.is_empty():
|
|
sel_text = "none"
|
|
elif sel.size() == 1:
|
|
sel_text = String(sel[0].name)
|
|
else:
|
|
sel_text = "%d objects" % sel.size()
|
|
_status_label.text = "Mode: %s | Objects: %d | Selected: %s" % [mode_text, count, sel_text]
|
|
if _mode_button != null:
|
|
_mode_button.set_pressed_no_signal(current_mode == StageMode.PLAY)
|
|
_mode_button.text = "Play" if current_mode == StageMode.EDIT else "Edit"
|
|
|
|
|
|
func _build_ui() -> void:
|
|
var ui := CanvasLayer.new()
|
|
ui.name = "UI"
|
|
add_child(ui)
|
|
|
|
var top_bar := PanelContainer.new()
|
|
top_bar.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE)
|
|
top_bar.offset_bottom = 40.0
|
|
ui.add_child(top_bar)
|
|
|
|
var hbox := HBoxContainer.new()
|
|
hbox.add_theme_constant_override("separation", 8)
|
|
top_bar.add_child(hbox)
|
|
|
|
_mode_button = Button.new()
|
|
_mode_button.toggle_mode = true
|
|
_mode_button.toggled.connect(_on_mode_toggled)
|
|
hbox.add_child(_mode_button)
|
|
|
|
for id: String in _spawner.get_spawnable_ids():
|
|
var btn := Button.new()
|
|
btn.text = _spawner.get_label(id)
|
|
btn.toggle_mode = true
|
|
btn.toggled.connect(_on_palette_toggled.bind(id))
|
|
hbox.add_child(btn)
|
|
_palette_buttons[id] = btn
|
|
|
|
_grid_check = CheckBox.new()
|
|
_grid_check.text = "Grid"
|
|
_grid_check.button_pressed = _show_grid
|
|
_grid_check.toggled.connect(_on_grid_toggled)
|
|
hbox.add_child(_grid_check)
|
|
|
|
_snap_check = CheckBox.new()
|
|
_snap_check.text = "Snap"
|
|
_snap_check.button_pressed = _snap_enabled
|
|
_snap_check.toggled.connect(_on_snap_toggled)
|
|
hbox.add_child(_snap_check)
|
|
|
|
var grid_label := Label.new()
|
|
grid_label.text = "Size"
|
|
hbox.add_child(grid_label)
|
|
|
|
_grid_size_spin = SpinBox.new()
|
|
_grid_size_spin.min_value = MIN_GRID_SIZE
|
|
_grid_size_spin.max_value = MAX_GRID_SIZE
|
|
_grid_size_spin.step = 1.0
|
|
_grid_size_spin.value = _grid_size
|
|
_grid_size_spin.rounded = true
|
|
_grid_size_spin.value_changed.connect(_on_grid_size_changed)
|
|
hbox.add_child(_grid_size_spin)
|
|
|
|
_status_label = Label.new()
|
|
_status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
hbox.add_child(_status_label)
|
|
|
|
|
|
func _build_gizmo_layer() -> void:
|
|
_gizmos = STAGE_GIZMOS.new()
|
|
_gizmos.name = "GizmoLayer"
|
|
_gizmos.camera = _camera
|
|
_gizmos.transform_committed.connect(_on_transform_committed)
|
|
add_child(_gizmos)
|
|
|
|
|
|
func _build_grid_layer() -> void:
|
|
_grid = STAGE_GRID.new()
|
|
_grid.name = "GridLayer"
|
|
_grid.camera = _camera
|
|
add_child(_grid)
|
|
move_child(_grid, 0)
|
|
|
|
|
|
func _build_ghost_holder() -> void:
|
|
_ghost_holder = Node2D.new()
|
|
_ghost_holder.name = "PlacementGhost"
|
|
add_child(_ghost_holder)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signal handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _on_mode_toggled(pressed: bool) -> void:
|
|
set_mode(StageMode.PLAY if pressed else StageMode.EDIT)
|
|
|
|
|
|
func _on_palette_toggled(pressed: bool, id: String) -> void:
|
|
if pressed:
|
|
set_placement_mode(id)
|
|
elif _placement_id == id:
|
|
set_placement_mode("")
|
|
|
|
|
|
func _on_grid_toggled(pressed: bool) -> void:
|
|
_show_grid = pressed
|
|
_apply_grid_settings()
|
|
_save_settings()
|
|
|
|
|
|
func _on_snap_toggled(pressed: bool) -> void:
|
|
_snap_enabled = pressed
|
|
_apply_grid_settings()
|
|
_save_settings()
|
|
|
|
|
|
func _on_grid_size_changed(value: float) -> void:
|
|
_grid_size = clampf(value, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
|
_apply_grid_settings()
|
|
_save_settings()
|
|
|
|
|
|
func _on_selection_changed(nodes: Array) -> void:
|
|
_gizmos.set_target(_selection.get_primary())
|
|
if nodes.is_empty():
|
|
object_deselected.emit()
|
|
else:
|
|
object_selected.emit(nodes)
|
|
_refresh_status()
|
|
|
|
|
|
func _on_hover_changed(node: Node2D) -> void:
|
|
_gizmos.set_hover(node)
|
|
|
|
|
|
func _on_transform_committed(node: Node2D) -> void:
|
|
_save_object_state(node)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _set_zoom(value: float) -> void:
|
|
var z := clampf(value, min_zoom, max_zoom)
|
|
_camera.zoom = Vector2(z, z)
|
|
|
|
|
|
func _is_mouse_over_ui() -> bool:
|
|
return get_viewport().gui_get_hovered_control() != null
|
|
|
|
|
|
func _snap_to_grid(v: Vector2) -> Vector2:
|
|
if _grid_size <= 0.0:
|
|
return v
|
|
return Vector2(roundf(v.x / _grid_size) * _grid_size, roundf(v.y / _grid_size) * _grid_size)
|
|
|
|
|
|
func _apply_grid_settings() -> void:
|
|
if _grid != null:
|
|
_grid.grid_size = _grid_size
|
|
_grid.enabled = _show_grid
|
|
if _gizmos != null:
|
|
_gizmos.snap_size = _grid_size if _snap_enabled else 0.0
|
|
|
|
|
|
## Direct Node2D children of World, excluding the ragdoll body container.
|
|
func _world_children_selectable() -> Array[Node2D]:
|
|
var result: Array[Node2D] = []
|
|
for child: Node in _world.get_children():
|
|
var node := child as Node2D
|
|
if node == null:
|
|
continue
|
|
if node.name == RAGDOLL_CONTAINER_NAME:
|
|
continue
|
|
result.append(node)
|
|
return result
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Settings persistence
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _load_settings() -> void:
|
|
if not FileAccess.file_exists(SETTINGS_PATH):
|
|
return
|
|
var file := FileAccess.open(SETTINGS_PATH, FileAccess.READ)
|
|
if file == null:
|
|
return
|
|
var json: Variant = JSON.parse_string(file.get_as_text())
|
|
file.close()
|
|
if not json is Dictionary:
|
|
return
|
|
var d := json as Dictionary
|
|
_grid_size = float(d.get("grid_size", DEFAULT_GRID_SIZE))
|
|
_grid_size = clampf(_grid_size, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
|
_snap_enabled = bool(d.get("snap_to_grid", false))
|
|
_show_grid = bool(d.get("show_grid", true))
|
|
|
|
|
|
func _save_settings() -> void:
|
|
var data := {
|
|
"version": "1.0",
|
|
"grid_size": _grid_size,
|
|
"snap_to_grid": _snap_enabled,
|
|
"show_grid": _show_grid,
|
|
}
|
|
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
|
if file:
|
|
file.store_string(JSON.stringify(data, "\t", false))
|
|
file.close()
|