c340b7bc48
Hand (CanvasLayer, PROCESS_MODE_ALWAYS): Space toggles the hand, Q/E play slot 1/2, charges accrue from auto-fire hits (4 hits/charge, cap 5). KINETIC card: radial shockwave shove + light damage. HEAT card: paints heat status in a radius; heat accumulates, glows, detonates at threshold (damage only, no knockback — tag purity). Freeze/dilate is an exposed knob: [ and ] step dilation 0.0-0.3 live. Freeze uses SceneTree.paused, not Engine.time_scale = 0: probed on 4.7 headless — time_scale 0 still steps physics with delta 0, so delta-independent logic (heat thresholds, contact callbacks) kept firing while nominally frozen. Dilation > 0 uses time_scale slow-mo. Hand._exit_tree restores pause + time_scale so hull-zero reloads can never strand a frozen world. Smoke gate extended: charge accrual from build hits, pause-on-open at knob 0, HEAT x2 spend + threshold, detonation on release. 3/3 green on nh3-dev headless 4.7.1.
115 lines
2.8 KiB
GDScript
115 lines
2.8 KiB
GDScript
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)
|