Uses for every material (clay jar, bedroll, pelt rug, fang necklace, bear cloak, pemmican, bait), Q to drop, K/M bag rows on 1-8, help pages; protocol 14

This commit is contained in:
Paul Bailey
2026-11-06 11:49:42 +00:00
parent 5778d61763
commit 4b2e687188
29 changed files with 1457 additions and 40 deletions

View File

@@ -83,6 +83,17 @@ func _do(cmd: Dictionary) -> Dictionary:
var ok: bool = pk != null and root.get_node("Actions").pickup(player, pk)
await create_timer(0.6).timeout
return {"asked": ok, "inv": player.inventory.to_data()}
"drop":
# Q on the first stack of cmd.item (count 0: the whole stack).
var slot := -1
for i in range(player.inventory.size()):
var st = player.inventory.get_slot(i)
if st != null and st.id == String(cmd.item):
slot = i
break
var asked: bool = slot >= 0 and root.get_node("Actions").drop(player, slot, int(cmd.get("count", 0)))
await create_timer(0.6).timeout
return {"asked": asked, "inv": player.inventory.to_data()}
"chest_take":
var c: Node3D = WN.pieces.get(int(cmd.nid))
if c == null:

View File

@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
echo "Importing the project first (fresh copy)..."
godot --headless --path . --import >/dev/null 2>&1
fi
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_terrain_edges test_boundary; do
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_terrain_edges test_boundary; do
extra=""
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
# A suite that waits on a world that failed to load would hang forever,
@@ -22,6 +22,10 @@ for t in test_survival_core test_survival_stats test_save_load test_weather_buil
if [ $code -ne 0 ] || printf '%s\n' "$out" | grep -q "SCRIPT ERROR"; then
echo "FAIL $t $result"
printf '%s\n' "$out" | grep -E "FAIL|SCRIPT ERROR|at: " | head -n 20
# The whole output, for failures that don't happen twice.
mkdir -p build/test-logs
printf '%s\n' "$out" > "build/test-logs/$t.log"
echo " (full output: build/test-logs/$t.log)"
status=1
else
echo "ok $t $result"

234
tests/test_items_more.gd Normal file
View File

@@ -0,0 +1,234 @@
extends SceneTree
## Uses for every material (the clay jar, bedroll, wolf-pelt rug, fang
## necklace, bear cloak, pemmican, and rotten food as bait), dropping items
## (Q), and picking from any bag row with 1-8 (K / M change row).
## Run: godot --headless --path . --script res://tests/test_items_more.gd
var _fails := 0
var _finished := false
var A: Node
var S: Node
var DB: Node
var world: Node3D
var player: CharacterBody3D
var inv: Inventory
func check(cond: bool, what: String) -> void:
print((" ok " if cond else " FAIL ") + what)
if not cond:
_fails += 1
func _initialize() -> void:
_run.call_deferred()
func _run() -> void:
A = root.get_node("Actions")
S = root.get_node("Saves")
DB = root.get_node("ItemDB")
S.root_dir = "user://test_worlds"
for w in S.list_worlds():
S.delete_world(w.id)
await _body()
check(_finished, "test ran to the end (no script errors)")
for w in S.list_worlds():
S.delete_world(w.id)
DirAccess.remove_absolute(S.root_dir)
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
quit(1 if _fails > 0 else 0)
func _ground(p: Vector3) -> Vector3:
return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z)
func _place(id: String, at: Vector3) -> Node3D:
return world.spawn_piece(DB.get_item(id), Transform3D(Basis(), _ground(at)))
func _body() -> void:
print("every material has a use")
var items: Dictionary = JSON.parse_string(FileAccess.get_file_as_string("res://data/items.json"))
var recipes: Dictionary = JSON.parse_string(FileAccess.get_file_as_string("res://data/recipes.json"))
var used := {}
for r in recipes.recipes:
for k in r.inputs:
used[k] = true
for i in items.items:
for k in i.get("repair", {}).get("inputs", {}):
used[k] = true
for k in i.get("bow", {}).get("ammo", []):
used[k] = true
if i.has("fuel"):
used[i.id] = true
# (used by a machine or a place rather than a recipe)
for k in ["iron_ore", "iron_nugget", "charcoal", "barrow_key", A.BAIT]:
used[k] = true
var idle: Array = []
for i in items.items:
if i.category == "resource" and not i.get("hidden", false) and not used.has(i.id):
idle.append(i.id)
check(idle.is_empty(), "no material is only good for filling your bag %s" % [idle])
var made := {}
for r in recipes.recipes:
made[r.output] = true
var missing: Array = []
for id in ["clay_jar", "bedroll", "pelt_rug", "fang_necklace", "bear_cloak", "pemmican"]:
if not made.has(id) or DB.get_item(id).is_empty() or DB.mesh_for(id) == null:
missing.append(id)
check(missing.is_empty(), "the six new things each have a recipe and a model %s" % [missing])
S.current_id = S.create_world("Items", 1337, {})
world = load("res://scenes/world.tscn").instantiate()
root.add_child(world)
for _i in range(5):
await physics_frame
player = world.player
player.set_physics_process(false)
player.survival.set_process(false)
world.day_night.frozen = true
world.weather.set_process(false)
world.weather.force("clear")
world.creatures.enabled = false
if world.wolves != null:
world.wolves.enabled = false
inv = player.inventory
var here := player.global_position
print("the clay jar")
var jar := _place("clay_jar", here + Vector3(3, 0, 0))
var chest := _place("chest", here + Vector3(-3, 0, 0))
check(jar != null and jar.inventory.size() == 6, "a clay jar holds 6 stacks")
jar.inventory.add("raw_meat", 1)
chest.inventory.add("raw_meat", 1)
jar.inventory.spoil()
chest.inventory.spoil()
var keep: float = Inventory.keeps_for("raw_meat")
var c0 := Inventory.clock
for k in range(1, 31): # a day and a half, in steps
Inventory.clock = c0 + keep * 1.5 * k / 30.0
jar.inventory.spoil()
chest.inventory.spoil()
check(chest.inventory.count("rotten_food") == 1, "raw meat in a chest is rotten after 1.5x its keep time")
check(jar.inventory.count("raw_meat") == 1, "in a clay jar it's still good")
var left: float = Inventory.hours_left(jar.inventory.get_slot(0))
# 1.5 keep-times in a jar ages it by half of one: half its life left
# (counted as if it came out now; in the jar that lasts three times as long).
var expect: float = keep * 0.5 / Inventory.day_seconds * 24.0
check(absf(left - expect) < 0.5, "with half its life left (%.0f h, expected %.0f h)" % [left, expect])
Inventory.clock = c0
print("the bedroll")
var roll := _place("bedroll", here + Vector3(0, 0, 6))
var bed := _place("bed", here + Vector3(0, 0, -6))
world.day_night.time = 22.0 / 24.0
world.day_night.apply(true)
check(roll.is_in_group("bed") and roll.open_air(), "a bedroll is a bed you can use outdoors")
check(world.sleep_block_reason(roll).is_empty(), "under the open sky on a clear night: you can sleep")
check(world.sleep_block_reason(bed).contains("roof"), "(a bed out in the open still needs a roof)")
world.weather.force("rain")
for _i in range(3):
world.weather._process(30.0) if world.weather.has_method("_process") else null
world.weather.precip = 1.0
check(not world.sleep_block_reason(roll).is_empty(), "in the rain: not out here")
world.weather.force("clear")
world.weather.precip = 0.0
player.global_position = roll.global_position + Vector3(1.0, 0, 0)
check(A.use_bed(player, roll) and roll.is_spawn(), "E on it: your spawn point")
world.day_night.time = 10.0 / 24.0
world.day_night.apply(true)
print("the fang necklace")
player.survival.health = 100.0
player.survival.damage(10.0, here, "a wolf")
check(absf(player.survival.health - 90.0) < 0.01, "without it a wolf's bite is 10")
inv.add("fang_necklace", 1)
player.survival.health = 100.0
player.survival.damage(10.0, here, "a alpha wolf")
check(absf(player.survival.health - 93.0) < 0.01, "with it in your bag the alpha's is 7")
player.survival.health = 100.0
player.survival.damage(10.0, here, "a boar")
check(absf(player.survival.health - 90.0) < 0.01, "(a boar's isn't softened)")
inv.remove("fang_necklace", 1)
print("the bear cloak, the rug and pemmican")
inv.add("bear_cloak", 1)
var slot := -1
for i in range(inv.size()):
if inv.get_slot(i) != null and inv.get_slot(i).id == "bear_cloak":
slot = i
check(A.wear(player, slot) and player.worn == "bear_cloak", "you can wear the bear cloak")
var warmest := 0.0
for i in items.items:
warmest = maxf(warmest, float(i.get("wear", {}).get("warmth", 0)))
check(float(DB.get_item("bear_cloak").wear.warmth) == warmest, "the warmest cloak there is")
check(_place("pelt_rug", here + Vector3(4, 0, 4)).is_in_group("rug"), "a wolf-pelt rug is a rug (Cozy, like the fur rug)")
player.global_position = here
var fire := _place("campfire", here + Vector3(1.5, 0, 0))
fire.fuel = fire.MAX_FUEL
inv.add("cooked_meat", 2)
inv.add("berries", 3)
inv.add("bear_fat", 1)
check(A.craft(player, "pemmican") and inv.count("pemmican") == 3, "pemmican at the campfire (3 from 2 cooked meat, 3 berries, a lump of fat)")
var d0: Dictionary = player.survival.diet.duplicate()
player.survival.hunger = 30.0
for i in range(inv.size()):
if inv.get_slot(i) != null and inv.get_slot(i).id == "pemmican":
A.eat(player, i)
break
check(player.survival.diet.meat > d0.meat and player.survival.diet.plants > d0.plants, "it counts as meat and greens")
print("bait")
var B: Script = load("res://scripts/fishing/bobber.gd")
var bob: Node3D = B.new()
bob.player = player
bob.world = world
bob.table = {"bite_wait": [10.0, 10.0], "fish": [{"id": "perch", "weight": 1}]}
var plain: float = bob._next_wait()
inv.add("rotten_food", 2)
var baited: float = bob._next_wait()
check(absf(baited - plain * A.BAIT_WAIT) < 0.01 and bob.baited, "rotten food in your bag: bites come sooner (%.0f s, not %.0f s)" % [baited, plain])
bob.free()
print("dropping (Q)")
var dropped0: int = world._dropped.get_child_count()
inv.add("stone", 5)
for i in range(inv.size()):
if inv.get_slot(i) != null and inv.get_slot(i).id == "stone":
slot = i
break
check(A.drop(player, slot, 1) and inv.count("stone") == 4, "shift+Q drops one")
check(A.drop(player, slot) and inv.count("stone") == 0, "Q drops the rest of the stack")
check(world._dropped.get_child_count() == dropped0 + 2, "both lie on the ground in front of you")
var rot0: int = inv.count("rotten_food")
for i in range(inv.size()):
if inv.get_slot(i) != null and inv.get_slot(i).id == "rotten_food":
A.drop(player, i)
check(rot0 > 0 and inv.count("rotten_food") == 0, "and at last you can get rid of rotten food")
inv.add("stone_axe", 1)
for i in range(inv.size()):
if inv.get_slot(i) != null and inv.get_slot(i).id == "stone_axe":
inv.get_slot(i).dur = 7
slot = i
A.drop(player, slot)
var axe: Node3D = world._dropped.get_child(world._dropped.get_child_count() - 1)
check(axe.item_id == "stone_axe" and int(axe.extra.get("dur", -1)) == 7, "a worn tool keeps its wear on the ground")
print("bag rows on 1-8 (K / M)")
inv.load_data(Inventory.new(inv.size()).to_data())
inv.set_slot(11, {"id": "torch", "count": 1})
player.select(0)
player.select_in_row(3)
check(player.selected == 3 and player.row() == 0, "4 on the top row: slot 4")
player.change_row(1)
check(player.selected == 11 and player.row() == 1 and player.held_item_id() == "torch", "M: the row below, same key: you're holding its torch")
player.select_in_row(8)
check(player.selected == 8, "the wheel wraps round within the row")
player.select(0)
player.change_row(-1)
check(player.row() == player.rows() - 1 and player.rows() == 5, "K from the top row: the bottom one (5 rows)")
var SP: Script = load("res://scripts/net/server_player.gd")
var sp: Node = SP.new()
sp.select(27)
check(sp.selected == 27, "a server lets you hold from any row")
sp.free()
world.queue_free()
await process_frame
_finished = true

View File

@@ -318,6 +318,11 @@ func _wolves() -> void:
check(wolves.size() >= 2, "Alice sees the pack: %d wolves, from the server" % wolves.size())
check(PN.hurts > a0, "the wolves bite Alice (%d)" % (PN.hurts - a0))
check(bob_hurts > b0, "and Bob, 10 m away, at the same time (%d), after %.0f s" % [bob_hurts - b0, t])
if bob_hurts == b0:
# (rare, and not reproduced: say what each wolf was doing, for next time)
for w in wolves:
print(" wolf: %s, %.1f m from Alice, %.1f m from Bob" % [w.state,
(w as Node3D).global_position.distance_to(player.global_position), (w as Node3D).global_position.distance_to(b)])
check(player.survival.health < 100.0, "Alice's health is down on her screen (%.0f)" % player.survival.health)
# --- helpers --------------------------------------------------------------------

View File

@@ -382,6 +382,18 @@ func _items_on_the_ground() -> void:
check(brep.get("sounds", []).has("pickup"), "and so does Bob")
await _sleep(0.3)
check(WN.dropped.is_empty() or not WN.dropped.has(int(key.substr(1))), "and it's gone for Alice")
print("dropping (Q)")
var dr := await _bob({"do": "drop", "item": "flint", "count": 1})
check(dr.get("asked", false) and _count(dr.get("inv", []), "flint") == 1, "Bob drops one flint (shift+Q): the server takes it from his bag")
var seen := false
t = 0.0
while not seen and t < 3.0:
for k in WN.dropped:
if WN.dropped[k].item_id == "flint" and int(WN.dropped[k].amount) == 1:
seen = true
await _sleep(0.05)
t += 0.05
check(seen, "and it lands in the shared world: Alice sees it at his feet")
print("first come, first served")
var pk := _nearest(world.props.pickups.values().filter(func(p): return not p.taken))