Add freshwater habitats and saltwater tide ribbons
This commit is contained in:
parent
53fdc9ef21
commit
69b7969e25
40 changed files with 1323 additions and 148 deletions
|
|
@ -16,6 +16,8 @@ enum Rarity {
|
|||
|
||||
@export var id: StringName
|
||||
@export var display_name: String
|
||||
@export_flags("Fresh Water", "Salt Water")
|
||||
var allowed_water_types: int = WaterType.ALL_FISHABLE_MASK
|
||||
@export var rarity: Rarity = Rarity.COMMON
|
||||
@export_range(0.0, 1000.0, 0.01) var base_catch_weight: float = 1.0
|
||||
@export var catch_profile: CatchDifficultyProfileType
|
||||
|
|
@ -47,6 +49,18 @@ func is_selectable() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func is_allowed_in_water(type: WaterType.Type) -> bool:
|
||||
return (allowed_water_types & WaterType.mask_for(type)) != 0
|
||||
|
||||
|
||||
func get_primary_water_type() -> WaterType.Type:
|
||||
if allowed_water_types & WaterType.FRESH_WATER_MASK:
|
||||
return WaterType.Type.FRESH_WATER
|
||||
if allowed_water_types & WaterType.SALT_WATER_MASK:
|
||||
return WaterType.Type.SALT_WATER
|
||||
return WaterType.Type.OTHER
|
||||
|
||||
|
||||
func get_minimum_weight() -> float:
|
||||
return maxf(weight_min_lb, 0.01)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ func select_fish(
|
|||
for fish: FishDataType in pool.candidates:
|
||||
if fish == null or not fish.is_selectable():
|
||||
continue
|
||||
if not fish.is_allowed_in_water(context.water_type):
|
||||
continue
|
||||
if fish.availability != null and not fish.availability.is_available(context):
|
||||
continue
|
||||
var final_weight: float = fish.base_catch_weight
|
||||
|
|
|
|||
9
fish/pools/starter_ocean_pool.tres
Normal file
9
fish/pools/starter_ocean_pool.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="Resource" load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
|
||||
[ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="2_bass"]
|
||||
[ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="3_sunfish"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pool")
|
||||
candidates = [ExtResource("2_bass"), ExtResource("3_sunfish")]
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
[gd_resource type="Resource" load_steps=10 format=3]
|
||||
[gd_resource type="Resource" load_steps=8 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
|
||||
[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"]
|
||||
[ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="3_bass"]
|
||||
[ext_resource type="Resource" path="res://fish/species/carp/carp.tres" id="4_carp"]
|
||||
[ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="5_sunfish"]
|
||||
[ext_resource type="Resource" path="res://fish/species/catfish_blue/catfish_blue.tres" id="6_catfish_blue"]
|
||||
[ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="7_catfish_channel"]
|
||||
[ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="8_catfish_flathead"]
|
||||
|
|
@ -12,4 +10,4 @@
|
|||
|
||||
[resource]
|
||||
script = ExtResource("1_pool")
|
||||
candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp"), ExtResource("5_sunfish"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white")]
|
||||
candidates = [ExtResource("2_bluegill"), ExtResource("4_carp"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white")]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ preferred_bait_weight_multiplier = 1.4
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"bass"
|
||||
display_name = "striped bass"
|
||||
allowed_water_types = 2
|
||||
rarity = 1
|
||||
base_catch_weight = 4.0
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"bluegill"
|
||||
display_name = "bluegill"
|
||||
allowed_water_types = 1
|
||||
rarity = 0
|
||||
base_catch_weight = 10.0
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ preferred_bait_weight_multiplier = 1.5
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"carp"
|
||||
display_name = "common carp"
|
||||
allowed_water_types = 1
|
||||
rarity = 2
|
||||
base_catch_weight = 1.5
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ preferred_bait_weight_multiplier = 1.35
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"catfish_blue"
|
||||
display_name = "blue catfish"
|
||||
allowed_water_types = 1
|
||||
rarity = 1
|
||||
base_catch_weight = 1.25
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ preferred_bait_weight_multiplier = 1.35
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"catfish_channel"
|
||||
display_name = "channel catfish"
|
||||
allowed_water_types = 1
|
||||
rarity = 0
|
||||
base_catch_weight = 2.0
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ preferred_bait_weight_multiplier = 1.35
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"catfish_flathead"
|
||||
display_name = "flathead catfish"
|
||||
allowed_water_types = 1
|
||||
rarity = 1
|
||||
base_catch_weight = 1.0
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ preferred_bait_weight_multiplier = 1.35
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"catfish_white"
|
||||
display_name = "white catfish"
|
||||
allowed_water_types = 1
|
||||
rarity = 0
|
||||
base_catch_weight = 2.0
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
script = ExtResource("1_fish_data")
|
||||
id = &"sunfish"
|
||||
display_name = "sunfish"
|
||||
allowed_water_types = 2
|
||||
rarity = 0
|
||||
base_catch_weight = 7.0
|
||||
catch_profile = ExtResource("2_profile")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name FishingContext
|
|||
extends RefCounted
|
||||
|
||||
var location_tags: Array[StringName] = []
|
||||
var water_type: WaterType.Type = WaterType.Type.FRESH_WATER
|
||||
var active_event_tags: Array[StringName] = []
|
||||
var active_bait_tags: Array[StringName] = []
|
||||
var is_night: bool = false
|
||||
|
|
|
|||
|
|
@ -1110,6 +1110,7 @@ func _build_fishing_context(
|
|||
) -> FishingContextType:
|
||||
var context := FishingContextType.new()
|
||||
context.location_tags = region.location_tags.duplicate()
|
||||
context.water_type = region.water_type
|
||||
context.active_event_tags = context_event_tags.duplicate()
|
||||
context.active_bait_tags = context_bait_tags.duplicate()
|
||||
context.is_night = context_is_night
|
||||
|
|
|
|||
|
|
@ -269,7 +269,9 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
|||
var selected_fish: FishDataType = _select_authoritative_fish(
|
||||
region, data, effects
|
||||
)
|
||||
if selected_fish == null:
|
||||
if selected_fish == null or not selected_fish.is_allowed_in_water(
|
||||
region.water_type
|
||||
):
|
||||
_record_and_reject(peer_id, request_id, "Nothing is biting here.")
|
||||
return
|
||||
var attempt := NetworkFishingAttempt.new()
|
||||
|
|
@ -389,7 +391,11 @@ func _select_authoritative_fish(
|
|||
)
|
||||
selector.begin_roll()
|
||||
var context: FishingContextType = _fishing_spot.build_network_context(region)
|
||||
return selector.select_fish(region.fish_pool, context, evidence_log)
|
||||
var selected_fish := selector.select_fish(
|
||||
region.fish_pool, context, evidence_log
|
||||
)
|
||||
evidence_log.free()
|
||||
return selected_fish
|
||||
|
||||
|
||||
func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
||||
|
|
|
|||
42
scripts/bake_shoreline_ribbons.gd
Normal file
42
scripts/bake_shoreline_ribbons.gd
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
extends SceneTree
|
||||
|
||||
const REGION_SCENE := preload("res://world/regions/starter_island_region.tscn")
|
||||
func _initialize() -> void:
|
||||
call_deferred("_bake")
|
||||
|
||||
|
||||
func _bake() -> void:
|
||||
var started := Time.get_ticks_usec()
|
||||
var region := REGION_SCENE.instantiate()
|
||||
root.add_child(region)
|
||||
var baker := region.get_node_or_null("ShorelineRibbonBaker") as ShorelineRibbonBaker
|
||||
if baker == null:
|
||||
push_error("The map does not contain a configured ShorelineRibbonBaker.")
|
||||
quit(1)
|
||||
return
|
||||
var results := baker.rebuild_all()
|
||||
if results.size() != baker.water_bodies.size():
|
||||
quit(1)
|
||||
return
|
||||
for result: Dictionary in results:
|
||||
if result.get("skipped", false):
|
||||
print(
|
||||
"Shoreline %s: type=%s skipped intentionally"
|
||||
% [result["output_path"], WaterType.label(result["water_type"])]
|
||||
)
|
||||
continue
|
||||
print(
|
||||
"Shoreline %s: type=%s segments=%d loops=%d raw=%d simplified=%d smoothed=%d triangles=%d"
|
||||
% [
|
||||
result["output_path"],
|
||||
WaterType.label(result["water_type"]),
|
||||
result["segment_count"],
|
||||
result["loop_count"],
|
||||
result["raw_point_count"],
|
||||
result["simplified_point_count"],
|
||||
result["smoothed_point_count"],
|
||||
result["triangle_count"],
|
||||
]
|
||||
)
|
||||
print("Shoreline bake completed in %.2f ms" % ((Time.get_ticks_usec() - started) / 1000.0))
|
||||
quit()
|
||||
1
scripts/bake_shoreline_ribbons.gd.uid
Normal file
1
scripts/bake_shoreline_ribbons.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://eosavaa6wu1e
|
||||
|
|
@ -10,15 +10,22 @@ const FishingContextType = preload("res://fishing/fishing_context.gd")
|
|||
const NetworkSaleServiceType = preload(
|
||||
"res://network/network_sale_service.gd"
|
||||
)
|
||||
const NetworkFishingServiceType = preload(
|
||||
"res://network/network_fishing_service.gd"
|
||||
)
|
||||
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||
|
||||
const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
|
||||
const PondPool: FishPoolType = preload(
|
||||
"res://fish/pools/starter_pond_pool.tres"
|
||||
)
|
||||
const OceanPool: FishPoolType = preload(
|
||||
"res://fish/pools/test_water_pool.tres"
|
||||
"res://fish/pools/starter_ocean_pool.tres"
|
||||
)
|
||||
const PelicanBuyer = preload("res://economy/buyers/pelicans.tres")
|
||||
const StarterRegionScene = preload(
|
||||
"res://world/regions/starter_island_region.tscn"
|
||||
)
|
||||
|
||||
const ORIGINAL_IDS: Array[StringName] = [
|
||||
&"bluegill", &"bass", &"carp", &"sunfish",
|
||||
|
|
@ -29,6 +36,11 @@ const CATFISH_IDS: Array[StringName] = [
|
|||
&"catfish_flathead",
|
||||
&"catfish_white",
|
||||
]
|
||||
const FRESH_WATER_IDS: Array[StringName] = [
|
||||
&"bluegill", &"carp", &"catfish_blue", &"catfish_channel",
|
||||
&"catfish_flathead", &"catfish_white",
|
||||
]
|
||||
const SALT_WATER_IDS: Array[StringName] = [&"bass", &"sunfish"]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -37,6 +49,8 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_catalog_and_pools()
|
||||
_validate_starter_water_bodies()
|
||||
_validate_authoritative_water_filter()
|
||||
_validate_catches_and_authoritative_sale()
|
||||
print("Fish catalog content validation: PASS")
|
||||
quit()
|
||||
|
|
@ -44,14 +58,26 @@ func _run() -> void:
|
|||
|
||||
func _validate_catalog_and_pools() -> void:
|
||||
assert(Catalog.candidates.size() == 8)
|
||||
assert(PondPool.candidates.size() == 8)
|
||||
assert(OceanPool.candidates.size() == 4)
|
||||
assert(PondPool.candidates.size() == 6)
|
||||
assert(OceanPool.candidates.size() == 2)
|
||||
for fish_id: StringName in ORIGINAL_IDS:
|
||||
var original_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
assert(original_fish != null)
|
||||
assert(original_fish.is_selectable())
|
||||
assert(PondPool.get_fish_by_id(fish_id) != null)
|
||||
assert(OceanPool.get_fish_by_id(fish_id) != null)
|
||||
for fish_id: StringName in FRESH_WATER_IDS:
|
||||
var fresh_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
assert(PondPool.get_fish_by_id(fish_id) == fresh_fish)
|
||||
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
||||
assert(fresh_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
|
||||
assert(not fresh_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
|
||||
assert(LogbookCatalog.category_for(fresh_fish) == WaterType.Type.FRESH_WATER)
|
||||
for fish_id: StringName in SALT_WATER_IDS:
|
||||
var salt_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
assert(OceanPool.get_fish_by_id(fish_id) == salt_fish)
|
||||
assert(PondPool.get_fish_by_id(fish_id) == null)
|
||||
assert(salt_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
|
||||
assert(not salt_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
|
||||
assert(LogbookCatalog.category_for(salt_fish) == WaterType.Type.SALT_WATER)
|
||||
for fish_id: StringName in CATFISH_IDS:
|
||||
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
assert(fish != null)
|
||||
|
|
@ -59,9 +85,7 @@ func _validate_catalog_and_pools() -> void:
|
|||
assert(PondPool.get_fish_by_id(fish_id) == fish)
|
||||
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
||||
assert(fish.availability.allowed_location_tags == [&"starter_pond"])
|
||||
assert(LogbookCatalog.category_for(fish) == (
|
||||
LogbookCatalog.Category.FRESH_WATER
|
||||
))
|
||||
assert(LogbookCatalog.category_for(fish) == WaterType.Type.FRESH_WATER)
|
||||
|
||||
var expected_values: Dictionary[StringName, Array] = {
|
||||
&"catfish_blue": [1, 1.25, 3.0, 12.0, 6, 9],
|
||||
|
|
@ -81,8 +105,10 @@ func _validate_catalog_and_pools() -> void:
|
|||
|
||||
var pond_context := FishingContextType.new()
|
||||
pond_context.location_tags = [&"starter_pond"]
|
||||
pond_context.water_type = WaterType.Type.FRESH_WATER
|
||||
var ocean_context := FishingContextType.new()
|
||||
ocean_context.location_tags = [&"coast", &"ocean"]
|
||||
ocean_context.water_type = WaterType.Type.SALT_WATER
|
||||
for fish_id: StringName in CATFISH_IDS:
|
||||
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
assert(fish.availability.is_available(pond_context))
|
||||
|
|
@ -99,6 +125,94 @@ func _validate_catalog_and_pools() -> void:
|
|||
) == fish
|
||||
)
|
||||
collection.free()
|
||||
for fish_id: StringName in SALT_WATER_IDS:
|
||||
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
var single_species_pool := FishPoolType.new()
|
||||
single_species_pool.candidates = [fish]
|
||||
var collection := CollectionLogType.new()
|
||||
var selector := FishSelectorType.new()
|
||||
selector.use_deterministic_test_seed = true
|
||||
selector.begin_roll()
|
||||
assert(
|
||||
selector.select_fish(single_species_pool, pond_context, collection)
|
||||
== null
|
||||
)
|
||||
assert(
|
||||
selector.select_fish(single_species_pool, ocean_context, collection)
|
||||
== fish
|
||||
)
|
||||
collection.free()
|
||||
|
||||
|
||||
func _validate_authoritative_water_filter() -> void:
|
||||
var fishing_spot := FishingSpotType.new()
|
||||
var service := NetworkFishingServiceType.new()
|
||||
service.set("_fish_catalog", Catalog)
|
||||
service.set("_fishing_spot", fishing_spot)
|
||||
var pond_region := FishableWaterRegion.new()
|
||||
pond_region.water_type = WaterType.Type.FRESH_WATER
|
||||
pond_region.fish_pool = PondPool
|
||||
pond_region.location_tags = [&"starter_pond"]
|
||||
var ocean_region := FishableWaterRegion.new()
|
||||
ocean_region.water_type = WaterType.Type.SALT_WATER
|
||||
ocean_region.fish_pool = OceanPool
|
||||
ocean_region.location_tags = [&"coast", &"ocean"]
|
||||
var stale_salt_pool_region := FishableWaterRegion.new()
|
||||
stale_salt_pool_region.water_type = WaterType.Type.FRESH_WATER
|
||||
stale_salt_pool_region.fish_pool = OceanPool
|
||||
stale_salt_pool_region.location_tags = [&"starter_pond"]
|
||||
var stale_fresh_pool_region := FishableWaterRegion.new()
|
||||
stale_fresh_pool_region.water_type = WaterType.Type.SALT_WATER
|
||||
stale_fresh_pool_region.fish_pool = PondPool
|
||||
stale_fresh_pool_region.location_tags = [&"coast", &"ocean"]
|
||||
var evidence := {"discovered_fish_ids": []}
|
||||
assert(
|
||||
service.call("_select_authoritative_fish", pond_region, evidence, null)
|
||||
!= null
|
||||
)
|
||||
assert(
|
||||
service.call("_select_authoritative_fish", ocean_region, evidence, null)
|
||||
!= null
|
||||
)
|
||||
assert(
|
||||
service.call(
|
||||
"_select_authoritative_fish", stale_salt_pool_region, evidence, null
|
||||
) == null
|
||||
)
|
||||
assert(
|
||||
service.call(
|
||||
"_select_authoritative_fish", stale_fresh_pool_region, evidence, null
|
||||
) == null
|
||||
)
|
||||
service.free()
|
||||
fishing_spot.free()
|
||||
pond_region.free()
|
||||
ocean_region.free()
|
||||
stale_salt_pool_region.free()
|
||||
stale_fresh_pool_region.free()
|
||||
|
||||
|
||||
func _validate_starter_water_bodies() -> void:
|
||||
var region := StarterRegionScene.instantiate()
|
||||
var pond := region.get_node("WaterBodies/Pond/FishingRegion") as FishableWaterRegion
|
||||
var ocean := region.get_node(
|
||||
"WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
|
||||
) as FishableWaterRegion
|
||||
assert(pond.water_type == WaterType.Type.FRESH_WATER)
|
||||
assert(ocean.water_type == WaterType.Type.SALT_WATER)
|
||||
assert(pond.fish_pool == PondPool)
|
||||
assert(ocean.fish_pool == OceanPool)
|
||||
assert(region.get_node_or_null("ShorelineRibbons/Pond") == null)
|
||||
assert(region.get_node_or_null("ShorelineRibbons/Ocean") != null)
|
||||
var pond_material := (
|
||||
region.get_node("WaterBodies/Pond/VisualWater") as MeshInstance3D
|
||||
).material_override as ShaderMaterial
|
||||
var ocean_material := (
|
||||
region.get_node("WaterBodies/Ocean/VisualWater") as MeshInstance3D
|
||||
).material_override as ShaderMaterial
|
||||
assert(not bool(pond_material.get_shader_parameter("tide_effect_enabled")))
|
||||
assert(bool(ocean_material.get_shader_parameter("tide_effect_enabled")))
|
||||
region.free()
|
||||
|
||||
|
||||
func _validate_catches_and_authoritative_sale() -> void:
|
||||
|
|
|
|||
|
|
@ -45,20 +45,20 @@ func _run() -> void:
|
|||
assert(not hotbar.visible)
|
||||
|
||||
var entry_buttons: Dictionary = logbook.get("_entry_buttons")
|
||||
assert(entry_buttons.size() == 4)
|
||||
assert(entry_buttons.size() == 0)
|
||||
await _capture_if_requested("-unknown")
|
||||
logbook.call(
|
||||
"_select_category", LogbookCatalog.Category.FRESH_WATER
|
||||
"_select_category", WaterType.Type.FRESH_WATER
|
||||
)
|
||||
await create_timer(0.25).timeout
|
||||
assert((logbook.get("_entry_buttons") as Dictionary).size() == 4)
|
||||
assert((logbook.get("_entry_buttons") as Dictionary).size() == 6)
|
||||
await _capture_if_requested("-fresh")
|
||||
logbook.call("_select_category", LogbookCatalog.Category.SALT_WATER)
|
||||
logbook.call("_select_category", WaterType.Type.SALT_WATER)
|
||||
await create_timer(0.25).timeout
|
||||
assert((logbook.get("_entry_buttons") as Dictionary).is_empty())
|
||||
logbook.call("_select_category", LogbookCatalog.Category.OTHER)
|
||||
assert((logbook.get("_entry_buttons") as Dictionary).size() == 2)
|
||||
logbook.call("_select_category", WaterType.Type.FRESH_WATER)
|
||||
await create_timer(0.25).timeout
|
||||
assert((logbook.get("_entry_buttons") as Dictionary).size() == 4)
|
||||
assert((logbook.get("_entry_buttons") as Dictionary).size() == 6)
|
||||
player.collection_log.mark_discovered(&"bluegill")
|
||||
await process_frame
|
||||
logbook.call("_select_entry", &"bluegill", &"bluegill")
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ func _validate_catalog() -> void:
|
|||
for index: int in expected_ids.size():
|
||||
var fish := CatalogResource.get_fish_by_id(expected_ids[index])
|
||||
assert(fish != null)
|
||||
var expected_category: LogbookCatalog.Category = (
|
||||
LogbookCatalog.Category.OTHER
|
||||
if index < 4
|
||||
else LogbookCatalog.Category.FRESH_WATER
|
||||
var expected_category: WaterType.Type = (
|
||||
WaterType.Type.SALT_WATER
|
||||
if expected_ids[index] in [&"bass", &"sunfish"]
|
||||
else WaterType.Type.FRESH_WATER
|
||||
)
|
||||
assert(
|
||||
LogbookCatalog.category_for(fish)
|
||||
|
|
@ -59,7 +59,7 @@ func _validate_catalog() -> void:
|
|||
)
|
||||
assert(LogbookCatalog.catalog_number(fish.id) == index + 1)
|
||||
assert(
|
||||
LogbookCatalog.empty_state(LogbookCatalog.Category.SALT_WATER)
|
||||
LogbookCatalog.empty_state(WaterType.Type.SALT_WATER)
|
||||
== "No saltwater catches cataloged yet."
|
||||
)
|
||||
|
||||
|
|
@ -78,14 +78,14 @@ func _validate_page() -> void:
|
|||
|
||||
var shared_material: Material
|
||||
var silhouette_count: int = 0
|
||||
for category: LogbookCatalog.Category in [
|
||||
LogbookCatalog.Category.FRESH_WATER,
|
||||
LogbookCatalog.Category.OTHER,
|
||||
for category: WaterType.Type in [
|
||||
WaterType.Type.FRESH_WATER,
|
||||
WaterType.Type.SALT_WATER,
|
||||
]:
|
||||
page.call("_select_category", category)
|
||||
await create_timer(0.25).timeout
|
||||
var entries: Dictionary = page.get("_entry_buttons")
|
||||
assert(entries.size() == 4)
|
||||
assert(entries.size() == (6 if category == WaterType.Type.FRESH_WATER else 2))
|
||||
for fish: FishDataType in LogbookCatalog.ordered_species(
|
||||
CatalogResource.candidates
|
||||
):
|
||||
|
|
@ -133,14 +133,14 @@ func _validate_page() -> void:
|
|||
)
|
||||
assert(silhouette_count == 8)
|
||||
|
||||
page.call("_select_category", LogbookCatalog.Category.SALT_WATER)
|
||||
page.call("_select_category", WaterType.Type.OTHER)
|
||||
await create_timer(0.25).timeout
|
||||
assert((page.get("_entry_buttons") as Dictionary).is_empty())
|
||||
assert(
|
||||
(page.get("_empty_state") as Label).text
|
||||
== "No saltwater catches cataloged yet."
|
||||
== "No entries available."
|
||||
)
|
||||
page.call("_select_category", LogbookCatalog.Category.OTHER)
|
||||
page.call("_select_category", WaterType.Type.FRESH_WATER)
|
||||
await create_timer(0.25).timeout
|
||||
|
||||
var bluegill = CatalogResource.get_fish_by_id(&"bluegill")
|
||||
|
|
@ -175,7 +175,7 @@ func _validate_page() -> void:
|
|||
await process_frame
|
||||
assert(_detail_text(page).contains("number owned\n1"))
|
||||
assert(_detail_text(page).contains("number caught\nUnknown"))
|
||||
assert(_detail_text(page).contains("body of water\nUnknown"))
|
||||
assert(_detail_text(page).contains("body of water\nFresh Water"))
|
||||
assert(_detail_text(page).contains("catalog number\n#001"))
|
||||
|
||||
inventory.remove_catch_by_id(fish_catch.catch_id)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,6 @@ extends RefCounted
|
|||
|
||||
const FishDataType = preload("res://fish/fish_data.gd")
|
||||
|
||||
enum Category {
|
||||
FRESH_WATER,
|
||||
SALT_WATER,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
# Catalog numbers are a presentation contract. Add future species deliberately;
|
||||
# filtering and display order must not redefine an existing number.
|
||||
const CATALOG_ORDER: Array[StringName] = [
|
||||
|
|
@ -22,41 +16,21 @@ const CATALOG_ORDER: Array[StringName] = [
|
|||
&"catfish_white",
|
||||
]
|
||||
|
||||
# These maps remain explicit until fish resources gain authored habitat fields.
|
||||
const FRESH_WATER_IDS: Array[StringName] = [
|
||||
&"catfish_blue",
|
||||
&"catfish_channel",
|
||||
&"catfish_flathead",
|
||||
&"catfish_white",
|
||||
]
|
||||
const SALT_WATER_IDS: Array[StringName] = []
|
||||
|
||||
|
||||
static func category_for(fish: FishDataType) -> Category:
|
||||
static func category_for(fish: FishDataType) -> WaterType.Type:
|
||||
if fish == null:
|
||||
return Category.OTHER
|
||||
if fish.id in FRESH_WATER_IDS:
|
||||
return Category.FRESH_WATER
|
||||
if fish.id in SALT_WATER_IDS:
|
||||
return Category.SALT_WATER
|
||||
return Category.OTHER
|
||||
return WaterType.Type.OTHER
|
||||
return fish.get_primary_water_type()
|
||||
|
||||
|
||||
static func category_label(category: Category) -> String:
|
||||
static func category_label(category: WaterType.Type) -> String:
|
||||
return WaterType.label(category)
|
||||
|
||||
|
||||
static func empty_state(category: WaterType.Type) -> String:
|
||||
match category:
|
||||
Category.FRESH_WATER:
|
||||
return "Fresh Water"
|
||||
Category.SALT_WATER:
|
||||
return "Salt Water"
|
||||
_:
|
||||
return "Other"
|
||||
|
||||
|
||||
static func empty_state(category: Category) -> String:
|
||||
match category:
|
||||
Category.FRESH_WATER:
|
||||
WaterType.Type.FRESH_WATER:
|
||||
return "No freshwater catches cataloged yet."
|
||||
Category.SALT_WATER:
|
||||
WaterType.Type.SALT_WATER:
|
||||
return "No saltwater catches cataloged yet."
|
||||
_:
|
||||
return "No entries available."
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const LOGBOOK_TAB_LEFT_INSET: float = 34.0
|
|||
var _collection_log: CollectionLogType
|
||||
var _inventory: FishInventoryType
|
||||
var _catalog: FishPoolType
|
||||
var _category: LogbookCatalog.Category = LogbookCatalog.Category.OTHER
|
||||
var _category: WaterType.Type = WaterType.Type.OTHER
|
||||
var _selected_id: StringName
|
||||
var _selected_entry_key: StringName
|
||||
var _active: bool = false
|
||||
|
|
@ -148,10 +148,10 @@ func _build_interface() -> void:
|
|||
tabs.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
tabs.add_theme_constant_override("separation", 6)
|
||||
stack.add_child(tabs)
|
||||
for category: LogbookCatalog.Category in [
|
||||
LogbookCatalog.Category.FRESH_WATER,
|
||||
LogbookCatalog.Category.SALT_WATER,
|
||||
LogbookCatalog.Category.OTHER,
|
||||
for category: WaterType.Type in [
|
||||
WaterType.Type.FRESH_WATER,
|
||||
WaterType.Type.SALT_WATER,
|
||||
WaterType.Type.OTHER,
|
||||
]:
|
||||
var tab := OrganizerTabType.new()
|
||||
tab.custom_minimum_size = Vector2(124, 38)
|
||||
|
|
@ -362,7 +362,7 @@ func _add_unknown_entry_content(
|
|||
content.add_child(discovery_hint)
|
||||
|
||||
|
||||
func _select_category(category: LogbookCatalog.Category) -> void:
|
||||
func _select_category(category: WaterType.Type) -> void:
|
||||
if category == _category:
|
||||
return
|
||||
_category = category
|
||||
|
|
@ -516,7 +516,7 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
"#%03d" % catalog_number if catalog_number > 0 else "Unknown",
|
||||
)
|
||||
_add_detail_row("rarity", fish.get_rarity_name())
|
||||
_add_detail_row("body of water", "Unknown")
|
||||
_add_detail_row("body of water", WaterType.label(fish.get_primary_water_type()))
|
||||
_add_detail_row("time of day", _availability_text(fish))
|
||||
_add_detail_row(
|
||||
"weight range",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ children. Child transforms are local offsets owned by that feature.
|
|||
|
||||
## Water bodies
|
||||
|
||||
Every fishable region must author its `water_type` explicitly. The starter pond
|
||||
is `FRESH_WATER`; the starter coast is `SALT_WATER`. Fish species use the same
|
||||
central type definition through an allowed-water-type bitmask, so pool contents
|
||||
and authoritative selection are both validated against the region. Do not infer
|
||||
habitat from node names, pool filenames, coordinates, or water height.
|
||||
|
||||
Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
|
||||
authoritative surface position. The `surface_size`, fishing padding, recovery
|
||||
padding, and recovery depth properties update the visible water and gameplay
|
||||
|
|
@ -86,3 +92,36 @@ for provisional props when visual and collision scale together. Avoid
|
|||
non-uniform root scaling because it can deform collision and imported geometry
|
||||
unpredictably. Make mutable per-instance shapes, meshes, and materials local to
|
||||
the scene so editing one prop does not change unrelated instances.
|
||||
|
||||
## Shoreline tide ribbons
|
||||
|
||||
Each map that uses generated tide ribbons owns a `ShorelineRibbonBaker` node and
|
||||
one `ShorelineRibbonConfig` resource per water body. Each fishable water body
|
||||
declares the shared `WaterType` value used by fishing, Logbook classification,
|
||||
and shoreline presentation. A configuration designates
|
||||
only the static terrain `CollisionShape3D` or `MeshInstance3D` to intersect, the
|
||||
water height, explicit generation bounds, water-facing reference, optional
|
||||
smoothing overrides, and the generated `.tres` output path. Props, docks,
|
||||
players, bobbers, and gameplay areas are not scanned unless a map author
|
||||
explicitly selects one as the terrain source. The baker reports every configured
|
||||
body but intentionally generates ribbons only for `SALT_WATER`; freshwater uses
|
||||
the shared depth-tinted shader without tide marks or an advancing wash.
|
||||
|
||||
To rebuild in the editor, select the map's `ShorelineRibbonBaker` node and press
|
||||
**Rebuild Shoreline Ribbons** in the Inspector. The equivalent validation/CI
|
||||
command for the starter map is:
|
||||
|
||||
```sh
|
||||
godot --headless --path . --script scripts/bake_shoreline_ribbons.gd
|
||||
```
|
||||
|
||||
Set the baker's `debug_path_stage` to Raw, Simplified, or Smoothed before an
|
||||
editor rebuild to compare the extraction stages. Keep it Off in the finished
|
||||
scene; the temporary editor-only line preview is never saved as gameplay data.
|
||||
|
||||
Rebuild a saltwater ribbon only when its terrain at the waterline, configured
|
||||
water height, or generation bounds change. Freshwater terrain changes do not
|
||||
require a ribbon rebuild because freshwater bodies have no ribbon. Adding or
|
||||
moving unrelated props, fishing areas, recovery volumes, or other gameplay
|
||||
nodes does not require a rebuild. Normal gameplay loads the committed generated
|
||||
saltwater meshes and never runs the extraction step.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ enum SurfaceHeightMode {
|
|||
}
|
||||
|
||||
@export var location_tags: Array[StringName] = []
|
||||
@export var water_type: WaterType.Type = WaterType.Type.FRESH_WATER
|
||||
@export var fish_pool: FishPoolType
|
||||
@export var selection_priority: int = 0
|
||||
@export_group("Surface")
|
||||
|
|
|
|||
14
world/generated/shorelines/starter_ocean_shoreline.tres
Normal file
14
world/generated/shorelines/starter_ocean_shoreline.tres
Normal file
File diff suppressed because one or more lines are too long
71
world/materials/shoreline_tide.gdshader
Normal file
71
world/materials/shoreline_tide.gdshader
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded;
|
||||
|
||||
uniform vec4 lead_color : source_color = vec4(0.620, 0.875, 0.855, 1.0);
|
||||
uniform vec4 trail_color : source_color = vec4(0.430, 0.780, 0.770, 1.0);
|
||||
uniform float ribbon_width : hint_range(0.2, 2.0, 0.01) = 0.85;
|
||||
uniform float land_inset : hint_range(0.0, 0.4, 0.01) = 0.10;
|
||||
uniform float tide_speed : hint_range(0.1, 3.0, 0.01) = 1.42;
|
||||
uniform float tide_distance : hint_range(0.0, 0.6, 0.01) = 0.30;
|
||||
uniform float lead_width : hint_range(0.01, 0.2, 0.005) = 0.065;
|
||||
uniform float trail_width : hint_range(0.02, 0.4, 0.005) = 0.18;
|
||||
uniform float lead_strength : hint_range(0.0, 1.0, 0.01) = 0.48;
|
||||
uniform float trail_strength : hint_range(0.0, 1.0, 0.01) = 0.25;
|
||||
uniform float phase_scale : hint_range(0.001, 0.2, 0.001) = 0.035;
|
||||
uniform float phase_strength : hint_range(0.0, 2.0, 0.01) = 0.55;
|
||||
|
||||
varying vec3 world_position;
|
||||
|
||||
|
||||
float hash_21(vec2 point) {
|
||||
return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453);
|
||||
}
|
||||
|
||||
|
||||
float value_noise(vec2 point) {
|
||||
vec2 cell = floor(point);
|
||||
vec2 local = fract(point);
|
||||
vec2 blend = local * local * (3.0 - 2.0 * local);
|
||||
float bottom = mix(hash_21(cell), hash_21(cell + vec2(1.0, 0.0)), blend.x);
|
||||
float top = mix(
|
||||
hash_21(cell + vec2(0.0, 1.0)),
|
||||
hash_21(cell + vec2(1.0, 1.0)),
|
||||
blend.x
|
||||
);
|
||||
return mix(bottom, top, blend.y);
|
||||
}
|
||||
|
||||
|
||||
float soft_line(float coordinate, float center, float width) {
|
||||
float normalized_width = max(width / ribbon_width, 0.001);
|
||||
float derivative_width = min(fwidth(coordinate) * 1.25, normalized_width * 0.65);
|
||||
return 1.0 - smoothstep(
|
||||
max(normalized_width * 0.25 - derivative_width, 0.0),
|
||||
normalized_width + derivative_width,
|
||||
abs(coordinate - center)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void vertex() {
|
||||
world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
float phase = (
|
||||
value_noise(world_position.xz * phase_scale) * 2.0 - 1.0
|
||||
) * phase_strength;
|
||||
float ebb = sin(TIME * tide_speed + phase) * 0.5 + 0.5;
|
||||
float shoreline = land_inset / ribbon_width;
|
||||
float lead_position = shoreline + ebb * tide_distance / ribbon_width;
|
||||
float trail_position = lead_position + max(tide_distance * 0.72, 0.10) / ribbon_width;
|
||||
float lead = soft_line(UV.y, lead_position, lead_width);
|
||||
float trail = soft_line(UV.y, trail_position, trail_width);
|
||||
float edge_fade = smoothstep(0.0, 0.055, UV.y) * (1.0 - smoothstep(0.90, 1.0, UV.y));
|
||||
float lead_alpha = lead * lead_strength;
|
||||
float trail_alpha = trail * trail_strength;
|
||||
float combined_alpha = max(lead_alpha, trail_alpha) * edge_fade;
|
||||
ALBEDO = mix(trail_color.rgb, lead_color.rgb, lead_alpha);
|
||||
ALPHA = combined_alpha;
|
||||
}
|
||||
1
world/materials/shoreline_tide.gdshader.uid
Normal file
1
world/materials/shoreline_tide.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://5dauni8hb2lv
|
||||
19
world/materials/shoreline_tide.tres
Normal file
19
world/materials/shoreline_tide.tres
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://world/materials/shoreline_tide.gdshader" id="1_shader"]
|
||||
|
||||
[resource]
|
||||
render_priority = 1
|
||||
shader = ExtResource("1_shader")
|
||||
shader_parameter/lead_color = Color(0.619608, 0.87451, 0.854902, 1)
|
||||
shader_parameter/trail_color = Color(0.431373, 0.780392, 0.768627, 1)
|
||||
shader_parameter/ribbon_width = 0.85
|
||||
shader_parameter/land_inset = 0.1
|
||||
shader_parameter/tide_speed = 1.42
|
||||
shader_parameter/tide_distance = 0.3
|
||||
shader_parameter/lead_width = 0.065
|
||||
shader_parameter/trail_width = 0.18
|
||||
shader_parameter/lead_strength = 0.48
|
||||
shader_parameter/trail_strength = 0.25
|
||||
shader_parameter/phase_scale = 0.035
|
||||
shader_parameter/phase_strength = 0.55
|
||||
|
|
@ -1,93 +1,241 @@
|
|||
shader_type spatial;
|
||||
render_mode cull_back, diffuse_burley, specular_schlick_ggx;
|
||||
render_mode blend_mix, depth_draw_never, cull_back, unshaded;
|
||||
|
||||
uniform vec4 deep_color : source_color = vec4(0.055, 0.31, 0.50, 1.0);
|
||||
uniform vec4 wave_color : source_color = vec4(0.18, 0.59, 0.74, 1.0);
|
||||
uniform vec4 highlight_color : source_color = vec4(0.64, 0.88, 0.93, 1.0);
|
||||
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
|
||||
|
||||
uniform float primary_wave_scale : hint_range(0.01, 2.0, 0.01) = 0.52;
|
||||
uniform float secondary_wave_scale : hint_range(0.01, 2.0, 0.01) = 0.29;
|
||||
uniform vec2 primary_speed = vec2(0.16, 0.07);
|
||||
uniform vec2 secondary_speed = vec2(-0.08, 0.12);
|
||||
uniform float wave_strength : hint_range(0.0, 1.0, 0.01) = 0.42;
|
||||
uniform vec4 shore_color : source_color = vec4(0.475, 0.788, 0.796, 1.0);
|
||||
uniform vec4 shallow_color : source_color = vec4(0.227, 0.604, 0.663, 1.0);
|
||||
uniform vec4 deep_color : source_color = vec4(0.110, 0.345, 0.435, 1.0);
|
||||
uniform vec4 tide_wash_color : source_color = vec4(0.360, 0.720, 0.735, 1.0);
|
||||
|
||||
uniform float highlight_fresnel_strength : hint_range(0.0, 1.0, 0.01) = 0.22;
|
||||
uniform float fresnel_power : hint_range(0.5, 12.0, 0.1) = 4.0;
|
||||
uniform float roughness : hint_range(0.0, 1.0, 0.01) = 0.32;
|
||||
uniform float specular_strength : hint_range(0.0, 1.0, 0.01) = 0.28;
|
||||
uniform float shallow_depth : hint_range(0.1, 3.0, 0.01) = 0.70;
|
||||
uniform float deep_depth : hint_range(0.5, 12.0, 0.05) = 3.60;
|
||||
uniform float shore_depth_width : hint_range(0.02, 1.0, 0.01) = 0.16;
|
||||
|
||||
uniform float shore_alpha : hint_range(0.1, 1.0, 0.01) = 0.44;
|
||||
uniform float shallow_alpha : hint_range(0.1, 1.0, 0.01) = 0.60;
|
||||
uniform float deep_alpha : hint_range(0.1, 1.0, 0.01) = 0.82;
|
||||
|
||||
uniform bool tide_effect_enabled = true;
|
||||
uniform float tide_speed : hint_range(0.1, 3.0, 0.01) = 1.42;
|
||||
uniform float tide_distance : hint_range(0.0, 0.6, 0.01) = 0.30;
|
||||
uniform float tide_wash_width : hint_range(0.05, 1.0, 0.01) = 0.38;
|
||||
uniform float tide_wash_strength : hint_range(0.0, 0.3, 0.01) = 0.11;
|
||||
uniform float tide_wash_alpha_shift : hint_range(0.0, 0.15, 0.005) = 0.045;
|
||||
|
||||
uniform float phase_noise_scale : hint_range(0.001, 0.5, 0.001) = 0.055;
|
||||
uniform float phase_noise_strength : hint_range(0.0, 2.0, 0.01) = 0.68;
|
||||
uniform float contour_corner_variation : hint_range(0.0, 0.15, 0.005) = 0.045;
|
||||
uniform float contour_smoothing_pixels : hint_range(0.0, 2.5, 0.05) = 1.0;
|
||||
uniform float contour_smoothing_strength : hint_range(0.0, 1.0, 0.01) = 0.72;
|
||||
uniform float contour_depth_reject : hint_range(0.05, 2.0, 0.01) = 0.60;
|
||||
uniform float open_water_drift_scale : hint_range(0.001, 0.3, 0.001) = 0.018;
|
||||
uniform vec2 open_water_drift_speed = vec2(0.010, -0.007);
|
||||
uniform float open_water_drift_strength : hint_range(0.0, 0.08, 0.001) = 0.045;
|
||||
|
||||
uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0;
|
||||
uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0;
|
||||
|
||||
varying vec3 world_position;
|
||||
varying float surface_view_depth;
|
||||
|
||||
|
||||
float hash_21(vec2 point) {
|
||||
return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453);
|
||||
}
|
||||
|
||||
|
||||
float value_noise(vec2 point) {
|
||||
vec2 cell = floor(point);
|
||||
vec2 local = fract(point);
|
||||
vec2 blend = local * local * (3.0 - 2.0 * local);
|
||||
float bottom = mix(hash_21(cell), hash_21(cell + vec2(1.0, 0.0)), blend.x);
|
||||
float top = mix(
|
||||
hash_21(cell + vec2(0.0, 1.0)),
|
||||
hash_21(cell + vec2(1.0, 1.0)),
|
||||
blend.x
|
||||
);
|
||||
return mix(bottom, top, blend.y);
|
||||
}
|
||||
|
||||
|
||||
float reconstruct_view_depth(
|
||||
vec2 screen_uv,
|
||||
float raw_depth,
|
||||
mat4 inverse_projection
|
||||
) {
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
vec3 ndc = vec3(screen_uv, raw_depth) * 2.0 - 1.0;
|
||||
#else
|
||||
vec3 ndc = vec3(screen_uv * 2.0 - 1.0, raw_depth);
|
||||
#endif
|
||||
vec4 view_position = inverse_projection * vec4(ndc, 1.0);
|
||||
return -view_position.z / max(abs(view_position.w), 0.00001);
|
||||
}
|
||||
|
||||
|
||||
float sample_water_depth(
|
||||
vec2 screen_uv,
|
||||
float water_surface_depth,
|
||||
mat4 inverse_projection
|
||||
) {
|
||||
float raw_depth = texture(depth_texture, screen_uv).r;
|
||||
float scene_depth = reconstruct_view_depth(
|
||||
screen_uv,
|
||||
raw_depth,
|
||||
inverse_projection
|
||||
);
|
||||
return scene_depth - water_surface_depth;
|
||||
}
|
||||
|
||||
|
||||
float contour_neighbor_weight(
|
||||
float center_depth,
|
||||
float neighbor_depth,
|
||||
float rejection_distance
|
||||
) {
|
||||
if (neighbor_depth <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
float safe_rejection = max(rejection_distance, 0.001);
|
||||
return 1.0 - smoothstep(
|
||||
safe_rejection * 0.35,
|
||||
safe_rejection,
|
||||
abs(neighbor_depth - center_depth)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void vertex() {
|
||||
world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
vec4 world_vertex = MODEL_MATRIX * vec4(VERTEX, 1.0);
|
||||
vec4 view_vertex = VIEW_MATRIX * world_vertex;
|
||||
world_position = world_vertex.xyz;
|
||||
surface_view_depth = -view_vertex.z;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
const vec2 primary_direction = vec2(0.835, 0.550);
|
||||
const vec2 secondary_direction = vec2(-0.485, 0.875);
|
||||
float raw_depth = texture(depth_texture, SCREEN_UV).r;
|
||||
float scene_view_depth = reconstruct_view_depth(
|
||||
SCREEN_UV,
|
||||
raw_depth,
|
||||
INV_PROJECTION_MATRIX
|
||||
);
|
||||
float water_depth = max(scene_view_depth - surface_view_depth, 0.0);
|
||||
water_depth = min(water_depth, deep_depth * 4.0);
|
||||
|
||||
vec2 primary_coordinates = (
|
||||
world_position.xz * primary_wave_scale
|
||||
+ TIME * primary_speed
|
||||
float contour_depth = water_depth;
|
||||
float contour_feature_support = 1.0;
|
||||
if (tide_effect_enabled) {
|
||||
vec2 contour_texel = (
|
||||
vec2(1.0) / vec2(textureSize(depth_texture, 0))
|
||||
* contour_smoothing_pixels
|
||||
);
|
||||
vec2 secondary_coordinates = (
|
||||
world_position.xz * secondary_wave_scale
|
||||
+ TIME * secondary_speed
|
||||
float contour_depth_sum = water_depth;
|
||||
float contour_weight_sum = 1.0;
|
||||
float left_depth = sample_water_depth(
|
||||
SCREEN_UV - vec2(contour_texel.x, 0.0),
|
||||
surface_view_depth,
|
||||
INV_PROJECTION_MATRIX
|
||||
);
|
||||
float primary_phase = dot(primary_coordinates, primary_direction);
|
||||
float secondary_phase = dot(
|
||||
secondary_coordinates,
|
||||
secondary_direction
|
||||
float right_depth = sample_water_depth(
|
||||
SCREEN_UV + vec2(contour_texel.x, 0.0),
|
||||
surface_view_depth,
|
||||
INV_PROJECTION_MATRIX
|
||||
);
|
||||
float primary_wave = sin(primary_phase);
|
||||
float secondary_wave = sin(secondary_phase);
|
||||
float combined_wave = (
|
||||
primary_wave * 0.62
|
||||
+ secondary_wave * 0.38
|
||||
float upper_depth = sample_water_depth(
|
||||
SCREEN_UV - vec2(0.0, contour_texel.y),
|
||||
surface_view_depth,
|
||||
INV_PROJECTION_MATRIX
|
||||
);
|
||||
float wave_band = smoothstep(
|
||||
0.34,
|
||||
0.66,
|
||||
combined_wave * 0.5 + 0.5
|
||||
float lower_depth = sample_water_depth(
|
||||
SCREEN_UV + vec2(0.0, contour_texel.y),
|
||||
surface_view_depth,
|
||||
INV_PROJECTION_MATRIX
|
||||
);
|
||||
float left_weight = contour_neighbor_weight(
|
||||
water_depth, left_depth, contour_depth_reject
|
||||
);
|
||||
float right_weight = contour_neighbor_weight(
|
||||
water_depth, right_depth, contour_depth_reject
|
||||
);
|
||||
float upper_weight = contour_neighbor_weight(
|
||||
water_depth, upper_depth, contour_depth_reject
|
||||
);
|
||||
float lower_weight = contour_neighbor_weight(
|
||||
water_depth, lower_depth, contour_depth_reject
|
||||
);
|
||||
contour_depth_sum += left_depth * left_weight;
|
||||
contour_depth_sum += right_depth * right_weight;
|
||||
contour_depth_sum += upper_depth * upper_weight;
|
||||
contour_depth_sum += lower_depth * lower_weight;
|
||||
float neighbor_support = left_weight + right_weight + upper_weight + lower_weight;
|
||||
contour_weight_sum += neighbor_support;
|
||||
float smoothed_contour_depth = contour_depth_sum / contour_weight_sum;
|
||||
contour_depth = mix(
|
||||
water_depth,
|
||||
smoothed_contour_depth,
|
||||
contour_smoothing_strength
|
||||
);
|
||||
contour_feature_support = smoothstep(0.75, 1.75, neighbor_support);
|
||||
}
|
||||
|
||||
vec2 wave_slope = (
|
||||
cos(primary_phase)
|
||||
* primary_direction
|
||||
* primary_wave_scale
|
||||
* 0.62
|
||||
+ cos(secondary_phase)
|
||||
* secondary_direction
|
||||
* secondary_wave_scale
|
||||
* 0.38
|
||||
);
|
||||
vec3 world_normal = normalize(
|
||||
vec3(
|
||||
-wave_slope.x * wave_strength * 0.34,
|
||||
1.0,
|
||||
-wave_slope.y * wave_strength * 0.34
|
||||
)
|
||||
);
|
||||
NORMAL = normalize(
|
||||
(VIEW_MATRIX * vec4(world_normal, 0.0)).xyz
|
||||
);
|
||||
float shallow_mix = smoothstep(0.0, shallow_depth, water_depth);
|
||||
float deep_mix = smoothstep(shallow_depth, deep_depth, water_depth);
|
||||
vec3 water_color = mix(shore_color.rgb, shallow_color.rgb, shallow_mix);
|
||||
water_color = mix(water_color, deep_color.rgb, deep_mix);
|
||||
float water_alpha = mix(shore_alpha, shallow_alpha, shallow_mix);
|
||||
water_alpha = mix(water_alpha, deep_alpha, deep_mix);
|
||||
|
||||
float fresnel = pow(
|
||||
1.0 - clamp(dot(NORMAL, VIEW), 0.0, 1.0),
|
||||
fresnel_power
|
||||
if (tide_effect_enabled) {
|
||||
vec2 phase_coordinates = world_position.xz * phase_noise_scale;
|
||||
float phase_variation = (
|
||||
value_noise(phase_coordinates)
|
||||
+ value_noise(phase_coordinates * 0.47 + vec2(9.7, 3.1)) * 0.5
|
||||
- 0.75
|
||||
) * phase_noise_strength;
|
||||
float corner_variation = (
|
||||
value_noise(phase_coordinates * 1.73 + vec2(4.2, 13.6)) * 2.0 - 1.0
|
||||
) * contour_corner_variation;
|
||||
float ebb = sin(TIME * tide_speed + phase_variation) * 0.5 + 0.5;
|
||||
float lead_threshold = shore_depth_width + ebb * tide_distance + corner_variation;
|
||||
float tide_wash = 1.0 - smoothstep(
|
||||
lead_threshold,
|
||||
lead_threshold + max(tide_wash_width, 0.001),
|
||||
contour_depth
|
||||
);
|
||||
vec3 water_color = mix(
|
||||
deep_color.rgb,
|
||||
wave_color.rgb,
|
||||
wave_band * wave_strength
|
||||
);
|
||||
ALBEDO = mix(
|
||||
tide_wash *= contour_feature_support;
|
||||
tide_wash *= 1.0 - smoothstep(shallow_depth, deep_depth, water_depth);
|
||||
water_color = mix(
|
||||
water_color,
|
||||
highlight_color.rgb,
|
||||
fresnel * highlight_fresnel_strength
|
||||
tide_wash_color.rgb,
|
||||
tide_wash * tide_wash_strength
|
||||
);
|
||||
ROUGHNESS = roughness;
|
||||
SPECULAR = specular_strength;
|
||||
water_alpha = min(
|
||||
water_alpha
|
||||
+ tide_wash * tide_wash_alpha_shift,
|
||||
0.96
|
||||
);
|
||||
}
|
||||
|
||||
vec2 drift_coordinates = (
|
||||
world_position.xz * open_water_drift_scale
|
||||
+ TIME * open_water_drift_speed
|
||||
);
|
||||
float broad_drift = value_noise(drift_coordinates) * 2.0 - 1.0;
|
||||
water_color *= 1.0 + broad_drift * open_water_drift_strength * deep_mix;
|
||||
|
||||
float distant_mix = smoothstep(
|
||||
distant_alpha_start,
|
||||
max(distant_alpha_end, distant_alpha_start + 1.0),
|
||||
surface_view_depth
|
||||
);
|
||||
water_color = mix(water_color, deep_color.rgb, distant_mix * 0.72);
|
||||
water_alpha = mix(water_alpha, 0.96, distant_mix);
|
||||
|
||||
ALBEDO = water_color;
|
||||
ALPHA = clamp(water_alpha, 0.1, 0.96);
|
||||
METALLIC = 0.0;
|
||||
SPECULAR = 0.0;
|
||||
ROUGHNESS = 1.0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,3 +5,30 @@
|
|||
[resource]
|
||||
render_priority = 0
|
||||
shader = ExtResource("1_water_shader")
|
||||
shader_parameter/shore_color = Color(0.47451, 0.788235, 0.796078, 1)
|
||||
shader_parameter/shallow_color = Color(0.227451, 0.603922, 0.662745, 1)
|
||||
shader_parameter/deep_color = Color(0.109804, 0.345098, 0.435294, 1)
|
||||
shader_parameter/tide_wash_color = Color(0.360784, 0.721569, 0.733333, 1)
|
||||
shader_parameter/shallow_depth = 0.7
|
||||
shader_parameter/deep_depth = 3.6
|
||||
shader_parameter/shore_depth_width = 0.16
|
||||
shader_parameter/shore_alpha = 0.44
|
||||
shader_parameter/shallow_alpha = 0.6
|
||||
shader_parameter/deep_alpha = 0.82
|
||||
shader_parameter/tide_effect_enabled = true
|
||||
shader_parameter/tide_speed = 1.42
|
||||
shader_parameter/tide_distance = 0.3
|
||||
shader_parameter/tide_wash_width = 0.38
|
||||
shader_parameter/tide_wash_strength = 0.11
|
||||
shader_parameter/tide_wash_alpha_shift = 0.045
|
||||
shader_parameter/phase_noise_scale = 0.055
|
||||
shader_parameter/phase_noise_strength = 0.68
|
||||
shader_parameter/contour_corner_variation = 0.045
|
||||
shader_parameter/contour_smoothing_pixels = 1.0
|
||||
shader_parameter/contour_smoothing_strength = 0.72
|
||||
shader_parameter/contour_depth_reject = 0.6
|
||||
shader_parameter/open_water_drift_scale = 0.018
|
||||
shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007)
|
||||
shader_parameter/open_water_drift_strength = 0.045
|
||||
shader_parameter/distant_alpha_start = 260.0
|
||||
shader_parameter/distant_alpha_end = 1400.0
|
||||
|
|
|
|||
23
world/materials/stylized_water_fresh.tres
Normal file
23
world/materials/stylized_water_fresh.tres
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://world/materials/stylized_water.gdshader" id="1_water_shader"]
|
||||
|
||||
[resource]
|
||||
render_priority = 0
|
||||
shader = ExtResource("1_water_shader")
|
||||
shader_parameter/shore_color = Color(0.47451, 0.788235, 0.796078, 1)
|
||||
shader_parameter/shallow_color = Color(0.227451, 0.603922, 0.662745, 1)
|
||||
shader_parameter/deep_color = Color(0.109804, 0.345098, 0.435294, 1)
|
||||
shader_parameter/tide_wash_color = Color(0.360784, 0.721569, 0.733333, 1)
|
||||
shader_parameter/shallow_depth = 0.7
|
||||
shader_parameter/deep_depth = 3.6
|
||||
shader_parameter/shore_depth_width = 0.16
|
||||
shader_parameter/shore_alpha = 0.44
|
||||
shader_parameter/shallow_alpha = 0.6
|
||||
shader_parameter/deep_alpha = 0.82
|
||||
shader_parameter/tide_effect_enabled = false
|
||||
shader_parameter/open_water_drift_scale = 0.018
|
||||
shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007)
|
||||
shader_parameter/open_water_drift_strength = 0.045
|
||||
shader_parameter/distant_alpha_start = 260.0
|
||||
shader_parameter/distant_alpha_end = 1400.0
|
||||
File diff suppressed because one or more lines are too long
138
world/water/shoreline_ribbon_baker.gd
Normal file
138
world/water/shoreline_ribbon_baker.gd
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
@tool
|
||||
class_name ShorelineRibbonBaker
|
||||
extends Node
|
||||
|
||||
@export var water_bodies: Array[ShorelineRibbonConfig] = []
|
||||
@export_enum("Off", "Raw", "Simplified", "Smoothed") var debug_path_stage := 0
|
||||
@export_tool_button("Rebuild Shoreline Ribbons")
|
||||
var rebuild_shorelines: Callable = rebuild_all
|
||||
|
||||
|
||||
func rebuild_all() -> Array[Dictionary]:
|
||||
var results: Array[Dictionary] = []
|
||||
for configuration: ShorelineRibbonConfig in water_bodies:
|
||||
var result := _rebuild(configuration)
|
||||
if result.is_empty():
|
||||
return []
|
||||
results.append(result)
|
||||
_update_debug_display(results)
|
||||
return results
|
||||
|
||||
|
||||
func _rebuild(configuration: ShorelineRibbonConfig) -> Dictionary:
|
||||
if configuration == null:
|
||||
push_error("Shoreline ribbon configuration is missing.")
|
||||
return {}
|
||||
if configuration.water_type != WaterType.Type.SALT_WATER:
|
||||
return {
|
||||
"skipped": true,
|
||||
"water_type": configuration.water_type,
|
||||
"output_path": configuration.output_resource_path,
|
||||
}
|
||||
var source := get_node_or_null(configuration.terrain_source) as Node3D
|
||||
if source == null:
|
||||
push_error(
|
||||
"Shoreline terrain source was not found: %s"
|
||||
% configuration.terrain_source
|
||||
)
|
||||
return {}
|
||||
if configuration.output_resource_path.is_empty():
|
||||
push_error("Shoreline output resource path is empty.")
|
||||
return {}
|
||||
var faces := _terrain_faces(source)
|
||||
if faces.is_empty():
|
||||
push_error("Configured shoreline terrain source exposes no triangles.")
|
||||
return {}
|
||||
var result := ShorelineRibbonGenerator.generate(
|
||||
faces,
|
||||
configuration.water_height,
|
||||
configuration.generation_bounds,
|
||||
configuration.water_reference,
|
||||
configuration.water_is_inside,
|
||||
configuration.simplification_tolerance,
|
||||
configuration.smoothing_iterations,
|
||||
configuration.resample_spacing
|
||||
)
|
||||
var output_directory := configuration.output_resource_path.get_base_dir()
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_directory))
|
||||
var save_error := ResourceSaver.save(
|
||||
result["mesh"], configuration.output_resource_path
|
||||
)
|
||||
if save_error != OK:
|
||||
push_error(
|
||||
"Failed to save %s: %s"
|
||||
% [configuration.output_resource_path, error_string(save_error)]
|
||||
)
|
||||
return {}
|
||||
result["output_path"] = configuration.output_resource_path
|
||||
result["water_height"] = configuration.water_height
|
||||
result["water_type"] = configuration.water_type
|
||||
result["skipped"] = false
|
||||
return result
|
||||
|
||||
|
||||
func _update_debug_display(results: Array[Dictionary]) -> void:
|
||||
var previous := get_node_or_null("_ShorelinePathDebug")
|
||||
if previous != null:
|
||||
previous.queue_free()
|
||||
if not Engine.is_editor_hint() or debug_path_stage == 0:
|
||||
return
|
||||
var debug_mesh := ImmediateMesh.new()
|
||||
var debug_material := StandardMaterial3D.new()
|
||||
debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
debug_material.albedo_color = Color(1.0, 0.35, 0.65, 1.0)
|
||||
var stage_key: String = [
|
||||
"", "debug_raw_paths", "debug_simplified_paths", "debug_smoothed_paths"
|
||||
][debug_path_stage]
|
||||
for result: Dictionary in results:
|
||||
if result.get("skipped", false):
|
||||
continue
|
||||
var height := float(result["water_height"]) + 0.06
|
||||
for path_data: Dictionary in result[stage_key]:
|
||||
var points: PackedVector2Array = path_data["points"]
|
||||
if points.size() < 2:
|
||||
continue
|
||||
debug_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP, debug_material)
|
||||
for point: Vector2 in points:
|
||||
debug_mesh.surface_add_vertex(Vector3(point.x, height, point.y))
|
||||
if path_data["closed"]:
|
||||
debug_mesh.surface_add_vertex(Vector3(points[0].x, height, points[0].y))
|
||||
debug_mesh.surface_end()
|
||||
var debug_instance := MeshInstance3D.new()
|
||||
debug_instance.name = "_ShorelinePathDebug"
|
||||
debug_instance.mesh = debug_mesh
|
||||
add_child(debug_instance)
|
||||
|
||||
|
||||
func _terrain_faces(source: Node3D) -> PackedVector3Array:
|
||||
var result := PackedVector3Array()
|
||||
if source is CollisionShape3D:
|
||||
var collision_shape := source as CollisionShape3D
|
||||
var concave_shape := collision_shape.shape as ConcavePolygonShape3D
|
||||
if concave_shape == null:
|
||||
return result
|
||||
for vertex: Vector3 in concave_shape.get_faces():
|
||||
result.append(_to_map_space(source, vertex))
|
||||
return result
|
||||
if source is MeshInstance3D:
|
||||
var mesh_instance := source as MeshInstance3D
|
||||
if mesh_instance.mesh == null:
|
||||
return result
|
||||
for surface: int in mesh_instance.mesh.get_surface_count():
|
||||
var arrays := mesh_instance.mesh.surface_get_arrays(surface)
|
||||
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
if indices.is_empty():
|
||||
for vertex: Vector3 in vertices:
|
||||
result.append(_to_map_space(source, vertex))
|
||||
else:
|
||||
for index: int in indices:
|
||||
result.append(_to_map_space(source, vertices[index]))
|
||||
return result
|
||||
|
||||
|
||||
func _to_map_space(source: Node3D, vertex: Vector3) -> Vector3:
|
||||
var map_root := get_parent() as Node3D
|
||||
if map_root == null:
|
||||
return source.to_global(vertex)
|
||||
return map_root.to_local(source.to_global(vertex))
|
||||
1
world/water/shoreline_ribbon_baker.gd.uid
Normal file
1
world/water/shoreline_ribbon_baker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bj704rvwwh77b
|
||||
14
world/water/shoreline_ribbon_config.gd
Normal file
14
world/water/shoreline_ribbon_config.gd
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class_name ShorelineRibbonConfig
|
||||
extends Resource
|
||||
|
||||
@export_node_path("CollisionShape3D", "MeshInstance3D") var terrain_source: NodePath
|
||||
@export var water_type: WaterType.Type = WaterType.Type.FRESH_WATER
|
||||
@export var water_height := 0.0
|
||||
@export var generation_bounds := Rect2()
|
||||
@export var water_reference := Vector2.ZERO
|
||||
@export var water_is_inside := true
|
||||
@export_file("*.tres") var output_resource_path := ""
|
||||
@export_group("Optional Smoothing Overrides")
|
||||
@export var simplification_tolerance := -1.0
|
||||
@export_range(-1, 4, 1) var smoothing_iterations := -1
|
||||
@export var resample_spacing := -1.0
|
||||
1
world/water/shoreline_ribbon_config.gd.uid
Normal file
1
world/water/shoreline_ribbon_config.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c8hk3eojujyax
|
||||
443
world/water/shoreline_ribbon_generator.gd
Normal file
443
world/water/shoreline_ribbon_generator.gd
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
class_name ShorelineRibbonGenerator
|
||||
extends RefCounted
|
||||
|
||||
const WATER_PLANE_EPSILON := 0.001
|
||||
const ENDPOINT_MERGE_TOLERANCE := 0.03
|
||||
const LOOP_CLOSURE_TOLERANCE := 0.06
|
||||
const MINIMUM_FRAGMENT_LENGTH := 2.0
|
||||
const SIMPLIFICATION_TOLERANCE := 0.20
|
||||
const SMOOTHING_ITERATIONS := 2
|
||||
const RESAMPLE_SPACING := 0.22
|
||||
const RIBBON_WIDTH := 0.85
|
||||
const LAND_INSET := 0.10
|
||||
const SURFACE_OFFSET := 0.018
|
||||
|
||||
|
||||
static func generate(
|
||||
faces: PackedVector3Array,
|
||||
water_height: float,
|
||||
bounds: Rect2,
|
||||
water_reference: Vector2,
|
||||
water_is_inside: bool,
|
||||
simplification_override := -1.0,
|
||||
smoothing_iterations_override := -1,
|
||||
resample_spacing_override := -1.0
|
||||
) -> Dictionary:
|
||||
var simplification := (
|
||||
SIMPLIFICATION_TOLERANCE
|
||||
if simplification_override < 0.0
|
||||
else simplification_override
|
||||
)
|
||||
var smoothing_iterations := (
|
||||
SMOOTHING_ITERATIONS
|
||||
if smoothing_iterations_override < 0
|
||||
else smoothing_iterations_override
|
||||
)
|
||||
var resample_spacing := (
|
||||
RESAMPLE_SPACING
|
||||
if resample_spacing_override < 0.0
|
||||
else resample_spacing_override
|
||||
)
|
||||
var segments := _extract_segments(faces, water_height, bounds)
|
||||
var raw_paths := _stitch_segments(segments)
|
||||
var processed_paths: Array[Dictionary] = []
|
||||
var simplified_paths: Array[Dictionary] = []
|
||||
var raw_point_count := 0
|
||||
var simplified_point_count := 0
|
||||
var smoothed_point_count := 0
|
||||
for path_data: Dictionary in raw_paths:
|
||||
var raw_points: PackedVector2Array = path_data["points"]
|
||||
var closed: bool = path_data["closed"]
|
||||
if _path_length(raw_points, closed) < MINIMUM_FRAGMENT_LENGTH:
|
||||
continue
|
||||
var simplified := _simplify(raw_points, closed, simplification)
|
||||
simplified_paths.append({"points": simplified, "closed": closed})
|
||||
var smoothed := _chaikin(simplified, closed, smoothing_iterations)
|
||||
var resampled := _resample(smoothed, closed, resample_spacing)
|
||||
if resampled.size() < (3 if closed else 2):
|
||||
continue
|
||||
raw_point_count += raw_points.size()
|
||||
simplified_point_count += simplified.size()
|
||||
smoothed_point_count += resampled.size()
|
||||
processed_paths.append({"points": resampled, "closed": closed})
|
||||
var mesh := _build_mesh(
|
||||
processed_paths,
|
||||
water_height,
|
||||
water_reference,
|
||||
water_is_inside
|
||||
)
|
||||
return {
|
||||
"mesh": mesh,
|
||||
"segment_count": segments.size(),
|
||||
"loop_count": processed_paths.size(),
|
||||
"raw_point_count": raw_point_count,
|
||||
"simplified_point_count": simplified_point_count,
|
||||
"smoothed_point_count": smoothed_point_count,
|
||||
"triangle_count": _mesh_triangle_count(mesh),
|
||||
"debug_raw_paths": raw_paths,
|
||||
"debug_simplified_paths": simplified_paths,
|
||||
"debug_smoothed_paths": processed_paths,
|
||||
}
|
||||
|
||||
|
||||
static func _extract_segments(
|
||||
faces: PackedVector3Array,
|
||||
water_height: float,
|
||||
bounds: Rect2
|
||||
) -> Array[PackedVector2Array]:
|
||||
var segments: Array[PackedVector2Array] = []
|
||||
for index: int in range(0, faces.size(), 3):
|
||||
var triangle: Array[Vector3] = [
|
||||
faces[index], faces[index + 1], faces[index + 2]
|
||||
]
|
||||
var hits := PackedVector2Array()
|
||||
for edge: int in 3:
|
||||
var a := triangle[edge]
|
||||
var b := triangle[(edge + 1) % 3]
|
||||
var distance_a := a.y - water_height
|
||||
var distance_b := b.y - water_height
|
||||
if (
|
||||
absf(distance_a) <= WATER_PLANE_EPSILON
|
||||
and absf(distance_b) <= WATER_PLANE_EPSILON
|
||||
):
|
||||
continue
|
||||
if not (
|
||||
distance_a * distance_b < 0.0
|
||||
or absf(distance_a) <= WATER_PLANE_EPSILON
|
||||
or absf(distance_b) <= WATER_PLANE_EPSILON
|
||||
):
|
||||
continue
|
||||
var denominator := distance_a - distance_b
|
||||
var amount := (
|
||||
0.0
|
||||
if absf(denominator) <= WATER_PLANE_EPSILON
|
||||
else distance_a / denominator
|
||||
)
|
||||
var hit3 := a.lerp(b, clampf(amount, 0.0, 1.0))
|
||||
var hit := Vector2(hit3.x, hit3.z)
|
||||
if bounds.has_point(hit) and not _contains_near(hits, hit):
|
||||
hits.append(hit)
|
||||
if (
|
||||
hits.size() == 2
|
||||
and hits[0].distance_to(hits[1]) > WATER_PLANE_EPSILON
|
||||
):
|
||||
segments.append(hits)
|
||||
return segments
|
||||
|
||||
|
||||
static func _stitch_segments(
|
||||
segments: Array[PackedVector2Array]
|
||||
) -> Array[Dictionary]:
|
||||
var point_by_key: Dictionary = {}
|
||||
var adjacency: Dictionary = {}
|
||||
var unused_edges: Dictionary = {}
|
||||
for segment: PackedVector2Array in segments:
|
||||
var a_key := _point_key(segment[0])
|
||||
var b_key := _point_key(segment[1])
|
||||
if a_key == b_key:
|
||||
continue
|
||||
point_by_key[a_key] = segment[0]
|
||||
point_by_key[b_key] = segment[1]
|
||||
if not adjacency.has(a_key):
|
||||
adjacency[a_key] = []
|
||||
if not adjacency.has(b_key):
|
||||
adjacency[b_key] = []
|
||||
var edge_key := _edge_key(a_key, b_key)
|
||||
if unused_edges.has(edge_key):
|
||||
continue
|
||||
(adjacency[a_key] as Array).append(b_key)
|
||||
(adjacency[b_key] as Array).append(a_key)
|
||||
unused_edges[edge_key] = true
|
||||
|
||||
var starts: Array = []
|
||||
for key: Vector2i in adjacency:
|
||||
if (adjacency[key] as Array).size() != 2:
|
||||
starts.append(key)
|
||||
for key: Vector2i in adjacency:
|
||||
if not starts.has(key):
|
||||
starts.append(key)
|
||||
|
||||
var paths: Array[Dictionary] = []
|
||||
for start: Vector2i in starts:
|
||||
while _has_unused_neighbor(start, adjacency, unused_edges):
|
||||
var walked := _walk_path(start, point_by_key, adjacency, unused_edges)
|
||||
if (walked["points"] as PackedVector2Array).size() >= 2:
|
||||
paths.append(walked)
|
||||
return paths
|
||||
|
||||
|
||||
static func _walk_path(
|
||||
start: Vector2i,
|
||||
point_by_key: Dictionary,
|
||||
adjacency: Dictionary,
|
||||
unused_edges: Dictionary
|
||||
) -> Dictionary:
|
||||
var points := PackedVector2Array()
|
||||
var previous := Vector2i(2147483647, 2147483647)
|
||||
var current := start
|
||||
var closed := false
|
||||
var guard := unused_edges.size() + 2
|
||||
while guard > 0:
|
||||
guard -= 1
|
||||
points.append(point_by_key[current])
|
||||
var next_key := Vector2i(2147483647, 2147483647)
|
||||
for candidate: Vector2i in adjacency[current]:
|
||||
if candidate == previous and (adjacency[current] as Array).size() > 1:
|
||||
continue
|
||||
if unused_edges.get(_edge_key(current, candidate), false):
|
||||
next_key = candidate
|
||||
break
|
||||
if next_key.x == 2147483647:
|
||||
for candidate: Vector2i in adjacency[current]:
|
||||
if unused_edges.get(_edge_key(current, candidate), false):
|
||||
next_key = candidate
|
||||
break
|
||||
if next_key.x == 2147483647:
|
||||
break
|
||||
unused_edges[_edge_key(current, next_key)] = false
|
||||
previous = current
|
||||
current = next_key
|
||||
if current == start:
|
||||
closed = true
|
||||
break
|
||||
return {"points": points, "closed": closed}
|
||||
|
||||
|
||||
static func _simplify(
|
||||
points: PackedVector2Array,
|
||||
closed: bool,
|
||||
tolerance: float
|
||||
) -> PackedVector2Array:
|
||||
if points.size() <= (4 if closed else 2):
|
||||
return points
|
||||
if not closed:
|
||||
return _rdp_open(points, tolerance)
|
||||
var split_a := 0
|
||||
var split_b := 1
|
||||
var greatest_distance := 0.0
|
||||
for a: int in points.size():
|
||||
for b: int in range(a + 1, points.size()):
|
||||
var distance := points[a].distance_squared_to(points[b])
|
||||
if distance > greatest_distance:
|
||||
greatest_distance = distance
|
||||
split_a = a
|
||||
split_b = b
|
||||
var first_arc := _closed_arc(points, split_a, split_b)
|
||||
var second_arc := _closed_arc(points, split_b, split_a)
|
||||
var first_result := _rdp_open(first_arc, tolerance)
|
||||
var second_result := _rdp_open(second_arc, tolerance)
|
||||
var result := PackedVector2Array()
|
||||
for index: int in first_result.size() - 1:
|
||||
result.append(first_result[index])
|
||||
for index: int in second_result.size() - 1:
|
||||
result.append(second_result[index])
|
||||
return result if result.size() >= 4 else points
|
||||
|
||||
|
||||
static func _rdp_open(
|
||||
points: PackedVector2Array,
|
||||
tolerance: float
|
||||
) -> PackedVector2Array:
|
||||
if points.size() <= 2:
|
||||
return points
|
||||
var greatest_distance := 0.0
|
||||
var split_index := 0
|
||||
for index: int in range(1, points.size() - 1):
|
||||
var distance := _point_segment_distance(
|
||||
points[index], points[0], points[points.size() - 1]
|
||||
)
|
||||
if distance > greatest_distance:
|
||||
greatest_distance = distance
|
||||
split_index = index
|
||||
if greatest_distance <= tolerance:
|
||||
return PackedVector2Array([points[0], points[points.size() - 1]])
|
||||
var left := _rdp_open(points.slice(0, split_index + 1), tolerance)
|
||||
var right := _rdp_open(points.slice(split_index), tolerance)
|
||||
var result := PackedVector2Array()
|
||||
for index: int in left.size() - 1:
|
||||
result.append(left[index])
|
||||
result.append_array(right)
|
||||
return result
|
||||
|
||||
|
||||
static func _closed_arc(
|
||||
points: PackedVector2Array,
|
||||
start: int,
|
||||
finish: int
|
||||
) -> PackedVector2Array:
|
||||
var result := PackedVector2Array()
|
||||
var index := start
|
||||
result.append(points[index])
|
||||
while index != finish:
|
||||
index = (index + 1) % points.size()
|
||||
result.append(points[index])
|
||||
return result
|
||||
|
||||
|
||||
static func _chaikin(
|
||||
points: PackedVector2Array,
|
||||
closed: bool,
|
||||
iterations: int
|
||||
) -> PackedVector2Array:
|
||||
var result := points
|
||||
for _iteration: int in iterations:
|
||||
var next := PackedVector2Array()
|
||||
if not closed:
|
||||
next.append(result[0])
|
||||
var edge_count := result.size() if closed else result.size() - 1
|
||||
for index: int in edge_count:
|
||||
var a := result[index]
|
||||
var b := result[(index + 1) % result.size()]
|
||||
next.append(a.lerp(b, 0.25))
|
||||
next.append(a.lerp(b, 0.75))
|
||||
if not closed:
|
||||
next.append(result[result.size() - 1])
|
||||
result = next
|
||||
return result
|
||||
|
||||
|
||||
static func _resample(
|
||||
points: PackedVector2Array,
|
||||
closed: bool,
|
||||
spacing: float
|
||||
) -> PackedVector2Array:
|
||||
var total_length := _path_length(points, closed)
|
||||
if total_length <= spacing:
|
||||
return points
|
||||
var count := maxi(roundi(total_length / spacing), 3 if closed else 2)
|
||||
var actual_spacing := total_length / float(count if closed else count - 1)
|
||||
var result := PackedVector2Array()
|
||||
var edge := 0
|
||||
var edge_start_distance := 0.0
|
||||
var edge_length := points[0].distance_to(points[1])
|
||||
for sample: int in count:
|
||||
var target := actual_spacing * sample
|
||||
while target > edge_start_distance + edge_length and edge < points.size() - 1:
|
||||
edge_start_distance += edge_length
|
||||
edge += 1
|
||||
if edge >= points.size() - 1:
|
||||
edge_length = points[edge].distance_to(points[0]) if closed else 0.0
|
||||
else:
|
||||
edge_length = points[edge].distance_to(points[edge + 1])
|
||||
var next_index := (edge + 1) % points.size()
|
||||
var amount := (
|
||||
0.0
|
||||
if edge_length <= WATER_PLANE_EPSILON
|
||||
else (target - edge_start_distance) / edge_length
|
||||
)
|
||||
result.append(points[edge].lerp(points[next_index], clampf(amount, 0.0, 1.0)))
|
||||
return result
|
||||
|
||||
|
||||
static func _build_mesh(
|
||||
paths: Array[Dictionary],
|
||||
water_height: float,
|
||||
water_reference: Vector2,
|
||||
water_is_inside: bool
|
||||
) -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var uvs := PackedVector2Array()
|
||||
var indices := PackedInt32Array()
|
||||
for path_data: Dictionary in paths:
|
||||
var points: PackedVector2Array = path_data["points"]
|
||||
var closed: bool = path_data["closed"]
|
||||
var base_index := vertices.size()
|
||||
var path_distance := 0.0
|
||||
for index: int in points.size():
|
||||
if index > 0:
|
||||
path_distance += points[index - 1].distance_to(points[index])
|
||||
var previous := points[(index - 1 + points.size()) % points.size()]
|
||||
var following := points[(index + 1) % points.size()]
|
||||
if not closed:
|
||||
previous = points[maxi(index - 1, 0)]
|
||||
following = points[mini(index + 1, points.size() - 1)]
|
||||
var tangent := previous.direction_to(following)
|
||||
var water_normal := Vector2(-tangent.y, tangent.x)
|
||||
var toward_reference := points[index].direction_to(water_reference)
|
||||
if (
|
||||
(water_is_inside and water_normal.dot(toward_reference) < 0.0)
|
||||
or (not water_is_inside and water_normal.dot(toward_reference) > 0.0)
|
||||
):
|
||||
water_normal = -water_normal
|
||||
var land_point := points[index] - water_normal * LAND_INSET
|
||||
var water_point := points[index] + water_normal * (RIBBON_WIDTH - LAND_INSET)
|
||||
vertices.append(Vector3(land_point.x, water_height + SURFACE_OFFSET, land_point.y))
|
||||
vertices.append(Vector3(water_point.x, water_height + SURFACE_OFFSET, water_point.y))
|
||||
normals.append(Vector3.UP)
|
||||
normals.append(Vector3.UP)
|
||||
uvs.append(Vector2(path_distance, 0.0))
|
||||
uvs.append(Vector2(path_distance, 1.0))
|
||||
var edge_count := points.size() if closed else points.size() - 1
|
||||
for index: int in edge_count:
|
||||
var next := (index + 1) % points.size()
|
||||
var a := base_index + index * 2
|
||||
var b := a + 1
|
||||
var c := base_index + next * 2
|
||||
var d := c + 1
|
||||
indices.append_array(PackedInt32Array([a, c, b, b, c, d]))
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
if not vertices.is_empty():
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
|
||||
static func _point_key(point: Vector2) -> Vector2i:
|
||||
return Vector2i(
|
||||
roundi(point.x / ENDPOINT_MERGE_TOLERANCE),
|
||||
roundi(point.y / ENDPOINT_MERGE_TOLERANCE)
|
||||
)
|
||||
|
||||
|
||||
static func _edge_key(a: Vector2i, b: Vector2i) -> String:
|
||||
if a.x < b.x or (a.x == b.x and a.y <= b.y):
|
||||
return "%d:%d|%d:%d" % [a.x, a.y, b.x, b.y]
|
||||
return "%d:%d|%d:%d" % [b.x, b.y, a.x, a.y]
|
||||
|
||||
|
||||
static func _has_unused_neighbor(
|
||||
key: Vector2i,
|
||||
adjacency: Dictionary,
|
||||
unused_edges: Dictionary
|
||||
) -> bool:
|
||||
for neighbor: Vector2i in adjacency[key]:
|
||||
if unused_edges.get(_edge_key(key, neighbor), false):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _contains_near(points: PackedVector2Array, point: Vector2) -> bool:
|
||||
for existing: Vector2 in points:
|
||||
if existing.distance_to(point) <= WATER_PLANE_EPSILON:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _point_segment_distance(point: Vector2, a: Vector2, b: Vector2) -> float:
|
||||
var segment := b - a
|
||||
if segment.length_squared() <= WATER_PLANE_EPSILON:
|
||||
return point.distance_to(a)
|
||||
var amount := clampf((point - a).dot(segment) / segment.length_squared(), 0.0, 1.0)
|
||||
return point.distance_to(a + segment * amount)
|
||||
|
||||
|
||||
static func _path_length(points: PackedVector2Array, closed: bool) -> float:
|
||||
var result := 0.0
|
||||
for index: int in points.size() - 1:
|
||||
result += points[index].distance_to(points[index + 1])
|
||||
if closed and points.size() > 2:
|
||||
result += points[points.size() - 1].distance_to(points[0])
|
||||
return result
|
||||
|
||||
|
||||
static func _mesh_triangle_count(mesh: ArrayMesh) -> int:
|
||||
if mesh.get_surface_count() == 0:
|
||||
return 0
|
||||
var arrays := mesh.surface_get_arrays(0)
|
||||
return (arrays[Mesh.ARRAY_INDEX] as PackedInt32Array).size() / 3
|
||||
1
world/water/shoreline_ribbon_generator.gd.uid
Normal file
1
world/water/shoreline_ribbon_generator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dhp7ctspabll
|
||||
26
world/water/water_type.gd
Normal file
26
world/water/water_type.gd
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
class_name WaterType
|
||||
extends RefCounted
|
||||
|
||||
enum Type {
|
||||
FRESH_WATER,
|
||||
SALT_WATER,
|
||||
OTHER,
|
||||
}
|
||||
|
||||
const FRESH_WATER_MASK := 1 << Type.FRESH_WATER
|
||||
const SALT_WATER_MASK := 1 << Type.SALT_WATER
|
||||
const ALL_FISHABLE_MASK := FRESH_WATER_MASK | SALT_WATER_MASK
|
||||
|
||||
|
||||
static func mask_for(type: Type) -> int:
|
||||
return 1 << int(type) if type != Type.OTHER else 0
|
||||
|
||||
|
||||
static func label(type: Type) -> String:
|
||||
match type:
|
||||
Type.FRESH_WATER:
|
||||
return "Fresh Water"
|
||||
Type.SALT_WATER:
|
||||
return "Salt Water"
|
||||
_:
|
||||
return "Other"
|
||||
1
world/water/water_type.gd.uid
Normal file
1
world/water/water_type.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://nao2xukkyoc8
|
||||
Loading…
Add table
Add a link
Reference in a new issue