Stage 3: inventory, ground loot, and two loot visibilities
ci / verify (push) Successful in 48s

Four always-on-screen slots, items as data, and loot tables on enemies and
bosses. Health potions drop rarely from trash and always from the Warden;
the Warden also drops a Warden's Ration, one per living player, which does
nothing at all.

The ration is not filler. Player-instanced loot is a separate code path from
shared loot -- a distinct entity per owner, filtered per peer in the snapshot
encoder -- and the cheapest way to keep that path honest is to have something
in the game that exercises it on every boss kill.

Item actions ride the input frame rather than becoming new client messages.
InputFrame gained BTN_USE, BTN_DROP and a slot byte, which buys the packet-loss
redundancy, the replay guard on last_input_tick, ordering against movement on
the same tick, and a rate limit of one action per tick -- all of which a
separate RPC would have needed bolted back on. The cost is that anything in
the frame which must not repeat has to be edge-triggered, since frames are
resent and a starved server coasts on the last one it holds.

Instanced loot is enforced in NetCodec.encode_snapshot, beside the actor
interest radius: a peer is never told another player's copy exists. Hiding it
client-side would have been the same mistake as relying on fog to hide enemies.

Inventories live on the character and are written to the store on every
transaction, so a crash between "picked it up" and "wrote it down" cannot lose
or duplicate an item. Anything dropped becomes world-shared whatever it was
before, and a potion used at full health is refused rather than spent.

tools/diag_loot.tscn covers drop -> snapshot -> pick up -> persist -> use ->
drop plus both visibilities on the wire, for the same reason diag_progression
exists: bots are poor shots and almost never produce a drop. It asserts each
input frame was actually consumed, after an early version silently dropped its
first press and every later check passed for the wrong reason.

check.sh clean, 266 tests, SMOKE PASS, all three diagnostics green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 21:16:15 +02:00
parent ded7bf96d5
commit 050b8251a7
50 changed files with 2159 additions and 125 deletions
+52 -41
View File
@@ -1,15 +1,25 @@
[gd_resource type="Resource" script_class="BossDef" format=3]
[ext_resource type="Script" path="res://src/actors/boss/boss_phase.gd" id="1_7m6h6"]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="2_b0fvw"]
[ext_resource type="Script" path="res://src/sim/patterns/ring_emitter.gd" id="3_f5abt"]
[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="4_tmo3e"]
[ext_resource type="Script" path="res://src/sim/patterns/wall_gap_emitter.gd" id="5_rswgd"]
[ext_resource type="Script" path="res://src/sim/patterns/arc_sweep_emitter.gd" id="6_v02no"]
[ext_resource type="Script" path="res://src/actors/boss/boss_def.gd" id="7_x6ufy"]
[ext_resource type="Script" path="res://src/actors/items/loot_drop.gd" id="1_b0fvw"]
[ext_resource type="Script" path="res://src/actors/boss/boss_phase.gd" id="2_f5abt"]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="3_tmo3e"]
[ext_resource type="Script" path="res://src/sim/patterns/ring_emitter.gd" id="4_rswgd"]
[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="5_v02no"]
[ext_resource type="Script" path="res://src/sim/patterns/wall_gap_emitter.gd" id="6_x6ufy"]
[ext_resource type="Script" path="res://src/sim/patterns/arc_sweep_emitter.gd" id="7_bbcbd"]
[ext_resource type="Script" path="res://src/actors/boss/boss_def.gd" id="8_axg03"]
[sub_resource type="Resource" id="Resource_bbcbd"]
script = ExtResource("3_f5abt")
[sub_resource type="Resource" id="Resource_s17mk"]
script = ExtResource("1_b0fvw")
item = &"health_potion"
[sub_resource type="Resource" id="Resource_0452f"]
script = ExtResource("1_b0fvw")
item = &"wardens_ration"
instanced = true
[sub_resource type="Resource" id="Resource_1p7x0"]
script = ExtResource("4_rswgd")
count = 14
spin_per_shot_deg = 9.0
muzzle_offset = 46.0
@@ -19,8 +29,8 @@ radius = 8.0
lifetime = 420
damage = 14
[sub_resource type="Resource" id="Resource_axg03"]
script = ExtResource("4_tmo3e")
[sub_resource type="Resource" id="Resource_a0mrn"]
script = ExtResource("5_v02no")
spread_deg = 26.0
muzzle_offset = 46.0
start_tick = 120
@@ -29,15 +39,15 @@ speed = 210.0
radius = 6.0
kind = 2
[sub_resource type="Resource" id="Resource_s17mk"]
script = ExtResource("1_7m6h6")
[sub_resource type="Resource" id="Resource_pfjxl"]
script = ExtResource("2_f5abt")
name = "Opening Fold"
loop_ticks = 480
telegraph_ticks = 60
emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_bbcbd"), SubResource("Resource_axg03")])
emitters = Array[ExtResource("3_tmo3e")]([SubResource("Resource_1p7x0"), SubResource("Resource_a0mrn")])
[sub_resource type="Resource" id="Resource_0452f"]
script = ExtResource("3_f5abt")
[sub_resource type="Resource" id="Resource_txml3"]
script = ExtResource("4_rswgd")
count = 4
spin_per_shot_deg = 23.0
muzzle_offset = 46.0
@@ -45,8 +55,8 @@ interval = 9
speed = 120.0
lifetime = 480
[sub_resource type="Resource" id="Resource_1p7x0"]
script = ExtResource("5_rswgd")
[sub_resource type="Resource" id="Resource_k1e8f"]
script = ExtResource("6_x6ufy")
count = 20
gap_step = 7
start_tick = 90
@@ -56,15 +66,15 @@ radius = 8.0
damage = 16
kind = 3
[sub_resource type="Resource" id="Resource_a0mrn"]
script = ExtResource("1_7m6h6")
[sub_resource type="Resource" id="Resource_4sp1g"]
script = ExtResource("2_f5abt")
name = "Sliding Lattice"
enter_at_hp_fraction = 0.72
telegraph_ticks = 50
emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_0452f"), SubResource("Resource_1p7x0")])
emitters = Array[ExtResource("3_tmo3e")]([SubResource("Resource_txml3"), SubResource("Resource_k1e8f")])
[sub_resource type="Resource" id="Resource_pfjxl"]
script = ExtResource("6_v02no")
[sub_resource type="Resource" id="Resource_aldxl"]
script = ExtResource("7_bbcbd")
arms = 3
bullets_per_arm = 2
arm_spacing = 26.0
@@ -75,8 +85,8 @@ speed = 165.0
lifetime = 400
damage = 13
[sub_resource type="Resource" id="Resource_txml3"]
script = ExtResource("4_tmo3e")
[sub_resource type="Resource" id="Resource_3fm5w"]
script = ExtResource("5_v02no")
count = 7
spread_deg = 40.0
jitter_deg = 3.0
@@ -87,8 +97,8 @@ speed = 195.0
radius = 6.0
kind = 2
[sub_resource type="Resource" id="Resource_k1e8f"]
script = ExtResource("3_f5abt")
[sub_resource type="Resource" id="Resource_20c8f"]
script = ExtResource("4_rswgd")
count = 18
spin_per_shot_deg = -14.0
start_tick = 240
@@ -97,16 +107,16 @@ interval = 60
speed = 105.0
lifetime = 420
[sub_resource type="Resource" id="Resource_4sp1g"]
script = ExtResource("1_7m6h6")
[sub_resource type="Resource" id="Resource_ymr56"]
script = ExtResource("2_f5abt")
name = "Reaping Arms"
enter_at_hp_fraction = 0.42
loop_ticks = 720
damage_taken_mult = 1.15
emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_pfjxl"), SubResource("Resource_txml3"), SubResource("Resource_k1e8f")])
emitters = Array[ExtResource("3_tmo3e")]([SubResource("Resource_aldxl"), SubResource("Resource_3fm5w"), SubResource("Resource_20c8f")])
[sub_resource type="Resource" id="Resource_aldxl"]
script = ExtResource("3_f5abt")
[sub_resource type="Resource" id="Resource_7xac7"]
script = ExtResource("4_rswgd")
count = 5
spin_per_shot_deg = 31.0
muzzle_offset = 46.0
@@ -116,8 +126,8 @@ turn_deg = 0.55
lifetime = 400
damage = 15
[sub_resource type="Resource" id="Resource_3fm5w"]
script = ExtResource("5_rswgd")
[sub_resource type="Resource" id="Resource_3n6oq"]
script = ExtResource("6_x6ufy")
direction = 2
count = 16
start_tick = 40
@@ -127,8 +137,8 @@ radius = 8.0
damage = 18
kind = 3
[sub_resource type="Resource" id="Resource_20c8f"]
script = ExtResource("4_tmo3e")
[sub_resource type="Resource" id="Resource_8ppic"]
script = ExtResource("5_v02no")
count = 3
spread_deg = 10.0
start_tick = 100
@@ -138,20 +148,21 @@ radius = 5.0
damage = 16
kind = 2
[sub_resource type="Resource" id="Resource_ymr56"]
script = ExtResource("1_7m6h6")
[sub_resource type="Resource" id="Resource_yqxmm"]
script = ExtResource("2_f5abt")
name = "Collapse"
enter_at_hp_fraction = 0.16
loop_ticks = 420
telegraph_ticks = 60
damage_taken_mult = 1.3
emitters = Array[ExtResource("2_b0fvw")]([SubResource("Resource_aldxl"), SubResource("Resource_3fm5w"), SubResource("Resource_20c8f")])
emitters = Array[ExtResource("3_tmo3e")]([SubResource("Resource_7xac7"), SubResource("Resource_3n6oq"), SubResource("Resource_8ppic")])
[resource]
script = ExtResource("7_x6ufy")
script = ExtResource("8_axg03")
id = &"warden"
display_name = "Warden of the Fold"
max_hp = 3600
radius = 42.0
spawn_pos = Vector2(0, -150)
phases = Array[ExtResource("1_7m6h6")]([SubResource("Resource_s17mk"), SubResource("Resource_a0mrn"), SubResource("Resource_4sp1g"), SubResource("Resource_ymr56")])
phases = Array[ExtResource("2_f5abt")]([SubResource("Resource_pfjxl"), SubResource("Resource_4sp1g"), SubResource("Resource_ymr56"), SubResource("Resource_yqxmm")])
loot = Array[ExtResource("1_b0fvw")]([SubResource("Resource_s17mk"), SubResource("Resource_0452f")])
+14 -7
View File
@@ -1,11 +1,12 @@
[gd_resource type="Resource" script_class="EnemyDef" format=3]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_801wv"]
[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="2_bjhtk"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="3_e6oj1"]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_bjhtk"]
[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="2_e6oj1"]
[ext_resource type="Script" path="res://src/actors/items/loot_drop.gd" id="3_vpd04"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="4_k36hi"]
[sub_resource type="Resource" id="Resource_vpd04"]
script = ExtResource("2_bjhtk")
[sub_resource type="Resource" id="Resource_rsrg2"]
script = ExtResource("2_e6oj1")
count = 3
spread_deg = 20.0
interval = 120
@@ -13,10 +14,16 @@ radius = 6.0
lifetime = 240
damage = 10
[sub_resource type="Resource" id="Resource_lnbnj"]
script = ExtResource("3_vpd04")
item = &"health_potion"
chance = 0.08
[resource]
script = ExtResource("3_e6oj1")
script = ExtResource("4_k36hi")
id = &"drifter"
display_name = "Drifter"
speed = 55.0
emitters = Array[ExtResource("1_801wv")]([SubResource("Resource_vpd04")])
emitters = Array[ExtResource("1_bjhtk")]([SubResource("Resource_rsrg2")])
pattern_loop_ticks = 120
loot = Array[ExtResource("3_vpd04")]([SubResource("Resource_lnbnj")])
+4 -3
View File
@@ -1,10 +1,11 @@
[gd_resource type="Resource" script_class="EnemyDef" format=3]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_ajtny"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="2_i7w5w"]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_i7w5w"]
[ext_resource type="Script" path="res://src/actors/items/loot_drop.gd" id="2_74ern"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="3_2tv1l"]
[resource]
script = ExtResource("2_i7w5w")
script = ExtResource("3_2tv1l")
id = &"dummy"
display_name = "Target Dummy"
max_hp = 100000
+14 -7
View File
@@ -1,11 +1,12 @@
[gd_resource type="Resource" script_class="EnemyDef" format=3]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_t1lyl"]
[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="2_e7xon"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="3_ehenj"]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_e7xon"]
[ext_resource type="Script" path="res://src/sim/patterns/aimed_spread_emitter.gd" id="2_ehenj"]
[ext_resource type="Script" path="res://src/actors/items/loot_drop.gd" id="3_tpjo4"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="4_yp71e"]
[sub_resource type="Resource" id="Resource_tpjo4"]
script = ExtResource("2_e7xon")
[sub_resource type="Resource" id="Resource_oeksl"]
script = ExtResource("2_ehenj")
spread_deg = 62.0
muzzle_offset = 10.0
interval = 45
@@ -15,8 +16,13 @@ lifetime = 18
damage = 14
kind = 3
[sub_resource type="Resource" id="Resource_ep81w"]
script = ExtResource("3_tpjo4")
item = &"health_potion"
chance = 0.08
[resource]
script = ExtResource("3_ehenj")
script = ExtResource("4_yp71e")
id = &"stalker"
display_name = "Stalker"
max_hp = 30
@@ -25,5 +31,6 @@ move = 3
speed = 95.0
retarget_interval = 20
visual = 2
emitters = Array[ExtResource("1_t1lyl")]([SubResource("Resource_tpjo4")])
emitters = Array[ExtResource("1_e7xon")]([SubResource("Resource_oeksl")])
pattern_loop_ticks = 45
loot = Array[ExtResource("3_tpjo4")]([SubResource("Resource_ep81w")])
+14 -7
View File
@@ -1,18 +1,24 @@
[gd_resource type="Resource" script_class="EnemyDef" format=3]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_asksf"]
[ext_resource type="Script" path="res://src/sim/patterns/ring_emitter.gd" id="2_7m5d5"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="3_5udip"]
[ext_resource type="Script" path="res://src/sim/patterns/bullet_emitter.gd" id="1_7m5d5"]
[ext_resource type="Script" path="res://src/sim/patterns/ring_emitter.gd" id="2_5udip"]
[ext_resource type="Script" path="res://src/actors/items/loot_drop.gd" id="3_hc01i"]
[ext_resource type="Script" path="res://src/actors/enemies/enemy_def.gd" id="4_6rb75"]
[sub_resource type="Resource" id="Resource_hc01i"]
script = ExtResource("2_7m5d5")
[sub_resource type="Resource" id="Resource_kv0ny"]
script = ExtResource("2_5udip")
count = 10
spin_per_shot_deg = 18.0
interval = 150
speed = 130.0
[sub_resource type="Resource" id="Resource_fttpl"]
script = ExtResource("3_hc01i")
item = &"health_potion"
chance = 0.08
[resource]
script = ExtResource("3_5udip")
script = ExtResource("4_6rb75")
id = &"turret"
display_name = "Turret"
max_hp = 70
@@ -20,5 +26,6 @@ radius = 16.0
move = 0
speed = 0.0
visual = 1
emitters = Array[ExtResource("1_asksf")]([SubResource("Resource_hc01i")])
emitters = Array[ExtResource("1_7m5d5")]([SubResource("Resource_kv0ny")])
pattern_loop_ticks = 150
loot = Array[ExtResource("3_hc01i")]([SubResource("Resource_fttpl")])