Complete M11 Creator Studio and M12 Trust & Safety for PC5
checkpoint / checkpoint (push) Failing after 3h14m9s
checkpoint / checkpoint (push) Failing after 3h14m9s
- Creator Studio: Visual behavior & property inspector, GLB model importer, binary persistence - Trust & Safety: 18+ TOS confirmation (DOB removed), creator guidelines & briefing card on join, in-game reporting HUD - Test Harness: Add --filter, --skip-backend, --skip-godot to run_checkpoint_tests.py
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
class_name InspectorPanel
|
||||
extends VBoxContainer
|
||||
## Property editor for the selected object (docs/architecture/editor.md
|
||||
## Property and behavior editor for the selected object (docs/architecture/editor.md
|
||||
## §7.2 `InspectorPanel`).
|
||||
##
|
||||
## 2D-tile-editor scope (docs/architecture/editor.md §8: the default world
|
||||
## is flat, `CHUNK_SIZE_Z=1`): position exposes X/Y (world-plane) plus a
|
||||
## single "height" Z for props that sit above the floor, and rotation
|
||||
## exposes one yaw field (rotation about the vertical axis) rather than a
|
||||
## full 3-axis gizmo -- deliberately not the volumetric 3D editing the doc
|
||||
## says to defer.
|
||||
##
|
||||
## Pure UI: every edit calls back into `EditorCore` (`move_object` /
|
||||
## `rotate_object` / `scale_object` / `delete_object`), so inspector edits
|
||||
## go through the exact same undo/redo path as viewport edits.
|
||||
## Visual Property & Behavior Inspector (Creator Studio):
|
||||
## - Position, yaw, scale, and delete operations.
|
||||
## - Visual rule builder for ScriptBehavior rules:
|
||||
## Triggers: on_interact, on_touch, on_enter, on_timer
|
||||
## Actions: give_item (Buffalo Chips / items), rotate, move, play_sound, spawn_from_catalog
|
||||
## - Pure UI: every property & behavior edit calls back into `EditorCore`
|
||||
## (`move_object`, `rotate_object`, `scale_object`, `set_object_behavior`, `delete_object`),
|
||||
## fully supporting undo and redo.
|
||||
|
||||
signal delete_requested(object_id: String)
|
||||
|
||||
@@ -20,6 +18,11 @@ var _core: EditorCore = null
|
||||
var _object_id: String = ""
|
||||
var _updating := false # guards against feedback loops while refreshing fields
|
||||
|
||||
var _empty_label: Label
|
||||
var _scroll: ScrollContainer
|
||||
var _fields_box: VBoxContainer
|
||||
|
||||
# Property fields
|
||||
var _id_label: Label
|
||||
var _type_label: Label
|
||||
var _asset_label: Label
|
||||
@@ -31,8 +34,43 @@ var _scale_x: SpinBox
|
||||
var _scale_y: SpinBox
|
||||
var _scale_z: SpinBox
|
||||
var _delete_button: Button
|
||||
var _empty_label: Label
|
||||
var _fields_box: VBoxContainer
|
||||
|
||||
# Behavior & Rules fields
|
||||
var _rules_vbox: VBoxContainer
|
||||
var _add_rule_btn: Button
|
||||
var _rule_form_box: VBoxContainer
|
||||
var _trigger_option: OptionButton
|
||||
var _timer_row: HBoxContainer
|
||||
var _timer_interval_spin: SpinBox
|
||||
var _action_option: OptionButton
|
||||
|
||||
# Dynamic Action Parameter boxes
|
||||
var _action_item_box: VBoxContainer
|
||||
var _item_id_edit: LineEdit
|
||||
var _item_count_spin: SpinBox
|
||||
|
||||
var _action_rotate_box: VBoxContainer
|
||||
var _rotate_axis_option: OptionButton
|
||||
var _rotate_degrees_spin: SpinBox
|
||||
var _rotate_duration_spin: SpinBox
|
||||
|
||||
var _action_move_box: VBoxContainer
|
||||
var _move_x_spin: SpinBox
|
||||
var _move_y_spin: SpinBox
|
||||
var _move_z_spin: SpinBox
|
||||
var _move_duration_spin: SpinBox
|
||||
|
||||
var _action_sound_box: VBoxContainer
|
||||
var _sound_id_edit: LineEdit
|
||||
|
||||
var _action_spawn_box: VBoxContainer
|
||||
var _spawn_catalog_id_edit: LineEdit
|
||||
var _spawn_x_spin: SpinBox
|
||||
var _spawn_y_spin: SpinBox
|
||||
var _spawn_z_spin: SpinBox
|
||||
|
||||
|
||||
var _is_built := false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -41,6 +79,17 @@ func _init() -> void:
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not _is_built:
|
||||
_build_ui()
|
||||
if _object_id != "":
|
||||
show_object(_object_id)
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
if _is_built:
|
||||
return
|
||||
_is_built = true
|
||||
|
||||
var header := Label.new()
|
||||
header.text = "Inspector"
|
||||
header.add_theme_font_size_override("font_size", 15)
|
||||
@@ -51,9 +100,15 @@ func _ready() -> void:
|
||||
_empty_label.text = "(nothing selected)"
|
||||
add_child(_empty_label)
|
||||
|
||||
_scroll = ScrollContainer.new()
|
||||
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
add_child(_scroll)
|
||||
|
||||
_fields_box = VBoxContainer.new()
|
||||
_fields_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_fields_box.visible = false
|
||||
add_child(_fields_box)
|
||||
_scroll.add_child(_fields_box)
|
||||
|
||||
_id_label = _add_readonly_row("id")
|
||||
_type_label = _add_readonly_row("type")
|
||||
@@ -80,10 +135,292 @@ func _ready() -> void:
|
||||
_scale_z.value_changed.connect(_on_scale_changed)
|
||||
|
||||
_delete_button = Button.new()
|
||||
_delete_button.text = "Delete"
|
||||
_delete_button.text = "Delete Object"
|
||||
_delete_button.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||
_delete_button.pressed.connect(_on_delete_pressed)
|
||||
_fields_box.add_child(_delete_button)
|
||||
|
||||
_setup_behavior_ui()
|
||||
|
||||
|
||||
func _setup_behavior_ui() -> void:
|
||||
var sep := HSeparator.new()
|
||||
sep.custom_minimum_size = Vector2(0, 16)
|
||||
_fields_box.add_child(sep)
|
||||
|
||||
var b_header := Label.new()
|
||||
b_header.text = "Behavior & Rules"
|
||||
b_header.add_theme_font_size_override("font_size", 14)
|
||||
b_header.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
|
||||
_fields_box.add_child(b_header)
|
||||
|
||||
var b_desc := Label.new()
|
||||
b_desc.text = "Attach interactive logic and triggers."
|
||||
b_desc.add_theme_font_size_override("font_size", 11)
|
||||
b_desc.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||
_fields_box.add_child(b_desc)
|
||||
|
||||
_rules_vbox = VBoxContainer.new()
|
||||
_rules_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_fields_box.add_child(_rules_vbox)
|
||||
|
||||
_add_rule_btn = Button.new()
|
||||
_add_rule_btn.text = "+ Add Behavior Rule"
|
||||
_add_rule_btn.pressed.connect(_on_add_rule_btn_pressed)
|
||||
_fields_box.add_child(_add_rule_btn)
|
||||
|
||||
_setup_rule_form()
|
||||
|
||||
|
||||
func _setup_rule_form() -> void:
|
||||
_rule_form_box = VBoxContainer.new()
|
||||
_rule_form_box.visible = false
|
||||
_rule_form_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_fields_box.add_child(_rule_form_box)
|
||||
|
||||
var form_panel := PanelContainer.new()
|
||||
var form_box := VBoxContainer.new()
|
||||
form_panel.add_child(form_box)
|
||||
_rule_form_box.add_child(form_panel)
|
||||
|
||||
var form_title := Label.new()
|
||||
form_title.text = "New Behavior Rule"
|
||||
form_title.add_theme_font_size_override("font_size", 13)
|
||||
form_title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.3))
|
||||
form_box.add_child(form_title)
|
||||
|
||||
# Trigger Selector
|
||||
var trig_row := HBoxContainer.new()
|
||||
var trig_lbl := Label.new()
|
||||
trig_lbl.text = "Trigger:"
|
||||
trig_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
trig_row.add_child(trig_lbl)
|
||||
_trigger_option = OptionButton.new()
|
||||
_trigger_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_trigger_option.add_item("on_interact (Click/Interact)", 0)
|
||||
_trigger_option.set_item_metadata(0, "on_interact")
|
||||
_trigger_option.add_item("on_touch (Physical contact)", 1)
|
||||
_trigger_option.set_item_metadata(1, "on_touch")
|
||||
_trigger_option.add_item("on_enter (Enters zone)", 2)
|
||||
_trigger_option.set_item_metadata(2, "on_enter")
|
||||
_trigger_option.add_item("on_timer (Repeats every X sec)", 3)
|
||||
_trigger_option.set_item_metadata(3, "on_timer")
|
||||
_trigger_option.item_selected.connect(_on_trigger_changed)
|
||||
trig_row.add_child(_trigger_option)
|
||||
form_box.add_child(trig_row)
|
||||
|
||||
# Timer interval row (only visible when on_timer is selected)
|
||||
_timer_row = HBoxContainer.new()
|
||||
_timer_row.visible = false
|
||||
var timer_lbl := Label.new()
|
||||
timer_lbl.text = "Interval (s):"
|
||||
timer_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
_timer_row.add_child(timer_lbl)
|
||||
_timer_interval_spin = SpinBox.new()
|
||||
_timer_interval_spin.min_value = 0.1
|
||||
_timer_interval_spin.max_value = 3600.0
|
||||
_timer_interval_spin.step = 0.5
|
||||
_timer_interval_spin.value = 2.0
|
||||
_timer_row.add_child(_timer_interval_spin)
|
||||
form_box.add_child(_timer_row)
|
||||
|
||||
# Action Selector
|
||||
var act_row := HBoxContainer.new()
|
||||
var act_lbl := Label.new()
|
||||
act_lbl.text = "Action:"
|
||||
act_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
act_row.add_child(act_lbl)
|
||||
_action_option = OptionButton.new()
|
||||
_action_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_action_option.add_item("give_item (Reward Chips/Items)", 0)
|
||||
_action_option.set_item_metadata(0, "give_item")
|
||||
_action_option.add_item("rotate (Spin Object)", 1)
|
||||
_action_option.set_item_metadata(1, "rotate")
|
||||
_action_option.add_item("move (Move / Elevate)", 2)
|
||||
_action_option.set_item_metadata(2, "move")
|
||||
_action_option.add_item("play_sound (Audio FX)", 3)
|
||||
_action_option.set_item_metadata(3, "play_sound")
|
||||
_action_option.add_item("spawn_from_catalog (Spawn)", 4)
|
||||
_action_option.set_item_metadata(4, "spawn_from_catalog")
|
||||
_action_option.item_selected.connect(_on_action_changed)
|
||||
act_row.add_child(_action_option)
|
||||
form_box.add_child(act_row)
|
||||
|
||||
# Dynamic Parameter Boxes
|
||||
# 1. give_item
|
||||
_action_item_box = VBoxContainer.new()
|
||||
var item_id_row := HBoxContainer.new()
|
||||
var item_id_lbl := Label.new()
|
||||
item_id_lbl.text = "Item ID:"
|
||||
item_id_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
item_id_row.add_child(item_id_lbl)
|
||||
_item_id_edit = LineEdit.new()
|
||||
_item_id_edit.text = "buffalo_chips"
|
||||
_item_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
item_id_row.add_child(_item_id_edit)
|
||||
_action_item_box.add_child(item_id_row)
|
||||
|
||||
var item_cnt_row := HBoxContainer.new()
|
||||
var item_cnt_lbl := Label.new()
|
||||
item_cnt_lbl.text = "Amount:"
|
||||
item_cnt_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
item_cnt_row.add_child(item_cnt_lbl)
|
||||
_item_count_spin = SpinBox.new()
|
||||
_item_count_spin.min_value = 1
|
||||
_item_count_spin.max_value = 10000
|
||||
_item_count_spin.value = 5
|
||||
item_cnt_row.add_child(_item_count_spin)
|
||||
_action_item_box.add_child(item_cnt_row)
|
||||
form_box.add_child(_action_item_box)
|
||||
|
||||
# 2. rotate
|
||||
_action_rotate_box = VBoxContainer.new()
|
||||
_action_rotate_box.visible = false
|
||||
var rot_axis_row := HBoxContainer.new()
|
||||
var rot_axis_lbl := Label.new()
|
||||
rot_axis_lbl.text = "Axis:"
|
||||
rot_axis_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
rot_axis_row.add_child(rot_axis_lbl)
|
||||
_rotate_axis_option = OptionButton.new()
|
||||
_rotate_axis_option.add_item("y (Yaw)")
|
||||
_rotate_axis_option.set_item_metadata(0, "y")
|
||||
_rotate_axis_option.add_item("x (Pitch)")
|
||||
_rotate_axis_option.set_item_metadata(1, "x")
|
||||
_rotate_axis_option.add_item("z (Roll)")
|
||||
_rotate_axis_option.set_item_metadata(2, "z")
|
||||
rot_axis_row.add_child(_rotate_axis_option)
|
||||
_action_rotate_box.add_child(rot_axis_row)
|
||||
|
||||
var rot_deg_row := HBoxContainer.new()
|
||||
var rot_deg_lbl := Label.new()
|
||||
rot_deg_lbl.text = "Degrees:"
|
||||
rot_deg_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
rot_deg_row.add_child(rot_deg_lbl)
|
||||
_rotate_degrees_spin = SpinBox.new()
|
||||
_rotate_degrees_spin.min_value = -360.0
|
||||
_rotate_degrees_spin.max_value = 360.0
|
||||
_rotate_degrees_spin.step = 15.0
|
||||
_rotate_degrees_spin.value = 90.0
|
||||
rot_deg_row.add_child(_rotate_degrees_spin)
|
||||
_action_rotate_box.add_child(rot_deg_row)
|
||||
|
||||
var rot_dur_row := HBoxContainer.new()
|
||||
var rot_dur_lbl := Label.new()
|
||||
rot_dur_lbl.text = "Duration (s):"
|
||||
rot_dur_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
rot_dur_row.add_child(rot_dur_lbl)
|
||||
_rotate_duration_spin = SpinBox.new()
|
||||
_rotate_duration_spin.min_value = 0.05
|
||||
_rotate_duration_spin.max_value = 5.0
|
||||
_rotate_duration_spin.step = 0.1
|
||||
_rotate_duration_spin.value = 0.4
|
||||
rot_dur_row.add_child(_rotate_duration_spin)
|
||||
_action_rotate_box.add_child(rot_dur_row)
|
||||
form_box.add_child(_action_rotate_box)
|
||||
|
||||
# 3. move
|
||||
_action_move_box = VBoxContainer.new()
|
||||
_action_move_box.visible = false
|
||||
var move_lbl := Label.new()
|
||||
move_lbl.text = "Offset (X, Y, Z):"
|
||||
_action_move_box.add_child(move_lbl)
|
||||
var move_coords := HBoxContainer.new()
|
||||
_move_x_spin = SpinBox.new()
|
||||
_move_x_spin.min_value = -100.0
|
||||
_move_x_spin.max_value = 100.0
|
||||
_move_x_spin.value = 0.0
|
||||
move_coords.add_child(_move_x_spin)
|
||||
_move_y_spin = SpinBox.new()
|
||||
_move_y_spin.min_value = -100.0
|
||||
_move_y_spin.max_value = 100.0
|
||||
_move_y_spin.value = 2.0
|
||||
move_coords.add_child(_move_y_spin)
|
||||
_move_z_spin = SpinBox.new()
|
||||
_move_z_spin.min_value = -100.0
|
||||
_move_z_spin.max_value = 100.0
|
||||
_move_z_spin.value = 0.0
|
||||
move_coords.add_child(_move_z_spin)
|
||||
_action_move_box.add_child(move_coords)
|
||||
|
||||
var move_dur_row := HBoxContainer.new()
|
||||
var move_dur_lbl := Label.new()
|
||||
move_dur_lbl.text = "Duration (s):"
|
||||
move_dur_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
move_dur_row.add_child(move_dur_lbl)
|
||||
_move_duration_spin = SpinBox.new()
|
||||
_move_duration_spin.min_value = 0.05
|
||||
_move_duration_spin.max_value = 5.0
|
||||
_move_duration_spin.step = 0.1
|
||||
_move_duration_spin.value = 0.4
|
||||
move_dur_row.add_child(_move_duration_spin)
|
||||
_action_move_box.add_child(move_dur_row)
|
||||
form_box.add_child(_action_move_box)
|
||||
|
||||
# 4. play_sound
|
||||
_action_sound_box = VBoxContainer.new()
|
||||
_action_sound_box.visible = false
|
||||
var snd_row := HBoxContainer.new()
|
||||
var snd_lbl := Label.new()
|
||||
snd_lbl.text = "Sound ID:"
|
||||
snd_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
snd_row.add_child(snd_lbl)
|
||||
_sound_id_edit = LineEdit.new()
|
||||
_sound_id_edit.text = "bell"
|
||||
_sound_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
snd_row.add_child(_sound_id_edit)
|
||||
_action_sound_box.add_child(snd_row)
|
||||
form_box.add_child(_action_sound_box)
|
||||
|
||||
# 5. spawn_from_catalog
|
||||
_action_spawn_box = VBoxContainer.new()
|
||||
_action_spawn_box.visible = false
|
||||
var sp_row := HBoxContainer.new()
|
||||
var sp_lbl := Label.new()
|
||||
sp_lbl.text = "Catalog ID:"
|
||||
sp_lbl.custom_minimum_size = Vector2(70, 0)
|
||||
sp_row.add_child(sp_lbl)
|
||||
_spawn_catalog_id_edit = LineEdit.new()
|
||||
_spawn_catalog_id_edit.text = "cube"
|
||||
_spawn_catalog_id_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
sp_row.add_child(_spawn_catalog_id_edit)
|
||||
_action_spawn_box.add_child(sp_row)
|
||||
|
||||
var sp_pos_lbl := Label.new()
|
||||
sp_pos_lbl.text = "Spawn Pos (X, Y, Z):"
|
||||
_action_spawn_box.add_child(sp_pos_lbl)
|
||||
var sp_coords := HBoxContainer.new()
|
||||
_spawn_x_spin = SpinBox.new()
|
||||
_spawn_x_spin.min_value = -100.0
|
||||
_spawn_x_spin.max_value = 100.0
|
||||
_spawn_x_spin.value = 0.0
|
||||
sp_coords.add_child(_spawn_x_spin)
|
||||
_spawn_y_spin = SpinBox.new()
|
||||
_spawn_y_spin.min_value = -100.0
|
||||
_spawn_y_spin.max_value = 100.0
|
||||
_spawn_y_spin.value = 1.0
|
||||
sp_coords.add_child(_spawn_y_spin)
|
||||
_spawn_z_spin = SpinBox.new()
|
||||
_spawn_z_spin.min_value = -100.0
|
||||
_spawn_z_spin.max_value = 100.0
|
||||
_spawn_z_spin.value = 0.0
|
||||
sp_coords.add_child(_spawn_z_spin)
|
||||
_action_spawn_box.add_child(sp_coords)
|
||||
form_box.add_child(_action_spawn_box)
|
||||
|
||||
# Form Buttons
|
||||
var btn_row := HBoxContainer.new()
|
||||
var save_btn := Button.new()
|
||||
save_btn.text = "Save Rule"
|
||||
save_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
save_btn.pressed.connect(_on_save_rule_pressed)
|
||||
btn_row.add_child(save_btn)
|
||||
|
||||
var cancel_btn := Button.new()
|
||||
cancel_btn.text = "Cancel"
|
||||
cancel_btn.pressed.connect(_on_cancel_rule_pressed)
|
||||
btn_row.add_child(cancel_btn)
|
||||
form_box.add_child(btn_row)
|
||||
|
||||
|
||||
func setup(core: EditorCore) -> void:
|
||||
_core = core
|
||||
@@ -91,19 +428,32 @@ func setup(core: EditorCore) -> void:
|
||||
|
||||
func show_object(object_id: String) -> void:
|
||||
_object_id = object_id
|
||||
if not _is_built:
|
||||
_build_ui()
|
||||
|
||||
if object_id == "" or _core == null:
|
||||
_empty_label.visible = true
|
||||
_fields_box.visible = false
|
||||
if _empty_label != null:
|
||||
_empty_label.visible = true
|
||||
if _fields_box != null:
|
||||
_fields_box.visible = false
|
||||
return
|
||||
|
||||
var obj := _core.model.find_object(object_id)
|
||||
if obj == null:
|
||||
_empty_label.visible = true
|
||||
_fields_box.visible = false
|
||||
if _empty_label != null:
|
||||
_empty_label.visible = true
|
||||
if _fields_box != null:
|
||||
_fields_box.visible = false
|
||||
return
|
||||
|
||||
_empty_label.visible = false
|
||||
_fields_box.visible = true
|
||||
if _empty_label != null:
|
||||
_empty_label.visible = false
|
||||
if _fields_box != null:
|
||||
_fields_box.visible = true
|
||||
if _rule_form_box != null:
|
||||
_rule_form_box.visible = false
|
||||
if _add_rule_btn != null:
|
||||
_add_rule_btn.visible = true
|
||||
|
||||
_updating = true
|
||||
_id_label.text = "id: %s" % obj.id
|
||||
@@ -118,6 +468,211 @@ func show_object(object_id: String) -> void:
|
||||
_scale_z.value = obj.scale.z
|
||||
_updating = false
|
||||
|
||||
_refresh_rules()
|
||||
|
||||
|
||||
func _refresh_rules() -> void:
|
||||
for child in _rules_vbox.get_children():
|
||||
child.queue_free()
|
||||
|
||||
if _core == null or _object_id == "":
|
||||
return
|
||||
|
||||
var obj := _core.model.find_object(_object_id)
|
||||
if obj == null:
|
||||
return
|
||||
|
||||
var b_dict: Variant = obj.properties.get("script_behavior", obj.properties.get("behavior"))
|
||||
var rules_arr: Array = []
|
||||
if b_dict is Dictionary and b_dict.has("rules") and b_dict["rules"] is Array:
|
||||
rules_arr = b_dict["rules"]
|
||||
|
||||
if rules_arr.is_empty():
|
||||
var empty_lbl := Label.new()
|
||||
empty_lbl.text = "(No behavior rules attached)"
|
||||
empty_lbl.add_theme_font_size_override("font_size", 11)
|
||||
empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
||||
_rules_vbox.add_child(empty_lbl)
|
||||
return
|
||||
|
||||
for i in range(rules_arr.size()):
|
||||
var rule: Variant = rules_arr[i]
|
||||
if not (rule is Dictionary):
|
||||
continue
|
||||
var card := _create_rule_card(i, rule as Dictionary)
|
||||
_rules_vbox.add_child(card)
|
||||
|
||||
|
||||
func _create_rule_card(rule_index: int, rule: Dictionary) -> Control:
|
||||
var panel := PanelContainer.new()
|
||||
var row := HBoxContainer.new()
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
panel.add_child(row)
|
||||
|
||||
var text_box := VBoxContainer.new()
|
||||
text_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(text_box)
|
||||
|
||||
var trigger_dict: Dictionary = rule.get("trigger", {})
|
||||
var trig_type: String = str(trigger_dict.get("type", "unknown"))
|
||||
var trig_label := Label.new()
|
||||
if trig_type == "on_timer":
|
||||
trig_label.text = "⏱️ on_timer (every %ss)" % str(trigger_dict.get("interval", 1.0))
|
||||
elif trig_type == "on_touch":
|
||||
trig_label.text = "💥 on_touch"
|
||||
elif trig_type == "on_enter":
|
||||
trig_label.text = "🚶 on_enter"
|
||||
else:
|
||||
trig_label.text = "⚡ on_interact"
|
||||
trig_label.add_theme_font_size_override("font_size", 12)
|
||||
trig_label.add_theme_color_override("font_color", Color(0.4, 0.9, 1.0))
|
||||
text_box.add_child(trig_label)
|
||||
|
||||
var actions_arr: Array = rule.get("actions", [])
|
||||
for act in actions_arr:
|
||||
if not (act is Dictionary):
|
||||
continue
|
||||
var act_dict: Dictionary = act
|
||||
var verb: String = str(act_dict.get("verb", ""))
|
||||
var act_lbl := Label.new()
|
||||
act_lbl.add_theme_font_size_override("font_size", 11)
|
||||
match verb:
|
||||
"give_item":
|
||||
var item_name: String = str(act_dict.get("item_id", "buffalo_chips"))
|
||||
if item_name == "buffalo_chips":
|
||||
item_name = "Buffalo Chips"
|
||||
act_lbl.text = "→ Give %d %s" % [int(act_dict.get("count", 1)), item_name]
|
||||
"rotate":
|
||||
act_lbl.text = "→ Rotate %.0f° (%s axis)" % [float(act_dict.get("degrees", 90)), str(act_dict.get("axis", "y"))]
|
||||
"move":
|
||||
var off: Array = act_dict.get("offset", [0, 0, 0])
|
||||
act_lbl.text = "→ Move offset %s" % str(off)
|
||||
"play_sound":
|
||||
act_lbl.text = "→ Play sound '%s'" % str(act_dict.get("sound_id", ""))
|
||||
"spawn_from_catalog":
|
||||
act_lbl.text = "→ Spawn '%s'" % str(act_dict.get("catalog_id", ""))
|
||||
_:
|
||||
act_lbl.text = "→ %s" % verb
|
||||
text_box.add_child(act_lbl)
|
||||
|
||||
var del_btn := Button.new()
|
||||
del_btn.text = "✕"
|
||||
del_btn.tooltip_text = "Delete this rule"
|
||||
del_btn.pressed.connect(_on_delete_rule_pressed.bind(rule_index))
|
||||
row.add_child(del_btn)
|
||||
|
||||
return panel
|
||||
|
||||
|
||||
func _on_add_rule_btn_pressed() -> void:
|
||||
_rule_form_box.visible = true
|
||||
_add_rule_btn.visible = false
|
||||
|
||||
|
||||
func _on_cancel_rule_pressed() -> void:
|
||||
_rule_form_box.visible = false
|
||||
_add_rule_btn.visible = true
|
||||
|
||||
|
||||
func _on_trigger_changed(index: int) -> void:
|
||||
var trig_key: String = _trigger_option.get_item_metadata(index)
|
||||
_timer_row.visible = (trig_key == "on_timer")
|
||||
|
||||
|
||||
func _on_action_changed(index: int) -> void:
|
||||
var act_key: String = _action_option.get_item_metadata(index)
|
||||
_action_item_box.visible = (act_key == "give_item")
|
||||
_action_rotate_box.visible = (act_key == "rotate")
|
||||
_action_move_box.visible = (act_key == "move")
|
||||
_action_sound_box.visible = (act_key == "play_sound")
|
||||
_action_spawn_box.visible = (act_key == "spawn_from_catalog")
|
||||
|
||||
|
||||
func _on_save_rule_pressed() -> void:
|
||||
if _core == null or _object_id == "":
|
||||
return
|
||||
|
||||
var obj := _core.model.find_object(_object_id)
|
||||
if obj == null:
|
||||
return
|
||||
|
||||
var trig_type: String = _trigger_option.get_item_metadata(_trigger_option.selected)
|
||||
var trig_dict: Dictionary = {"type": trig_type}
|
||||
if trig_type == "on_timer":
|
||||
trig_dict["interval"] = _timer_interval_spin.value
|
||||
|
||||
var act_verb: String = _action_option.get_item_metadata(_action_option.selected)
|
||||
var act_dict: Dictionary = {"verb": act_verb}
|
||||
match act_verb:
|
||||
"give_item":
|
||||
var item_id := _item_id_edit.text.strip_edges()
|
||||
if item_id.is_empty():
|
||||
item_id = "buffalo_chips"
|
||||
act_dict["item_id"] = item_id
|
||||
act_dict["count"] = int(_item_count_spin.value)
|
||||
"rotate":
|
||||
var axis_idx := _rotate_axis_option.selected
|
||||
act_dict["axis"] = _rotate_axis_option.get_item_metadata(axis_idx)
|
||||
act_dict["degrees"] = _rotate_degrees_spin.value
|
||||
act_dict["duration"] = _rotate_duration_spin.value
|
||||
act_dict["target"] = "self"
|
||||
"move":
|
||||
act_dict["target"] = "self"
|
||||
act_dict["offset"] = [_move_x_spin.value, _move_y_spin.value, _move_z_spin.value]
|
||||
act_dict["duration"] = _move_duration_spin.value
|
||||
"play_sound":
|
||||
var snd := _sound_id_edit.text.strip_edges()
|
||||
if snd.is_empty():
|
||||
snd = "bell"
|
||||
act_dict["sound_id"] = snd
|
||||
"spawn_from_catalog":
|
||||
var cat_id := _spawn_catalog_id_edit.text.strip_edges()
|
||||
if cat_id.is_empty():
|
||||
cat_id = "cube"
|
||||
act_dict["catalog_id"] = cat_id
|
||||
act_dict["position"] = [_spawn_x_spin.value, _spawn_y_spin.value, _spawn_z_spin.value]
|
||||
|
||||
var new_rule: Dictionary = {
|
||||
"trigger": trig_dict,
|
||||
"conditions": [],
|
||||
"actions": [act_dict]
|
||||
}
|
||||
|
||||
var cur_behavior: Dictionary = {}
|
||||
var raw_b = obj.properties.get("script_behavior", obj.properties.get("behavior"))
|
||||
if raw_b is Dictionary:
|
||||
cur_behavior = (raw_b as Dictionary).duplicate(true)
|
||||
|
||||
var cur_rules: Array = cur_behavior.get("rules", []).duplicate(true)
|
||||
cur_rules.append(new_rule)
|
||||
cur_behavior["rules"] = cur_rules
|
||||
|
||||
_core.set_object_behavior(_object_id, cur_behavior)
|
||||
_rule_form_box.visible = false
|
||||
_add_rule_btn.visible = true
|
||||
_refresh_rules()
|
||||
|
||||
|
||||
func _on_delete_rule_pressed(index: int) -> void:
|
||||
if _core == null or _object_id == "":
|
||||
return
|
||||
|
||||
var obj := _core.model.find_object(_object_id)
|
||||
if obj == null:
|
||||
return
|
||||
|
||||
var raw_b = obj.properties.get("script_behavior", obj.properties.get("behavior"))
|
||||
if not (raw_b is Dictionary):
|
||||
return
|
||||
|
||||
var cur_behavior: Dictionary = (raw_b as Dictionary).duplicate(true)
|
||||
var cur_rules: Array = cur_behavior.get("rules", []).duplicate(true)
|
||||
if index >= 0 and index < cur_rules.size():
|
||||
cur_rules.remove_at(index)
|
||||
cur_behavior["rules"] = cur_rules
|
||||
_core.set_object_behavior(_object_id, cur_behavior)
|
||||
_refresh_rules()
|
||||
|
||||
|
||||
func _on_position_changed(_v: float) -> void:
|
||||
if _updating or _core == null or _object_id == "":
|
||||
|
||||
@@ -4,13 +4,26 @@ extends VBoxContainer
|
||||
## `PalettePanel`). Lists every id in an `EditorObjectCatalog` and emits
|
||||
## which one the player picked; `InGameEditor` remembers it as the pending
|
||||
## `catalog_id` for the next `ObjectPlaceTool` placement.
|
||||
##
|
||||
## Creator Studio feature: In-game .glb/.gltf 3D Model Importer.
|
||||
## Creators can import custom 3D models from disk, validate them via GLTFDocument,
|
||||
## and place them directly into their worlds.
|
||||
|
||||
signal object_selected(catalog_id: String)
|
||||
signal model_imported(catalog_id: String)
|
||||
|
||||
var _list: ItemList
|
||||
var _catalog: EditorObjectCatalog = null
|
||||
var _ids: Array[String] = []
|
||||
|
||||
var _btn_import_model: Button
|
||||
var _import_panel: VBoxContainer
|
||||
var _model_name_edit: LineEdit
|
||||
var _model_path_edit: LineEdit
|
||||
var _btn_browse: Button
|
||||
var _file_dialog: FileDialog
|
||||
var _import_status_label: Label
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
name = "PalettePanel"
|
||||
@@ -33,10 +46,82 @@ func _ready() -> void:
|
||||
|
||||
_list = ItemList.new()
|
||||
_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_list.custom_minimum_size = Vector2(0, 180)
|
||||
_list.custom_minimum_size = Vector2(0, 160)
|
||||
_list.item_selected.connect(_on_item_selected)
|
||||
add_child(_list)
|
||||
|
||||
_setup_import_ui()
|
||||
|
||||
|
||||
func _setup_import_ui() -> void:
|
||||
_btn_import_model = Button.new()
|
||||
_btn_import_model.text = "+ Import .glb Model"
|
||||
_btn_import_model.add_theme_color_override("font_color", Color(0.35, 0.85, 1.0))
|
||||
_btn_import_model.pressed.connect(_on_import_button_pressed)
|
||||
add_child(_btn_import_model)
|
||||
|
||||
_import_panel = VBoxContainer.new()
|
||||
_import_panel.visible = false
|
||||
add_child(_import_panel)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Import Custom Model (.glb)"
|
||||
title.add_theme_font_size_override("font_size", 12)
|
||||
title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
|
||||
_import_panel.add_child(title)
|
||||
|
||||
var name_row := HBoxContainer.new()
|
||||
var name_lbl := Label.new()
|
||||
name_lbl.text = "Name:"
|
||||
name_lbl.custom_minimum_size = Vector2(50, 0)
|
||||
name_row.add_child(name_lbl)
|
||||
_model_name_edit = LineEdit.new()
|
||||
_model_name_edit.placeholder_text = "e.g. Vintage Chair"
|
||||
_model_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_row.add_child(_model_name_edit)
|
||||
_import_panel.add_child(name_row)
|
||||
|
||||
var path_row := HBoxContainer.new()
|
||||
var path_lbl := Label.new()
|
||||
path_lbl.text = "Path:"
|
||||
path_lbl.custom_minimum_size = Vector2(50, 0)
|
||||
path_row.add_child(path_lbl)
|
||||
_model_path_edit = LineEdit.new()
|
||||
_model_path_edit.placeholder_text = "Path to .glb or .gltf"
|
||||
_model_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
path_row.add_child(_model_path_edit)
|
||||
|
||||
_btn_browse = Button.new()
|
||||
_btn_browse.text = "..."
|
||||
_btn_browse.pressed.connect(_on_browse_pressed)
|
||||
path_row.add_child(_btn_browse)
|
||||
_import_panel.add_child(path_row)
|
||||
|
||||
_import_status_label = Label.new()
|
||||
_import_status_label.text = ""
|
||||
_import_status_label.add_theme_font_size_override("font_size", 10)
|
||||
_import_panel.add_child(_import_status_label)
|
||||
|
||||
var btn_row := HBoxContainer.new()
|
||||
var do_import_btn := Button.new()
|
||||
do_import_btn.text = "Import"
|
||||
do_import_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
do_import_btn.pressed.connect(_on_do_import_pressed)
|
||||
btn_row.add_child(do_import_btn)
|
||||
|
||||
var cancel_btn := Button.new()
|
||||
cancel_btn.text = "Cancel"
|
||||
cancel_btn.pressed.connect(_on_cancel_import_pressed)
|
||||
btn_row.add_child(cancel_btn)
|
||||
_import_panel.add_child(btn_row)
|
||||
|
||||
_file_dialog = FileDialog.new()
|
||||
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_file_dialog.filters = ["*.glb ; GLTF Binary Files", "*.gltf ; GLTF Scene Files"]
|
||||
_file_dialog.file_selected.connect(_on_file_selected)
|
||||
add_child(_file_dialog)
|
||||
|
||||
|
||||
func set_catalog(catalog: EditorObjectCatalog) -> void:
|
||||
_catalog = catalog
|
||||
@@ -51,6 +136,8 @@ func _refresh() -> void:
|
||||
for id in _catalog.get_all_ids():
|
||||
var entry := _catalog.resolve(id)
|
||||
var label: String = entry.get("display_name", id)
|
||||
if entry.get("category", "") == "custom":
|
||||
label = "★ " + label
|
||||
_list.add_item(label)
|
||||
_ids.append(id)
|
||||
|
||||
@@ -66,3 +153,104 @@ func get_selected_catalog_id() -> String:
|
||||
if selected.is_empty():
|
||||
return ""
|
||||
return _ids[selected[0]]
|
||||
|
||||
|
||||
func _on_import_button_pressed() -> void:
|
||||
_import_panel.visible = true
|
||||
_btn_import_model.visible = false
|
||||
_import_status_label.text = ""
|
||||
|
||||
|
||||
func _on_cancel_import_pressed() -> void:
|
||||
_import_panel.visible = false
|
||||
_btn_import_model.visible = true
|
||||
|
||||
|
||||
func _on_browse_pressed() -> void:
|
||||
_file_dialog.popup_centered(Vector2i(600, 400))
|
||||
|
||||
|
||||
func _on_file_selected(path: String) -> void:
|
||||
_model_path_edit.text = path
|
||||
if _model_name_edit.text.strip_edges().is_empty():
|
||||
_model_name_edit.text = path.get_file().get_basename().capitalize()
|
||||
|
||||
|
||||
func _on_do_import_pressed() -> void:
|
||||
var path := _model_path_edit.text.strip_edges()
|
||||
var display_name := _model_name_edit.text.strip_edges()
|
||||
if path.is_empty():
|
||||
_import_status_label.text = "Error: Please select a file path."
|
||||
_import_status_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||
return
|
||||
if display_name.is_empty():
|
||||
display_name = path.get_file().get_basename().capitalize()
|
||||
|
||||
var cat_id := import_model_file(path, display_name)
|
||||
if cat_id.is_empty():
|
||||
_import_status_label.text = "Error: Failed to import/validate model."
|
||||
_import_status_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||
return
|
||||
|
||||
_import_panel.visible = false
|
||||
_btn_import_model.visible = true
|
||||
_model_name_edit.text = ""
|
||||
_model_path_edit.text = ""
|
||||
|
||||
|
||||
## Import a 3D model (.glb/.gltf), validate it, copy to user custom assets,
|
||||
## and register it into the active EditorObjectCatalog.
|
||||
func import_model_file(file_path: String, display_name: String, category: String = "custom") -> String:
|
||||
if not FileAccess.file_exists(file_path):
|
||||
push_error("PalettePanel: File does not exist: %s" % file_path)
|
||||
return ""
|
||||
|
||||
# Validate model using GLTFDocument
|
||||
var gltf := GLTFDocument.new()
|
||||
var state := GLTFState.new()
|
||||
var err := gltf.append_from_file(file_path, state)
|
||||
if err != OK:
|
||||
push_error("PalettePanel: GLTFDocument failed to parse file (%d): %s" % [err, file_path])
|
||||
return ""
|
||||
|
||||
# Copy to user custom assets directory
|
||||
var dir := DirAccess.open("user://")
|
||||
if dir != null:
|
||||
if not dir.dir_exists("custom_assets"):
|
||||
dir.make_dir("custom_assets")
|
||||
|
||||
var filename := file_path.get_file()
|
||||
var target_path := "user://custom_assets/%s" % filename
|
||||
var copy_err := DirAccess.copy_absolute(file_path, target_path)
|
||||
if copy_err != OK:
|
||||
# If direct copy failed, use original path
|
||||
target_path = file_path
|
||||
|
||||
# Generate clean unique catalog id
|
||||
var clean_slug := display_name.to_lower().replace(" ", "_").replace("-", "_")
|
||||
var regex := RegEx.new()
|
||||
regex.compile("[^a-z0-9_]")
|
||||
clean_slug = regex.sub(clean_slug, "", true)
|
||||
if clean_slug.is_empty():
|
||||
clean_slug = "model"
|
||||
var cat_id := "custom_%s" % clean_slug
|
||||
|
||||
var catalog_entry: Dictionary = {
|
||||
"asset_id": target_path,
|
||||
"type": "prop",
|
||||
"display_name": display_name,
|
||||
"category": category
|
||||
}
|
||||
|
||||
if _catalog != null:
|
||||
_catalog.register_custom_object(cat_id, catalog_entry)
|
||||
_refresh()
|
||||
# Select the newly imported model
|
||||
for i in range(_ids.size()):
|
||||
if _ids[i] == cat_id:
|
||||
_list.select(i)
|
||||
object_selected.emit(cat_id)
|
||||
break
|
||||
|
||||
model_imported.emit(cat_id)
|
||||
return cat_id
|
||||
|
||||
Reference in New Issue
Block a user