extends CharacterBody2D ## Enemy body — used by both greybox enemy types via scene exports: ## the untagged grunt (neutral hue, no tag behavior of its own) and ## the self-heating burster (self_heat_rate > 0: HEAT-tagged walking ## bomb, orange base per the one-color legibility floor — the literal ## tag mirror made playable: its own mechanic is your ammo if you ## knock it into the swarm before it pops). ## ## Heat: accumulates, glows, detonates at threshold (HEAT's signature). ## Detonation is symmetric — it damages enemies AND the mech in radius ## (one grammar on both sides; your heat play prices standing close). ## Detonation deals no knockback: moving things is KINETIC's verb. ## ## Chemistry row KINETIC+HEAT (*Spread*): a heated enemy slammed into ## another (knockback impact, not a walking bump) spreads its heat on ## contact, passes momentum, and fires the authorship-loud feedback ## stack via the arena. Struck enemies are heated AND shoved, so ## chains can cascade — chemistry feeding chemistry, decaying with ## each hop. 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 var body_radius := 8.0 @export var base_color := Color(0.45, 0.47, 0.4) @export var salvage_value := 1 ## > 0 makes this a burster: heat/second it applies to itself. @export var self_heat_rate := 0.0 @export_group("Heat status") @export var heat_threshold := 3.0 @export var detonation_damage := 1.5 @export var detonation_mech_damage := 6.0 @export var detonation_radius := 56.0 @export_group("Chemistry KINETIC+HEAT") ## Minimum knockback speed for an impact to count as "knocked into". @export var chem_min_knockback := 120.0 ## Fraction of the striker's heat the struck enemy receives. @export var chem_spread_factor := 1.0 ## Fraction of the striker's knockback passed to the struck enemy. @export var chem_momentum_factor := 0.45 const FX_RING := preload("res://scripts/fx_ring.gd") var heat := 0.0 var _knockback := Vector2.ZERO var _contact_cd := 0.0 var _chem_cd := 0.0 var _flash := 0.0 var _dead := false var _mech: Node2D func _ready() -> void: add_to_group("enemies") if self_heat_rate > 0.0: add_to_group("bursters") _mech = get_tree().get_first_node_in_group("mech") func _physics_process(delta: float) -> void: if self_heat_rate > 0.0: heat += self_heat_rate * delta queue_redraw() if heat >= heat_threshold: _detonate() return _contact_cd -= delta _chem_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 col := get_slide_collision(i) var collider := col.get_collider() if collider == _mech: if _contact_cd <= 0.0: _mech.take_damage(contact_damage) _contact_cd = contact_interval elif collider is CharacterBody2D and collider.is_in_group("enemies"): if heat > 0.0 and _chem_cd <= 0.0 and _knockback.length() >= chem_min_knockback: _chem_cd = 0.12 collider.add_heat(heat * chem_spread_factor) collider.hit(0.0, _knockback * chem_momentum_factor) var arena := get_tree().get_first_node_in_group("arena") if arena != null: var axis: Vector2 = (collider.global_position - global_position).normalized() arena.chemistry_proc(col.get_position(), axis) 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) if is_instance_valid(_mech) and global_position.distance_to(_mech.global_position) <= detonation_radius: _mech.take_damage(detonation_mech_damage) 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 := base_color 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, body_radius, body) if heat_k > 0.0: draw_arc(Vector2.ZERO, body_radius + 2.0, 0.0, TAU, 24, Color(0.95, 0.55, 0.2, 0.4 + heat_k * 0.5), 1.5)