feat: card layer — 2-slot hand, build-generated charges, card-time freeze/dilate
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.
This commit is contained in:
@@ -45,6 +45,21 @@ move_down={
|
||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
hand_toggle={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
card_1={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
card_2={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
|
||||
+19
-1
@@ -1,8 +1,10 @@
|
||||
[gd_scene load_steps=6 format=3]
|
||||
[gd_scene load_steps=8 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/arena.gd" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/mech.tscn" id="2"]
|
||||
[ext_resource type="Script" path="res://scripts/spawner.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://scripts/hand.gd" id="4"]
|
||||
[ext_resource type="Script" path="res://scripts/hand_hud.gd" id="5"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="wall_h"]
|
||||
size = Vector2(640, 16)
|
||||
@@ -66,3 +68,19 @@ offset_top = 9.0
|
||||
offset_right = 127.0
|
||||
offset_bottom = 13.0
|
||||
color = Color(0.55, 0.8, 0.55, 1)
|
||||
|
||||
[node name="Hand" type="CanvasLayer" parent="."]
|
||||
layer = 2
|
||||
script = ExtResource("4")
|
||||
|
||||
[node name="Dim" type="ColorRect" parent="Hand"]
|
||||
visible = false
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0, 0, 0, 0.35)
|
||||
|
||||
[node name="HUD" type="Control" parent="Hand"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 2
|
||||
script = ExtResource("5")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
extends Node2D
|
||||
## One-shot expanding ring, greybox stand-in for any radial event
|
||||
## (card blasts, heat detonations). Runs on scaled time, so a ring
|
||||
## spawned during freeze blooms on release — which is the rhythm.
|
||||
|
||||
var max_radius := 56.0
|
||||
var color := Color(1.0, 0.6, 0.2)
|
||||
var duration := 0.18
|
||||
|
||||
var _t := 0.0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_t += delta
|
||||
queue_redraw()
|
||||
if _t >= duration:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var k := clampf(_t / duration, 0.0, 1.0)
|
||||
var c := Color(color.r, color.g, color.b, 1.0 - k)
|
||||
draw_arc(Vector2.ZERO, maxf(max_radius * k, 1.0), 0.0, TAU, 48, c, 3.0)
|
||||
+61
-5
@@ -1,8 +1,13 @@
|
||||
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. Knockback state lives here so KINETIC (and later
|
||||
## chemistry) has something to shove.
|
||||
## 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
|
||||
|
||||
@@ -12,9 +17,18 @@ signal died
|
||||
@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
|
||||
|
||||
|
||||
@@ -24,6 +38,10 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if heat >= heat_threshold:
|
||||
_detonate()
|
||||
return
|
||||
|
||||
_contact_cd -= delta
|
||||
var seek := Vector2.ZERO
|
||||
if is_instance_valid(_mech):
|
||||
@@ -44,15 +62,53 @@ func _physics_process(delta: float) -> void:
|
||||
|
||||
|
||||
func hit(damage: float, kb: Vector2) -> void:
|
||||
if _dead:
|
||||
return
|
||||
hp -= damage
|
||||
_knockback += kb
|
||||
_flash = 0.12
|
||||
queue_redraw()
|
||||
if hp <= 0.0:
|
||||
died.emit()
|
||||
queue_free()
|
||||
_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.95, 0.95, 0.95) if _flash > 0.0 else Color(0.45, 0.47, 0.4)
|
||||
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)
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
extends CanvasLayer
|
||||
## 2-slot hand + universal charge pool + card-time dilation.
|
||||
##
|
||||
## Canon mapping (CONCEPT.md § loop stack / § tag grammar): the build
|
||||
## generates charges (auto-fire hits feed the pool — tightest
|
||||
## build<->hand coupling), opening the hand dilates or freezes time
|
||||
## (flow -> freeze -> think -> release), charges are one universal
|
||||
## currency. The freeze/dilate scale is THE exposed playtest knob:
|
||||
## [ and ] step it live, 0.0 = full freeze.
|
||||
##
|
||||
## Greybox shortcut, deliberate: the two cards are entries in CARDS
|
||||
## with a method each, not card resources — structured cards arrive
|
||||
## with the belt (post-greybox, parked).
|
||||
|
||||
@export var dilation_scale := 0.0
|
||||
@export var dilation_step := 0.05
|
||||
@export var dilation_max := 0.3
|
||||
@export var hits_per_charge := 4
|
||||
@export var max_charges := 5
|
||||
|
||||
@export_group("KINETIC card")
|
||||
@export var kinetic_cost := 1
|
||||
@export var kinetic_radius := 160.0
|
||||
@export var kinetic_impulse := 480.0
|
||||
@export var kinetic_damage := 0.5
|
||||
|
||||
@export_group("HEAT card")
|
||||
@export var heat_cost := 1
|
||||
@export var heat_radius := 140.0
|
||||
@export var heat_amount := 2.0
|
||||
|
||||
const FX_RING := preload("res://scripts/fx_ring.gd")
|
||||
|
||||
var charges := 0
|
||||
var hand_open := false
|
||||
var _hit_progress := 0
|
||||
|
||||
# Slot order is the hand: Q = slot 1, E = slot 2.
|
||||
var cards: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("hand")
|
||||
# The hand must keep processing (and receiving input) while the
|
||||
# world is paused for a full freeze.
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
cards = [
|
||||
{"name": "KINETIC", "hue": Color(0.75, 0.85, 0.95), "cost_of": func() -> int: return kinetic_cost, "play": _play_kinetic},
|
||||
{"name": "HEAT", "hue": Color(0.95, 0.55, 0.2), "cost_of": func() -> int: return heat_cost, "play": _play_heat},
|
||||
]
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# Scene reload while frozen must never leave the world stopped.
|
||||
Engine.time_scale = 1.0
|
||||
get_tree().paused = false
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if Input.is_action_just_pressed("hand_toggle"):
|
||||
_set_open(not hand_open)
|
||||
if hand_open:
|
||||
if Input.is_action_just_pressed("card_1"):
|
||||
try_play(0)
|
||||
if Input.is_action_just_pressed("card_2"):
|
||||
try_play(1)
|
||||
$HUD.queue_redraw()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
if event.physical_keycode == KEY_BRACKETLEFT:
|
||||
dilation_scale = maxf(0.0, dilation_scale - dilation_step)
|
||||
_apply_time_scale()
|
||||
elif event.physical_keycode == KEY_BRACKETRIGHT:
|
||||
dilation_scale = minf(dilation_max, dilation_scale + dilation_step)
|
||||
_apply_time_scale()
|
||||
|
||||
|
||||
func on_build_hit() -> void:
|
||||
_hit_progress += 1
|
||||
if _hit_progress >= hits_per_charge:
|
||||
_hit_progress = 0
|
||||
charges = mini(charges + 1, max_charges)
|
||||
|
||||
|
||||
func try_play(slot: int) -> void:
|
||||
var card := cards[slot]
|
||||
var cost: int = card.cost_of.call()
|
||||
if charges < cost:
|
||||
return
|
||||
charges -= cost
|
||||
card.play.call()
|
||||
|
||||
|
||||
func _set_open(open: bool) -> void:
|
||||
hand_open = open
|
||||
$Dim.visible = open
|
||||
_apply_time_scale()
|
||||
|
||||
|
||||
func _apply_time_scale() -> void:
|
||||
# Knob 0 = a REAL pause: Engine.time_scale = 0 still steps physics
|
||||
# with delta 0 on 4.7 (probed headless — delta-independent logic
|
||||
# like heat thresholds kept firing "frozen"). SceneTree.paused
|
||||
# stops physics outright; time_scale covers dilation > 0.
|
||||
if hand_open and dilation_scale == 0.0:
|
||||
get_tree().paused = true
|
||||
Engine.time_scale = 1.0
|
||||
else:
|
||||
get_tree().paused = false
|
||||
Engine.time_scale = dilation_scale if hand_open else 1.0
|
||||
|
||||
|
||||
func _mech() -> Node2D:
|
||||
return get_tree().get_first_node_in_group("mech") as Node2D
|
||||
|
||||
|
||||
func _play_kinetic() -> void:
|
||||
# Shockwave: radial shove from M03, falling off to the rim. No aim —
|
||||
# where you're standing IS the targeting (positioning <-> build bet).
|
||||
var mech := _mech()
|
||||
if mech == null:
|
||||
return
|
||||
for e in get_tree().get_nodes_in_group("enemies"):
|
||||
var offset: Vector2 = e.global_position - mech.global_position
|
||||
var dist := offset.length()
|
||||
if dist > kinetic_radius:
|
||||
continue
|
||||
var falloff := 1.0 - dist / kinetic_radius * 0.6
|
||||
e.hit(kinetic_damage, offset.normalized() * kinetic_impulse * falloff)
|
||||
_ring(mech.global_position, kinetic_radius, Color(0.75, 0.85, 0.95))
|
||||
|
||||
|
||||
func _play_heat() -> void:
|
||||
# Heat wave: paints heat on everything near M03. Heat is a status —
|
||||
# it detonates at threshold (grunt-side), and next slice's
|
||||
# KINETIC+HEAT chemistry row reads the same state.
|
||||
var mech := _mech()
|
||||
if mech == null:
|
||||
return
|
||||
for e in get_tree().get_nodes_in_group("enemies"):
|
||||
if e.global_position.distance_to(mech.global_position) <= heat_radius:
|
||||
e.add_heat(heat_amount)
|
||||
_ring(mech.global_position, heat_radius, Color(0.95, 0.55, 0.2))
|
||||
|
||||
|
||||
func _ring(at: Vector2, radius: float, color: Color) -> void:
|
||||
var ring: Node2D = FX_RING.new()
|
||||
ring.position = at
|
||||
ring.max_radius = radius
|
||||
ring.color = color
|
||||
_mech().get_parent().add_child(ring)
|
||||
@@ -0,0 +1,40 @@
|
||||
extends Control
|
||||
## Greybox hand HUD: two card slots (bottom center), charge pips above
|
||||
## them, dilation knob readout (dev instrumentation, not game UI).
|
||||
## Iconic, not textual — slots are tag-hue blocks, no card text.
|
||||
|
||||
@onready var _hand: CanvasLayer = get_parent()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var slot_w := 24.0
|
||||
var slot_h := 32.0
|
||||
var gap := 6.0
|
||||
var total := slot_w * 2 + gap
|
||||
var x0 := (640.0 - total) / 2.0
|
||||
var y0 := 320.0
|
||||
|
||||
for i in _hand.cards.size():
|
||||
var card: Dictionary = _hand.cards[i]
|
||||
var rect := Rect2(x0 + i * (slot_w + gap), y0, slot_w, slot_h)
|
||||
var affordable: bool = _hand.charges >= card.cost_of.call()
|
||||
var hue: Color = card.hue
|
||||
if not affordable:
|
||||
hue = Color(hue.r, hue.g, hue.b, 0.25)
|
||||
elif not _hand.hand_open:
|
||||
hue = Color(hue.r, hue.g, hue.b, 0.6)
|
||||
draw_rect(rect, hue)
|
||||
if _hand.hand_open:
|
||||
draw_rect(rect.grow(1.0), Color(1, 1, 1, 0.9 if affordable else 0.3), false, 1.0)
|
||||
|
||||
var pip := 5.0
|
||||
var pips_w: float = float(_hand.max_charges) * (pip + 3.0) - 3.0
|
||||
var px0: float = (640.0 - pips_w) / 2.0
|
||||
for i in int(_hand.max_charges):
|
||||
var filled: bool = i < int(_hand.charges)
|
||||
var c := Color(0.9, 0.9, 0.95) if filled else Color(0.9, 0.9, 0.95, 0.18)
|
||||
draw_rect(Rect2(px0 + i * (pip + 3.0), y0 - 10.0, pip, pip), c)
|
||||
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font, Vector2(560, 352), "dt x%.2f" % _hand.dilation_scale,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color(0.6, 0.65, 0.7, 0.7))
|
||||
@@ -28,6 +28,10 @@ func _physics_process(delta: float) -> void:
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if body.is_in_group("enemies"):
|
||||
body.hit(damage, direction * knockback)
|
||||
# The build generates charges: every landed hit feeds the hand.
|
||||
var hand := get_tree().get_first_node_in_group("hand")
|
||||
if hand != null:
|
||||
hand.on_build_hit()
|
||||
queue_free()
|
||||
|
||||
|
||||
|
||||
+90
-3
@@ -2,8 +2,14 @@ extends Node
|
||||
## Headless smoke test for the tracer bullet. Run:
|
||||
## godot --headless --path . res://tests/smoke.tscn
|
||||
## Asserts: arena boots, piloting moves the mech, grunts spawn, the
|
||||
## weapon auto-fires with no input, a grunt dies, knockback displaces.
|
||||
## weapon auto-fires with no input, a grunt dies, knockback displaces,
|
||||
## build hits accrue charges, opening the hand freezes time, the HEAT
|
||||
## card paints heat, and stacked heat detonates on release.
|
||||
## Exit 0 = pass, 1 = fail.
|
||||
##
|
||||
## Await discipline: physics_frame only while time flows — with the
|
||||
## hand open at dilation 0.0 physics never steps, so frozen phases
|
||||
## await process_frame instead.
|
||||
|
||||
const ARENA := preload("res://scenes/arena.tscn")
|
||||
|
||||
@@ -12,14 +18,14 @@ var _done := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().create_timer(25.0).timeout.connect(_on_global_timeout)
|
||||
get_tree().create_timer(60.0).timeout.connect(_on_global_timeout)
|
||||
await _run()
|
||||
_finish()
|
||||
|
||||
|
||||
func _on_global_timeout() -> void:
|
||||
if not _done:
|
||||
_failures.append("global 25s timeout — test hung")
|
||||
_failures.append("global 60s timeout — test hung")
|
||||
_finish()
|
||||
|
||||
|
||||
@@ -33,6 +39,10 @@ func _run() -> void:
|
||||
if mech == null:
|
||||
_failures.append("no Mech node in arena")
|
||||
return
|
||||
# God-mode the mech: the long sim phases would otherwise drain hull
|
||||
# to zero mid-test and reload the scene under our assertions.
|
||||
mech.hull_max = 1.0e9
|
||||
mech.hull = 1.0e9
|
||||
|
||||
# Piloting: press right, mech must move right.
|
||||
var x0 := mech.global_position.x
|
||||
@@ -70,6 +80,83 @@ func _run() -> void:
|
||||
elif g.global_position.x <= gx + 2.0:
|
||||
_failures.append("knockback: grunt did not displace (x %.1f -> %.1f)" % [gx, g.global_position.x])
|
||||
|
||||
await _test_hand(arena, mech)
|
||||
|
||||
|
||||
func _test_hand(arena: Node, mech: CharacterBody2D) -> void:
|
||||
var hand := arena.get_node_or_null("Hand")
|
||||
if hand == null:
|
||||
_failures.append("no Hand node in arena")
|
||||
return
|
||||
|
||||
# Charges accrue from build hits alone (~4 hits per charge).
|
||||
if not await _until(func() -> bool: return hand.charges >= 2, 20.0, "build hits never accrued 2 charges"):
|
||||
return
|
||||
|
||||
# A grunt must be near the mech before we freeze — cards are radial
|
||||
# around M03 and nothing walks while time is stopped.
|
||||
if not await _until(func() -> bool: return _nearest_grunt(mech) != null, 10.0, "no grunt approached within heat radius"):
|
||||
return
|
||||
|
||||
# Open the hand: time must freeze to the dilation knob (default 0).
|
||||
await _tap("hand_toggle")
|
||||
if not hand.hand_open:
|
||||
_failures.append("hand did not open on hand_toggle")
|
||||
return
|
||||
if hand.dilation_scale == 0.0:
|
||||
if not get_tree().paused:
|
||||
_failures.append("hand open at knob 0 did not pause the world")
|
||||
elif absf(Engine.time_scale - hand.dilation_scale) > 0.001:
|
||||
_failures.append("time_scale %.3f != dilation knob %.3f with hand open" % [Engine.time_scale, hand.dilation_scale])
|
||||
|
||||
# Pick the target NOW — the world is frozen, so it can neither die
|
||||
# nor wander out of the heat radius before the casts land.
|
||||
var c0: int = hand.charges
|
||||
var target := _nearest_grunt(mech)
|
||||
if target == null:
|
||||
_failures.append("grunt near mech vanished before freeze")
|
||||
return
|
||||
|
||||
# Play HEAT twice: charges spent, heat painted past threshold.
|
||||
await _tap("card_2")
|
||||
await _tap("card_2")
|
||||
if hand.charges != c0 - 2:
|
||||
_failures.append("HEAT x2 spent %d charges, expected 2" % (c0 - hand.charges))
|
||||
if target.heat < target.heat_threshold:
|
||||
_failures.append("HEAT x2 left target below threshold (heat %.1f)" % target.heat)
|
||||
|
||||
# Release: time resumes, the over-threshold grunt detonates.
|
||||
await _tap("hand_toggle")
|
||||
if hand.hand_open or get_tree().paused or Engine.time_scale != 1.0:
|
||||
_failures.append("hand did not close / world did not resume")
|
||||
return
|
||||
for i in 10:
|
||||
await get_tree().physics_frame
|
||||
if is_instance_valid(target):
|
||||
_failures.append("heated grunt failed to detonate after release (heat %.1f)" % target.heat)
|
||||
|
||||
|
||||
func _nearest_grunt(mech: CharacterBody2D) -> Node2D:
|
||||
var best: Node2D = null
|
||||
var best_d := 120.0 * 120.0
|
||||
for e in get_tree().get_nodes_in_group("enemies"):
|
||||
var d: float = mech.global_position.distance_squared_to(e.global_position)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = e
|
||||
return best
|
||||
|
||||
|
||||
## Synthetic key tap via the action layer, safe under freeze: presses,
|
||||
## lets two process frames run so is_action_just_pressed is seen, then
|
||||
## releases and lets one more run.
|
||||
func _tap(action: String) -> void:
|
||||
Input.action_press(action)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
Input.action_release(action)
|
||||
await get_tree().process_frame
|
||||
|
||||
|
||||
func _until(pred: Callable, timeout: float, what: String) -> bool:
|
||||
var t := 0.0
|
||||
|
||||
Reference in New Issue
Block a user