feat: salvage-pickup loop — kill-fed charges, scrap drops, salvage-gated card levels
Per canon decisions 2026-08-07 (advisor CONCEPT.md f12cb15, falsifier response): charges now generate on kills (kills_per_charge, chemistry chain kills feed the hand; per-hit generation removed). Kills drop scrap as physical pickups (amber diamond, magnet radius 70, drift to mech, 20s fade; grunt 1 / burster 3) — collection is the positioning reward gradient. Cumulative collected scrap gates card levels 1-3 (thresholds 12/30; per-level radius x1.25/x1.5 and power x1.15/x1.5 — HEAT L3 hits one-cast detonation threshold). HUD: level notches under slots + scrap counter. Salvage spawn is call_deferred: death can fire inside a physics signal flush where Area2D setup is blocked. Smoke gate: charge accrual now kill-fed, salvage drop + collection + level gating phases added, heat-target selection made retry-tolerant. 4/4 green on nh3-dev headless 4.7.1.
This commit is contained in:
+25
-17
@@ -16,24 +16,24 @@ greybox falsifier (ROADMAP.md v1) that tests the design's three core bets.
|
|||||||
|
|
||||||
## Current state / in-flight
|
## Current state / in-flight
|
||||||
|
|
||||||
_As of 2026-08-07:_
|
_As of 2026-08-07 (post-falsifier):_
|
||||||
|
|
||||||
- **Greybox build COMPLETE** at `1976149`, pushed to gitea (vh/urm03) —
|
- **Greybox build complete AND falsifier verdicts rendered** (ROADMAP
|
||||||
all five build capabilities live: arena + positioning-only piloting,
|
v1 target met through capability 6). Verdicts: bet 2 (card-freeze
|
||||||
auto-fire KINETIC weapon, cards + Freeze-charge meter + thaw/release
|
rhythm) CONFIRMED; bet 1 (positioning) shaky → salvage-pickup loop
|
||||||
ramps, KINETIC+HEAT chemistry row with authorship-loud feedback stack
|
adopted and built; bet 3 (chemistry) inconclusive → in-card
|
||||||
(hitstop + hues meeting + `_ph` sting), grunt swarm + self-heating
|
randomness parked to belt stage.
|
||||||
burster. Headless smoke gate green 3/3.
|
- **Post-falsifier loop built**: charges generate on kills; kills drop
|
||||||
- **Awaiting the falsification playtest** (ROADMAP capability 6):
|
scrap pickups (magnet radius, fade); collected scrap gates card
|
||||||
operator verdicts on the three bets — positioning↔build interaction,
|
levels 1→3 (thresholds + per-level radius/power scaling, HEAT L3
|
||||||
card-freeze rhythm, chemistry delight. Early signal on bet 2 is
|
reaches one-cast threshold). Smoke gate green 4/4.
|
||||||
positive: freeze meter + release ramp "feels very good" / "starting to
|
- Canon amendments landed in the advisor repo at `f12cb15`
|
||||||
feel game-like" (operator, 2026-08-07). Tuning everywhere still open.
|
(**local-only — that repo has NO git remote**): Freeze charge
|
||||||
- Operator hit a pull conflict on the Mac (untracked `.uid` files vs
|
ratified, kill-charges + salvage-gated levels decided, randomness
|
||||||
`1976149`) — fix being applied this session; an operator-surfaced
|
parked.
|
||||||
issue is expected next.
|
- Everything is knobs; operator says "everything needs tuning" — no
|
||||||
- After verdicts: draft the CONCEPT.md canon entries in the advisor repo
|
tuning pass done yet. v1.0 cut not yet made (operator sign-off
|
||||||
(freeze-meter ratification + whatever else the playtest settles).
|
needed; ROADMAP v1 target arguably met).
|
||||||
|
|
||||||
## Recent decisions
|
## Recent decisions
|
||||||
|
|
||||||
@@ -51,6 +51,10 @@ _As of 2026-08-07:_
|
|||||||
- `[2026-08-06]` Placeholder tag hues: KINETIC pale steel-blue, HEAT
|
- `[2026-08-06]` Placeholder tag hues: KINETIC pale steel-blue, HEAT
|
||||||
orange — canon assigns no tag colors yet (naming/color pass parked
|
orange — canon assigns no tag colors yet (naming/color pass parked
|
||||||
post-greybox).
|
post-greybox).
|
||||||
|
- `[2026-08-07]` Falsifier verdicts + resulting design decisions live
|
||||||
|
in advisor-repo CONCEPT.md (§ loop stack amendments, decisions-log
|
||||||
|
rows, open-question #0 closure) at `f12cb15` — design rationale is
|
||||||
|
NOT duplicated here; that log owns it.
|
||||||
|
|
||||||
## Tried and abandoned
|
## Tried and abandoned
|
||||||
|
|
||||||
@@ -63,3 +67,7 @@ _As of 2026-08-07:_
|
|||||||
the Mac editor generated its own untracked copies and the next
|
the Mac editor generated its own untracked copies and the next
|
||||||
`git pull` refused to merge over them. Import headless before
|
`git pull` refused to merge over them. Import headless before
|
||||||
committing so `.uid` files ride the same commit as their scripts.
|
committing so `.uid` files ride the same commit as their scripts.
|
||||||
|
- `[2026-08-07]` Spawning physics nodes (Area2D) directly inside a
|
||||||
|
physics signal flush (projectile `body_entered` → death → spawn) —
|
||||||
|
Godot blocks collision setup mid-flush. Spawn via
|
||||||
|
`add_child.call_deferred()` from any death/hit signal path.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ hp = 5.0
|
|||||||
contact_damage = 3.0
|
contact_damage = 3.0
|
||||||
body_radius = 11.0
|
body_radius = 11.0
|
||||||
base_color = Color(0.6, 0.4, 0.25, 1)
|
base_color = Color(0.6, 0.4, 0.25, 1)
|
||||||
|
salvage_value = 3
|
||||||
self_heat_rate = 0.5
|
self_heat_rate = 0.5
|
||||||
detonation_damage = 2.5
|
detonation_damage = 2.5
|
||||||
detonation_mech_damage = 15.0
|
detonation_mech_damage = 15.0
|
||||||
|
|||||||
+16
-1
@@ -6,6 +6,7 @@ extends Node2D
|
|||||||
## no proc text, the light and the sting are the noun).
|
## no proc text, the light and the sting are the noun).
|
||||||
|
|
||||||
const CHEM_FX := preload("res://scripts/chemistry_fx.gd")
|
const CHEM_FX := preload("res://scripts/chemistry_fx.gd")
|
||||||
|
const SALVAGE := preload("res://scripts/salvage.gd")
|
||||||
|
|
||||||
var kills: int = 0
|
var kills: int = 0
|
||||||
var chem_procs: int = 0
|
var chem_procs: int = 0
|
||||||
@@ -30,7 +31,21 @@ func chemistry_proc(at: Vector2, axis: Vector2) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_grunt_spawned(grunt: Node) -> void:
|
func _on_grunt_spawned(grunt: Node) -> void:
|
||||||
grunt.died.connect(func() -> void: kills += 1)
|
grunt.died.connect(func() -> void: _on_grunt_died(grunt))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_grunt_died(grunt: Node2D) -> void:
|
||||||
|
# died fires before queue_free, so position/value are still valid.
|
||||||
|
kills += 1
|
||||||
|
var hand := get_tree().get_first_node_in_group("hand")
|
||||||
|
if hand != null:
|
||||||
|
hand.on_kill()
|
||||||
|
var s: Area2D = SALVAGE.new()
|
||||||
|
s.position = grunt.global_position
|
||||||
|
s.value = grunt.salvage_value
|
||||||
|
# Deferred: death can fire inside a physics signal flush (projectile
|
||||||
|
# body_entered), where Area2D collision setup is blocked.
|
||||||
|
add_child.call_deferred(s)
|
||||||
|
|
||||||
|
|
||||||
func _on_hull_changed(hull: float, hull_max: float) -> void:
|
func _on_hull_changed(hull: float, hull_max: float) -> void:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ signal died
|
|||||||
@export var knockback_decay := 5.0
|
@export var knockback_decay := 5.0
|
||||||
@export var body_radius := 8.0
|
@export var body_radius := 8.0
|
||||||
@export var base_color := Color(0.45, 0.47, 0.4)
|
@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.
|
## > 0 makes this a burster: heat/second it applies to itself.
|
||||||
@export var self_heat_rate := 0.0
|
@export var self_heat_rate := 0.0
|
||||||
|
|
||||||
|
|||||||
+41
-13
@@ -33,9 +33,20 @@ extends CanvasLayer
|
|||||||
@export var dilation_scale := 0.0
|
@export var dilation_scale := 0.0
|
||||||
@export var dilation_step := 0.05
|
@export var dilation_step := 0.05
|
||||||
@export var dilation_max := 0.3
|
@export var dilation_max := 0.3
|
||||||
@export var hits_per_charge := 4
|
@export var kills_per_charge := 2
|
||||||
@export var max_charges := 5
|
@export var max_charges := 5
|
||||||
|
|
||||||
|
@export_group("Salvage / card levels")
|
||||||
|
## Cumulative collected scrap gates card levels (canon 2026-08-07:
|
||||||
|
## same-currency rule — this is the run-timescale reading of the same
|
||||||
|
## scrap the meta will bank; it is NOT a separate XP).
|
||||||
|
@export var level2_salvage := 12
|
||||||
|
@export var level3_salvage := 30
|
||||||
|
## Per-level scaling, index = level - 1. Radius grows reach; power
|
||||||
|
## scales impulse/heat (HEAT at L3 reaches one-cast threshold).
|
||||||
|
@export var level_radius_scale: Array[float] = [1.0, 1.25, 1.5]
|
||||||
|
@export var level_power_scale: Array[float] = [1.0, 1.15, 1.5]
|
||||||
|
|
||||||
@export_group("Freeze meter")
|
@export_group("Freeze meter")
|
||||||
## Seconds of open-hand time a full meter holds.
|
## Seconds of open-hand time a full meter holds.
|
||||||
@export var meter_max := 3.0
|
@export var meter_max := 3.0
|
||||||
@@ -77,7 +88,8 @@ const FX_RING := preload("res://scripts/fx_ring.gd")
|
|||||||
var charges := 0
|
var charges := 0
|
||||||
var hand_open := false
|
var hand_open := false
|
||||||
var meter := 0.0
|
var meter := 0.0
|
||||||
var _hit_progress := 0
|
var salvage_total := 0
|
||||||
|
var _kill_progress := 0
|
||||||
var _last_ms := 0
|
var _last_ms := 0
|
||||||
var _release_from := 1.0
|
var _release_from := 1.0
|
||||||
var _release_t := -1.0
|
var _release_t := -1.0
|
||||||
@@ -170,13 +182,23 @@ func _input(event: InputEvent) -> void:
|
|||||||
_apply_time_state()
|
_apply_time_state()
|
||||||
|
|
||||||
|
|
||||||
func on_build_hit() -> void:
|
func on_kill() -> void:
|
||||||
_hit_progress += 1
|
# Charges generate on kills — build-generated (the build does the
|
||||||
if _hit_progress >= hits_per_charge:
|
# killing), and chemistry-chain kills feed the hand (canon 2026-08-07).
|
||||||
_hit_progress = 0
|
_kill_progress += 1
|
||||||
|
if _kill_progress >= kills_per_charge:
|
||||||
|
_kill_progress = 0
|
||||||
charges = mini(charges + 1, max_charges)
|
charges = mini(charges + 1, max_charges)
|
||||||
|
|
||||||
|
|
||||||
|
func add_salvage(value: int) -> void:
|
||||||
|
salvage_total += value
|
||||||
|
|
||||||
|
|
||||||
|
func card_level() -> int:
|
||||||
|
return 1 + int(salvage_total >= level2_salvage) + int(salvage_total >= level3_salvage)
|
||||||
|
|
||||||
|
|
||||||
func hitstop(dur := -1.0) -> void:
|
func hitstop(dur := -1.0) -> void:
|
||||||
_hitstop_t = minf(maxf(_hitstop_t, dur if dur > 0.0 else hitstop_s), 0.15)
|
_hitstop_t = minf(maxf(_hitstop_t, dur if dur > 0.0 else hitstop_s), 0.15)
|
||||||
|
|
||||||
@@ -261,14 +283,17 @@ func _play_kinetic() -> void:
|
|||||||
var mech := _mech()
|
var mech := _mech()
|
||||||
if mech == null:
|
if mech == null:
|
||||||
return
|
return
|
||||||
|
var lvl := card_level()
|
||||||
|
var radius: float = kinetic_radius * level_radius_scale[lvl - 1]
|
||||||
|
var impulse: float = kinetic_impulse * level_power_scale[lvl - 1]
|
||||||
for e in get_tree().get_nodes_in_group("enemies"):
|
for e in get_tree().get_nodes_in_group("enemies"):
|
||||||
var offset: Vector2 = e.global_position - mech.global_position
|
var offset: Vector2 = e.global_position - mech.global_position
|
||||||
var dist := offset.length()
|
var dist := offset.length()
|
||||||
if dist > kinetic_radius:
|
if dist > radius:
|
||||||
continue
|
continue
|
||||||
var falloff := 1.0 - dist / kinetic_radius * 0.6
|
var falloff := 1.0 - dist / radius * 0.6
|
||||||
e.hit(kinetic_damage, offset.normalized() * kinetic_impulse * falloff)
|
e.hit(kinetic_damage, offset.normalized() * impulse * falloff)
|
||||||
_ring(mech.global_position, kinetic_radius, Color(0.75, 0.85, 0.95))
|
_ring(mech.global_position, radius, Color(0.75, 0.85, 0.95))
|
||||||
|
|
||||||
|
|
||||||
func _play_heat() -> void:
|
func _play_heat() -> void:
|
||||||
@@ -278,10 +303,13 @@ func _play_heat() -> void:
|
|||||||
var mech := _mech()
|
var mech := _mech()
|
||||||
if mech == null:
|
if mech == null:
|
||||||
return
|
return
|
||||||
|
var lvl := card_level()
|
||||||
|
var radius: float = heat_radius * level_radius_scale[lvl - 1]
|
||||||
|
var amount: float = heat_amount * level_power_scale[lvl - 1]
|
||||||
for e in get_tree().get_nodes_in_group("enemies"):
|
for e in get_tree().get_nodes_in_group("enemies"):
|
||||||
if e.global_position.distance_to(mech.global_position) <= heat_radius:
|
if e.global_position.distance_to(mech.global_position) <= radius:
|
||||||
e.add_heat(heat_amount)
|
e.add_heat(amount)
|
||||||
_ring(mech.global_position, heat_radius, Color(0.95, 0.55, 0.2))
|
_ring(mech.global_position, radius, Color(0.95, 0.55, 0.2))
|
||||||
|
|
||||||
|
|
||||||
func _ring(at: Vector2, radius: float, color: Color) -> void:
|
func _ring(at: Vector2, radius: float, color: Color) -> void:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func _draw() -> void:
|
|||||||
var x0 := (640.0 - total) / 2.0
|
var x0 := (640.0 - total) / 2.0
|
||||||
var y0 := 320.0
|
var y0 := 320.0
|
||||||
|
|
||||||
|
var lvl: int = _hand.card_level()
|
||||||
for i in _hand.cards.size():
|
for i in _hand.cards.size():
|
||||||
var card: Dictionary = _hand.cards[i]
|
var card: Dictionary = _hand.cards[i]
|
||||||
var rect := Rect2(x0 + i * (slot_w + gap), y0, slot_w, slot_h)
|
var rect := Rect2(x0 + i * (slot_w + gap), y0, slot_w, slot_h)
|
||||||
@@ -26,6 +27,17 @@ func _draw() -> void:
|
|||||||
draw_rect(rect, hue)
|
draw_rect(rect, hue)
|
||||||
if _hand.hand_open:
|
if _hand.hand_open:
|
||||||
draw_rect(rect.grow(1.0), Color(1, 1, 1, 0.9 if affordable else 0.3), false, 1.0)
|
draw_rect(rect.grow(1.0), Color(1, 1, 1, 0.9 if affordable else 0.3), false, 1.0)
|
||||||
|
# Card level: notches under the slot.
|
||||||
|
for n in lvl:
|
||||||
|
draw_rect(Rect2(rect.position.x + n * 8.0, rect.end.y + 3.0, 5.0, 3.0), Color(0.95, 0.8, 0.35))
|
||||||
|
|
||||||
|
# Salvage counter: scrap diamond + running total, right of the slots.
|
||||||
|
var dx := x0 + total + 16.0
|
||||||
|
var dy := y0 + 14.0
|
||||||
|
var dia := PackedVector2Array([Vector2(dx, dy - 5), Vector2(dx + 4, dy), Vector2(dx, dy + 5), Vector2(dx - 4, dy)])
|
||||||
|
draw_colored_polygon(dia, Color(0.95, 0.8, 0.35))
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(dx + 8.0, dy + 4.0), str(_hand.salvage_total),
|
||||||
|
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.85, 0.85, 0.9))
|
||||||
|
|
||||||
var pip := 5.0
|
var pip := 5.0
|
||||||
var pips_w: float = float(_hand.max_charges) * (pip + 3.0) - 3.0
|
var pips_w: float = float(_hand.max_charges) * (pip + 3.0) - 3.0
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ func _physics_process(delta: float) -> void:
|
|||||||
func _on_body_entered(body: Node) -> void:
|
func _on_body_entered(body: Node) -> void:
|
||||||
if body.is_in_group("enemies"):
|
if body.is_in_group("enemies"):
|
||||||
body.hit(damage, direction * knockback)
|
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()
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
extends Area2D
|
||||||
|
## Scrap drop — the positioning reward gradient (canon 2026-08-07:
|
||||||
|
## kills drop scrap physically; collecting it gates in-run card
|
||||||
|
## levels). Drifts to the mech inside the magnet radius, collected on
|
||||||
|
## contact, fades out if ignored. Constructed in code, no scene.
|
||||||
|
|
||||||
|
var value := 1
|
||||||
|
var magnet_radius := 70.0
|
||||||
|
var drift_speed := 150.0
|
||||||
|
var lifetime := 20.0
|
||||||
|
|
||||||
|
var _t := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group("salvage")
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 2
|
||||||
|
monitorable = false
|
||||||
|
var cs := CollisionShape2D.new()
|
||||||
|
var sh := CircleShape2D.new()
|
||||||
|
sh.radius = 6.0
|
||||||
|
cs.shape = sh
|
||||||
|
add_child(cs)
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
_t += delta
|
||||||
|
if _t >= lifetime:
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
var mech := get_tree().get_first_node_in_group("mech") as Node2D
|
||||||
|
if mech != null and global_position.distance_to(mech.global_position) <= magnet_radius:
|
||||||
|
global_position = global_position.move_toward(mech.global_position, drift_speed * delta)
|
||||||
|
if _t >= lifetime - 3.0:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node) -> void:
|
||||||
|
if body.is_in_group("mech"):
|
||||||
|
var hand := get_tree().get_first_node_in_group("hand")
|
||||||
|
if hand != null:
|
||||||
|
hand.add_salvage(value)
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var a := 1.0 if _t < lifetime - 3.0 else clampf((lifetime - _t) / 3.0, 0.0, 1.0)
|
||||||
|
var pts := PackedVector2Array([Vector2(0, -5), Vector2(4, 0), Vector2(0, 5), Vector2(-4, 0)])
|
||||||
|
draw_colored_polygon(pts, Color(0.95, 0.8, 0.35, a))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bs1xhn4u5821d
|
||||||
+39
-12
@@ -89,32 +89,35 @@ func _test_hand(arena: Node, mech: CharacterBody2D) -> void:
|
|||||||
_failures.append("no Hand node in arena")
|
_failures.append("no Hand node in arena")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Charges accrue from build hits alone (~4 hits per charge).
|
# Charges accrue from kills alone (kills_per_charge, auto-fire only).
|
||||||
if not await _until(func() -> bool: return hand.charges >= 2, 20.0, "build hits never accrued 2 charges"):
|
if not await _until(func() -> bool: return hand.charges >= 2, 25.0, "kills never accrued 2 charges"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# A grunt must be near the mech before we freeze — cards are radial
|
# A grunt must be near the mech when we freeze — cards are radial
|
||||||
# around M03 and nothing walks while time is stopped.
|
# around M03 and nothing walks while time is stopped. The nearest
|
||||||
|
# grunt can die to auto-fire during the toggle tap, so retry.
|
||||||
|
var target: Node2D = null
|
||||||
|
for attempt in 4:
|
||||||
if not await _until(func() -> bool: return _nearest_grunt(mech) != null, 10.0, "no grunt approached within heat radius"):
|
if not await _until(func() -> bool: return _nearest_grunt(mech) != null, 10.0, "no grunt approached within heat radius"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Open the hand: time must freeze to the dilation knob (default 0).
|
|
||||||
await _tap("hand_toggle")
|
await _tap("hand_toggle")
|
||||||
if not hand.hand_open:
|
if not hand.hand_open:
|
||||||
_failures.append("hand did not open on hand_toggle")
|
_failures.append("hand did not open on hand_toggle")
|
||||||
return
|
return
|
||||||
|
# Pick the target while frozen — it can neither die nor wander.
|
||||||
|
target = _nearest_grunt(mech)
|
||||||
|
if target != null:
|
||||||
|
break
|
||||||
|
await _tap("hand_toggle")
|
||||||
|
if target == null:
|
||||||
|
_failures.append("grunt near mech vanished before freeze on 4 attempts")
|
||||||
|
return
|
||||||
var expect_pause: bool = (hand.meter_mode and hand.frac() >= hand.band_thaw) \
|
var expect_pause: bool = (hand.meter_mode and hand.frac() >= hand.band_thaw) \
|
||||||
or (not hand.meter_mode and hand.dilation_scale == 0.0)
|
or (not hand.meter_mode and hand.dilation_scale == 0.0)
|
||||||
if expect_pause and not get_tree().paused:
|
if expect_pause and not get_tree().paused:
|
||||||
_failures.append("hand open in freeze state did not pause the world")
|
_failures.append("hand open in freeze state did not pause the world")
|
||||||
|
|
||||||
# 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 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.
|
# Play HEAT twice: charges spent, heat painted past threshold.
|
||||||
await _tap("card_2")
|
await _tap("card_2")
|
||||||
@@ -140,9 +143,33 @@ func _test_hand(arena: Node, mech: CharacterBody2D) -> void:
|
|||||||
_failures.append("heated grunt failed to detonate after release (heat %.1f)" % target.heat)
|
_failures.append("heated grunt failed to detonate after release (heat %.1f)" % target.heat)
|
||||||
|
|
||||||
await _test_meter(hand)
|
await _test_meter(hand)
|
||||||
|
await _test_salvage(hand, mech)
|
||||||
await _test_chemistry(arena, mech)
|
await _test_chemistry(arena, mech)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_salvage(hand: Node, mech: CharacterBody2D) -> void:
|
||||||
|
# Kills drop scrap pickups.
|
||||||
|
if not await _until(func() -> bool: return get_tree().get_first_node_in_group("salvage") != null, 15.0, "kills never dropped salvage"):
|
||||||
|
return
|
||||||
|
# Collection: teleport a drop onto the mech, expect pickup + count.
|
||||||
|
var s: Area2D = get_tree().get_first_node_in_group("salvage")
|
||||||
|
var s0: int = hand.salvage_total
|
||||||
|
s.global_position = mech.global_position
|
||||||
|
for i in 10:
|
||||||
|
await get_tree().physics_frame
|
||||||
|
if hand.salvage_total <= s0:
|
||||||
|
_failures.append("salvage on the mech was not collected (total %d)" % hand.salvage_total)
|
||||||
|
return
|
||||||
|
if is_instance_valid(s):
|
||||||
|
_failures.append("collected salvage node was not freed")
|
||||||
|
# Level gating: cumulative scrap crosses thresholds, white-box.
|
||||||
|
if hand.card_level() != 1 and hand.salvage_total < hand.level2_salvage:
|
||||||
|
_failures.append("card level %d with only %d salvage" % [hand.card_level(), hand.salvage_total])
|
||||||
|
hand.add_salvage(hand.level3_salvage)
|
||||||
|
if hand.card_level() != 3:
|
||||||
|
_failures.append("card level %d after crossing level3 threshold" % hand.card_level())
|
||||||
|
|
||||||
|
|
||||||
func _test_meter(hand: Node) -> void:
|
func _test_meter(hand: Node) -> void:
|
||||||
# Band ladder, white-box: set the meter, open, check the time state.
|
# Band ladder, white-box: set the meter, open, check the time state.
|
||||||
# THAW band: open, unpaused, slow-mo strictly between 0 and 1.
|
# THAW band: open, unpaused, slow-mo strictly between 0 and 1.
|
||||||
|
|||||||
Reference in New Issue
Block a user