OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / game / bot_editor_panel.gd

144 lines · gd ref:
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()