P3 alpha: wolf moon every 7th night, the alpha breaks wooden doors, iron-banded door, broken-door repair, alpha pelt and cloak (protocol 7)
This commit is contained in:
@@ -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_den_sites test_fishing_diet test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_host 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_den_sites test_fishing_diet test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_host 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,
|
||||
|
||||
265
tests/test_alpha.gd
Normal file
265
tests/test_alpha.gd
Normal file
@@ -0,0 +1,265 @@
|
||||
extends SceneTree
|
||||
## P3 the alpha (docs/PROGRESSION_DESIGN.md §4): the wolf moon every 7th
|
||||
## night, the alpha that hunts with the pack, wooden doors it breaks and
|
||||
## iron-banded ones it can't, mending a door, and what the alpha drops.
|
||||
## Run: godot --headless --path . --script res://tests/test_alpha.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var A: Node
|
||||
var S: Node
|
||||
var DB: Node
|
||||
var _toasts: Array[String] = []
|
||||
var world: Node3D
|
||||
var player: CharacterBody3D
|
||||
var pack: Node
|
||||
|
||||
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")
|
||||
A.notify.connect(func(t): _toasts.append(t))
|
||||
S.root_dir = "user://test_worlds"
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
S.current_id = S.create_world("Alpha", 1337)
|
||||
await _load()
|
||||
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 _load() -> void:
|
||||
world = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
world.creatures.enabled = false
|
||||
for c in world.creatures.alive():
|
||||
c.queue_free()
|
||||
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")
|
||||
pack = world.wolves
|
||||
pack.enabled = false
|
||||
|
||||
func _settle() -> void:
|
||||
for _i in range(3):
|
||||
await physics_frame
|
||||
|
||||
func _set_time(day: int, hours: float) -> void:
|
||||
world.day_night.day = day
|
||||
world.day_night.time = hours / 24.0
|
||||
world.day_night.apply(true)
|
||||
|
||||
## Runs the pack and every wolf for up to `secs`, stopping early once
|
||||
## `until` (if given) returns true. Returns the seconds it took.
|
||||
func _sim(secs: float, until: Callable = Callable()) -> float:
|
||||
var t := 0.0
|
||||
while t < secs:
|
||||
pack.step(0.1)
|
||||
for w in pack.alive():
|
||||
w.set_physics_process(false)
|
||||
w.step(0.1)
|
||||
t += 0.1
|
||||
if until.is_valid() and until.call():
|
||||
break
|
||||
if int(t * 10) % 10 == 0:
|
||||
await physics_frame # (let the physics server catch up with moved bodies)
|
||||
return t
|
||||
|
||||
func _toast_has(s: String) -> bool:
|
||||
for t in _toasts:
|
||||
if t.contains(s):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _body() -> void:
|
||||
_moon()
|
||||
await _arrives()
|
||||
await _doors()
|
||||
await _dawn()
|
||||
_drops()
|
||||
await _saving()
|
||||
_finished = true
|
||||
|
||||
# --- the wolf moon -------------------------------------------------------------------
|
||||
|
||||
func _moon() -> void:
|
||||
print("the wolf moon")
|
||||
var DN: Script = load("res://scripts/world/day_night.gd")
|
||||
check(DN.wolf_moon_day(7) and DN.wolf_moon_day(14) and DN.wolf_moon_day(21), "nights 7, 14 and 21 are wolf moons")
|
||||
check(not DN.wolf_moon_day(6) and not DN.wolf_moon_day(8) and not DN.wolf_moon_day(0), "6, 8 (and day 0) aren't")
|
||||
var dn: Node = world.day_night
|
||||
_set_time(7, 22.0)
|
||||
check(dn.wolf_moon_now() and dn.moon.light_color.r > dn.moon.light_color.b * 2.0, "on night 7 the moon is red (%s)" % dn.moon.light_color)
|
||||
_set_time(8, 3.0)
|
||||
check(dn.wolf_moon_now(), "still red in the small hours of day 8")
|
||||
_set_time(7, 12.0)
|
||||
check(not dn.wolf_moon_now(), "not by day")
|
||||
_set_time(8, 22.0)
|
||||
check(not dn.wolf_moon_now() and dn.moon.light_color.b > dn.moon.light_color.r, "night 8 is an ordinary moon")
|
||||
check(pack.wolf_moon(7) and not pack.wolf_moon(8), "the pack keeps the same calendar")
|
||||
|
||||
# --- the alpha comes -------------------------------------------------------------------
|
||||
|
||||
func _arrives() -> void:
|
||||
print("the alpha")
|
||||
_set_time(7, 12.0)
|
||||
await _sim(2.0)
|
||||
check(pack.mode == "den" and not pack.alpha_alive(), "day 7: the pack rests, no alpha yet")
|
||||
_toasts.clear()
|
||||
_set_time(7, 21.0)
|
||||
await _sim(1.0)
|
||||
check(pack.mode == "hunt" and pack.alpha_alive(), "night 7: an alpha hunts with the pack")
|
||||
var al: Node = pack.alpha
|
||||
check(al.kind == "alpha" and absf(al.hp - 3.0 * al.KINDS.wolf.hp) < 0.01, "with three times a wolf's health (%.0f)" % al.hp)
|
||||
check(_toast_has("wolf moon"), "a toast warns you (%s)" % [_toasts])
|
||||
check(pack.alive().has(al) and pack.alive_count() == pack.pack_size(7) + 1, "it runs with the pack (%d wolves and the alpha)" % pack.pack_size(7))
|
||||
al.take_hit(100.0, al.global_position + Vector3(3, 0, 0))
|
||||
check(al.state != "flee" and not al.wounded, "badly hurt, it doesn't run (%s)" % al.state)
|
||||
al.hp = al.def.hp
|
||||
|
||||
# --- doors ------------------------------------------------------------------------------
|
||||
|
||||
var hut: Dictionary
|
||||
var door: Node3D
|
||||
var door_xf: Transform3D
|
||||
var inside: Vector3
|
||||
|
||||
func _doors() -> void:
|
||||
print("a wooden door")
|
||||
var bs: Node = world.build
|
||||
var yaw: float = player.rotation.y
|
||||
var b := Basis(Vector3.UP, yaw)
|
||||
var site: Vector3 = bs.debug_find_site(player.global_position - player.global_basis.z * 14.0, 3.0)
|
||||
hut = await bs.debug_build_hut(site, yaw)
|
||||
await _settle()
|
||||
check(hut.placed == 20, "a closed hut (%d pieces, %s)" % [hut.placed, hut.failed])
|
||||
for p in hut.pieces:
|
||||
if p.item_id == "wood_door":
|
||||
door = p
|
||||
door_xf = door.global_transform
|
||||
var center: Vector3 = hut.pieces[0].global_transform * Vector3(1, 0, 1)
|
||||
inside = center + b * Vector3(-0.6, 0.05, -0.6)
|
||||
player.global_position = inside
|
||||
var fire: Node3D = world.spawn_piece(DB.get_item("campfire"), Transform3D(b, center + b * Vector3(0.6, 0.0, -0.8)))
|
||||
fire.fuel = fire.MAX_FUEL
|
||||
await _settle()
|
||||
var al: Node3D = pack.alpha
|
||||
var out_front: Vector3 = door.global_position + door.global_basis.z * 7.0
|
||||
out_front.y = world.terrain.sample_height(out_front.x, out_front.z)
|
||||
al.global_position = out_front
|
||||
for w in pack.alive():
|
||||
if w != al:
|
||||
w.global_position = out_front + Vector3(4, 0, 4)
|
||||
_toasts.clear()
|
||||
check(pack.fire_near(player) != null, "you're inside by a fire (wolves won't close in)")
|
||||
var took: float = await _sim(45.0, func(): return door.broken)
|
||||
check(door.broken and not door.blocks(), "the alpha breaks the door down anyway (%.0f s)" % took)
|
||||
check(_toast_has("battering") and _toast_has("smashed"), "and you hear it coming (%s)" % [_toasts])
|
||||
var space := world.get_world_3d().direct_space_state
|
||||
var q := PhysicsRayQueryParameters3D.create(door.global_transform * Vector3(0, 1.0, 1.5), door.global_transform * Vector3(0, 1.0, -1.5), 1)
|
||||
check(space.intersect_ray(q).is_empty(), "the doorway is open")
|
||||
check(door.interact_prompt(player).begins_with("Broken door"), "it shows as broken: \"%s\"" % door.interact_prompt(player))
|
||||
check(door.bash(50.0) == "", "(a broken door takes no more blows)")
|
||||
var hp0: float = player.survival.health
|
||||
var got_in: float = await _sim(20.0, func(): return player.survival.health < hp0)
|
||||
check(player.survival.health < hp0, "then it comes in through the doorway and bites (%.0f s, health %.0f)" % [got_in, player.survival.health])
|
||||
player.survival.health = 100.0
|
||||
al.global_position = out_front
|
||||
al._entry = null
|
||||
print("mending it")
|
||||
player.global_position = door.global_transform * Vector3(0.0, 0.05, -1.0)
|
||||
player.inventory.load_data(Inventory.new(40).to_data())
|
||||
check(not A.repair_door(player, door) and door.broken, "no wood, no repair")
|
||||
player.inventory.add("wood", 3)
|
||||
check(A.repair_door(player, door) and not door.broken and door.blocks(), "two wood and it's shut and whole again")
|
||||
check(player.inventory.count("wood") == 1 and door.door_hp == door.DOOR_HP, "(for 2 wood; back to full strength)")
|
||||
player.global_position = inside
|
||||
|
||||
print("an iron-banded door")
|
||||
player.inventory.add("iron_bar", 2)
|
||||
player.inventory.add("wood", 4)
|
||||
player.inventory.add("nails", 4)
|
||||
var forge: Node3D = world.spawn_piece(DB.get_item("forge"), Transform3D(b, center + b * Vector3(-5.0, 0, 0)))
|
||||
player.global_position = forge.global_position + b * Vector3(0, 0, 1.5)
|
||||
check(A.craft(player, "iron_door"), "made at the forge (2 iron, 4 wood, 4 nails)")
|
||||
world.remove_piece(forge)
|
||||
world.remove_piece(door)
|
||||
await _settle()
|
||||
var idef: Dictionary = DB.get_item("iron_door")
|
||||
check(bs.check(idef, door_xf) == "", "it goes where the wooden one was")
|
||||
door = world.spawn_piece(idef, door_xf)
|
||||
player.global_position = inside
|
||||
await _settle()
|
||||
al.global_position = out_front
|
||||
al._door = null
|
||||
_toasts.clear()
|
||||
await _sim(30.0, func(): return door.broken)
|
||||
check(not door.broken and door.blocks(), "30 s of the alpha's blows and it holds")
|
||||
check(_toast_has("iron bands hold"), "you hear it holding (%s)" % [_toasts])
|
||||
check(player.survival.health > 99.0, "and the alpha never got at you (health %.0f)" % player.survival.health)
|
||||
world.remove_piece(fire)
|
||||
|
||||
func _dawn() -> void:
|
||||
print("dawn")
|
||||
var al: Node = pack.alpha
|
||||
_set_time(8, 6.5)
|
||||
await _sim(1.0)
|
||||
check(pack.mode == "den" and not pack.alpha_alive() and not pack.members.has(al), "at dawn the alpha goes back into the hills")
|
||||
_set_time(8, 21.0)
|
||||
await _sim(1.0)
|
||||
check(pack.mode == "hunt" and not pack.alpha_alive(), "night 8: just the wolves")
|
||||
|
||||
func _drops() -> void:
|
||||
print("its pelt")
|
||||
var al: Node = pack.spawn_alpha(14)
|
||||
al.take_hit(500.0, al.global_position + Vector3(2, 0, 0))
|
||||
check(al.is_dead(), "an alpha can be killed")
|
||||
var got: Dictionary = al.butcher_drops()
|
||||
check(int(got.get("alpha_pelt", 0)) == 1 and int(got.get("wolf_fang", 0)) >= 1 and int(got.get("raw_meat", 0)) >= 2,
|
||||
"butchered: an alpha pelt, fangs and meat (%s)" % [got])
|
||||
check(int(DB.get_item("alpha_cloak").wear.warmth) > int(DB.get_item("wolf_cloak").wear.warmth), "the alpha cloak is the warmest thing to wear")
|
||||
check(DB.recipes.has("alpha_cloak") and DB.recipes.alpha_cloak.inputs.has("alpha_pelt"), "made from its pelt")
|
||||
print("online")
|
||||
var CN: Node = root.get_node("CreatureNet")
|
||||
al.set_meta("cid", 77)
|
||||
al.state = "bash"
|
||||
var buf := PackedByteArray()
|
||||
buf.resize(CN.ENTRY)
|
||||
CN._encode(buf, 0, al)
|
||||
var e: Array = CN._decode(buf, 0)
|
||||
check(e[1] == "alpha" and e[4] == "bash", "it goes over the network as an alpha (%s, %s)" % [e[1], e[4]])
|
||||
al.queue_free()
|
||||
|
||||
func _saving() -> void:
|
||||
print("saving")
|
||||
door.queue_free()
|
||||
await _settle()
|
||||
var wd: Node3D = world.spawn_piece(DB.get_item("wood_door"), door_xf)
|
||||
await _settle()
|
||||
check(wd.bash(250.0) == "broke", "a wooden door, broken")
|
||||
check(world.save_now(), "saved")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
await _load()
|
||||
var back: Node3D = null
|
||||
for p in world.all_pieces():
|
||||
if p.item_id == "wood_door" and p.global_position.distance_to(door_xf.origin) < 0.05:
|
||||
back = p
|
||||
check(back != null and back.broken and not back.blocks(), "comes back broken")
|
||||
Reference in New Issue
Block a user