feat(studio,stacking,animations,collections): M16 ModBison Studio, object stacking, model animations, and poly collections for MVP 2
checkpoint / checkpoint (push) Successful in 6m32s

This commit is contained in:
2026-09-22 18:30:57 +00:00
parent 654c1ec6bf
commit 721dbe5ae7
37 changed files with 5202 additions and 17 deletions
+140
View File
@@ -0,0 +1,140 @@
class_name StudioSimulationManager
extends Node
## Manages Play / Run / Stop simulation harness for ModBison Studio.
## Takes full state snapshot before play and restores on stop.
signal simulation_started(mode: String)
signal simulation_stopped
enum SimulationState { EDIT, PLAYING, PAUSED }
var state: SimulationState = SimulationState.EDIT
var _snapshot: Array[Dictionary] = []
var _spawned_avatar: Node = null
var _core: EditorCore = null
var _world_container: Node3D = null
func setup(core: EditorCore, world_container: Node3D = null) -> void:
_core = core
_world_container = world_container
func start_play(spawn_player: bool = true) -> void:
if state == SimulationState.PLAYING:
return
# 1. Capture snapshot of all objects
_capture_snapshot()
state = SimulationState.PLAYING
# 2. Spawn test avatar if requested
if spawn_player and _world_container != null:
_spawn_test_avatar()
# 3. Activate moving objects
for m in _get_moving_nodes():
if m.has_method("start_motion"):
m.start_motion()
simulation_started.emit("play" if spawn_player else "run")
func pause_simulation() -> void:
if state != SimulationState.PLAYING:
return
state = SimulationState.PAUSED
for m in _get_moving_nodes():
if m.has_method("stop_motion"):
m.stop_motion()
func resume_simulation() -> void:
if state != SimulationState.PAUSED:
return
state = SimulationState.PLAYING
for m in _get_moving_nodes():
if m.has_method("start_motion"):
m.start_motion()
func stop_simulation() -> void:
if state == SimulationState.EDIT:
return
# 1. Stop all moving objects
for m in _get_moving_nodes():
if m.has_method("stop_motion"):
m.stop_motion()
# 2. Despawn test avatar
if _spawned_avatar != null and is_instance_valid(_spawned_avatar):
_spawned_avatar.queue_free()
_spawned_avatar = null
# 3. Restore state snapshot
_restore_snapshot()
state = SimulationState.EDIT
simulation_stopped.emit()
func _get_moving_nodes() -> Array:
if is_inside_tree() and get_tree() != null:
return get_tree().get_nodes_in_group("moving_objects")
var main_loop = Engine.get_main_loop()
if main_loop is SceneTree:
return (main_loop as SceneTree).get_nodes_in_group("moving_objects")
return []
func _capture_snapshot() -> void:
_snapshot.clear()
if _core == null or _core.model == null:
return
for obj in _core.model.payload.objects:
_snapshot.append({
"id": obj.id,
"position": obj.position,
"rotation": obj.rotation,
"scale": obj.scale,
"properties": obj.properties.duplicate(true)
})
func _restore_snapshot() -> void:
if _core == null or _core.model == null:
return
for snap in _snapshot:
var obj = _core.model.find_object(snap["id"])
if obj != null:
obj.position = snap["position"]
obj.rotation = snap["rotation"]
obj.scale = snap["scale"]
obj.properties = snap["properties"].duplicate(true)
_snapshot.clear()
func _spawn_test_avatar() -> void:
var player_scene = load("res://scenes/player.tscn")
if player_scene != null:
var p = player_scene.instantiate()
p.name = "TestStudioPlayer"
p.position = Vector3(0, 2, 0)
_world_container.add_child(p)
_spawned_avatar = p
else:
var p: CharacterBody3D = CharacterBody3D.new()
p.name = "TestStudioPlayer"
p.position = Vector3(0, 2, 0)
_world_container.add_child(p)
_spawned_avatar = p
func is_playing() -> bool:
return state == SimulationState.PLAYING
func is_editing() -> bool:
return state == SimulationState.EDIT