feat: tracer bullet — greybox arena, positioning piloting, auto-fire KINETIC weapon
One-screen 640x360 arena (walls, grid floor, fixed view), mech with positioning-only movement (WASD/arrows, accel/decel, no aim), auto-fire KINETIC hardpoint (nearest-target, visible range ring) firing slugs that damage + knock back, untagged grunt trickle spawner with contact damage, hull bar HUD, scene-reload on hull zero. Headless smoke gate: tests/smoke.tscn asserts boot, piloting movement, spawning, input-free auto-fire, grunt lethality, knockback displacement. 3/3 passes on nh3-dev headless 4.7.1.
This commit is contained in:
@@ -7,13 +7,52 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="U R M03"
|
||||
run/main_scene="res://scenes/arena.tscn"
|
||||
config/features=PackedStringArray("4.7")
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=640
|
||||
window/size/viewport_height=360
|
||||
window/size/window_width_override=1280
|
||||
window/size/window_height_override=720
|
||||
window/stretch/mode="viewport"
|
||||
window/stretch/scale_mode="integer"
|
||||
|
||||
[input]
|
||||
|
||||
move_left={
|
||||
"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":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, 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":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_right={
|
||||
"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":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, 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":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_up={
|
||||
"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":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, 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":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_down={
|
||||
"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":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, 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)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
2d_physics/layer_1="walls"
|
||||
2d_physics/layer_2="mech"
|
||||
2d_physics/layer_3="enemies"
|
||||
2d_physics/layer_4="projectiles"
|
||||
|
||||
[rendering]
|
||||
|
||||
textures/canvas_textures/default_texture_filter=0
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
[gd_scene load_steps=6 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"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="wall_h"]
|
||||
size = Vector2(640, 16)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="wall_v"]
|
||||
size = Vector2(16, 360)
|
||||
|
||||
[node name="Arena" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="WallTop" type="StaticBody2D" parent="."]
|
||||
position = Vector2(320, 8)
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="WallTop"]
|
||||
shape = SubResource("wall_h")
|
||||
|
||||
[node name="WallBottom" type="StaticBody2D" parent="."]
|
||||
position = Vector2(320, 352)
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="WallBottom"]
|
||||
shape = SubResource("wall_h")
|
||||
|
||||
[node name="WallLeft" type="StaticBody2D" parent="."]
|
||||
position = Vector2(8, 180)
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="WallLeft"]
|
||||
shape = SubResource("wall_v")
|
||||
|
||||
[node name="WallRight" type="StaticBody2D" parent="."]
|
||||
position = Vector2(632, 180)
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="WallRight"]
|
||||
shape = SubResource("wall_v")
|
||||
|
||||
[node name="Mech" parent="." instance=ExtResource("2")]
|
||||
position = Vector2(320, 180)
|
||||
|
||||
[node name="Spawner" type="Node2D" parent="."]
|
||||
script = ExtResource("3")
|
||||
|
||||
[node name="HUD" type="CanvasLayer" parent="."]
|
||||
|
||||
[node name="HullBack" type="ColorRect" parent="HUD"]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 128.0
|
||||
offset_bottom = 14.0
|
||||
color = Color(0.06, 0.07, 0.08, 1)
|
||||
|
||||
[node name="HullFill" type="ColorRect" parent="HUD"]
|
||||
offset_left = 9.0
|
||||
offset_top = 9.0
|
||||
offset_right = 127.0
|
||||
offset_bottom = 13.0
|
||||
color = Color(0.55, 0.8, 0.55, 1)
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/grunt.gd" id="1"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="body"]
|
||||
radius = 8.0
|
||||
|
||||
[node name="Grunt" type="CharacterBody2D"]
|
||||
collision_layer = 4
|
||||
collision_mask = 7
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("body")
|
||||
@@ -0,0 +1,15 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/kinetic_projectile.gd" id="1"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="body"]
|
||||
radius = 4.0
|
||||
|
||||
[node name="KineticProjectile" type="Area2D"]
|
||||
collision_layer = 8
|
||||
collision_mask = 5
|
||||
monitorable = false
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("body")
|
||||
@@ -0,0 +1,18 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/mech.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://scripts/kinetic_weapon.gd" id="2"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="body"]
|
||||
size = Vector2(20, 24)
|
||||
|
||||
[node name="Mech" type="CharacterBody2D"]
|
||||
collision_layer = 2
|
||||
collision_mask = 5
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Collision" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("body")
|
||||
|
||||
[node name="Weapon" type="Node2D" parent="."]
|
||||
script = ExtResource("2")
|
||||
@@ -0,0 +1,31 @@
|
||||
extends Node2D
|
||||
## One greybox arena, one screen (640x360), fixed camera. Draws its own
|
||||
## floor and wall slabs; tracks kills for instrumentation.
|
||||
|
||||
var kills: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
$Spawner.grunt_spawned.connect(_on_grunt_spawned)
|
||||
$Mech.hull_changed.connect(_on_hull_changed)
|
||||
|
||||
|
||||
func _on_grunt_spawned(grunt: Node) -> void:
|
||||
grunt.died.connect(func() -> void: kills += 1)
|
||||
|
||||
|
||||
func _on_hull_changed(hull: float, hull_max: float) -> void:
|
||||
$HUD/HullFill.size.x = 118.0 * (hull / hull_max)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_rect(Rect2(0, 0, 640, 360), Color(0.1, 0.11, 0.13))
|
||||
for x in range(0, 641, 32):
|
||||
draw_line(Vector2(x, 0), Vector2(x, 360), Color(0.13, 0.145, 0.17))
|
||||
for y in range(0, 361, 32):
|
||||
draw_line(Vector2(0, y), Vector2(640, y), Color(0.13, 0.145, 0.17))
|
||||
var wall := Color(0.3, 0.32, 0.36)
|
||||
draw_rect(Rect2(0, 0, 640, 16), wall)
|
||||
draw_rect(Rect2(0, 344, 640, 16), wall)
|
||||
draw_rect(Rect2(0, 0, 16, 360), wall)
|
||||
draw_rect(Rect2(624, 0, 16, 360), wall)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cw1g4kdg6hn82
|
||||
@@ -0,0 +1,58 @@
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
var _knockback := Vector2.ZERO
|
||||
var _contact_cd := 0.0
|
||||
var _flash := 0.0
|
||||
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:
|
||||
_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:
|
||||
hp -= damage
|
||||
_knockback += kb
|
||||
_flash = 0.12
|
||||
queue_redraw()
|
||||
if hp <= 0.0:
|
||||
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)
|
||||
draw_circle(Vector2.ZERO, 8.0, body)
|
||||
@@ -0,0 +1 @@
|
||||
uid://k5sas40w41a6
|
||||
@@ -0,0 +1,36 @@
|
||||
extends Area2D
|
||||
## KINETIC slug: damage plus knockback along its travel direction.
|
||||
## Knockback is the load-bearing part — KINETIC is the one tag that
|
||||
## touches the piloting layer, and the HEAT+KINETIC chemistry row
|
||||
## (next slice) rides on impacts this weapon causes.
|
||||
|
||||
@export var speed := 420.0
|
||||
@export var damage := 1.0
|
||||
@export var knockback := 220.0
|
||||
@export var lifetime := 0.8
|
||||
|
||||
var direction := Vector2.RIGHT
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("projectiles")
|
||||
rotation = direction.angle()
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
position += direction * speed * delta
|
||||
lifetime -= delta
|
||||
if lifetime <= 0.0:
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if body.is_in_group("enemies"):
|
||||
body.hit(damage, direction * knockback)
|
||||
queue_free()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# KINETIC hue: pale steel-blue (placeholder — tag colors unassigned in canon).
|
||||
draw_rect(Rect2(-5, -1.5, 10, 3), Color(0.75, 0.85, 0.95))
|
||||
@@ -0,0 +1 @@
|
||||
uid://ose3uhxglar
|
||||
@@ -0,0 +1,44 @@
|
||||
extends Node2D
|
||||
## Auto-fire KINETIC hardpoint. No player input ever touches this —
|
||||
## the build expresses itself while the pilot only positions. Draws its
|
||||
## own range ring so the kill zone is legible (positioning <-> build bet).
|
||||
|
||||
const PROJECTILE := preload("res://scenes/kinetic_projectile.tscn")
|
||||
|
||||
@export var fire_interval := 0.4
|
||||
@export var range_px := 220.0
|
||||
|
||||
var _cooldown := 0.0
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_cooldown -= delta
|
||||
if _cooldown > 0.0:
|
||||
return
|
||||
var target := _nearest_enemy()
|
||||
if target == null:
|
||||
return
|
||||
_fire(target)
|
||||
_cooldown = fire_interval
|
||||
|
||||
|
||||
func _nearest_enemy() -> Node2D:
|
||||
var best: Node2D = null
|
||||
var best_d := range_px * range_px
|
||||
for e in get_tree().get_nodes_in_group("enemies"):
|
||||
var d: float = global_position.distance_squared_to(e.global_position)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = e
|
||||
return best
|
||||
|
||||
|
||||
func _fire(target: Node2D) -> void:
|
||||
var p := PROJECTILE.instantiate()
|
||||
p.direction = (target.global_position - global_position).normalized()
|
||||
p.position = global_position
|
||||
owner.get_parent().add_child(p)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_arc(Vector2.ZERO, range_px, 0.0, TAU, 64, Color(0.55, 0.7, 0.85, 0.18), 1.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dn6fsoa45vgu1
|
||||
@@ -0,0 +1,37 @@
|
||||
class_name Mech
|
||||
extends CharacterBody2D
|
||||
## M03, greybox slab. Piloting is positioning ONLY (canon: no aim, no
|
||||
## combos) — movement input is the entire control surface.
|
||||
|
||||
signal hull_changed(hull: float, hull_max: float)
|
||||
|
||||
@export var move_speed := 160.0
|
||||
@export var accel := 900.0
|
||||
@export var decel := 1200.0
|
||||
@export var hull_max := 100.0
|
||||
|
||||
var hull: float
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("mech")
|
||||
hull = hull_max
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
var dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||
var rate := accel if dir != Vector2.ZERO else decel
|
||||
velocity = velocity.move_toward(dir * move_speed, rate * delta)
|
||||
move_and_slide()
|
||||
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
hull = maxf(hull - amount, 0.0)
|
||||
hull_changed.emit(hull, hull_max)
|
||||
if hull <= 0.0:
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_rect(Rect2(-10, -12, 20, 24), Color(0.82, 0.84, 0.88))
|
||||
draw_rect(Rect2(-6, -6, 12, 12), Color(0.55, 0.6, 0.68))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cyw4tmaglsdu4
|
||||
@@ -0,0 +1,44 @@
|
||||
extends Node2D
|
||||
## Keeps a floor count of grunts alive, spawning at the arena's inset
|
||||
## edges. Endless trickle — the tracer needs targets, not waves.
|
||||
|
||||
signal grunt_spawned(grunt: Node)
|
||||
|
||||
const GRUNT := preload("res://scenes/grunt.tscn")
|
||||
|
||||
@export var target_count := 8
|
||||
@export var spawn_interval := 0.8
|
||||
@export var spawn_rect := Rect2(32, 32, 576, 296)
|
||||
@export var min_dist_from_mech := 120.0
|
||||
|
||||
var _timer := 0.0
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_timer -= delta
|
||||
if _timer > 0.0:
|
||||
return
|
||||
_timer = spawn_interval
|
||||
if get_tree().get_nodes_in_group("enemies").size() < target_count:
|
||||
_spawn()
|
||||
|
||||
|
||||
func _spawn() -> void:
|
||||
var g := GRUNT.instantiate()
|
||||
g.position = _pick_point()
|
||||
add_child(g)
|
||||
grunt_spawned.emit(g)
|
||||
|
||||
|
||||
func _pick_point() -> Vector2:
|
||||
var mech := get_tree().get_first_node_in_group("mech") as Node2D
|
||||
var p := Vector2.ZERO
|
||||
for i in 12:
|
||||
match randi() % 4:
|
||||
0: p = Vector2(randf_range(spawn_rect.position.x, spawn_rect.end.x), spawn_rect.position.y)
|
||||
1: p = Vector2(randf_range(spawn_rect.position.x, spawn_rect.end.x), spawn_rect.end.y)
|
||||
2: p = Vector2(spawn_rect.position.x, randf_range(spawn_rect.position.y, spawn_rect.end.y))
|
||||
3: p = Vector2(spawn_rect.end.x, randf_range(spawn_rect.position.y, spawn_rect.end.y))
|
||||
if mech == null or p.distance_to(mech.global_position) >= min_dist_from_mech:
|
||||
return p
|
||||
return p
|
||||
@@ -0,0 +1 @@
|
||||
uid://bo1kuwuh0xjyb
|
||||
@@ -0,0 +1,95 @@
|
||||
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.
|
||||
## Exit 0 = pass, 1 = fail.
|
||||
|
||||
const ARENA := preload("res://scenes/arena.tscn")
|
||||
|
||||
var _failures: Array[String] = []
|
||||
var _done := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
get_tree().create_timer(25.0).timeout.connect(_on_global_timeout)
|
||||
await _run()
|
||||
_finish()
|
||||
|
||||
|
||||
func _on_global_timeout() -> void:
|
||||
if not _done:
|
||||
_failures.append("global 25s timeout — test hung")
|
||||
_finish()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var arena := ARENA.instantiate()
|
||||
add_child(arena)
|
||||
await get_tree().physics_frame
|
||||
await get_tree().physics_frame
|
||||
|
||||
var mech := arena.get_node_or_null("Mech") as CharacterBody2D
|
||||
if mech == null:
|
||||
_failures.append("no Mech node in arena")
|
||||
return
|
||||
|
||||
# Piloting: press right, mech must move right.
|
||||
var x0 := mech.global_position.x
|
||||
Input.action_press("move_right")
|
||||
for i in 30:
|
||||
await get_tree().physics_frame
|
||||
Input.action_release("move_right")
|
||||
if mech.global_position.x <= x0 + 10.0:
|
||||
_failures.append("piloting: mech did not move right (x %.1f -> %.1f)" % [x0, mech.global_position.x])
|
||||
|
||||
# Spawner: grunts appear.
|
||||
if not await _until(func() -> bool: return get_tree().get_nodes_in_group("enemies").size() > 0, 5.0, "no grunts spawned"):
|
||||
return
|
||||
|
||||
# Auto-fire: a projectile appears with zero input.
|
||||
await _until(func() -> bool: return get_tree().get_nodes_in_group("projectiles").size() > 0, 5.0, "weapon never auto-fired")
|
||||
|
||||
# Lethality: at least one grunt dies to auto-fire alone.
|
||||
await _until(func() -> bool: return arena.kills >= 1, 10.0, "no grunt died to auto-fire")
|
||||
|
||||
# Knockback: a zero-damage hit must displace a grunt along the hit
|
||||
# direction. Probe the healthiest grunt — one mid-death from
|
||||
# auto-fire would vanish under the probe and fake a failure.
|
||||
var g: CharacterBody2D = null
|
||||
for cand in get_tree().get_nodes_in_group("enemies"):
|
||||
if g == null or cand.hp > g.hp:
|
||||
g = cand
|
||||
if g != null:
|
||||
var gx := g.global_position.x
|
||||
g.hit(0.0, Vector2(400, 0))
|
||||
for i in 5:
|
||||
await get_tree().physics_frame
|
||||
if not is_instance_valid(g):
|
||||
_failures.append("knockback probe grunt died to a zero-damage hit")
|
||||
elif g.global_position.x <= gx + 2.0:
|
||||
_failures.append("knockback: grunt did not displace (x %.1f -> %.1f)" % [gx, g.global_position.x])
|
||||
|
||||
|
||||
func _until(pred: Callable, timeout: float, what: String) -> bool:
|
||||
var t := 0.0
|
||||
while t < timeout:
|
||||
if pred.call():
|
||||
return true
|
||||
await get_tree().physics_frame
|
||||
t += get_physics_process_delta_time()
|
||||
_failures.append("timeout: " + what)
|
||||
return false
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if _done:
|
||||
return
|
||||
_done = true
|
||||
if _failures.is_empty():
|
||||
print("SMOKE PASS")
|
||||
get_tree().quit(0)
|
||||
else:
|
||||
for f in _failures:
|
||||
printerr("SMOKE FAIL: " + f)
|
||||
get_tree().quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://qcvs3eu210cv
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/smoke.gd" id="1"]
|
||||
|
||||
[node name="Smoke" type="Node"]
|
||||
script = ExtResource("1")
|
||||
Reference in New Issue
Block a user