OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / prefabs / bot_object.gd

191 lines · gd ref:
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