2026-08-14 22:11:49 -04:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
const Gatherables: GatherableCatalog = preload(
|
|
|
|
|
"res://gathering/catalog/gatherable_catalog.tres"
|
|
|
|
|
)
|
|
|
|
|
const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
2026-08-17 17:50:30 -04:00
|
|
|
const GatheringControllerType = preload(
|
|
|
|
|
"res://gathering/gathering_controller.gd"
|
|
|
|
|
)
|
2026-08-17 19:06:45 -04:00
|
|
|
const WorldGatherableType = preload("res://gathering/world_gatherable.gd")
|
2026-08-17 17:50:30 -04:00
|
|
|
|
2026-08-17 19:06:45 -04:00
|
|
|
const CRAB_PIXEL_SIZE: float = 0.0005
|
2026-08-17 17:50:30 -04:00
|
|
|
const REDUCED_CATCH_RING_RADIUS: float = 0.175
|
2026-08-17 19:06:45 -04:00
|
|
|
const NET_STRIKE_MARKER_DISTANCE: float = 0.85
|
2026-08-18 21:20:02 -04:00
|
|
|
const CalendarSeasonType = preload("res://world/calendar_season.gd")
|
2026-08-14 22:11:49 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
2026-08-29 17:41:10 -04:00
|
|
|
call_deferred(&"_run")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run() -> void:
|
2026-08-30 22:00:04 -04:00
|
|
|
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
2026-08-14 22:11:49 -04:00
|
|
|
assert(
|
|
|
|
|
NetworkWorldSpawnProtocol.CAPABILITY
|
|
|
|
|
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
|
|
|
|
)
|
|
|
|
|
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
|
|
|
|
|
_validate_catalog_statuses()
|
2026-08-29 17:41:10 -04:00
|
|
|
await _validate_billboard_presentation()
|
2026-08-14 22:11:49 -04:00
|
|
|
_validate_envelopes()
|
|
|
|
|
print("World spawn protocol validation: PASS")
|
|
|
|
|
quit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _validate_catalog_statuses() -> void:
|
|
|
|
|
var brown: GatherableData = Gatherables.get_entry(&"crab_brown")
|
|
|
|
|
assert(brown != null and brown.is_available())
|
|
|
|
|
assert(brown.catch_data.active)
|
|
|
|
|
assert(brown.catch_data.collection_method == FishData.CollectionMethod.NET)
|
|
|
|
|
assert(brown.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
|
|
|
|
|
assert(brown.population == 2)
|
|
|
|
|
assert(is_equal_approx(brown.charge_duration, 2.0))
|
2026-08-17 17:50:30 -04:00
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
brown.sprite_pixel_size,
|
2026-08-17 19:06:45 -04:00
|
|
|
CRAB_PIXEL_SIZE,
|
2026-08-17 17:50:30 -04:00
|
|
|
)
|
|
|
|
|
)
|
2026-08-15 09:53:45 -04:00
|
|
|
_validate_quality_behavior(brown)
|
2026-08-14 22:11:49 -04:00
|
|
|
assert(is_equal_approx(brown.capture_respawn_min_seconds, 480.0))
|
|
|
|
|
assert(is_equal_approx(brown.capture_respawn_max_seconds, 720.0))
|
|
|
|
|
assert(is_equal_approx(brown.scare_respawn_min_seconds, 45.0))
|
|
|
|
|
assert(is_equal_approx(brown.scare_respawn_max_seconds, 90.0))
|
|
|
|
|
assert(is_equal_approx(brown.minimum_respawn_spacing_seconds, 180.0))
|
|
|
|
|
assert(brown.required_tool_id == &"crab_net")
|
2026-08-17 19:06:45 -04:00
|
|
|
assert(is_equal_approx(brown.minimum_surface_y, -0.44))
|
2026-08-31 16:04:34 -04:00
|
|
|
assert(
|
|
|
|
|
brown.surface_water_relation
|
|
|
|
|
== GatherableData.SurfaceWaterRelation.ABOVE_SALT_WATER
|
|
|
|
|
)
|
|
|
|
|
assert(is_equal_approx(
|
|
|
|
|
brown.surface_population_per_100_square_meters,
|
|
|
|
|
0.15,
|
|
|
|
|
))
|
|
|
|
|
assert(brown.maximum_surface_population == 24)
|
|
|
|
|
assert(brown.target_population_for_surface_area(100.0) == 2)
|
|
|
|
|
assert(brown.target_population_for_surface_area(7500.0) == 12)
|
2026-08-14 22:11:49 -04:00
|
|
|
assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
|
|
|
|
|
assert(not brown.catch_data.is_fishable())
|
2026-08-18 18:19:36 -04:00
|
|
|
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
|
|
|
|
|
assert(clam != null and clam.is_available())
|
|
|
|
|
assert(clam.catch_data.collection_method == FishData.CollectionMethod.DIGGING)
|
|
|
|
|
assert(clam.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
|
|
|
|
|
assert(clam.required_tool_id == &"standard_shovel")
|
|
|
|
|
assert(clam.diggable_area_id == &"starter_beach")
|
|
|
|
|
assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
|
|
|
|
|
assert(clam.is_stationary_hotspot())
|
|
|
|
|
assert(not clam.requires_sneaking)
|
|
|
|
|
assert(not clam.can_be_scared())
|
|
|
|
|
assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
|
|
|
|
|
assert(FishCatalog.get_fish_by_id(&"clam_manila") == clam.catch_data)
|
|
|
|
|
assert(not clam.catch_data.is_fishable())
|
|
|
|
|
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
|
|
|
|
|
assert(beetle != null and beetle.is_available())
|
2026-08-18 21:20:02 -04:00
|
|
|
assert(beetle.is_available(CalendarSeasonType.Season.SUMMER))
|
|
|
|
|
assert(not beetle.is_available(CalendarSeasonType.Season.WINTER))
|
2026-08-18 18:19:36 -04:00
|
|
|
assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
|
|
|
|
|
assert(beetle.required_tool_id == &"crab_net")
|
|
|
|
|
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
|
2026-08-24 10:36:45 -04:00
|
|
|
assert(is_equal_approx(beetle.spawn_anchor_occupancy_ratio, 0.35))
|
|
|
|
|
assert(beetle.maximum_anchor_population == 64)
|
2026-08-18 18:19:36 -04:00
|
|
|
assert(beetle.is_stationary_spawn())
|
|
|
|
|
assert(not beetle.is_stationary_hotspot())
|
|
|
|
|
assert(not beetle.requires_sneaking)
|
|
|
|
|
assert(not beetle.can_be_scared())
|
|
|
|
|
assert(FishCatalog.get_fish_by_id(&"beetle_stag_common") == beetle.catch_data)
|
|
|
|
|
assert(not beetle.catch_data.is_fishable())
|
2026-08-14 22:11:49 -04:00
|
|
|
|
2026-08-31 16:04:34 -04:00
|
|
|
var blue: GatherableData = Gatherables.get_entry(&"crab_blue")
|
|
|
|
|
assert(blue != null and blue.is_available())
|
|
|
|
|
assert(blue.catch_data.active)
|
|
|
|
|
assert(blue.is_available(CalendarSeasonType.Season.SUMMER))
|
|
|
|
|
assert(blue.is_available(CalendarSeasonType.Season.FALL))
|
|
|
|
|
assert(not blue.is_available(CalendarSeasonType.Season.WINTER))
|
|
|
|
|
assert(
|
|
|
|
|
blue.surface_water_relation
|
|
|
|
|
== GatherableData.SurfaceWaterRelation.BELOW_SALT_WATER
|
|
|
|
|
)
|
|
|
|
|
assert(is_equal_approx(
|
|
|
|
|
blue.surface_population_per_100_square_meters,
|
|
|
|
|
0.4,
|
|
|
|
|
))
|
|
|
|
|
assert(blue.maximum_surface_population == 16)
|
|
|
|
|
assert(blue.target_population_for_surface_area(100.0) == 4)
|
|
|
|
|
assert(blue.target_population_for_surface_area(2250.0) == 9)
|
|
|
|
|
assert(FishCatalog.get_fish_by_id(&"crab_blue") == blue.catch_data)
|
|
|
|
|
assert(not blue.catch_data.is_fishable())
|
|
|
|
|
|
|
|
|
|
for type_id: StringName in [&"crab_ghost", &"crab_dungeness"]:
|
2026-08-14 22:11:49 -04:00
|
|
|
var entry: GatherableData = Gatherables.get_entry(type_id)
|
|
|
|
|
assert(entry != null)
|
|
|
|
|
assert(entry.is_valid())
|
|
|
|
|
assert(not entry.catch_data.active)
|
|
|
|
|
assert(not entry.is_available())
|
2026-08-17 17:50:30 -04:00
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
entry.sprite_pixel_size,
|
2026-08-17 19:06:45 -04:00
|
|
|
CRAB_PIXEL_SIZE,
|
2026-08-17 17:50:30 -04:00
|
|
|
)
|
|
|
|
|
)
|
2026-08-14 22:11:49 -04:00
|
|
|
assert(entry.catch_data.collection_method == FishData.CollectionMethod.NET)
|
|
|
|
|
assert(
|
|
|
|
|
entry.catch_data.logbook_section
|
|
|
|
|
== FishData.LogbookSection.SHELLFISH
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var available: Array[GatherableData] = Gatherables.get_available_entries()
|
2026-08-31 16:04:34 -04:00
|
|
|
assert(available.size() == 4)
|
2026-08-18 18:19:36 -04:00
|
|
|
assert(available.has(brown))
|
2026-08-31 16:04:34 -04:00
|
|
|
assert(available.has(blue))
|
2026-08-18 18:19:36 -04:00
|
|
|
assert(available.has(clam))
|
|
|
|
|
assert(available.has(beetle))
|
2026-08-18 21:20:02 -04:00
|
|
|
var winter_available: Array[GatherableData] = (
|
|
|
|
|
Gatherables.get_available_entries(CalendarSeasonType.Season.WINTER)
|
|
|
|
|
)
|
|
|
|
|
assert(winter_available.has(brown))
|
|
|
|
|
assert(winter_available.has(clam))
|
2026-08-31 16:04:34 -04:00
|
|
|
assert(not winter_available.has(blue))
|
2026-08-18 21:20:02 -04:00
|
|
|
assert(not winter_available.has(beetle))
|
2026-08-14 22:11:49 -04:00
|
|
|
var rng := RandomNumberGenerator.new()
|
|
|
|
|
rng.seed = 24680
|
|
|
|
|
var captured_delay: float = brown.get_respawn_delay(&"captured", rng)
|
|
|
|
|
var scared_delay: float = brown.get_respawn_delay(&"scared", rng)
|
|
|
|
|
assert(captured_delay >= 480.0 and captured_delay <= 720.0)
|
|
|
|
|
assert(scared_delay >= 45.0 and scared_delay <= 90.0)
|
|
|
|
|
|
2026-08-17 17:50:30 -04:00
|
|
|
var gathering_controller := GatheringControllerType.new()
|
|
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
gathering_controller.marker_radius,
|
|
|
|
|
REDUCED_CATCH_RING_RADIUS,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-08-17 19:06:45 -04:00
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
gathering_controller.marker_distance,
|
|
|
|
|
NET_STRIKE_MARKER_DISTANCE,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-08-17 17:50:30 -04:00
|
|
|
gathering_controller.free()
|
|
|
|
|
|
2026-08-14 22:11:49 -04:00
|
|
|
|
2026-08-17 19:06:45 -04:00
|
|
|
func _validate_billboard_presentation() -> void:
|
|
|
|
|
var gatherable := WorldGatherableType.new()
|
|
|
|
|
gatherable.call("_ensure_visual")
|
|
|
|
|
var sprite := gatherable.get_node("GatherableSprite") as Sprite3D
|
|
|
|
|
assert(sprite != null)
|
|
|
|
|
assert(sprite.billboard == BaseMaterial3D.BILLBOARD_ENABLED)
|
|
|
|
|
assert(sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST)
|
|
|
|
|
assert(not sprite.shaded)
|
|
|
|
|
gatherable.free()
|
|
|
|
|
|
2026-08-29 17:41:10 -04:00
|
|
|
var unconfigured_hotspot := WorldGatherableType.new()
|
|
|
|
|
unconfigured_hotspot.call("_ensure_water_spurt_visual")
|
|
|
|
|
assert(
|
|
|
|
|
unconfigured_hotspot.get_node_or_null(
|
|
|
|
|
"WaterSpurt/WaterDroplets"
|
|
|
|
|
) == null
|
|
|
|
|
)
|
|
|
|
|
unconfigured_hotspot.free()
|
|
|
|
|
|
|
|
|
|
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
|
|
|
|
|
assert(clam != null)
|
2026-08-18 18:19:36 -04:00
|
|
|
var hotspot := WorldGatherableType.new()
|
2026-08-29 17:41:10 -04:00
|
|
|
root.add_child(hotspot)
|
|
|
|
|
var configured_position := Vector3(24.0, 0.3, -11.0)
|
|
|
|
|
hotspot.configure(
|
|
|
|
|
"world:clam-presentation-regression",
|
|
|
|
|
clam,
|
|
|
|
|
configured_position,
|
|
|
|
|
0.0,
|
|
|
|
|
)
|
|
|
|
|
assert(hotspot.global_position.is_equal_approx(configured_position))
|
2026-08-18 18:19:36 -04:00
|
|
|
var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D
|
|
|
|
|
assert(hole != null)
|
|
|
|
|
assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF)
|
|
|
|
|
var hole_material := hole.mesh.surface_get_material(0) as StandardMaterial3D
|
|
|
|
|
assert(hole_material != null)
|
|
|
|
|
assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
|
|
|
|
|
assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
|
|
|
|
|
assert(is_equal_approx(hole_material.albedo_color.a, 1.0))
|
2026-08-29 17:41:10 -04:00
|
|
|
var particles := hotspot.get_node(
|
|
|
|
|
"WaterSpurt/WaterDroplets"
|
|
|
|
|
) as CPUParticles3D
|
|
|
|
|
assert(particles != null and particles.emitting)
|
|
|
|
|
assert(particles.local_coords)
|
|
|
|
|
assert(particles.global_position.is_equal_approx(
|
|
|
|
|
configured_position + Vector3.UP * 0.035
|
|
|
|
|
))
|
|
|
|
|
hotspot.set_process(false)
|
|
|
|
|
await create_timer(0.6).timeout
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(hotspot.get_node_or_null("WaterSpurt/WaterDroplets") == null)
|
2026-08-18 18:19:36 -04:00
|
|
|
hotspot.queue_free()
|
|
|
|
|
|
2026-08-17 19:06:45 -04:00
|
|
|
|
2026-08-15 09:53:45 -04:00
|
|
|
func _validate_quality_behavior(entry: GatherableData) -> void:
|
|
|
|
|
var prior_speed: float = -1.0
|
|
|
|
|
var prior_scare_radius: float = -1.0
|
|
|
|
|
for quality: int in FishQuality.TIER_COUNT:
|
|
|
|
|
var movement_speed: float = entry.get_movement_speed_for_quality(
|
|
|
|
|
quality
|
|
|
|
|
)
|
|
|
|
|
var scare_radius: float = entry.get_scare_radius_for_quality(quality)
|
|
|
|
|
assert(movement_speed > prior_speed)
|
|
|
|
|
assert(scare_radius > prior_scare_radius)
|
|
|
|
|
prior_speed = movement_speed
|
|
|
|
|
prior_scare_radius = scare_radius
|
|
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
entry.get_movement_speed_for_quality(FishQuality.Tier.BORING),
|
|
|
|
|
entry.movement_speed,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
entry.get_movement_speed_for_quality(FishQuality.Tier.SHINY),
|
|
|
|
|
entry.movement_speed * 2.0,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
entry.get_scare_radius_for_quality(FishQuality.Tier.SHINY),
|
|
|
|
|
entry.scare_radius * 1.7,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
var service := NetworkWorldSpawnService.new()
|
|
|
|
|
var spawned_catch := service.call(
|
|
|
|
|
"_create_catch_for_state",
|
|
|
|
|
entry,
|
|
|
|
|
{"quality": FishQuality.Tier.EXCEPTIONAL},
|
|
|
|
|
) as FishCatch
|
|
|
|
|
assert(spawned_catch != null and spawned_catch.is_valid())
|
|
|
|
|
assert(spawned_catch.quality == FishQuality.Tier.EXCEPTIONAL)
|
|
|
|
|
assert(
|
|
|
|
|
spawned_catch.sale_value
|
|
|
|
|
== FishQuality.apply_sale_value(
|
|
|
|
|
entry.catch_data.get_sale_value_for_weight(
|
|
|
|
|
spawned_catch.weight_lb
|
|
|
|
|
),
|
|
|
|
|
FishQuality.Tier.EXCEPTIONAL,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
spawned_catch = null
|
|
|
|
|
service.free()
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 22:11:49 -04:00
|
|
|
func _validate_envelopes() -> void:
|
|
|
|
|
var entity: Dictionary = {
|
|
|
|
|
"entity_id": "world:sample",
|
|
|
|
|
"type_id": "future_shellfish",
|
|
|
|
|
"position": [1.0, 2.0, 3.0],
|
|
|
|
|
"yaw": 0.5,
|
|
|
|
|
"revision": 7,
|
|
|
|
|
}
|
|
|
|
|
assert(NetworkWorldSpawnProtocol.validate_entity_state(entity))
|
|
|
|
|
var future_entity: Dictionary = entity.duplicate(true)
|
|
|
|
|
future_entity["future_state"] = {"buried": false}
|
|
|
|
|
assert(NetworkWorldSpawnProtocol.validate_entity_state(future_entity))
|
|
|
|
|
|
|
|
|
|
var envelope: Dictionary = NetworkWorldSpawnProtocol.make_envelope(
|
|
|
|
|
"session-test",
|
|
|
|
|
12,
|
|
|
|
|
&"future_spawn_type",
|
|
|
|
|
{"entity": entity, "future_payload": [1, 2, 3]},
|
|
|
|
|
)
|
|
|
|
|
assert(NetworkWorldSpawnProtocol.validate_envelope(envelope))
|
|
|
|
|
|
|
|
|
|
var malformed_position: Dictionary = entity.duplicate(true)
|
|
|
|
|
malformed_position["position"] = [1.0, "not-a-number", 3.0]
|
|
|
|
|
assert(
|
|
|
|
|
not NetworkWorldSpawnProtocol.validate_entity_state(
|
|
|
|
|
malformed_position
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
not NetworkWorldSpawnProtocol.array_to_vector3(
|
|
|
|
|
malformed_position["position"]
|
|
|
|
|
).is_finite()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var invalid_envelope: Dictionary = envelope.duplicate(true)
|
|
|
|
|
invalid_envelope["envelope_version"] = 0
|
|
|
|
|
assert(not NetworkWorldSpawnProtocol.validate_envelope(invalid_envelope))
|