OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / bots / patrol.gd

69 lines · gd ref:
extends RefCounted
## A bot you can SEE: it walks a circle on the flat map.
##
##   BotRegistry.register("walker", ["map"], "position",
##       {"kind": "script_file", "path": "res://bots/patrol.gd"})
##
## The point of this one is the DIVISION OF LABOUR, not the path it walks. A
## headless peer renders nothing, so it only simulates: it integrates the motion
## and writes world:bots:pos:. Every other peer renders a body from that state and
## never runs this script. That is the same authority-simulates / peers-render
## split creatures use, and the one a player body will use later -- so if a box
## moves smoothly on a remote machine, the replication path is working.

## Injected by BotRegistry before _bot_init. Declare it and you get it.
var bot_id: String = ""

## The knobs a placed bot shows in the Object panel (BrainCatalogue.params_of). Same
## spec shape as PrefabCatalogue.PROPS; each `id` is a var below, set before _bot_init.
const PARAMS := [
	{"id": "radius", "label": "Radius", "kind": "float", "default": 8.0, "min": 1.0, "max": 64.0, "step": 0.5, "unit": "m"},
	{"id": "period", "label": "Seconds per lap", "kind": "float", "default": 12.0, "min": 2.0, "max": 120.0, "step": 1.0},
]

@warning_ignore("unused_private_class_variable")
var centre := Vector3(0.0, 1.0, 0.0)
var radius: float = 8.0
var period: float = 12.0          ## seconds per lap

## Publish only once the body has actually moved this far. See _bot_tick.
const MOVE_EPSILON := 0.25

var _t: float = 0.0
var _last_published := Vector3(INF, INF, INF)


func _bot_init() -> void:
	# A PLACED bot circles where it was put.
	if BotRegistry != null and BotRegistry.has_method("is_object") and BotRegistry.is_object(bot_id):
		centre = BotRegistry.start_position(bot_id)
	print("[patrol] walking as '%s'" % bot_id)
	_publish(_point_at(0.0))


func _bot_tick(delta: float) -> void:
	_t += delta
	var p := _point_at(_t)
	# ⚠ INTEGRATE EVERY FRAME, PUBLISH ON CHANGE. Motion needs time -- that is
	# animation, not polling -- but WRITING every frame would put 60 Hz of traffic
	# on the mesh for a bot that visibly moves a few centimetres. So the write is
	# change-driven and bounded, the same shape the network recorder uses: compare
	# against what was last published and send only when it differs enough.
	if p.distance_to(_last_published) >= MOVE_EPSILON:
		_publish(p)


func _bot_exit() -> void:
	print("[patrol] '%s' stopping after %.1fs" % [bot_id, _t])


func _point_at(t: float) -> Vector3:
	var a: float = TAU * (t / period)
	return centre + Vector3(cos(a) * radius, 0.0, sin(a) * radius)


func _publish(p: Vector3) -> void:
	_last_published = p
	if BotRegistry != null and bot_id != "":
		BotRegistry.move(bot_id, p)