straywild/tests/beach_bottle_service_validation.gd

202 lines
6 KiB
GDScript

extends SceneTree
const ServiceType = preload("res://messages/beach_bottle_service.gd")
const StateType = preload("res://messages/player_bottle_message_state.gd")
const Catalog: BottleMessageCatalog = preload(
"res://messages/catalog/bottle_message_catalog.tres"
)
const ItemCatalogResource: ItemCatalog = preload(
"res://items/catalog/item_catalog.tres"
)
class FakeWorld:
extends Node
func is_world_ready() -> bool:
return true
func get_saltwater_surface_height() -> float:
return -0.45
func get_spawn_surface_triangles(
_materials: Array[StringName],
_minimum_y: float,
_minimum_up_dot: float,
_maximum_y: float,
) -> Array[PackedVector3Array]:
return [PackedVector3Array([
Vector3(8.0, 0.0, -4.0),
Vector3(16.0, 0.0, -4.0),
Vector3(12.0, 0.0, 4.0),
])]
class FakeSaveManager:
extends PlayerSaveManager
var allow_save: bool = true
var save_count: int = 0
func save_if_dirty() -> bool:
save_count += 1
return allow_save
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var root := Node.new()
get_root().add_child(root)
var world := FakeWorld.new()
var player := Node3D.new()
var presentation_root := Node3D.new()
var bag := PlayerBag.new()
var state := StateType.new()
var saves := FakeSaveManager.new()
var service := ServiceType.new()
root.add_child(world)
root.add_child(player)
root.add_child(presentation_root)
root.add_child(bag)
root.add_child(state)
root.add_child(saves)
root.add_child(service)
bag.setup(ItemCatalogResource)
var now_unix: int = int(Time.get_unix_time_from_system())
assert(state.restore_from_save_data({
"schema_version": 1,
"received_message_ids": [],
"pending_message_id": "",
"pending_bottle_picked_up": false,
"next_bottle_at_unix": now_unix + 300000,
"last_observed_unix": now_unix,
}))
service.setup(
world,
player,
bag,
state,
Catalog,
saves,
presentation_root,
false,
)
service.force_spawn_for_testing = true
service.set_gameplay_active(true)
assert(service.call("_try_spawn_due_bottle"))
assert(service.has_active_bottle())
assert(state.has_pending_message())
assert(not state.is_pending_bottle_picked_up())
var presentation := service.get("_presentation") as BeachBottlePresentation
var spawned_message: BottleMessageData = Catalog.get_message(
presentation.message_id
)
assert(spawned_message != null)
var bottle_sprite := presentation.get_node("%BottleSprite") as Sprite3D
assert(bottle_sprite.texture.resource_path.ends_with(
"message_in_a_bottle_sand.png"
))
assert(bottle_sprite.texture.get_size() == Vector2(64.0, 64.0))
assert(bottle_sprite.position.y > 0.0)
assert(bottle_sprite.offset == Vector2.ZERO)
assert(
bottle_sprite.billboard == BaseMaterial3D.BILLBOARD_FIXED_Y
)
var bottle_image: Image = bottle_sprite.texture.get_image()
if bottle_image.is_compressed():
assert(bottle_image.decompress() == OK)
var visible_bounds: Rect2i = bottle_image.get_used_rect()
assert(
visible_bounds.position.y + visible_bounds.size.y
== int(presentation.AUTHORED_VISIBLE_BOTTOM_PIXEL)
)
var texture_height: float = float(bottle_sprite.texture.get_height())
var visible_bottom_pixel: float = presentation.AUTHORED_VISIBLE_BOTTOM_PIXEL
var expected_sprite_height: float = (
visible_bottom_pixel
- texture_height * 0.5
- presentation.VISIBLE_BASE_TUCK_PIXELS
) * bottle_sprite.pixel_size
assert(
is_equal_approx(bottle_sprite.position.y, expected_sprite_height)
)
var first_position: Vector3 = service.get_active_bottle_position()
assert(first_position.is_finite())
assert(is_equal_approx(first_position.y, ServiceType.SURFACE_OFFSET))
var visible_base_y: float = (
first_position.y
+ bottle_sprite.position.y
+ (
texture_height * 0.5
- visible_bottom_pixel
) * bottle_sprite.pixel_size
)
assert(is_equal_approx(visible_base_y, -bottle_sprite.pixel_size))
assert(
Vector2(first_position.x, first_position.z).length()
>= ServiceType.MINIMUM_SPAWN_DISTANCE
)
assert(
Vector2(first_position.x, first_position.z).length()
<= ServiceType.MAXIMUM_SPAWN_DISTANCE
)
player.global_position = first_position
assert(service.is_local_player_in_range())
assert(service.get_prompt_anchor_position().is_finite())
service.call("_process", 1.0)
assert(not bag.owns_item(ServiceType.BOTTLE_ITEM_ID))
assert(service.has_active_bottle())
saves.allow_save = false
assert(not service.try_collect_nearby())
assert(not bag.owns_item(ServiceType.BOTTLE_ITEM_ID))
assert(state.has_pending_message())
assert(not state.is_pending_bottle_picked_up())
assert(service.has_active_bottle())
saves.allow_save = true
# A failed disk transaction remains blocked until the player leaves pickup
# range, preventing a save-conflict failure from being retried every frame.
assert(not service.try_collect_nearby())
player.global_position = first_position + Vector3.RIGHT * 2.0
service.call("_process", 0.1)
assert(not service.is_local_player_in_range())
player.global_position = first_position
assert(service.try_collect_nearby())
assert(bag.owns_item(ServiceType.BOTTLE_ITEM_ID))
assert(state.is_pending_bottle_picked_up())
assert(not service.has_active_bottle())
saves.allow_save = false
var failed_read: Dictionary = service.read_held_bottle()
assert(not bool(failed_read.get("ok", false)))
assert(bag.owns_item(ServiceType.BOTTLE_ITEM_ID))
assert(state.is_pending_bottle_picked_up())
saves.allow_save = true
var read_result: Dictionary = service.read_held_bottle()
assert(bool(read_result.get("ok", false)))
assert(
str(read_result.get("text", ""))
== spawned_message.body
)
assert(
str(read_result.get("message_id", ""))
== String(spawned_message.message_id)
)
assert(not bag.owns_item(ServiceType.BOTTLE_ITEM_ID))
assert(not state.has_pending_message())
var point := ServiceType.sample_triangle(
PackedVector3Array([
Vector3.ZERO,
Vector3(1.0, 0.0, 0.0),
Vector3(0.0, 0.0, 1.0),
]),
0.0,
0.75,
)
assert(point.is_equal_approx(Vector3.ZERO))
assert(not ServiceType.sample_triangle(PackedVector3Array(), 0.5, 0.5).is_finite())
print("Beach bottle service validation: PASS")
root.queue_free()
await process_frame
quit()