extends CharacterBody2D ## Untagged grunt — the Act-1 learning-language enemy. Walks at the ## mech, deals contact damage on a tick. Untagged = neutral hue, no ## tag behavior of its own; heat is a status OTHERS put on it. ## ## Heat: accumulates, glows, detonates at threshold (HEAT's signature). ## The threshold check runs in physics so detonations resolve when time ## flows — stack heat while frozen, release, pop. Detonation is damage ## only, zero knockback: moving things is KINETIC's verb, not HEAT's ## (tag purity keeps the chemistry legible). signal died @export var speed := 70.0 @export var hp := 3.0 @export var contact_damage := 5.0 @export var contact_interval := 0.5 @export var knockback_decay := 5.0 @export_group("Heat status") @export var heat_threshold := 3.0 @export var detonation_damage := 1.5 @export var detonation_radius := 56.0 const FX_RING := preload("res://scripts/fx_ring.gd") var heat := 0.0 var _knockback := Vector2.ZERO var _contact_cd := 0.0 var _flash := 0.0 var _dead := false var _mech: Node2D func _ready() -> void: add_to_group("enemies") _mech = get_tree().get_first_node_in_group("mech") func _physics_process(delta: float) -> void: if heat >= heat_threshold: _detonate() return _contact_cd -= delta var seek := Vector2.ZERO if is_instance_valid(_mech): seek = (_mech.global_position - global_position).normalized() * speed _knockback = _knockback.lerp(Vector2.ZERO, 1.0 - exp(-knockback_decay * delta)) velocity = seek + _knockback move_and_slide() for i in get_slide_collision_count(): var collider := get_slide_collision(i).get_collider() if collider == _mech and _contact_cd <= 0.0: _mech.take_damage(contact_damage) _contact_cd = contact_interval if _flash > 0.0: _flash -= delta queue_redraw() func hit(damage: float, kb: Vector2) -> void: if _dead: return hp -= damage _knockback += kb _flash = 0.12 queue_redraw() if hp <= 0.0: _die() func add_heat(amount: float) -> void: if _dead: return heat += amount queue_redraw() func _detonate() -> void: if _dead: return for e in get_tree().get_nodes_in_group("enemies"): if e == self: continue if global_position.distance_to(e.global_position) <= detonation_radius: e.hit(detonation_damage, Vector2.ZERO) var ring: Node2D = FX_RING.new() ring.position = global_position ring.max_radius = detonation_radius get_parent().add_child(ring) _die() func _die() -> void: if _dead: return _dead = true died.emit() queue_free() func _draw() -> void: var body := Color(0.45, 0.47, 0.4) var heat_k := clampf(heat / heat_threshold, 0.0, 1.0) if heat_k > 0.0: body = body.lerp(Color(0.95, 0.5, 0.15), heat_k * 0.8) if _flash > 0.0: body = Color(0.95, 0.95, 0.95) draw_circle(Vector2.ZERO, 8.0, body) if heat_k > 0.0: draw_arc(Vector2.ZERO, 10.0, 0.0, TAU, 24, Color(0.95, 0.55, 0.2, 0.4 + heat_k * 0.5), 1.5)