Scale coastal crab spawning by habitat

This commit is contained in:
Alexander Sellite 2026-08-31 16:04:34 -04:00
parent d8bf59bca0
commit 0158b0215f
9 changed files with 261 additions and 59 deletions

View file

@ -40,39 +40,17 @@ const StarterRegionScene = preload(
const SharedFishPlaceholder: Texture2D = preload(
"res://art/exported/creatures/fish_placeholder.png"
)
const FINAL_CREATURE_TEXTURE_PATHS: Dictionary[StringName, String] = {
&"bowfin": "res://art/exported/creatures/101.png",
&"sturgeon_lake": "res://art/exported/creatures/102.png",
&"gar_longnose": "res://art/exported/creatures/103.png",
&"paddlefish": "res://art/exported/creatures/104.png",
&"sturgeon_shovelnose": "res://art/exported/creatures/105.png",
&"gar_spotted": "res://art/exported/creatures/106.png",
&"lungfish_west_african": "res://art/exported/creatures/107.png",
&"mudskipper_atlantic": "res://art/exported/creatures/201.png",
&"flounder_winter": "res://art/exported/creatures/403.png",
&"mackerel_atlantic": "res://art/exported/creatures/701.png",
&"mackerel_chub": "res://art/exported/creatures/704.png",
&"sand_lance_american": "res://art/exported/creatures/801.png",
&"menhaden_atlantic": "res://art/exported/creatures/802.png",
&"anchovy_european": "res://art/exported/creatures/805.png",
&"sardine_european": "res://art/exported/creatures/806.png",
&"anchovy_northern": "res://art/exported/creatures/808.png",
&"crab_brown": "res://art/exported/creatures/3906.png",
&"salmon_atlantic": "res://art/exported/creatures/4401.png",
&"mahi_mahi": "res://art/exported/creatures/4701.png",
&"clownfish_ocellaris": "res://art/exported/creatures/5702.png",
&"chromis_blue_green": "res://art/exported/creatures/5901.png",
&"hogfish": "res://art/exported/creatures/6103.png",
&"mullet_striped": "res://art/exported/creatures/6804.png",
&"sheepshead": "res://art/exported/creatures/6905.png",
&"milkfish": "res://art/exported/creatures/7101.png",
&"ladyfish": "res://art/exported/creatures/7403.png",
&"tilapia_nile": "res://art/exported/creatures/7506.png",
&"betta_siamese": "res://art/exported/creatures/7508.png",
}
const FINAL_CREATURE_CATALOG_NUMBERS: Array[int] = [
101, 102, 103, 104, 105, 106, 107, 201, 403, 701, 704, 801,
802, 805, 806, 808, 2502, 2504, 2505, 2508, 2601, 2801, 2802,
2803, 2804, 2806, 2807, 3002, 3102, 3103, 3104, 3107, 3201,
3401, 3902, 3906, 4101, 4401, 4701, 5001, 5702, 5901, 5902,
5903, 5904, 6001, 6103, 6406, 6601, 6801, 6804, 6807, 6901,
6902, 6904, 6905, 7101, 7403, 7506, 7508,
]
const ACTIVE_CATALOG_COUNT: int = 313
const INACTIVE_CATALOG_COUNT: int = 3
const ACTIVE_CATALOG_COUNT: int = 314
const INACTIVE_CATALOG_COUNT: int = 2
const FISHING_SPECIES_COUNT: int = 310
const GENERATED_POND_COUNT: int = 33
const GENERATED_LAKE_COUNT: int = 20
@ -235,10 +213,15 @@ func _validate_catalog_and_pools() -> void:
else:
inactive_count += 1
assert(not fish.is_selectable())
assert(fish.display_texture == null)
if FINAL_CREATURE_TEXTURE_PATHS.has(fish.id):
if fish.catalog_number not in FINAL_CREATURE_CATALOG_NUMBERS:
assert(fish.display_texture == null)
if fish.catalog_number in FINAL_CREATURE_CATALOG_NUMBERS:
var expected_path := (
"res://art/exported/creatures/%d.png"
% fish.catalog_number
)
var expected_texture: Texture2D = load(
FINAL_CREATURE_TEXTURE_PATHS[fish.id]
expected_path
) as Texture2D
assert(expected_texture != null)
assert(fish.display_texture == expected_texture)
@ -246,14 +229,14 @@ func _validate_catalog_and_pools() -> void:
if fish.collection_method == FishDataType.CollectionMethod.FISHING:
fishing_count += 1
assert(fish.active)
if not FINAL_CREATURE_TEXTURE_PATHS.has(fish.id):
if fish.catalog_number not in FINAL_CREATURE_CATALOG_NUMBERS:
assert(fish.display_texture == SharedFishPlaceholder)
placeholder_fishing_count += 1
assert(active_count == ACTIVE_CATALOG_COUNT)
assert(inactive_count == INACTIVE_CATALOG_COUNT)
assert(fishing_count == FISHING_SPECIES_COUNT)
assert(final_art_count == FINAL_CREATURE_TEXTURE_PATHS.size())
assert(placeholder_fishing_count == 283)
assert(final_art_count == FINAL_CREATURE_CATALOG_NUMBERS.size())
assert(placeholder_fishing_count == 253)
var chum: FishDataType = Catalog.get_fish_by_id(&"salmon_chum")
assert(chum != null)
assert(chum.get_season_text() == "fall")
@ -291,7 +274,7 @@ func _validate_catalog_and_pools() -> void:
) == chum
)
seasonal_collection.free()
var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_blue")
var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_dungeness")
assert(inactive_fish != null and not inactive_fish.active)
var inactive_pool := FishPoolType.new()
inactive_pool.candidates = [inactive_fish]

View file

@ -12,6 +12,9 @@ const GeneratedLakePool: FishPool = preload(
const GeneratedRiverPool: FishPool = preload(
"res://fish/pools/generated_river_pool.tres"
)
const Gatherables: GatherableCatalog = preload(
"res://gathering/catalog/gatherable_catalog.tres"
)
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
@ -402,7 +405,7 @@ func _validate_generated_region(
+ generator.stacked_elevated_placement_keys().size()
)
)
_validate_elevated_cliff_feature(generator)
_validate_elevated_cliff_feature(region, generator)
var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
@ -536,6 +539,7 @@ func _validate_generated_region(
-INF,
0.35,
)
_validate_crab_spawn_surfaces(region)
_validate_water_recovery_positions(
region,
grass_prop_surface_triangles,
@ -691,6 +695,7 @@ func _validate_generated_region(
func _validate_elevated_cliff_feature(
region: GeneratedWorldRegion,
generator: TerrainChunkGenerator,
) -> void:
var elevated_coordinates: Dictionary[Vector2i, StringName] = {}
@ -824,6 +829,27 @@ func _validate_elevated_cliff_feature(
or generator.placement_keys()[low_index].begins_with("chunk_spawn@")
)
assert(found_primary_ramp == not primary_is_third_tier)
var primary_surface_height := (
generator.elevated_cliff_level_height
* (2.0 if primary_is_third_tier else 1.0)
)
var primary_surface_position := generator.to_global(
generator.chunk_position(feature_center)
+ Vector3.UP * primary_surface_height
)
assert(
region.is_home_exterior_position_accessible(primary_surface_position)
== not primary_is_third_tier
)
assert(region.is_home_exterior_position_accessible(generator.to_global(
generator.chunk_position(feature_center)
)))
# A higher surface sharing a ramp-accessible base cell is still invalid
# unless that elevation has its own authored ramp connection.
assert(not region.is_home_exterior_position_accessible(generator.to_global(
generator.chunk_position(feature_center)
+ Vector3.UP * (generator.elevated_cliff_level_height * 2.0)
)))
var generated := generator.get_generated_chunks_root()
assert(generated != null)
@ -1251,6 +1277,58 @@ func _validate_decoration_transform(
)
func _validate_crab_spawn_surfaces(region: GeneratedWorldRegion) -> void:
var brown := Gatherables.get_entry(&"crab_brown")
var blue := Gatherables.get_entry(&"crab_blue")
assert(brown != null and blue != null)
assert(is_equal_approx(
region.get_saltwater_surface_height(),
GeneratedWorldRegion.WATER_HEIGHT,
))
var brown_minimum := maxf(
brown.minimum_surface_y,
GeneratedWorldRegion.WATER_HEIGHT + brown.waterline_margin,
)
var blue_maximum := minf(
blue.maximum_surface_y,
GeneratedWorldRegion.WATER_HEIGHT - blue.waterline_margin,
)
assert(brown_minimum > blue_maximum)
var brown_triangles := region.get_spawn_surface_triangles(
brown.surface_materials,
brown_minimum,
0.6,
brown.maximum_surface_y,
)
var blue_triangles := region.get_spawn_surface_triangles(
blue.surface_materials,
blue.minimum_surface_y,
0.6,
blue_maximum,
)
assert(not brown_triangles.is_empty())
assert(not blue_triangles.is_empty())
assert(
brown.target_population_for_surface_area(
_surface_triangle_area(brown_triangles)
) > brown.population
)
assert(
blue.target_population_for_surface_area(
_surface_triangle_area(blue_triangles)
) > blue.population
)
func _surface_triangle_area(triangles: Array[PackedVector3Array]) -> float:
var area := 0.0
for triangle: PackedVector3Array in triangles:
area += (triangle[1] - triangle[0]).cross(
triangle[2] - triangle[0]
).length() * 0.5
return area
func _triangle_surface_height_at(
position: Vector3,
triangles: Array[PackedVector3Array],

View file

@ -46,7 +46,7 @@ func _run() -> void:
func _validate_catalog() -> void:
assert(CatalogResource.candidates.size() == 316)
var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
assert(ordered.size() == 313)
assert(ordered.size() == 314)
var previous_number: int = 0
var catalog_numbers: Dictionary[int, bool] = {}
for fish: FishDataType in CatalogResource.candidates:
@ -640,7 +640,10 @@ func _validate_page() -> void:
page.call("_select_category", LogbookCatalog.Category.SHELLFISH)
await create_timer(0.25).timeout
assert((page.get("_entry_buttons") as Dictionary).size() == 2)
assert((page.get("_entry_buttons") as Dictionary).size() == 3)
assert(
(page.get("_entry_buttons") as Dictionary).has(&"unknown_3902")
)
assert(
(page.get("_entry_buttons") as Dictionary).has(&"unknown_3906")
)

View file

@ -2,9 +2,10 @@ extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const TEST_PORT: int = 18170
const EXPECTED_POPULATION: int = 8
const EXPECTED_POPULATION: int = 12
const EXPECTED_BY_TYPE: Dictionary = {
&"crab_brown": 2,
&"crab_blue": 4,
&"clam_manila": 3,
&"beetle_stag_common": 3,
}
@ -161,6 +162,10 @@ func _validate_population(
(position as Vector3).y
>= entry.minimum_surface_y - 0.001
)
if entry.type_id == &"crab_brown":
assert((position as Vector3).y > -0.45)
elif entry.type_id == &"crab_blue":
assert((position as Vector3).y < -0.45)
if expect_authoritative_state:
var quality: int = int(state.get("quality", -1))
assert(FishQuality.is_valid(quality))
@ -196,7 +201,10 @@ func _validate_respawn_budget(service: Node) -> void:
false,
true,
)
assert((service.get("_entities") as Dictionary).size() == 6)
assert(
(service.get("_entities") as Dictionary).size()
== EXPECTED_POPULATION - crab_ids.size()
)
var now: float = Time.get_ticks_msec() / 1000.0
var respawns: Array = service.get("_respawns")
@ -209,18 +217,27 @@ func _validate_respawn_budget(service: Node) -> void:
respawns[index] = respawn
service.call("_update_respawns")
assert((service.get("_entities") as Dictionary).size() == 7)
assert(
(service.get("_entities") as Dictionary).size()
== EXPECTED_POPULATION - 1
)
assert((service.get("_respawns") as Array).size() == 1)
var next_by_type: Dictionary = service.get("_next_respawn_by_type")
var next_allowed: float = float(next_by_type.get(&"crab_brown", 0.0))
assert(next_allowed - now >= 179.0)
service.call("_update_respawns")
assert((service.get("_entities") as Dictionary).size() == 7)
assert(
(service.get("_entities") as Dictionary).size()
== EXPECTED_POPULATION - 1
)
assert((service.get("_respawns") as Array).size() == 1)
next_by_type[&"crab_brown"] = now - 1.0
service.call("_update_respawns")
assert((service.get("_entities") as Dictionary).size() == 8)
assert(
(service.get("_entities") as Dictionary).size()
== EXPECTED_POPULATION
)
assert((service.get("_respawns") as Array).is_empty())
_validate_population(service, true)

View file

@ -55,6 +55,17 @@ func _validate_catalog_statuses() -> void:
assert(is_equal_approx(brown.minimum_respawn_spacing_seconds, 180.0))
assert(brown.required_tool_id == &"crab_net")
assert(is_equal_approx(brown.minimum_surface_y, -0.44))
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)
assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
assert(not brown.catch_data.is_fishable())
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
@ -86,11 +97,27 @@ func _validate_catalog_statuses() -> void:
assert(FishCatalog.get_fish_by_id(&"beetle_stag_common") == beetle.catch_data)
assert(not beetle.catch_data.is_fishable())
for type_id: StringName in [
&"crab_ghost",
&"crab_blue",
&"crab_dungeness",
]:
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"]:
var entry: GatherableData = Gatherables.get_entry(type_id)
assert(entry != null)
assert(entry.is_valid())
@ -109,8 +136,9 @@ func _validate_catalog_statuses() -> void:
)
var available: Array[GatherableData] = Gatherables.get_available_entries()
assert(available.size() == 3)
assert(available.size() == 4)
assert(available.has(brown))
assert(available.has(blue))
assert(available.has(clam))
assert(available.has(beetle))
var winter_available: Array[GatherableData] = (
@ -118,6 +146,7 @@ func _validate_catalog_statuses() -> void:
)
assert(winter_available.has(brown))
assert(winter_available.has(clam))
assert(not winter_available.has(blue))
assert(not winter_available.has(beetle))
var rng := RandomNumberGenerator.new()
rng.seed = 24680