extends Node3D
## A BOT you place like any object — the `bot` prefab (bot MVP, 2026-10-04).
##
## The object journal is its declaration: object.create places it, object.set_prop
## sets its brain / body / name / the brain's own settings ("p.<id>"), object.remove
## deletes it, Ctrl+Z undoes any of it — and all of that replicates and persists the
## way every object does. Nothing here writes durable state.
##
## RUNTIME is BotRegistry's: this node attach()es as "obj-<dot>", and only the room
## AUTHORITY runs the brain (BrainCatalogue resolves the id to a script — no peer-
## supplied path is ever run). The brain moves the bot through BotRegistry.move ->
## set_target here -> the body's own set_target, so a crawler walks with its own
## gait; every other peer gets the replicated position the same way and the body
## animates there too.
##
## The body is built from the id's hash, so every peer builds the SAME creature.
const BODY_SCRIPTS := {"crawler": "GroundCrawler", "serpent": "FlyingSerpent", "hopper": "Hopper"}
var _props: Dictionary = {} # prop id -> sanitized wire value (set by the fold)
var _body: Node3D = null
var _body_kind := ""
var _label: Label3D = null
var _attach_sig := ""
var _placed := Transform3D()
# ---- the fold's interface (SceneObjects.apply_prop / props_applied) -----------
func set_bot_prop(id: String, value: Variant) -> void:
_props[id] = value
func get_bot_prop(id: String) -> Variant:
return _props.get(id)
## Every property of this fold has been applied: rebuild what changed, once.
func props_applied() -> void:
if not is_inside_tree():
return
var kind := String(_props.get("body", "crawler"))
if kind != _body_kind or _body == null or not is_instance_valid(_body):
_build_body(kind)
_update_label()
# Brain, its settings, or WHERE it was placed changed: restart it from there.
var sig := "%s|%s|%s" % [brain_id(), var_to_str(brain_params()), transform]
if sig != _attach_sig:
_attach_sig = sig
_placed = transform
_restart()
# ---- identity ----------------------------------------------------------------
## The id BotRegistry knows this bot by: its object dot, made key-safe.
func bot_id() -> String:
return "obj-" + String(get_meta("object_dot", "")).replace(":", "-")
func brain_id() -> String:
var b := String(_props.get("brain", "roam")) # PrefabCatalogue BOT_BASE default
return b if BrainCatalogue.has(b) else "idle"
## The brain's settings as ENGINE values (a vec3 setting arrives as 3 floats).
func brain_params() -> Dictionary:
var out: Dictionary = {}
for s: Variant in BrainCatalogue.params_of(brain_id()):
var spec: Dictionary = s
var pid := String(spec["id"])
var v: Variant = _props.get("p." + pid, spec.get("default"))
match String(spec.get("kind", "")):
"vec3":
if v is Array and (v as Array).size() == 3:
v = Vector3(float(v[0]), float(v[1]), float(v[2]))
"color":
if v is Array and (v as Array).size() == 3:
v = Color(float(v[0]), float(v[1]), float(v[2]))
"int":
v = int(v)
out[pid] = v
return out
# ---- runtime -----------------------------------------------------------------
func _restart() -> void:
var reg: Node = get_node_or_null(^"/root/BotRegistry")
if reg == null or String(get_meta("object_dot", "")) == "":
return
# Stand at the placement first, identically on EVERY peer: an idle bot then stays
# put everywhere instead of each peer's creature wandering its own way.
if _body != null and _body.has_method("set_target"):
_body.set_target(global_position)
var path := BrainCatalogue.path_of(brain_id())
var brain := {"kind": "none"}
if path != "":
brain = {"kind": "script_file", "path": path, "params": brain_params()}
reg.attach(bot_id(), self, brain)
## The bot body contract BotRegistry.move / the replicated position call.
func set_target(pos: Vector3) -> void:
if _body != null and is_instance_valid(_body) and _body.has_method("set_target"):
_body.set_target(pos)
func _exit_tree() -> void:
var reg: Node = get_node_or_null(^"/root/BotRegistry")
if reg != null:
reg.detach(bot_id(), self) # only if WE are still the attached node
reg.unschedule_body(_body) # the body goes with this object
_attach_sig = ""
# ---- body --------------------------------------------------------------------
func _build_body(kind: String) -> void:
var reg0: Node = get_node_or_null(^"/root/BotRegistry")
if _body != null and is_instance_valid(_body):
if reg0 != null:
reg0.unschedule_body(_body) # before the free: the scheduler holds it
remove_child(_body)
_body.queue_free()
_body = null
_label = null
_body_kind = kind
var b: Node3D
match kind:
"crawler":
b = GroundCrawler.new()
"serpent":
b = FlyingSerpent.new()
"hopper":
b = Hopper.new()
_:
b = _marker()
b.name = "Body"
add_child(b)
if b.has_method("initialize"):
var rng := RandomNumberGenerator.new()
# The same creature on every peer. BotRegistry's FNV hash, NOT hash(): GDScript's
# hash() is not promised stable across engine versions, and peers can differ.
var reg: Node = get_node_or_null(^"/root/BotRegistry")
rng.seed = reg._id_hash(bot_id()) if reg != null else 0
b.initialize(rng)
_body = b
# Ticked at its tier's rate (near / mid / far / asleep), like every bot body.
if reg0 != null:
reg0.schedule_body(b)
_label = Label3D.new()
_label.name = "Label"
_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
_label.no_depth_test = true
_label.pixel_size = 0.004
_label.font_size = 40
_label.position = Vector3(0, 1.6, 0)
b.add_child(_label)
_update_label()
func _marker() -> Node3D:
var m := Node3D.new()
m.set_script(load("res://scenes/game/bot_marker.gd"))
var mesh := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(0.6, 1.2, 0.6)
mesh.mesh = box
mesh.position = Vector3(0, 0.6, 0)
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.7, 0.2)
mat.emission_enabled = true
mat.emission = mat.albedo_color
mesh.material_override = mat
m.add_child(mesh)
return m
## "<name>.bot" + what it runs — a bot never passes for a person.
func _update_label() -> void:
if _label == null or not is_instance_valid(_label):
return
var nm := String(_props.get("name", ""))
_label.text = "%s.bot\n%s" % [nm if nm != "" else _body_kind, BrainCatalogue.label_of(brain_id())]
## For gates / the bot editor: the live body.
func body() -> Node3D:
return _body