extends VBoxContainer
## THE BOT EDITOR's panel (user, 2026-10-04: a "Bot editor" tab beside the level
## editor). Shown only in the bot workspace (BuildDock).
##
## Place: brain + body -> "Place bot" (a `bot` scene object at the crosshair, one
## gesture, selected so the Object panel shows its settings at once)
## Bots: every bot in this map; a click selects it (Shift adds), like the scene
##
## Everything a bot IS lives in the object journal and the Object panel; this panel
## only places and finds them. A new brain appears here by being added to
## BrainCatalogue — nothing in this file changes.
var _map: Node = null
var _brain: OptionButton
var _body: OptionButton
var _list: ItemList
var _rows: Array = [] # dots behind _list's items
var _sig := ""
var _tick := 0
func setup(map: Node) -> void:
_map = map
name = "BotEditorPanel"
add_theme_constant_override("separation", 4)
var r1 := HBoxContainer.new()
var l1 := Label.new()
l1.text = "Brain"
l1.custom_minimum_size = Vector2(56, 0)
r1.add_child(l1)
_brain = OptionButton.new()
_brain.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_brain.focus_mode = Control.FOCUS_NONE
for i in BrainCatalogue.ids().size():
_brain.add_item(BrainCatalogue.label_of(BrainCatalogue.ids()[i]), i)
_brain.select(maxi(0, BrainCatalogue.ids().find("roam")))
r1.add_child(_brain)
add_child(r1)
var r2 := HBoxContainer.new()
var l2 := Label.new()
l2.text = "Body"
l2.custom_minimum_size = Vector2(56, 0)
r2.add_child(l2)
_body = OptionButton.new()
_body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_body.focus_mode = Control.FOCUS_NONE
for b: String in PrefabCatalogue.BOT_BODIES:
_body.add_item(b.capitalize())
_body.select(maxi(0, PrefabCatalogue.BOT_BODIES.find("crawler")))
r2.add_child(_body)
add_child(r2)
var place := Button.new()
place.text = "Place bot"
place.tooltip_text = "Place a bot at the crosshair (its settings open in the Object panel)"
place.focus_mode = Control.FOCUS_NONE
place.pressed.connect(place_selected)
add_child(place)
var head := Label.new()
head.text = "Bots in this map"
add_child(head)
_list = ItemList.new()
_list.custom_minimum_size = Vector2(0, 140)
_list.select_mode = ItemList.SELECT_MULTI
_list.focus_mode = Control.FOCUS_NONE
_list.multi_selected.connect(func(_i: int, _s: bool) -> void: _apply_list_selection.call_deferred())
_list.item_selected.connect(func(_i: int) -> void: _apply_list_selection.call_deferred())
add_child(_list)
var hint := Label.new()
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.text = "Click a bot to select (Shift adds) · G move · R rotate · Del delete · settings in the Object panel"
hint.add_theme_font_size_override("font_size", 11)
add_child(hint)
refresh()
## "Place bot" — the button and gates take this path.
func place_selected() -> String:
if _map == null or not _map.has_method("place_bot"):
return ""
var brain: String = BrainCatalogue.ids()[_brain.selected]
var body: String = PrefabCatalogue.BOT_BODIES[_body.selected]
var dot: String = _map.place_bot(brain, body)
refresh()
return dot
func _process(_d: float) -> void:
_tick += 1
if _tick % 15 == 0 and is_visible_in_tree(): # ~4 Hz while shown
refresh()
## Rebuild the bot list when bots or their names / selection changed.
func refresh() -> void:
if _map == null or _map.get("_objects") == null:
return
var rows: Array = []
var labels: Array = []
for e: Dictionary in _map._objects.entries():
if String(e["type"]) != "bot":
continue
var n: Node = e["node"]
var nm := String(n.get_bot_prop("name")) if n.has_method("get_bot_prop") and n.get_bot_prop("name") != null else ""
var body := String(n.get_bot_prop("body")) if n.get_bot_prop("body") != null else "?"
rows.append(String(e["dot"]))
labels.append("%s · %s" % [nm if nm != "" else body, BrainCatalogue.label_of(String(n.brain_id()))])
var sel: Array = _map.selected_objects() if _map.has_method("selected_objects") else []
var sig := var_to_str([rows, labels, sel])
if sig == _sig:
return
_sig = sig
_rows = rows
_following = true
_list.clear()
for i in rows.size():
_list.add_item(String(labels[i]))
if sel.has(rows[i]):
_list.select(i, false)
_following = false
var _following := false
func _apply_list_selection() -> void:
if _following or _map == null:
return
var dots: Array = []
for i: int in _list.get_selected_items():
if i < _rows.size():
dots.append(_rows[i])
if not dots.is_empty():
_map.select_objects(dots, dots[-1])
## For gates.
func rows() -> Array:
return _rows.duplicate()